diff --git a/data/systems/custom/00_sol.json b/data/systems/custom/00_sol.json index 51362402ea5..c0b88b1c34f 100644 --- a/data/systems/custom/00_sol.json +++ b/data/systems/custom/00_sol.json @@ -687,7 +687,7 @@ "radius": "f0/0", "rotationPeriod": "f0/0", "rotationPhase": "f0/0", - "seed": 4126130448, + "seed": 201299135, "semiMajorAxis": "f0/0", "type": "STARPORT_SURFACE", "volatileIces": "f0/0", @@ -723,7 +723,7 @@ "radius": "f0/0", "rotationPeriod": "f0/0", "rotationPhase": "f0/0", - "seed": 1148906070, + "seed": 2874781459, "semiMajorAxis": "f0/0", "type": "STARPORT_SURFACE", "volatileIces": "f0/0", @@ -759,7 +759,7 @@ "radius": "f0/0", "rotationPeriod": "f0/0", "rotationPhase": "f0/0", - "seed": 1064378724, + "seed": 3046926584, "semiMajorAxis": "f0/0", "type": "STARPORT_SURFACE", "volatileIces": "f0/0", @@ -2759,4 +2759,4 @@ "stars": [ "STAR_G" ] -} \ No newline at end of file +} diff --git a/src/SystemView.cpp b/src/SystemView.cpp index e438b37986e..395ad92f156 100644 --- a/src/SystemView.cpp +++ b/src/SystemView.cpp @@ -509,6 +509,9 @@ void SystemMapViewport::AddBodyTrack(const SystemBody *b, const vector3d &offset void SystemMapViewport::RenderBody(const SystemBody *b, const vector3d &position, const matrix4x4f &trans) { + if (b->GetSuperType() == SystemBody::SUPERTYPE_STARPORT) + return; + const double radius = b->GetRadius(); matrix4x4f invRot = trans; diff --git a/src/editor/ActionBinder.cpp b/src/editor/ActionBinder.cpp new file mode 100644 index 00000000000..fdcc1aad469 --- /dev/null +++ b/src/editor/ActionBinder.cpp @@ -0,0 +1,208 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "ActionBinder.h" +#include "core/StringUtils.h" +#include "fmt/core.h" + +#include +#include +#include + +using namespace Editor; + +// wrap double-dereference when working with variant of pointer types +template +auto get_if(std::variant &pv) +{ + T *ptr = std::get_if(&pv); + return ptr ? *ptr : nullptr; +} + +ActionBinder::ActionBinder() +{ +} + +ActionBinder::~ActionBinder() +{ +} + +// static +std::string ActionBinder::FormatShortcut(ImGuiKeyChord shortcut) +{ + char name[24]; + ImGui::GetKeyChordName(shortcut, name, sizeof(name)); + + return std::string(name); +} + +void ActionBinder::Update() +{ + // Don't process shortcuts while a popup is open + if (ImGui::IsPopupOpen(ImGuiID(0), ImGuiPopupFlags_AnyPopupId)) { + for (auto &popup : ImGui::GetCurrentContext()->OpenPopupStack) { + if (popup.Window && popup.Window->Flags & ImGuiWindowFlags_Modal) + return; + } + } + + for (auto &[id, action] : m_actionStorage) { + if (!action.shortcut) + continue; + + if (ImGui::Shortcut(action.shortcut, 0, ImGuiInputFlags_RouteGlobal)) { + if (action.predicate.empty() || action.predicate()) + action.action(); + } + } +} + +void ActionBinder::DrawGroupOverviewEntry(Group &group) +{ + if (group.isMenu) + if (!ImGui::TreeNodeEx(group.label.c_str(), ImGuiTreeNodeFlags_FramePadding)) + return; + + for (auto &entry : group.entries) { + if (auto *action = get_if(entry)) { + ImGui::TextUnformatted(action->label.c_str()); + + if (action->shortcut) { + ImGui::SameLine(ImGui::CalcItemWidth()); + ImGui::TextUnformatted(FormatShortcut(action->shortcut).c_str()); + } + } else if (auto *group = get_if(entry)) { + DrawGroupOverviewEntry(*group); + } + } + + if (group.isMenu) + ImGui::TreePop(); +} + +void ActionBinder::DrawOverview(const char *title, bool *pOpen) +{ + if (ImGui::Begin(title, pOpen)) { + for (auto &groupId : m_topLevelGroups) { + DrawGroupOverviewEntry(m_groupStorage.at(groupId)); + } + } + ImGui::End(); +} + +void ActionBinder::DrawMenuBar() +{ + for (auto &groupId : m_topLevelGroups) { + Group &group = m_groupStorage.at(groupId); + + if (group.isMenu) + DrawMenuInternal(group, true); + } +} + +void ActionBinder::DrawMenuInternal(Group &group, bool submitMenuItem) +{ + if (group.entries.empty()) + return; + + if (submitMenuItem && !ImGui::BeginMenu(group.label.c_str())) + return; + + ImGui::PushID(group.label.c_str()); + + size_t numActions = 0; + for (auto &entry : group.entries) { + + if (ActionEntry *action = get_if(entry)) { + + numActions++; + + bool enabled = action->predicate.empty() || action->predicate(); + ImGui::BeginDisabled(!enabled); + + std::string shortcut = action->shortcut ? FormatShortcut(action->shortcut) : ""; + if (ImGui::MenuItem(action->label.c_str(), shortcut.c_str())) { + action->action(); + } + + ImGui::EndDisabled(); + + } else if (Group *subGroup = get_if(entry)) { + + if (numActions) { + numActions = 0; + ImGui::Separator(); + } + + DrawMenuInternal(*subGroup, subGroup->isMenu); + + } + } + + ImGui::PopID(); + + if (submitMenuItem) + ImGui::EndMenu(); +} + +void ActionBinder::BeginInternal(std::string id, bool menu) +{ + for (std::string_view name : SplitString(id, ".")) { + + std::string lookupId = !m_activeGroupStack.empty() ? + fmt::format("{}.{}", m_activeGroupStack.back().first, name) : + std::string(name); + + // Create the new group if it doesn't exist + if (!m_groupStorage.count(lookupId)) { + m_groupStorage.try_emplace(lookupId, name, menu); + + // nothing on the stack, could be a top-level entry + if (m_activeGroupStack.empty()) + m_topLevelGroups.push_back(lookupId); + // add a group entry to our previous group + else { + Group *group = m_activeGroupStack.back().second; + group->entries.emplace_back(&m_groupStorage.at(lookupId)); + } + } + + m_activeGroupStack.push_back({ lookupId, &m_groupStorage.at(lookupId) }); + } +} + +void ActionBinder::EndInternal() +{ + assert(!m_activeGroupStack.empty()); + + m_activeGroupStack.pop_back(); +} + +void ActionBinder::AddInternal(std::string id, ActionEntry &&entry) +{ + if (m_activeGroupStack.empty()) + return; + + Group *group = m_activeGroupStack.back().second; + + std::string qualified_id = fmt::format("{}.{}", m_activeGroupStack.back().first, id); + auto iter = m_actionStorage.emplace(qualified_id, std::move(entry)).first; + + group->entries.push_back(&iter->second); +} + +ActionEntry *ActionBinder::GetAction(std::string id) +{ + if (!m_actionStorage.count(id)) + return nullptr; + + return &m_actionStorage.at(id); +} + +void ActionBinder::TriggerAction(std::string id) +{ + ActionEntry *entry = GetAction(id); + + if (entry && (entry->predicate.empty() || entry->predicate())) + entry->action(); +} diff --git a/src/editor/ActionBinder.h b/src/editor/ActionBinder.h new file mode 100644 index 00000000000..d61ac020775 --- /dev/null +++ b/src/editor/ActionBinder.h @@ -0,0 +1,135 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "imgui/imgui.h" + +#include + +#include +#include +#include + +namespace Editor { + + // Represents an action the user can execute within an editor context + // that should be associated with some window-global shortcut. + // The predicate will be evaluated to determine if the entry is enabled. + struct ActionEntry { + template + ActionEntry(std::string_view label, ImGuiKeyChord shortcut, Functor f) : + label(label), + shortcut(shortcut), + action(f) + {} + + template + ActionEntry(std::string_view label, ImGuiKeyChord shortcut, Predicate p, Functor f) : + label(label), + shortcut(shortcut), + predicate(p), + action(f) + {} + + template + ActionEntry(std::string_view label, const char *icon, ImGuiKeyChord shortcut, Functor f) : + label(label), + fontIcon(icon), + shortcut(shortcut), + action(f) + {} + + template + ActionEntry(std::string_view label, const char *icon, ImGuiKeyChord shortcut, Predicate p, Functor f) : + label(label), + fontIcon(icon), + shortcut(shortcut), + predicate(p), + action(f) + {} + + std::string label; + const char *fontIcon; + ImGuiKeyChord shortcut; + + sigc::slot predicate; + sigc::slot action; + }; + + class ActionBinder { + public: + ActionBinder(); + ~ActionBinder(); + + struct Group; + + using GroupEntry = std::variant; + struct Group { + Group(std::string_view name, bool menu) : + label(std::string(name)), + isMenu(menu) + {} + + std::string label; + std::vector entries; + bool isMenu; + }; + + // process all actions and determine if their shortcuts are activated + void Update(); + + // Draw debug window displaying all registered actions + void DrawOverview(const char *title, bool *pOpen = nullptr); + + // Draw all groups registered in this ActionBinder as a main menu bar + void DrawMenuBar(); + + // draw the GroupEntry named by 'id' in the context of an existing + // dropdown menu (i.e. do not submit a top-level BeginMenu) + void DrawGroup(std::string id) { DrawMenuInternal(m_groupStorage.at(id), false); } + + // draw the GroupEntry named by 'id' as a submenu (with a top-level BeginMenu) + void DrawMenu(std::string id) { DrawMenuInternal(m_groupStorage.at(id), true); } + + // draw the GroupEntry named by 'id' in the context of a button toolbar + // void DrawToolbar(std::string id) + + static std::string FormatShortcut(ImGuiKeyChord shortcut); + + // Begin a group or menu with the given ID. ID is a qualified domain + // name relative to the current ID stack. + ActionBinder &BeginGroup(std::string id) { BeginInternal(id, false); return *this; } + ActionBinder &BeginMenu(std::string id) { BeginInternal(id, true); return *this; } + void EndGroup() { EndInternal(); } + void EndMenu() { EndInternal(); } + + // Add the given action to the currently open group. Fails if no group + // is open. The action can be referenced by the fully-qualified id + // 'group-id[.group-id[...]].action-id'. + ActionBinder &AddAction(std::string id, ActionEntry &&entry) { AddInternal(id, std::move(entry)); return *this; } + + std::vector &GetGroups() { return m_topLevelGroups; } + + ActionEntry *GetAction(std::string id); + void TriggerAction(std::string id); + + private: + void BeginInternal(std::string id, bool menu); + void EndInternal(); + + void AddInternal(std::string id, ActionEntry &&entry); + + void DrawMenuInternal(Group &group, bool menuHeading); + + void DrawGroupOverviewEntry(Group &group); + + std::map m_actionStorage; + std::map m_groupStorage; + + std::vector m_topLevelGroups; + + std::vector> m_activeGroupStack; + }; + +} diff --git a/src/editor/CMakeLists.txt b/src/editor/CMakeLists.txt index 729b15c668d..f068d729720 100644 --- a/src/editor/CMakeLists.txt +++ b/src/editor/CMakeLists.txt @@ -2,6 +2,7 @@ list(APPEND EDITOR_SRC_FOLDERS ${CMAKE_CURRENT_SOURCE_DIR} mfd/ + system/ ) # Creates variables EDITOR_CXX_FILES and EDITOR_HXX_FILES diff --git a/src/editor/EditorApp.cpp b/src/editor/EditorApp.cpp index de704e200f5..1f61a5e8cca 100644 --- a/src/editor/EditorApp.cpp +++ b/src/editor/EditorApp.cpp @@ -4,11 +4,14 @@ #include "EditorApp.h" #include "EditorDraw.h" +#include "Modal.h" #include "FileSystem.h" #include "Lang.h" #include "ModManager.h" #include "ModelViewer.h" +#include "SDL_keycode.h" +#include "EnumStrings.h" #include "argh/argh.h" #include "core/IniConfig.h" @@ -17,7 +20,7 @@ #include "lua/Lua.h" #include "graphics/opengl/RendererGL.h" -#include "SDL_keycode.h" +#include "system/SystemEditor.h" using namespace Editor; @@ -53,6 +56,21 @@ void EditorApp::Initialize(argh::parser &cmdline) SetAppName("ModelViewer"); return; } + + if (cmdline["--system"]) { + std::string systemPath = cmdline[1]; + + RefCountedPtr systemEditor(new SystemEditor(this)); + + if (!systemPath.empty()) { + systemPath = FileSystem::JoinPathBelow(FileSystem::GetDataDir(), systemPath); + systemEditor->LoadSystemFromDisk(systemPath); + } + + QueueLifecycle(systemEditor); + SetAppName("SystemEditor"); + return; + } } void EditorApp::AddLoadingTask(TaskSet::Handle handle) @@ -65,6 +83,11 @@ void EditorApp::SetAppName(std::string_view name) m_appName = name; } +void EditorApp::PushModalInternal(Modal *modal) +{ + m_modalStack.push_back(RefCountedPtr(modal)); +} + void EditorApp::OnStartup() { Log::GetLog()->SetLogFile("editor.txt"); @@ -78,8 +101,8 @@ void EditorApp::OnStartup() cfg.Read(FileSystem::userFiles, "editor.ini"); cfg.Save(); // write defaults if the file doesn't exist + EnumStrings::Init(); Lua::Init(); - ModManager::Init(); ModManager::LoadMods(&cfg); @@ -127,6 +150,18 @@ void EditorApp::PreUpdate() void EditorApp::PostUpdate() { + // Clean up finished modals + for (int idx = int(m_modalStack.size()) - 1; idx >= 0; --idx) { + if (m_modalStack[idx]->Ready()) + m_modalStack.erase(m_modalStack.begin() + idx); + } + + // Draw modals after cleaning, to ensure application has all of frame+1 + // to process modal results + for (auto &modal : m_modalStack) { + modal->Draw(); + } + GetRenderer()->ClearDepthBuffer(); GetPiGui()->Render(); diff --git a/src/editor/EditorApp.h b/src/editor/EditorApp.h index 5b5a0b45e3f..6bdc772e7bc 100644 --- a/src/editor/EditorApp.h +++ b/src/editor/EditorApp.h @@ -15,6 +15,7 @@ namespace Graphics { } // namespace Graphics namespace Editor { + class Modal; class EditorApp : public GuiApplication { public: @@ -29,6 +30,14 @@ namespace Editor { void SetAppName(std::string_view name); + template + RefCountedPtr PushModal(Args&& ...args) + { + T *modal = new T(this, args...); + PushModalInternal(modal); + return RefCountedPtr(modal); + } + protected: void OnStartup() override; void OnShutdown() override; @@ -40,9 +49,13 @@ namespace Editor { std::vector &GetLoadingTasks() { return m_loadingTasks; } private: + void PushModalInternal(Modal *m); + std::vector m_loadingTasks; Graphics::Renderer *m_renderer; + std::vector> m_modalStack; + std::string m_appName; }; diff --git a/src/editor/EditorDraw.cpp b/src/editor/EditorDraw.cpp index fbf50ddf419..f5dc654af82 100644 --- a/src/editor/EditorDraw.cpp +++ b/src/editor/EditorDraw.cpp @@ -2,10 +2,18 @@ // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt #include "EditorDraw.h" -#include "UndoSystem.h" + +#include "Color.h" +#include "EnumStrings.h" + +#include "UndoStepType.h" +#include "editor/EditorIcons.h" +#include "editor/UndoSystem.h" #include "imgui/imgui.h" +#include "fmt/format.h" + using namespace Editor; ImRect Draw::RectCut(ImRect &orig, float amount, RectSide side) @@ -92,6 +100,70 @@ void Draw::EndLayout() ImGui::Spacing(); } +void Draw::BeginHorizontalBar() +{ + ImGui::BeginGroup(); + ImGui::GetCurrentWindow()->DC.LayoutType = ImGuiLayoutType_Horizontal; +} + +void Draw::EndHorizontalBar() +{ + ImGui::GetCurrentWindow()->DC.LayoutType = ImGuiLayoutType_Vertical; + ImGui::EndGroup(); +} + +void Draw::ShowUndoDebugWindow(UndoSystem *undo, bool *p_open) +{ + if (!ImGui::Begin("Undo Stack", p_open, 0)) { + ImGui::End(); + return; + } + + ImGui::AlignTextToFramePadding(); + ImGui::Text("Undo Depth: %ld", undo->GetEntryDepth()); + + if (ImGui::IsKeyDown(ImGuiKey_LeftAlt) && undo->GetEntryDepth()) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().WindowPadding.x * 2.f, 0.f); + + // Get out of jail free card to fix a broken undo state + if (ImGui::Button("X")) { + undo->ResetEntry(); + while (undo->GetEntryDepth() > 0) + undo->EndEntry(); + } + } + + ImGui::Separator(); + + size_t numEntries = undo->GetNumEntries(); + size_t currentIdx = undo->GetCurrentEntry(); + size_t selectedIdx = currentIdx; + + if (ImGui::Selectable("", currentIdx == 0)) + selectedIdx = 0; + + for (size_t idx = 0; idx < numEntries; idx++) + { + const UndoEntry *entry = undo->GetEntry(idx); + + bool isSelected = currentIdx == idx + 1; + std::string label = fmt::format("{}##{}", entry->GetName(), idx); + + if (ImGui::Selectable(label.c_str(), isSelected)) + selectedIdx = idx + 1; + } + + ImGui::End(); + + // If we selected an earlier history entry, undo to that point + for (; currentIdx > selectedIdx; --currentIdx) + undo->Undo(); + + // If we selected a later history entry, redo to that point + for (; currentIdx < selectedIdx; ++currentIdx) + undo->Redo(); +} + bool Draw::UndoHelper(std::string_view label, UndoSystem *undo) { if (ImGui::IsItemDeactivated()) { @@ -135,6 +207,27 @@ bool Draw::ComboUndoHelper(std::string_view label, const char *preview, UndoSyst return ComboUndoHelper(label, label.data(), preview, undo); } +void Draw::EditEnum(std::string_view label, const char *name, const char *ns, int *val, size_t val_max, UndoSystem *undo) +{ + size_t selected = size_t(*val); + const char *preview = EnumStrings::GetString(ns, selected); + if (!preview) + preview = ""; + + if (ComboUndoHelper(label, name, preview, undo)) { + if (ImGui::IsWindowAppearing()) + AddUndoSingleValue(undo, val); + + for (size_t idx = 0; idx <= val_max; ++idx) { + const char *name = EnumStrings::GetString(ns, idx); + if (name && ImGui::Selectable(name, selected == idx)) + *val = int(idx); + } + + ImGui::EndCombo(); + } +} + bool Draw::MenuButton(const char *label) { ImVec2 screenPos = ImGui::GetCursorScreenPos(); @@ -167,3 +260,108 @@ bool Draw::ToggleButton(const char *label, bool *value, ImVec4 activeColor) return changed; } + +bool Draw::ColorEdit3(const char *label, Color *color) +{ + Color4f _c = color->ToColor4f(); + bool changed = ImGui::ColorEdit3(label, &_c[0]); + *color = Color(_c); + return changed; +} + +Draw::DragDropTarget Draw::HierarchyDragDrop(const char *type, ImGuiID targetID, void *data, void *outData, size_t dataSize) +{ + ImGuiContext &g = *ImGui::GetCurrentContext(); + + ImU32 col_highlight = ImGui::GetColorU32(ImGuiCol_ButtonHovered); + ImU32 col_trans = ImGui::GetColorU32(ImGuiCol_ButtonHovered, 0.f); + + Draw::DragDropTarget ret = DragDropTarget::DROP_NONE; + + ImGui::PushID(targetID); + + if (ImGui::BeginDragDropSource()) { + ImGui::SetDragDropPayload(type, data, dataSize); + ImGui::EndDragDropSource(); + } + + ImVec2 min = ImGui::GetItemRectMin(); + ImVec2 max = ImGui::GetItemRectMax(); + float halfHeight = ImGui::GetItemRectSize().y * 0.4f; + float text_offset = g.FontSize + ImGui::GetStyle().FramePadding.x * 2.f; + float inner_x = ImGui::GetCursorScreenPos().x + text_offset; + + ImGuiID beforeTarget = ImGui::GetID("##drop_before"); + ImGuiID afterTarget = ImGui::GetID("##drop_after"); + ImGuiID innerTarget = ImGui::GetID("##drop-in"); + + ImRect beforeRect(min.x, min.y, max.x, min.y + halfHeight); + ImRect afterRect(min.x, max.y - halfHeight, max.x, max.y); + ImRect innerRect(inner_x, min.y, max.x, max.y); + + if (ImGui::BeginDragDropTargetCustom(beforeRect, beforeTarget)) { + const ImGuiPayload *payload = ImGui::AcceptDragDropPayload(type, ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect); + if (payload && payload->Preview) { + ImGui::GetWindowDrawList()->AddRectFilledMultiColor(beforeRect.Min, beforeRect.Max, col_highlight, col_highlight, col_trans, col_trans); + } + + if (payload && payload->Delivery) { + assert(size_t(payload->DataSize) == dataSize); + memcpy(outData, payload->Data, payload->DataSize); + + ret = DragDropTarget::DROP_BEFORE; + } + + ImGui::EndDragDropTarget(); + } + + if (ImGui::BeginDragDropTargetCustom(afterRect, afterTarget)) { + const ImGuiPayload *payload = ImGui::AcceptDragDropPayload(type, ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect); + if (payload && payload->Preview) { + ImGui::GetWindowDrawList()->AddRectFilledMultiColor(afterRect.Min, afterRect.Max, col_trans, col_trans, col_highlight, col_highlight); + } + + if (payload && payload->Delivery) { + assert(size_t(payload->DataSize) == dataSize); + memcpy(outData, payload->Data, payload->DataSize); + + ret = DragDropTarget::DROP_AFTER; + } + + ImGui::EndDragDropTarget(); + } + + if (ImGui::BeginDragDropTargetCustom(innerRect, innerTarget)) { + const ImGuiPayload *payload = ImGui::AcceptDragDropPayload(type, ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect); + if (payload && payload->Preview) { + ImGui::GetWindowDrawList()->AddRectFilledMultiColor(innerRect.Min, innerRect.Max, col_trans, col_highlight, col_highlight, col_trans); + } + + if (payload && payload->Delivery) { + assert(size_t(payload->DataSize) == dataSize); + memcpy(outData, payload->Data, payload->DataSize); + + ret = DragDropTarget::DROP_CHILD; + } + + ImGui::EndDragDropTarget(); + } + + ImGui::PopID(); + return ret; +} + +void Draw::HelpMarker(const char* desc, bool same_line) +{ + if (same_line) + ImGui::SameLine(ImGui::GetContentRegionAvail().x /*- ImGui::GetFontSize()*/); + + ImGui::TextDisabled(EICON_INFO); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} diff --git a/src/editor/EditorDraw.h b/src/editor/EditorDraw.h index f177be8a478..3247988be7a 100644 --- a/src/editor/EditorDraw.h +++ b/src/editor/EditorDraw.h @@ -3,7 +3,6 @@ #pragma once -#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui/imgui.h" #include "imgui/imgui_internal.h" @@ -11,6 +10,8 @@ #include +struct Color4ub; + namespace Editor { class UndoSystem; } @@ -42,6 +43,15 @@ namespace Editor::Draw { // End a horizontal layout block void EndLayout(); + // Setup horizontal layout for a button bar + void BeginHorizontalBar(); + + // End a horizontal layout block + void EndHorizontalBar(); + + // Show a window to debug the state of the passed undo system + void ShowUndoDebugWindow(UndoSystem *undo, bool *p_open = nullptr); + // Manage pushing/popping an UndoEntry after an input widget that provides IsItemActivated() and IsItemDeactivated() // Note: this helper relies on the widget *not* changing the underlying value the frame IsItemActivated() is true bool UndoHelper(std::string_view label, UndoSystem *undo); @@ -53,12 +63,31 @@ namespace Editor::Draw { // The above, but defaulting the label to the entryName bool ComboUndoHelper(std::string_view label, const char *preview, UndoSystem *undo); + // Show an edit dialog box to chose a value from an enumeration + void EditEnum(std::string_view label, const char *name, const char *ns, int *val, size_t val_max, UndoSystem *undo); + // Simple button that summons a popup menu underneath it bool MenuButton(const char *label); // Simple on/off toggle button with a text label bool ToggleButton(const char *label, bool *value, ImVec4 activeColor); + // Color edit button + bool ColorEdit3(const char *label, Color4ub *color); + + enum DragDropTarget { + DROP_NONE = 0, + DROP_BEFORE = 1, + DROP_CHILD = 2, + DROP_AFTER = 3 + }; + + // Begin tri-mode drag-drop handling on a + DragDropTarget HierarchyDragDrop(const char *type, ImGuiID targetID, void *data, void *outData, size_t dataSize); + + // Show a help tooltip + void HelpMarker(const char* desc, bool same_line = true); + } inline bool operator==(const ImVec2 &a, const ImVec2 &b) diff --git a/src/editor/EditorIcons.h b/src/editor/EditorIcons.h index b43b2a01ab1..c3ea8e112cb 100644 --- a/src/editor/EditorIcons.h +++ b/src/editor/EditorIcons.h @@ -3,17 +3,32 @@ #pragma once -#define EICON_GRAVPOINT "\uF01F" #define EICON_SUN "\uF023" #define EICON_ASTEROID "\uF024" +#define EICON_GRAVPOINT "\uF025" #define EICON_ROCKY_PLANET "\uF033" #define EICON_MOON "\uF043" #define EICON_GAS_GIANT "\uF053" #define EICON_SPACE_STATION "\uF063" -#define EICON_SURFACE_STATION "\uF0F2" +#define EICON_SURFACE_STATION "\uF073" +#define EICON_STOP "\uF054" #define EICON_PAUSE "\uF055" #define EICON_PLAY "\uF056" +#define EICON_RESET "\uF05F" + +#define EICON_REWIND3 "\uF064" +#define EICON_REWIND2 "\uF065" +#define EICON_REWIND1 "\uF066" +#define EICON_TIMESTOP "\uF067" +#define EICON_FORWARD1 "\uF068" +#define EICON_FORWARD2 "\uF069" +#define EICON_FORWARD3 "\uF06A" + +#define EICON_INFO "\uF088" + +#define EICON_RANDOM "\uF0C7" + #define EICON_AXES "\uF0CA" #define EICON_GRID "\uF0CB" diff --git a/src/editor/EditorWindow.cpp b/src/editor/EditorWindow.cpp index 5256163a347..d612df26e8b 100644 --- a/src/editor/EditorWindow.cpp +++ b/src/editor/EditorWindow.cpp @@ -5,7 +5,6 @@ #include "editor/EditorApp.h" -#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui/imgui.h" #include "imgui/imgui_internal.h" diff --git a/src/editor/Modal.cpp b/src/editor/Modal.cpp new file mode 100644 index 00000000000..b15974f17b1 --- /dev/null +++ b/src/editor/Modal.cpp @@ -0,0 +1,57 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "Modal.h" + +#include "editor/EditorApp.h" +#include "editor/EditorDraw.h" +#include "pigui/PiGui.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" + +using namespace Editor; + +Modal::Modal(EditorApp *app, const char *title, bool canClose) : + m_app(app), + m_title(title), + m_id(0), + m_shouldClose(false), + m_canClose(canClose) +{ +} + +bool Modal::Ready() +{ + return m_id != 0 && !ImGui::IsPopupOpen(m_id, ImGuiPopupFlags_AnyPopupLevel); +} + +void Modal::Close() +{ + m_shouldClose = true; +} + +void Modal::Draw() +{ + if (!m_id) { + m_id = ImGui::GetID(m_title); + ImGui::OpenPopup(m_id); + } + + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5, ImGuiCond_Always, ImVec2(0.5, 0.5)); + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 16)); + + if (ImGui::BeginPopupModal(m_title, m_canClose ? &m_canClose : nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) { + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 14)); + + if (m_shouldClose) + ImGui::CloseCurrentPopup(); + else + DrawInternal(); + + ImGui::PopFont(); + ImGui::EndPopup(); + } + + ImGui::PopFont(); +} diff --git a/src/editor/Modal.h b/src/editor/Modal.h new file mode 100644 index 00000000000..b18f4e635ba --- /dev/null +++ b/src/editor/Modal.h @@ -0,0 +1,34 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "RefCounted.h" + +// forward declaration +using ImGuiID = unsigned int; + +namespace Editor { + + class EditorApp; + + class Modal : public RefCounted { + public: + Modal(EditorApp *app, const char *title, bool canClose); + + bool Ready(); + void Close(); + + virtual void Draw(); + + protected: + virtual void DrawInternal() {} + + EditorApp *m_app; + const char *m_title; + ImGuiID m_id; + bool m_shouldClose; + bool m_canClose; + }; + +} // namespace Editor diff --git a/src/editor/ModelViewer.cpp b/src/editor/ModelViewer.cpp index 47535e4a4d9..a424864d607 100644 --- a/src/editor/ModelViewer.cpp +++ b/src/editor/ModelViewer.cpp @@ -33,16 +33,6 @@ namespace { static constexpr const char *LOG_WND_NAME = "Log"; } -namespace ImGui { - bool ColorEdit3(const char *label, Color &color) - { - Color4f _c = color.ToColor4f(); - bool changed = ColorEdit3(label, &_c[0]); - color = Color(_c); - return changed; - } -} // namespace ImGui - ModelViewer::ModelViewer(EditorApp *app, LuaManager *lm) : m_app(app), m_input(app->GetInput()), diff --git a/src/editor/ModelViewerWidget.cpp b/src/editor/ModelViewerWidget.cpp index 575588ac05d..a1753715c1e 100644 --- a/src/editor/ModelViewerWidget.cpp +++ b/src/editor/ModelViewerWidget.cpp @@ -28,7 +28,6 @@ #include "SDL_keycode.h" -#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui/imgui.h" #include "imgui/imgui_internal.h" @@ -49,18 +48,6 @@ namespace { } } -namespace ImGui { - - static bool ColorEdit3(const char *label, Color &color) - { - Color4f _c = color.ToColor4f(); - bool changed = ColorEdit3(label, &_c[0]); - color = Color(_c); - return changed; - } - -} - // ─── Setup ─────────────────────────────────────────────────────────────────── ModelViewerWidget::ModelViewerWidget(EditorApp *app) : @@ -762,9 +749,9 @@ void ModelViewerWidget::DrawMenus() SetRandomColors(); bool valuesChanged = false; - valuesChanged |= ImGui::ColorEdit3("##Color 1", m_colors[0]); - valuesChanged |= ImGui::ColorEdit3("##Color 2", m_colors[1]); - valuesChanged |= ImGui::ColorEdit3("##Color 3", m_colors[2]); + valuesChanged |= Draw::ColorEdit3("##Color 1", &m_colors[0]); + valuesChanged |= Draw::ColorEdit3("##Color 2", &m_colors[1]); + valuesChanged |= Draw::ColorEdit3("##Color 3", &m_colors[2]); if (valuesChanged) m_model->SetColors(m_colors); diff --git a/src/editor/UndoStepType.h b/src/editor/UndoStepType.h index b5d4920fa1f..e319ee1ce06 100644 --- a/src/editor/UndoStepType.h +++ b/src/editor/UndoStepType.h @@ -7,6 +7,31 @@ namespace Editor { + // ======================================================================== + // UndoClosure Helper + // ======================================================================== + + // UndoClosure wraps an UndoStep with a closure to be executed after an + // Undo() or Redo() operation. + + template + class UndoClosure : public T { + public: + template + UndoClosure(ClosureType &&closure, Args ...args) : + m_onUpdate(closure), + T(args...) + {} + + void Swap() override { + T::Swap(); + m_onUpdate(); + } + + private: + ClosureType m_onUpdate; + }; + // ======================================================================== // UndoSingleValue Helper // ======================================================================== @@ -38,8 +63,7 @@ namespace Editor { std::swap(m_state, *m_dataRef); } - void Undo() override { std::swap(*m_dataRef, m_state); } - void Redo() override { std::swap(*m_dataRef, m_state); } + void Swap() override { std::swap(*m_dataRef, m_state); } // Implement HasChanged as !(a == b) to reduce the number of operator overloads required bool HasChanged() const override { return !(*m_dataRef == m_state); } @@ -49,47 +73,6 @@ namespace Editor { ValueType m_state; }; - template - class UndoSingleValueClosureStep : public UndoStep - { - public: - UndoSingleValueClosureStep(ValueType *type, ClosureType &&updateClosure) : - m_dataRef(type), - m_state(*m_dataRef), - m_onUpdate(std::move(updateClosure)) - { - } - - UndoSingleValueClosureStep(ValueType *type, const ValueType &newValue, ClosureType &&updateClosure) : - m_dataRef(type), - m_state(newValue), - m_onUpdate(std::move(updateClosure)) - { - std::swap(*m_dataRef, m_state); - m_onUpdate(); - } - - void Undo() override - { - std::swap(*m_dataRef, m_state); - m_onUpdate(); - } - - void Redo() override - { - std::swap(*m_dataRef, m_state); - m_onUpdate(); - } - - bool HasChanged() const override { return !(*m_dataRef == m_state); } - - private: - ValueType *m_dataRef; - ValueType m_state; - - ClosureType m_onUpdate; - }; - // Helper functions to construct the above UndoStep helpers template @@ -105,15 +88,15 @@ namespace Editor { } template - inline void AddUndoSingleValueClosure(UndoSystem *s, T *dataRef, UpdateClosure &&closure) + inline void AddUndoSingleValueClosure(UndoSystem *s, T *dataRef, UpdateClosure closure) { - s->AddUndoStep>(dataRef, std::move(closure)); + s->AddUndoStep>>(std::move(closure), dataRef); } template - inline void AddUndoSingleValueClosure(UndoSystem *s, T *dataRef, const T &newValue, UpdateClosure &&closure) + inline void AddUndoSingleValueClosure(UndoSystem *s, T *dataRef, const T &newValue, UpdateClosure closure) { - s->AddUndoStep>(dataRef, newValue, std::move(closure)); + s->AddUndoStep>>(std::move(closure), dataRef, newValue); } // ======================================================================== @@ -141,63 +124,21 @@ namespace Editor { m_dataRef(data), m_state(newValue) { - swap(); + Swap(); } - void Undo() override { swap(); } - void Redo() override { swap(); } - - bool HasChanged() const override { return !((m_dataRef->*GetterFn)() == m_state); } - - private: // two-way swap with opaque setter/getter functions - void swap() { + void Swap() override { ValueType t = (m_dataRef->*GetterFn)(); std::swap(t, m_state); (m_dataRef->*SetterFn)(std::move(t)); } - Obj *m_dataRef; - ValueType m_state; - }; - - template - class UndoGetSetValueClosureStep : public UndoStep - { - public: - UndoGetSetValueClosureStep(Obj *data, ClosureType &&updateClosure) : - m_dataRef(data), - m_state((m_dataRef->*GetterFn)()), - m_update(std::move(updateClosure)) - { - } - - UndoGetSetValueClosureStep(Obj *data, const ValueType &newValue, ClosureType &&updateClosure) : - m_dataRef(data), - m_state(newValue), - m_update(std::move(updateClosure)) - { - swap(); - } - - void Undo() override { swap(); } - void Redo() override { swap(); } - bool HasChanged() const override { return !((m_dataRef->*GetterFn)() == m_state); } private: - // two-way swap with opaque setter/getter functions and update closure - void swap() { - ValueType t = (m_dataRef->*GetterFn)(); - std::swap(t, m_state); - (m_dataRef->*SetterFn)(std::move(t)); - - m_update(); - } - Obj *m_dataRef; ValueType m_state; - ClosureType m_update; }; // Helper functions to construct the above UndoStep helpers @@ -220,14 +161,157 @@ namespace Editor { inline void AddUndoGetSetValueClosure(UndoSystem *s, Obj *dataRef, UpdateClosure &&closure) { using ValueType = decltype((dataRef->*GetterFn)()); - s->AddUndoStep>(dataRef, std::move(closure)); + s->AddUndoStep>>(std::move(closure), dataRef); } template inline void AddUndoGetSetValueClosure(UndoSystem *s, Obj *dataRef, const T &newValue, UpdateClosure &&closure) { using ValueType = decltype((dataRef->*GetterFn)()); - s->AddUndoStep>(dataRef, newValue, std::move(closure)); + s->AddUndoStep>>(std::move(closure), dataRef, newValue); + } + + // ======================================================================== + // UndoVectorStep Helper + // ======================================================================== + + // UndoVectorStep implements an UndoStep that expresses mutation of a + // std::vector or similar container variable providing insert(), erase(), + // begin(), and size(). + // + // The only requirements are that the value type be Move-Constructible or + // Copy-Constructible, and that the container's memory location will not + // change between creation of the UndoStep and when it is undone/redone. + + template + class UndoVectorStep : public UndoStep + { + public: + using ValueType = typename Container::value_type; + + UndoVectorStep(Container *container, const ValueType &newValue, size_t insertIdx) : + m_container(*container), + m_value {}, + m_idx(insertIdx), + m_insert(true) + { + Swap(); + } + + UndoVectorStep(Container *container, size_t removeIdx) : + m_container(*container), + m_value {}, + m_idx(removeIdx), + m_insert(false) + { + Swap(); + } + + void Swap() override + { + if (m_insert) { + m_container.insert(m_container.begin() + m_idx, std::move(m_value)); + } else { + m_value = std::move(m_container[m_idx]); + m_container.erase(m_container.begin() + m_idx); + } + + m_insert = !m_insert; + } + + private: + Container &m_container; + ValueType m_value; + size_t m_idx; + bool m_insert; + }; + + // ======================================================================== + // UndoVectorSingleValue Helper + // ======================================================================== + + // UndoVectorSingleValue implements an UndoStep that expresses mutation of + // a value contained in a std::vector or similar container variable + // providing operator[](size_t) and size() + // + // The only requirements are that the value type be Move-Constructible or + // Copy-Constructible, and that the container's memory location will not + // change between creation of the UndoStep and when it is undone/redone. + // + // For containers that reallocate memory on mutation (e.g. std::vector) + // this UndoStep is a replacement for UndoSingleValueStep. + + template + class UndoVectorSingleValueStep : public UndoStep + { + public: + using ValueType = typename Container::value_type; + + UndoVectorSingleValueStep(Container *container, size_t idx) : + m_container(*container), + m_index(idx), + m_state(m_container[idx]) + { + } + + void Swap() override { std::swap(m_container[m_index], m_state); } + + bool HasChanged() const override { return !(m_container[m_index] == m_state); } + + private: + Container &m_container; + size_t m_index; + ValueType m_state; + }; + + // Helper functions to construct the above UndoStep helpers + + template + inline void AddUndoVectorInsert(UndoSystem *s, T *containerRef, const Value &value, size_t idx = -1) + { + if (idx == size_t(-1)) + idx = containerRef->size(); + + s->AddUndoStep>(containerRef, value, idx); + } + + template + inline void AddUndoVectorErase(UndoSystem *s, T *containerRef, size_t idx = -1) + { + if (idx == size_t(-1)) + idx = containerRef->size() - 1; + + s->AddUndoStep>(containerRef, idx); + } + + template + inline void AddUndoVectorSingleValue(UndoSystem *s, T *containerRef, size_t idx = -1) + { + if (idx == size_t(-1)) + idx = containerRef->size() - 1; + + s->AddUndoStep>(containerRef, idx); + } + + template + inline void AddUndoVectorInsertClosure(UndoSystem *s, T *containerRef, const Value &value, size_t idx, UpdateClosure closure) + { + s->AddUndoStep>>(std::move(closure), containerRef, value, idx); + } + + template + inline void AddUndoVectorEraseClosure(UndoSystem *s, T *containerRef, size_t idx, UpdateClosure closure) + { + s->AddUndoStep>>(std::move(closure), containerRef, idx); + } + + template + inline void AddUndoVectorSingleValue(UndoSystem *s, T *containerRef, size_t idx, UpdateClosure closure) + { + if (idx == size_t(-1)) + idx = containerRef->size() - 1; + + s->AddUndoStep>>(std::move(closure), containerRef, idx); } } // namespace Editor diff --git a/src/editor/UndoSystem.cpp b/src/editor/UndoSystem.cpp index aef12fa3e0a..02720c51103 100644 --- a/src/editor/UndoSystem.cpp +++ b/src/editor/UndoSystem.cpp @@ -4,6 +4,9 @@ #include "UndoSystem.h" #include "utils.h" +#define XXH_INLINE_ALL +#include "lz4/xxhash.h" + #include using namespace Editor; @@ -33,6 +36,7 @@ bool UndoEntry::HasChanged() const UndoSystem::UndoSystem() : m_openUndoDepth(0), + m_entryId(0), m_doing(false) { } @@ -64,6 +68,15 @@ const UndoEntry *UndoSystem::GetEntry(size_t stackIdx) const return nullptr; } +size_t UndoSystem::GetStateHash() +{ + size_t hash = "UndoState"_hash; + for (auto &entry_ptr : m_undoStack) + hash = XXH64(&entry_ptr->m_id, sizeof(size_t), hash); + + return hash; +} + void UndoSystem::Undo() { if (!CanUndo()) @@ -102,6 +115,8 @@ void UndoSystem::Clear() m_openUndoEntry.reset(); m_openUndoDepth = 0; + m_entryId = 0; + m_redoStack.clear(); m_undoStack.clear(); } @@ -116,6 +131,7 @@ void UndoSystem::BeginEntry(std::string_view name) m_openUndoEntry.reset(new UndoEntry()); m_openUndoEntry->m_name = name; + m_openUndoEntry->m_id = ++m_entryId; } void UndoSystem::ResetEntry() diff --git a/src/editor/UndoSystem.h b/src/editor/UndoSystem.h index 6ab2994177b..7ec54198ecc 100644 --- a/src/editor/UndoSystem.h +++ b/src/editor/UndoSystem.h @@ -34,10 +34,13 @@ class UndoStep { virtual ~UndoStep() = default; // Execute an undo step (replace application state with stored state) - virtual void Undo() = 0; + virtual void Undo() { Swap(); } // Execute a redo step (replace stored state with application state) - virtual void Redo() = 0; + virtual void Redo() { Swap(); } + + // Helper method to allow state changes in a single function + virtual void Swap() {}; // Optimization step: entries for which none of the steps represent a // change in state will not be added to the undo stack @@ -66,6 +69,7 @@ class UndoEntry { bool HasChanged() const; StringName m_name; + size_t m_id; std::vector> m_steps; }; @@ -120,6 +124,9 @@ class UndoSystem { // Return a pointer to the given undo entry if stackIdx < GetNumEntries(). const UndoEntry *GetEntry(size_t stackIdx) const; + // Calculate a hash value used to describe the current undo state + size_t GetStateHash(); + bool CanUndo() const { return !m_undoStack.empty(); } bool CanRedo() const { return !m_redoStack.empty(); } @@ -159,6 +166,7 @@ class UndoSystem { std::unique_ptr m_openUndoEntry; size_t m_openUndoDepth; + size_t m_entryId; bool m_doing; }; diff --git a/src/editor/ViewportWindow.cpp b/src/editor/ViewportWindow.cpp index 1dae2b2dc3f..60c29d86352 100644 --- a/src/editor/ViewportWindow.cpp +++ b/src/editor/ViewportWindow.cpp @@ -9,7 +9,6 @@ #include "graphics/Renderer.h" #include "graphics/Texture.h" -#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui/imgui.h" #include "imgui/imgui_internal.h" @@ -90,20 +89,6 @@ void ViewportWindow::Update(float deltaTime) r->ResolveRenderTarget(m_renderTarget.get(), m_resolveTarget.get(), m_viewportExtents); } - ImGui::BeginChild("##ViewportContainer", ImVec2(0, 0), false, - ImGuiWindowFlags_NoBackground | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse | - ImGuiWindowFlags_AlwaysUseWindowPadding); - - // set Horizontal layout type since we're using this window effectively as a toolbar - ImGui::GetCurrentWindow()->DC.LayoutType = ImGuiLayoutType_Horizontal; - - // Draw all "viewport overlay" UI here, to properly route inputs - OnDraw(); - - ImGui::EndChild(); - ImGuiID viewportID = ImGui::GetID("##ViewportOverlay"); ImGui::KeepAliveID(viewportID); @@ -112,6 +97,8 @@ void ViewportWindow::Update(float deltaTime) ImGuiButtonFlags flags = ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_PressedOnClick | + ImGuiButtonFlags_AllowOverlap | + ImGuiButtonFlags_NoSetKeyOwner | ImGuiButtonFlags_MouseButtonMask_; ImRect area = { screenPos, screenPos + size }; @@ -120,6 +107,9 @@ void ViewportWindow::Update(float deltaTime) bool clicked = ImGui::ButtonBehavior(area, viewportID, &m_viewportHovered, &m_viewportActive, flags); + if (m_viewportActive) + ImGui::GetCurrentContext()->ActiveIdAllowOverlap = true; + // if the viewport is hovered/active or we just released it, // update mouse interactions with it if (m_viewportHovered || m_viewportActive || wasPressed) { @@ -133,6 +123,22 @@ void ViewportWindow::Update(float deltaTime) OnHandleInput(clicked, wasPressed && !m_viewportActive, mousePos); } + ImGui::BeginChild("##ViewportContainer", ImVec2(0, 0), false, + ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_AlwaysUseWindowPadding); + + // set Horizontal layout type since we're using this window effectively as a toolbar + ImGui::GetCurrentWindow()->DC.LayoutType = ImGuiLayoutType_Horizontal; + + // Draw all "viewport overlay" UI here, to properly route inputs + OnDraw(); + + ImGui::EndChild(); + + if (m_viewportActive) + ImGui::GetCurrentContext()->ActiveIdAllowOverlap = false; } } diff --git a/src/editor/system/GalaxyEditAPI.cpp b/src/editor/system/GalaxyEditAPI.cpp new file mode 100644 index 00000000000..7577a2b2671 --- /dev/null +++ b/src/editor/system/GalaxyEditAPI.cpp @@ -0,0 +1,812 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt +#include "GalaxyEditAPI.h" + + +#include "EditorIcons.h" +#include "SystemEditorHelpers.h" + +#include "core/Log.h" +#include "core/macros.h" +#include "editor/UndoStepType.h" +#include "editor/EditorDraw.h" + +#include "EnumStrings.h" +#include "galaxy/Sector.h" +#include "galaxy/Galaxy.h" +#include "galaxy/NameGenerator.h" +#include "galaxy/StarSystemGenerator.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_stdlib.h" +#include "lua/LuaNameGen.h" +#include "system/SystemBodyUndo.h" + +using namespace Editor; + +namespace { + bool InvalidSystemNameChar(char c) + { + return !( + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9')); + } + + static constexpr double SECONDS_TO_DAYS = 1.0 / (3600.0 * 24.0); + + static const char *explored_labels[] = { + "Randomly Generated", + "Explored at Start", + "Unexplored", + }; +} + +namespace Editor::Draw { + + // Essentially CollapsingHeader without the frame and with consistent ID regardless of edited body + bool DerivedValues(std::string_view sectionLabel) { + constexpr ImGuiID detailSeed = "Editor Details"_hash32; + + if (ImGui::GetCurrentWindow()->SkipItems) + return false; + + ImGuiID treeId = ImGui::GetIDWithSeed(sectionLabel.data(), sectionLabel.data() + sectionLabel.size(), detailSeed); + return ImGui::TreeNodeBehavior(treeId, ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, "Derived Values"); + } + +} // namespace Editor::Draw + +void StarSystem::EditorAPI::RemoveFromCache(StarSystem *system) +{ + if (system->m_cache) { + system->m_cache->RemoveFromAttic(system->GetPath()); + system->m_cache = nullptr; + } +} + +SystemBody *StarSystem::EditorAPI::NewBody(StarSystem *system) +{ + return system->NewBody(); +} + +SystemBody *StarSystem::EditorAPI::NewBodyAround(StarSystem *system, Random &rng, SystemBody *primary, size_t idx) +{ + StarSystemRandomGenerator gen = {}; + + // Ensure consistent density distribution parameters across multiple runs + const SystemPath &path = system->GetPath(); + Random shellRng { StarSystemRandomGenerator::BODY_SATELLITE_SALT, primary->GetSeed(), uint32_t(path.sectorX), uint32_t(path.sectorY), uint32_t(path.sectorZ), UNIVERSE_SEED }; + + fixed discMin, discBound, discMax; + fixed discDensity = gen.CalcBodySatelliteShellDensity(shellRng, primary, discMin, discMax); + + discBound = fixed(0); + + size_t numChildren = primary->GetNumChildren(); + + // Set orbit slice parameters from surrounding bodies + if (idx > 0) + discMin = primary->m_children[idx - 1]->m_orbMax * fixed(105, 100); + if (idx < numChildren) + discBound = primary->m_children[idx]->m_orbMin; + + // Ensure we have enough discMax to generate a body, even if it means "cheating" + + if (discMin * fixed(12, 10) > discMax) { + Log::Warning("Creating body outside of parent {} natural satellite radius {:.8f}, generation may not be correct.", primary->GetName(), discMax.ToDouble()); + discMax = numChildren > 0 ? primary->m_children[numChildren - 1]->m_orbMax : discMin; + discMax *= fixed(12, 10); + } + + if (discMin > discMax || (discBound != 0 && discMin > discBound)) + return nullptr; + + SystemBody *body = gen.MakeBodyInOrbitSlice(rng, static_cast(system), primary, discMin, discBound, discMax, discDensity); + if (!body) + return nullptr; + + gen.PickPlanetType(body, rng); + + return body; +} + +void StarSystem::EditorAPI::AddBody(StarSystem *system, SystemBody *body, size_t idx) +{ + if (idx == size_t(-1)) + idx = system->m_bodies.size(); + + auto iter = system->m_bodies.begin() + idx; + system->m_bodies.emplace(iter, body); +} + +void StarSystem::EditorAPI::RemoveBody(StarSystem *system, SystemBody *body) +{ + auto iter = std::find(system->m_bodies.begin(), system->m_bodies.end(), body); + if (iter != system->m_bodies.end()) + system->m_bodies.erase(iter); +} + +void StarSystem::EditorAPI::ReorderBodyIndex(StarSystem *system) +{ + size_t index = 0; + std::vector> orderStack { + { system->GetRootBody().Get(), 0 } + }; + + while (!orderStack.empty()) { + auto &pair = orderStack.back(); + SystemBody *body = pair.first; + + if (pair.second == 0) + // Set body index from hierarchy order + body->m_path.bodyIndex = index++; + + if (pair.second < body->GetNumChildren()) + orderStack.push_back({ body->GetChildren()[pair.second++], 0 }); + else + orderStack.pop_back(); + } + + std::sort(system->m_bodies.begin(), system->m_bodies.end(), [](auto a, auto b) { + return a->m_path.bodyIndex < b->m_path.bodyIndex; + }); +} + +void StarSystem::EditorAPI::ReorderBodyHierarchy(StarSystem *system) +{ + size_t index = 0; + std::vector> orderStack { + { system->GetRootBody().Get(), 0 } + }; + + while (!orderStack.empty()) { + auto &pair = orderStack.back(); + SystemBody *body = pair.first; + + if (pair.second == 0) + // Sort body order from index + std::sort(body->m_children.begin(), body->m_children.end(), + [](auto *a, auto *b) { return a->m_path.bodyIndex < b->m_path.bodyIndex; }); + + if (pair.second < body->GetNumChildren()) + orderStack.push_back({ body->GetChildren()[pair.second++], 0 }); + else + orderStack.pop_back(); + } + + std::sort(system->m_bodies.begin(), system->m_bodies.end(), [](auto a, auto b) { + return a->m_path.bodyIndex < b->m_path.bodyIndex; + }); +} + +void StarSystem::EditorAPI::SortBodyHierarchy(StarSystem *system, UndoSystem *undo) +{ + size_t index = 0; + std::vector> orderStack { + { system->GetRootBody().Get(), 0 } + }; + + undo->AddUndoStep(system, false); + + while (!orderStack.empty()) { + auto &pair = orderStack.back(); + SystemBody *body = pair.first; + + if (pair.second == 0) { + + std::stable_sort(body->m_children.begin(), body->m_children.end(), + [](auto *a, auto *b) { return a->m_semiMajorAxis < b->m_semiMajorAxis; }); + + for (SystemBody *body : body->m_children) + AddUndoSingleValue(undo, &body->m_path.bodyIndex); + + body->m_path.bodyIndex = index++; + + } + + if (pair.second < body->GetNumChildren()) { + orderStack.push_back({ body->GetChildren()[pair.second++], 0 }); + } else { + orderStack.pop_back(); + } + } + + std::sort(system->m_bodies.begin(), system->m_bodies.end(), [](auto a, auto b) { + return a->m_path.bodyIndex < b->m_path.bodyIndex; + }); + + undo->AddUndoStep(system, true); +} + +void StarSystem::EditorAPI::EditName(StarSystem *system, Random &rng, UndoSystem *undo) +{ + ImGui::BeginGroup(); + if (Draw::RandomButton()) { + system->m_name.clear(); + NameGenerator::GetSystemName(*&system->m_name, rng); + } + + ImGui::InputText("Name", &system->m_name); + ImGui::EndGroup(); + + if (Draw::UndoHelper("Edit System Name", undo)) + AddUndoSingleValue(undo, &system->m_name); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Other Names"); + + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().WindowPadding.x * 2.f, 0.f); + ImGui::Button("+", ImVec2(ImGui::GetFrameHeight(), ImGui::GetFrameHeight())); + if (Draw::UndoHelper("Add System Other Name", undo)) + AddUndoVectorInsert(undo, &system->m_other_names, ""); + + float window_height = ImGui::GetFrameHeightWithSpacing() * std::min(size_t(4), system->m_other_names.size()) + ImGui::GetStyle().WindowPadding.y * 2.f; + + if (system->m_other_names.size() > 0) { + ImGui::BeginChild("##Other Names", ImVec2(0, window_height), true); + + for (size_t idx = 0; idx < system->m_other_names.size(); idx++) { + + ImGui::PushID(idx); + + if (ImGui::Button("-")) { + undo->BeginEntry("Remove System Other Name"); + AddUndoVectorErase(undo, &system->m_other_names, idx); + undo->EndEntry(); + + ImGui::PopID(); + continue; + } + + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("##name", &system->m_other_names[idx]); + + if (Draw::UndoHelper("Edit System Other Name", undo)) + AddUndoVectorSingleValue(undo, &system->m_other_names, idx); + + ImGui::PopID(); + + } + ImGui::EndChild(); + + ImGui::Spacing(); + } + + ImGui::SeparatorText("Short Description"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("##Short Description", &system->m_shortDesc); + if (Draw::UndoHelper("Edit System Short Description", undo)) + AddUndoSingleValue(undo, &system->m_shortDesc); + + ImGui::SeparatorText("Long Description"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputTextMultiline("##Long Description", &system->m_longDesc, ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 5.f + ImGui::GetStyle().WindowPadding.y * 2.f)); + if (Draw::UndoHelper("Edit System Long Description", undo)) + AddUndoSingleValue(undo, &system->m_longDesc); +} + +void StarSystem::EditorAPI::EditProperties(StarSystem *system, CustomSystemInfo &custom, FactionsDatabase *factions, UndoSystem *undo) +{ + ImGui::SeparatorText("Generation Parameters"); + + ImGui::InputInt("Seed", reinterpret_cast(&system->m_seed)); + if (Draw::UndoHelper("Edit Seed", undo)) + AddUndoSingleValue(undo, &system->m_seed); + + bool comboOpen = Draw::ComboUndoHelper("Edit System Exploration State", "Explored State", explored_labels[custom.explored], undo); + if (comboOpen) { + if (ImGui::IsWindowAppearing()) { + AddUndoSingleValue(undo, &custom.explored); + } + + for (size_t idx = 0; idx < COUNTOF(explored_labels); idx++) { + if (ImGui::Selectable(explored_labels[idx], idx == custom.explored)) + custom.explored = CustomSystemInfo::ExplorationState(idx); + } + + ImGui::EndCombo(); + } + + if (Draw::LayoutHorizontal("Sector Path", 3, ImGui::GetFontSize())) { + ImGui::InputInt("X", &system->m_path.sectorX, 0, 0); + ImGui::InputInt("Y", &system->m_path.sectorY, 0, 0); + ImGui::InputInt("Z", &system->m_path.sectorZ, 0, 0); + + Draw::EndLayout(); + } + + if (Draw::UndoHelper("Edit System Sector", undo)) + AddUndoSingleValue(undo, &system->m_path); + + if (Draw::LayoutHorizontal("Position in Sector", 3, ImGui::GetFontSize())) { + ImGui::SliderFloat("X", &system->m_pos.x, 0.f, 8.f, "%.3f ly", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat("Y", &system->m_pos.y, 0.f, 8.f, "%.3f ly", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat("Z", &system->m_pos.z, 0.f, 8.f, "%.3f ly", ImGuiSliderFlags_AlwaysClamp); + + Draw::EndLayout(); + } + + if (Draw::UndoHelper("Edit System Position", undo)) + AddUndoSingleValue(undo, &system->m_pos); + + ImGui::SeparatorText("Legal Parameters"); + + Draw::EditEnum("Edit System Government", "Government", "PolitGovType", + reinterpret_cast(&system->m_polit.govType), Polit::GovType::GOV_MAX - 1, undo); + + ImGui::Checkbox("Random Faction", &custom.randomFaction); + if (Draw::UndoHelper("Edit Faction", undo)) + AddUndoSingleValue(undo, &custom.randomFaction); + + ImGui::BeginDisabled(custom.randomFaction); + + if (Draw::ComboUndoHelper("Edit Faction", "Faction", custom.faction.c_str(), undo)) { + if (ImGui::IsWindowAppearing()) + AddUndoSingleValue(undo, &custom.faction); + + for (size_t factionIdx = 0; factionIdx < factions->GetNumFactions(); factionIdx++) { + const Faction *fac = factions->GetFaction(factionIdx); + + if (ImGui::Selectable(fac->name.c_str(), fac->name == custom.faction)) + custom.faction = fac->name; + } + + ImGui::EndCombo(); + } + + ImGui::EndDisabled(); + + ImGui::Checkbox("Random Lawlessness", &custom.randomLawlessness); + if (Draw::UndoHelper("Edit Lawlessness", undo)) + AddUndoSingleValue(undo, &custom.randomLawlessness); + + ImGui::BeginDisabled(custom.randomLawlessness); + + Draw::InputFixedSlider("Lawlessness", &system->m_polit.lawlessness); + if (Draw::UndoHelper("Edit System Lawlessness", undo)) + AddUndoSingleValue(undo, &system->m_polit.lawlessness); + + ImGui::EndDisabled(); + + ImGui::SeparatorText("Author Comments"); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputTextMultiline("##Comment", &custom.comment, ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 5.f + ImGui::GetStyle().WindowPadding.y * 2.f)); + + if (Draw::UndoHelper("Edit Comments", undo)) + AddUndoSingleValue(undo, &custom.comment); + +} + +// ─── SystemBody::EditorAPI ─────────────────────────────────────────────────── + +void SystemBody::EditorAPI::GenerateDefaultName(SystemBody *body) +{ + SystemBody *parent = body->GetParent(); + + // We're the root body, should probably be named after the system + if (!parent) { + body->m_name = body->GetStarSystem()->GetName(); + return; + } + + // Starports get a consistent default 'identifier' name + if (body->GetSuperType() == SUPERTYPE_STARPORT) { + Random rand({ body->GetSeed(), UNIVERSE_SEED }); + + char ident_1 = rand.Int32('A', 'Z'); + char ident_2 = rand.Int32('A', 'Z'); + + body->m_name = fmt::format("{} {}{}-{:04d}", + body->GetType() == TYPE_STARPORT_ORBITAL ? "Orbital" : "Port", + ident_1, ident_2, rand.Int32(10000)); + return; + } + + // Other bodies get a "hierarchy" name + + size_t idx = 0; + for (SystemBody *child : parent->m_children) { + if (child == body) + break; + if (child->GetSuperType() != SUPERTYPE_STARPORT) + idx++; + } + + if (parent->GetSuperType() <= SystemBody::SUPERTYPE_STAR) { + if (idx <= 26) + body->m_name = fmt::format("{} {}", parent->GetName(), char('a' + idx)); + else + body->m_name = fmt::format("{} {}{}", parent->GetName(), char('a' + idx / 26), char('a' + idx % 26)); + } else { + body->m_name = fmt::format("{} {}", parent->GetName(), 1 + idx); + } +} + +void SystemBody::EditorAPI::AddChild(SystemBody *parent, SystemBody *child, size_t idx) +{ + if (idx == size_t(-1)) + idx = parent->m_children.size(); + + auto iter = parent->m_children.begin() + idx; + parent->m_children.emplace(iter, child); + + child->m_parent = parent; +} + +SystemBody *SystemBody::EditorAPI::RemoveChild(SystemBody *parent, size_t idx) +{ + if (idx == size_t(-1)) + idx = parent->m_children.size() - 1; + + SystemBody *outBody = parent->m_children[idx]; + parent->m_children.erase(parent->m_children.begin() + idx); + + outBody->m_parent = nullptr; + + return outBody; +} + +size_t SystemBody::EditorAPI::GetIndexInParent(SystemBody *body) +{ + SystemBody *parent = body->GetParent(); + if (!parent) + return size_t(-1); + + auto iter = std::find(parent->GetChildren().begin(), parent->GetChildren().end(), body); + if (iter == parent->GetChildren().end()) + return size_t(-1); + + return std::distance(parent->GetChildren().begin(), iter); +} + +void SystemBody::EditorAPI::EditOrbitalParameters(SystemBody *body, UndoSystem *undo) +{ + ImGui::SeparatorText("Orbital Parameters"); + + bool orbitChanged = false; + auto updateBodyOrbit = [=](){ body->SetOrbitFromParameters(); }; + + orbitChanged |= Draw::InputFixedDistance("Semi-Major Axis", &body->m_semiMajorAxis); + if (Draw::UndoHelper("Edit Semi-Major Axis", undo)) + AddUndoSingleValueClosure(undo, &body->m_semiMajorAxis, updateBodyOrbit); + + orbitChanged |= Draw::InputFixedSlider("Eccentricity", &body->m_eccentricity); + if (Draw::UndoHelper("Edit Eccentricity", undo)) + AddUndoSingleValueClosure(undo, &body->m_eccentricity, updateBodyOrbit); + + orbitChanged |= Draw::InputFixedDegrees("Inclination", &body->m_inclination, 0.0, 180.0); + if (Draw::UndoHelper("Edit Inclination", undo)) + AddUndoSingleValueClosure(undo, &body->m_inclination, updateBodyOrbit); + + orbitChanged |= Draw::InputFixedDegrees("Orbital Offset", &body->m_orbitalOffset); + if (Draw::UndoHelper("Edit Orbital Offset", undo)) + AddUndoSingleValueClosure(undo, &body->m_orbitalOffset, updateBodyOrbit); + Draw::HelpMarker("Longitude of Ascending Node"); + + orbitChanged |= Draw::InputFixedDegrees("Arg. of Periapsis", &body->m_argOfPeriapsis); + if (Draw::UndoHelper("Edit Argument of Periapsis", undo)) + AddUndoSingleValueClosure(undo, &body->m_argOfPeriapsis, updateBodyOrbit); + Draw::HelpMarker("Argument of Periapsis\nRelative to Longitude of Ascending Node"); + + orbitChanged |= Draw::InputFixedDegrees("Orbital Phase", &body->m_orbitalPhaseAtStart); + if (Draw::UndoHelper("Edit Orbital Phase", undo)) + AddUndoSingleValueClosure(undo, &body->m_orbitalPhaseAtStart, updateBodyOrbit); + Draw::HelpMarker("True Anomaly at Epoch\nRelative to Argument of Periapsis"); + + + if (Draw::DerivedValues("Orbital Parameters")) { + ImGui::BeginDisabled(); + + ImGui::InputFixed("Periapsis", &body->m_orbMin, 0.0, 0.0, "%0.6f AU"); + ImGui::InputFixed("Apoapsis", &body->m_orbMax, 0.0, 0.0, "%0.6f AU"); + + double orbit_period = body->GetOrbit().Period() * SECONDS_TO_DAYS; + ImGui::InputDouble("Orbital Period", &orbit_period, 0.0, 0.0, "%.2f days"); + + if (body->GetParent()) { + // calculate the time offset from periapsis at epoch + double orbit_time_at_start = (body->GetOrbit().GetOrbitalPhaseAtStart() / (2.0 * M_PI)) * body->GetOrbit().Period(); + + double orbit_vel_ap = body->GetOrbit().OrbitalVelocityAtTime( + body->GetParent()->GetMass(), + body->GetOrbit().Period() * 0.5 - orbit_time_at_start).Length() / 1000.0; + + double orbit_vel_pe = body->GetOrbit().OrbitalVelocityAtTime( + body->GetParent()->GetMass(), + -orbit_time_at_start).Length() / 1000.0; + + ImGui::InputDouble("Orbital Velocity (AP)", &orbit_vel_ap, 0.0, 0.0, "%.2f km/s"); + ImGui::InputDouble("Orbital Velocity (PE)", &orbit_vel_pe, 0.0, 0.0, "%.2f km/s"); + } + + ImGui::EndDisabled(); + } + + ImGui::SeparatorText("Rotation Parameters"); + + orbitChanged |= Draw::InputFixedDegrees("Axial Tilt", &body->m_axialTilt); + if (Draw::UndoHelper("Edit Axial Tilt", undo)) + AddUndoSingleValueClosure(undo, &body->m_axialTilt, updateBodyOrbit); + + orbitChanged |= Draw::InputFixedDegrees("Rotation at Start", &body->m_rotationalPhaseAtStart); + if (Draw::UndoHelper("Edit Rotational Phase at Start", undo)) + AddUndoSingleValueClosure(undo, &body->m_rotationalPhaseAtStart, updateBodyOrbit); + + orbitChanged |= ImGui::InputFixed("Rotation Period", &body->m_rotationPeriod, 1.0, 10.0, "%.3f days"); + if (Draw::UndoHelper("Edit Rotation Period", undo)) + AddUndoSingleValueClosure(undo, &body->m_rotationPeriod, updateBodyOrbit); + + if (orbitChanged) + body->SetOrbitFromParameters(); + +} + +void SystemBody::EditorAPI::EditEconomicProperties(SystemBody *body, UndoSystem *undo) +{ + // TODO: system generation currently ignores these fields of a system body + // and overwrites them with randomly-rolled values. + return; + + ImGui::SeparatorText("Economic Parameters"); + + ImGui::InputFixed("Population", &body->m_population); + if (Draw::UndoHelper("Edit Population", undo)) + AddUndoSingleValue(undo, &body->m_population); + + ImGui::InputFixed("Agricultural Activity", &body->m_agricultural); + if (Draw::UndoHelper("Edit Agricultural Activity", undo)) + AddUndoSingleValue(undo, &body->m_agricultural); +} + +void SystemBody::EditorAPI::EditStarportProperties(SystemBody *body, UndoSystem *undo) +{ + bool orbitChanged = false; + + if (body->GetType() == TYPE_STARPORT_SURFACE) { + ImGui::SeparatorText("Surface Parameters"); + + orbitChanged |= Draw::InputFixedDegrees("Latitude", &body->m_inclination); + if (Draw::UndoHelper("Edit Latitude", undo)) + AddUndoSingleValueClosure(undo, &body->m_inclination, [=](){ body->SetOrbitFromParameters(); }); + + orbitChanged |= Draw::InputFixedDegrees("Longitude", &body->m_orbitalOffset); + if (Draw::UndoHelper("Edit Longitude", undo)) + AddUndoSingleValueClosure(undo, &body->m_orbitalOffset, [=](){ body->SetOrbitFromParameters(); }); + + if (orbitChanged) + body->SetOrbitFromParameters(); + + } else { + EditOrbitalParameters(body, undo); + } + + EditEconomicProperties(body, undo); + + ImGui::SeparatorText("Misc. Properties"); + + ImGui::InputText("Model Name", &body->m_spaceStationType); + if (Draw::UndoHelper("Edit Station Model Name", undo)) + AddUndoSingleValue(undo, &body->m_spaceStationType); + + Draw::HelpMarker("Model name (without extension) to use for this starport.\nA random model is chosen if not specified."); +} + +void SystemBody::EditorAPI::EditBodyName(SystemBody *body, Random &rng, LuaNameGen *nameGen, UndoSystem *undo) +{ + ImGui::BeginGroup(); + ImGui::InputText("Name", &body->m_name); + + if (ImGui::Button(EICON_RESET " Default Name")) + GenerateDefaultName(body); + + ImGui::SameLine(); + + // allocate a new random generator here so it can be pushed to lua + RefCountedPtr rand { new Random({ rng.Int32() }) }; + + if (ImGui::Button(EICON_RANDOM " Random Name")) + body->m_name = nameGen->BodyName(body, rand); + + ImGui::EndGroup(); + + if (Draw::UndoHelper("Edit Name", undo)) + AddUndoSingleValue(undo, &body->m_name); +} + +void SystemBody::EditorAPI::EditProperties(SystemBody *body, Random &rng, UndoSystem *undo) +{ + bool isStar = body->GetSuperType() <= SUPERTYPE_STAR; + + bool bodyChanged = false; + auto updateBodyDerived = [=]() { + body->SetAtmFromParameters(); + }; + + Draw::EditEnum("Edit Body Type", "Body Type", "BodyType", reinterpret_cast(&body->m_type), BodyType::TYPE_MAX, undo); + + ImGui::InputInt("Seed", reinterpret_cast(&body->m_seed)); + if (Draw::UndoHelper("Edit Seed", undo)) + AddUndoSingleValue(undo, &body->m_seed); + + if (body->GetSuperType() < SUPERTYPE_STARPORT) { + + if ((!isStar || body->GetType() == TYPE_BROWN_DWARF) && ImGui::Button(EICON_RANDOM " Body Stats")) { + GenerateDerivedStats(body, rng, undo); + bodyChanged = true; + } + + ImGui::SetItemTooltip("Generate body type, radius, temperature, and surface parameters using the same method as procedural system generation."); + + ImGui::SeparatorText("Body Parameters"); + + bodyChanged |= Draw::InputFixedMass("Mass", &body->m_mass, isStar); + if (Draw::UndoHelper("Edit Mass", undo)) + AddUndoSingleValueClosure(undo, &body->m_mass, updateBodyDerived); + + bodyChanged |= Draw::InputFixedRadius("Radius", &body->m_radius, isStar); + if (Draw::UndoHelper("Edit Radius", undo)) + AddUndoSingleValueClosure(undo, &body->m_radius, updateBodyDerived); + + ImGui::BeginDisabled(); + + double surfaceGrav = body->CalcSurfaceGravity(); + ImGui::InputDouble("Surface Gravity", &surfaceGrav, 0, 0, "%.4fg"); + + ImGui::EndDisabled(); + + if (body->GetSuperType() <= SUPERTYPE_GAS_GIANT && body->GetType() != TYPE_PLANET_ASTEROID) { + + Draw::InputFixedSlider("Aspect Ratio", &body->m_aspectRatio, 0.0, 2.0); + if (Draw::UndoHelper("Edit Aspect Ratio", undo)) + AddUndoSingleValue(undo, &body->m_aspectRatio); + + Draw::HelpMarker("Ratio of body equatorial radius to polar radius, or \"bulge\" around axis of spin."); + + } + + bodyChanged |= ImGui::InputInt("Temperature (K)", &body->m_averageTemp, 1, 10, "%d°K"); + if (Draw::UndoHelper("Edit Temperature", undo)) + AddUndoSingleValueClosure(undo, &body->m_averageTemp, updateBodyDerived); + + ImGui::Spacing(); + + const bool hasDerived = (body->GetType() != TYPE_GRAVPOINT || body->HasChildren()); + + if (hasDerived && Draw::DerivedValues("Body Parameters")) { + ImGui::BeginDisabled(); + + StarSystemRandomGenerator gen = {}; + + // Ensure consistent density distribution parameters across multiple runs + const SystemPath &path = body->GetStarSystem()->GetPath(); + Random shellRng { StarSystemRandomGenerator::BODY_SATELLITE_SALT, body->GetSeed(), uint32_t(path.sectorX), uint32_t(path.sectorY), uint32_t(path.sectorZ), UNIVERSE_SEED }; + + fixed discMin, discBound, discMax; + fixed discDensity = gen.CalcBodySatelliteShellDensity(shellRng, body, discMin, discMax); + + Draw::InputFixedDistance("Satellite Shell Min", &discMin); + Draw::InputFixedDistance("Satellite Shell Max", &discMax); + ImGui::InputFixed("Shell Density Dist", &discDensity, 0, 0, "%.6f"); + + ImGui::EndDisabled(); + } + + } else { + EditStarportProperties(body, undo); + return; + } + + if (body->GetParent()) { + EditOrbitalParameters(body, undo); + } + + if (isStar) { + return; + } + + ImGui::SeparatorText("Surface Parameters"); + + Draw::InputFixedSlider("Metallicity", &body->m_metallicity); + if (Draw::UndoHelper("Edit Metallicity", undo)) + AddUndoSingleValue(undo, &body->m_metallicity); + + Draw::InputFixedSlider("Volcanicity", &body->m_volcanicity); + if (Draw::UndoHelper("Edit Volcanicity", undo)) + AddUndoSingleValue(undo, &body->m_volcanicity); + + bool gasGiant = body->GetSuperType() == SystemBody::SUPERTYPE_GAS_GIANT; + + bodyChanged |= Draw::InputFixedSlider("Atm. Density", &body->m_volatileGas, + 0.0, gasGiant ? 50.0 : 1.225, "%.3f kg/m³", 0); + if (Draw::UndoHelper("Edit Atmosphere Density", undo)) + AddUndoSingleValueClosure(undo, &body->m_volatileGas, updateBodyDerived); + + Draw::HelpMarker("Atmospheric density at the body's nominal surface.\n" + "Earth has a density of 1.225kg/m³ at normal surface pressure and temperature."); + + Draw::InputFixedSlider("Atm. Oxygen", &body->m_atmosOxidizing); + if (Draw::UndoHelper("Edit Atmosphere Oxygen", undo)) + AddUndoSingleValue(undo, &body->m_atmosOxidizing); + + Draw::HelpMarker("Proportion of oxidizing elements in atmosphere, e.g. CO², O².\n" + "Oxidizing elements are a hallmark of outdoor worlds and are needed for human life to survive."); + + Draw::InputFixedSlider("Ocean Coverage", &body->m_volatileLiquid); + if (Draw::UndoHelper("Edit Ocean Coverage", undo)) + AddUndoSingleValue(undo, &body->m_volatileLiquid); + + Draw::InputFixedSlider("Ice Coverage", &body->m_volatileIces); + if (Draw::UndoHelper("Edit Ice Coverage", undo)) + AddUndoSingleValue(undo, &body->m_volatileIces); + + // TODO: unused by other code + // Draw::InputFixedSlider("Human Activity", &body->m_humanActivity); + // if (Draw::UndoHelper("Edit Human Activity", undo)) + // AddUndoSingleValue(undo, &body->m_humanActivity); + + Draw::InputFixedSlider("Life", &body->m_life); + if (Draw::UndoHelper("Edit Life", undo)) + AddUndoSingleValue(undo, &body->m_life); + + ImGui::InputText("HMap Path", &body->m_heightMapFilename); + if (Draw::UndoHelper("Edit Heightmap Path", undo)) + AddUndoSingleValue(undo, &body->m_heightMapFilename); + + Draw::HelpMarker("Path to a custom heightmap file for this body, relative to the game's data directory."); + + ImGui::SliderInt("HMap Fractal", reinterpret_cast(&body->m_heightMapFractal), 0, 1, "%d", ImGuiSliderFlags_AlwaysClamp); + if (Draw::UndoHelper("Edit Heightmap Fractal", undo)) + AddUndoSingleValue(undo, &body->m_heightMapFractal); + + Draw::HelpMarker("Fractal type index for use with a custom heightmap file."); + + if (Draw::DerivedValues("Surface Parameters")) { + ImGui::BeginDisabled(); + + if (bodyChanged) + body->SetAtmFromParameters(); + + double pressure_p0 = body->GetAtmSurfacePressure(); + ImGui::InputDouble("Surface Pressure", &pressure_p0, 0.0, 0.0, "%.4f atm"); + + double atmRadius = body->GetAtmRadius() / 1000.0; + ImGui::InputDouble("Atmosphere Height", &atmRadius, 0.0, 0.0, "%.2f km"); + + bool scoopable = body->IsScoopable(); + ImGui::Checkbox("Is Scoopable", &scoopable); + + ImGui::EndDisabled(); + } + + EditEconomicProperties(body, undo); +} + +void SystemBody::EditorAPI::GenerateDerivedStats(SystemBody *body, Random &rng, UndoSystem *undo) +{ + undo->BeginEntry("Generate Body Parameters"); + + // Back up all potentially-modified body variables + AddUndoSingleValue(undo, &body->m_mass); + AddUndoSingleValue(undo, &body->m_type); + AddUndoSingleValue(undo, &body->m_radius); + AddUndoSingleValue(undo, &body->m_averageTemp); + + AddUndoSingleValue(undo, &body->m_axialTilt); + AddUndoSingleValue(undo, &body->m_rotationPeriod); + + AddUndoSingleValue(undo, &body->m_metallicity); + AddUndoSingleValue(undo, &body->m_volcanicity); + AddUndoSingleValue(undo, &body->m_volatileGas); + AddUndoSingleValue(undo, &body->m_atmosOxidizing); + AddUndoSingleValue(undo, &body->m_volatileLiquid); + AddUndoSingleValue(undo, &body->m_volatileIces); + // AddUndoSingleValue(undo, &body->m_humanActivity); + AddUndoSingleValue(undo, &body->m_life); + + StarSystemRandomGenerator().PickPlanetType(body, rng); + + undo->EndEntry(); +} diff --git a/src/editor/system/GalaxyEditAPI.h b/src/editor/system/GalaxyEditAPI.h new file mode 100644 index 00000000000..9e28f98537b --- /dev/null +++ b/src/editor/system/GalaxyEditAPI.h @@ -0,0 +1,69 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "galaxy/StarSystem.h" +#include "galaxy/SystemBody.h" + +class LuaNameGen; +class FactionsDatabase; + +namespace Editor { + class UndoSystem; + + struct CustomSystemInfo { + enum ExplorationState { + EXPLORE_Random = 0, + EXPLORE_ExploredAtStart, + EXPLORE_Unexplored, + }; + + ExplorationState explored = EXPLORE_Random; + bool randomLawlessness = true; + bool randomFaction = true; + + std::string faction; + std::string comment; + }; +} + +class StarSystem::EditorAPI { +public: + static void RemoveFromCache(StarSystem *system); + + static SystemBody *NewBody(StarSystem *system); + static SystemBody *NewBodyAround(StarSystem *system, Random &rng, SystemBody *primary, size_t idx); + + static void AddBody(StarSystem *system, SystemBody *body, size_t idx = -1); + static void RemoveBody(StarSystem *system, SystemBody *body); + + // Set body index from hierarchy order + static void ReorderBodyIndex(StarSystem *system); + // Set body hierarchy from index order + static void ReorderBodyHierarchy(StarSystem *system); + + // Sort the bodies in the system based on semi-major axis + static void SortBodyHierarchy(StarSystem *system, Editor::UndoSystem *undo); + + static void EditName(StarSystem *system, Random &rng, Editor::UndoSystem *undo); + static void EditProperties(StarSystem *system, Editor::CustomSystemInfo &custom, FactionsDatabase *factions, Editor::UndoSystem *undo); +}; + +class SystemBody::EditorAPI { +public: + static void GenerateDefaultName(SystemBody *body); + static void GenerateCustomName(SystemBody *body, Random &rng); + + static void AddChild(SystemBody *parent, SystemBody *child, size_t idx = -1); + static SystemBody *RemoveChild(SystemBody *parent, size_t idx = -1); + static size_t GetIndexInParent(SystemBody *body); + + static void EditOrbitalParameters(SystemBody *body, Editor::UndoSystem *undo); + static void EditEconomicProperties(SystemBody *body, Editor::UndoSystem *undo); + static void EditStarportProperties(SystemBody *body, Editor::UndoSystem *undo); + static void EditBodyName(SystemBody *body, Random &rng, LuaNameGen *nameGen, Editor::UndoSystem *undo); + static void EditProperties(SystemBody *body, Random &rng, Editor::UndoSystem *undo); + + static void GenerateDerivedStats(SystemBody *body, Random &rng, Editor::UndoSystem *undo); +}; diff --git a/src/editor/system/SystemBodyUndo.h b/src/editor/system/SystemBodyUndo.h new file mode 100644 index 00000000000..fefb965f858 --- /dev/null +++ b/src/editor/system/SystemBodyUndo.h @@ -0,0 +1,130 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "GalaxyEditAPI.h" + +#include "editor/UndoSystem.h" +#include "galaxy/StarSystem.h" +#include "galaxy/SystemBody.h" + +namespace Editor::SystemEditorUndo { + + class ManageStarSystemBody : public UndoStep { + public: + ManageStarSystemBody(StarSystem *system, SystemBody *add, SystemBody *rem = nullptr, bool apply = false) : + m_system(system), + m_addBody(add), + m_remBody(rem) + { + if (apply) + Swap(); + } + + void Swap() override { + if (m_addBody) + StarSystem::EditorAPI::AddBody(m_system, m_addBody.Get()); + if (m_remBody) + StarSystem::EditorAPI::RemoveBody(m_system, m_remBody.Get()); + std::swap(m_addBody, m_remBody); + } + + private: + StarSystem *m_system; + RefCountedPtr m_addBody; + RefCountedPtr m_remBody; + }; + + // Helper to reorder body indexes at the end of an undo / redo operation by adding two undo steps + class ReorderStarSystemBodies : public UndoStep { + public: + ReorderStarSystemBodies(StarSystem *system, bool onRedo = false) : + m_system(system), + m_onRedo(onRedo) + { + Redo(); + } + + void Undo() override { + if (!m_onRedo) + StarSystem::EditorAPI::ReorderBodyIndex(m_system); + } + + void Redo() override { + if (m_onRedo) + StarSystem::EditorAPI::ReorderBodyIndex(m_system); + } + + private: + StarSystem *m_system; + bool m_onRedo; + }; + + // Helper to sort body hierarchy from index at the end of an undo / redo operation by adding two undo steps + class SortStarSystemBodies : public UndoStep { + public: + SortStarSystemBodies(StarSystem *system, bool isRedo) : + m_system(system), + m_isRedo(isRedo) + { + } + + void Undo() override { + if (!m_isRedo) + StarSystem::EditorAPI::ReorderBodyHierarchy(m_system); + } + + void Redo() override { + if (m_isRedo) + StarSystem::EditorAPI::ReorderBodyHierarchy(m_system); + } + + private: + StarSystem *m_system; + bool m_isRedo; + }; + + // UndoStep helper to handle adding or deleting a child SystemBody from a parent + class AddRemoveChildBody : public UndoStep { + public: + AddRemoveChildBody(SystemBody *parent, SystemBody *add, size_t idx) : + m_parent(parent), + m_add(add), + m_idx(idx) + { + Swap(); + } + + AddRemoveChildBody(SystemBody *parent, SystemBody *add) : + m_parent(parent), + m_add(add), + m_idx(-1) + { + Swap(); + } + + AddRemoveChildBody(SystemBody *parent, size_t idx) : + m_parent(parent), + m_add(nullptr), + m_idx(idx) + { + Swap(); + } + + void Swap() override { + if (m_add) { + SystemBody::EditorAPI::AddChild(m_parent, m_add.Get(), m_idx); + m_add.Reset(); + } else { + m_add.Reset(SystemBody::EditorAPI::RemoveChild(m_parent, m_idx)); + } + } + + private: + SystemBody *m_parent; + RefCountedPtr m_add; + size_t m_idx; + }; + +} // namespace Editor diff --git a/src/editor/system/SystemEditor.cpp b/src/editor/system/SystemEditor.cpp new file mode 100644 index 00000000000..04943ef6669 --- /dev/null +++ b/src/editor/system/SystemEditor.cpp @@ -0,0 +1,1043 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "SystemEditor.h" + +#include "GalaxyEditAPI.h" +#include "SystemBodyUndo.h" +#include "SystemEditorHelpers.h" +#include "SystemEditorViewport.h" +#include "SystemEditorModals.h" + +#include "EnumStrings.h" +#include "FileSystem.h" +#include "JsonUtils.h" +#include "ModManager.h" +#include "Pi.h" // just here for Pi::luaNameGen +#include "SystemView.h" +#include "core/StringUtils.h" + +#include "editor/ActionBinder.h" +#include "editor/EditorApp.h" +#include "editor/EditorDraw.h" +#include "editor/EditorIcons.h" +#include "editor/UndoSystem.h" +#include "editor/UndoStepType.h" + +#include "galaxy/Galaxy.h" +#include "galaxy/GalaxyGenerator.h" +#include "galaxy/StarSystemGenerator.h" +#include "graphics/Renderer.h" +#include "lua/Lua.h" +#include "lua/LuaNameGen.h" +#include "lua/LuaObject.h" +#include "pigui/PiGui.h" + +#include "imgui/imgui.h" + +#include "portable-file-dialogs/pfd.h" +// PFD pulls in windows headers sadly +#undef RegisterClass +#undef min +#undef max + +#include +#include +#include + +using namespace Editor; + +namespace { + static constexpr const char *OUTLINE_WND_ID = "Outline"; + static constexpr const char *PROPERTIES_WND_ID = "Properties"; + static constexpr const char *VIEWPORT_WND_ID = "Viewport"; +} + +const char *Editor::GetBodyIcon(const SystemBody *body) { + if (body->GetType() == SystemBody::TYPE_GRAVPOINT) + return EICON_GRAVPOINT; + if (body->GetType() == SystemBody::TYPE_STARPORT_ORBITAL) + return EICON_SPACE_STATION; + if (body->GetType() == SystemBody::TYPE_STARPORT_SURFACE) + return EICON_SURFACE_STATION; + if (body->GetType() == SystemBody::TYPE_PLANET_ASTEROID) + return EICON_ASTEROID; + if (body->GetSuperType() == SystemBody::SUPERTYPE_ROCKY_PLANET) + return (!body->GetParent() || body->GetParent()->GetSuperType() < SystemBody::SUPERTYPE_ROCKY_PLANET) ? + EICON_ROCKY_PLANET : EICON_MOON; + if (body->GetSuperType() == SystemBody::SUPERTYPE_GAS_GIANT) + return EICON_GAS_GIANT; + if (body->GetSuperType() == SystemBody::SUPERTYPE_STAR) + return EICON_SUN; + + return "?"; +} + +class SystemEditor::UndoSetSelection : public UndoStep { +public: + UndoSetSelection(SystemEditor *editor, SystemBody *newSelection) : + m_editor(editor), + m_selection(newSelection) + { + Swap(); + } + + void Swap() override { + std::swap(m_editor->m_selectedBody, m_selection); + } + +private: + SystemEditor *m_editor; + SystemBody *m_selection; +}; + +SystemEditor::SystemEditor(EditorApp *app) : + m_app(app), + m_undo(new UndoSystem()), + m_system(nullptr), + m_systemInfo(), + m_selectedBody(nullptr), + m_contextBody(nullptr), + m_pendingOp(), + m_pendingFileReq(FileRequest_None), + m_newSystemPath(), + m_menuBinder(new ActionBinder()) +{ + GalacticEconomy::Init(); + + LuaObject::RegisterClass(); + LuaObject::RegisterClass(); + + m_nameGen.reset(new LuaNameGen(Lua::manager)); + + Pi::luaNameGen = m_nameGen.get(); + + m_galaxy = GalaxyGenerator::Create(); + m_systemLoader.reset(new CustomSystemsDatabase(m_galaxy.Get(), "systems")); + + m_viewport.reset(new SystemEditorViewport(m_app, this)); + + m_random.seed({ + // generate random values not dependent on app runtime + uint32_t(std::chrono::system_clock::now().time_since_epoch().count()), + UNIVERSE_SEED + }); + + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + RegisterMenuActions(); +} + +SystemEditor::~SystemEditor() +{ +} + +void SystemEditor::NewSystem(SystemPath path) +{ + ClearSystem(); + + auto *newSystem = new StarSystem::GeneratorAPI(path, m_galaxy, nullptr, GetRng()); + m_system.Reset(newSystem); + + newSystem->SetRootBody(newSystem->NewBody()); + + SysPolit polit = {}; + polit.govType = Polit::GOV_NONE; + + newSystem->SetSysPolit(polit); + + // mark current file as unsaved + m_lastSavedUndoStack = size_t(-1); +} + +bool SystemEditor::LoadSystemFromDisk(const std::string &absolutePath) +{ + if (ends_with_ci(absolutePath, ".lua")) { + std::string filepath = FileSystem::GetRelativePath(FileSystem::GetDataDir(), absolutePath); + + if (filepath.empty()) { + Log::Error("Cannot read .lua Custom System files from outside the game's data directory!"); + return false; + } + + return LoadSystemFromFile(FileSystem::gameDataFiles.Lookup(filepath)); + } else { + std::string dirpath = absolutePath.substr(0, absolutePath.find_last_of("/\\")); + std::string filename = absolutePath.substr(dirpath.size() + 1); + + // Hack: construct a temporary FileSource to load from an arbitrary path + auto fs = FileSystem::FileSourceFS(dirpath); + + return LoadSystemFromFile(fs.Lookup(filename)); + } +} + +bool SystemEditor::LoadSystem(SystemPath path) +{ + RefCountedPtr sec = m_galaxy->GetSector(path.SectorOnly()); + + if (path.systemIndex >= sec->m_systems.size()) { + Log::Error("System {} in sector ({},{},{}) does not exist", path.systemIndex, path.sectorX, path.sectorY, path.sectorZ); + return false; + } + + const Sector::System &system = sec->m_systems.at(path.systemIndex); + + // Load a fully-defined custom system from the custom system def + // NOTE: we cannot (currently) determine which file this custom system originated from + if (system.GetCustomSystem() && !system.GetCustomSystem()->IsRandom()) + LoadCustomSystem(system.GetCustomSystem()); + else + LoadSystemFromGalaxy(m_galaxy->GetStarSystem(path)); + + m_filepath = ""; + m_filedir = ""; + + // mark as unsaved + m_lastSavedUndoStack = size_t(-1); + + return true; +} + +bool SystemEditor::LoadSystemFromFile(const FileSystem::FileInfo &file) +{ + if (!file.Exists()) { + Log::Error("Cannot open file path {}", file.GetAbsolutePath()); + return false; + } + + ClearSystem(); + + m_filepath = file.GetAbsolutePath(); + m_filedir = file.GetAbsoluteDir(); + + bool ok = false; + if (ends_with_ci(file.GetPath(), ".json")) { + const Json &data = JsonUtils::LoadJson(file.Read()); + + const CustomSystem *csys = m_systemLoader->LoadSystemFromJSON(file.GetName(), data); + if (csys) + ok = LoadCustomSystem(csys); + + if (ok) + m_systemInfo.comment = data["comment"]; + } else if (ends_with_ci(file.GetPath(), ".lua")) { + const CustomSystem *csys = m_systemLoader->LoadSystem(file.GetPath()); + if (csys) + ok = LoadCustomSystem(csys); + } + + if (ok) { + std::string windowTitle = fmt::format("System Editor - {}", m_filepath); + SDL_SetWindowTitle(m_app->GetRenderer()->GetSDLWindow(), windowTitle.c_str()); + } else { + m_filepath.clear(); + } + + + return ok; +} + +bool SystemEditor::WriteSystem(const std::string &filepath) +{ + Log::Info("Writing to path: {}/{}", FileSystem::GetDataDir(), filepath); + // FIXME: need better file-saving interface for the user + // FileSystem::FileSourceFS(FileSystem::GetDataDir()).OpenWriteStream(filepath, FileSystem::FileSourceFS::WRITE_TEXT); + + FILE *f = fopen(filepath.c_str(), "w"); + + if (!f) { + OnSaveComplete(false); + return false; + } + + Json systemdef = Json::object(); + + m_system->DumpToJson(systemdef); + + if (m_systemInfo.randomFaction) + systemdef.erase("faction"); + else + systemdef["faction"] = m_systemInfo.faction; + + if (m_systemInfo.randomLawlessness) + systemdef.erase("lawlessness"); + + if (m_systemInfo.explored == CustomSystemInfo::EXPLORE_Random) + systemdef.erase("explored"); + else + systemdef["explored"] = m_systemInfo.explored == CustomSystemInfo::EXPLORE_ExploredAtStart; + + systemdef["comment"] = m_systemInfo.comment; + + std::string jsonData = systemdef.dump(1, '\t'); + + fwrite(jsonData.data(), 1, jsonData.size(), f); + fclose(f); + + OnSaveComplete(true); + return true; +} + +bool SystemEditor::LoadCustomSystem(const CustomSystem *csys) +{ + SystemPath path = {csys->sectorX, csys->sectorY, csys->sectorZ, csys->systemIndex}; + Uint32 _init[5] = { Uint32(csys->seed), Uint32(csys->sectorX), Uint32(csys->sectorY), Uint32(csys->sectorZ), UNIVERSE_SEED }; + Random rng(_init, 5); + + RefCountedPtr system(new StarSystem::GeneratorAPI(path, m_galaxy, nullptr, rng)); + auto customStage = std::make_unique(); + + if (!customStage->ApplyToSystem(rng, system, csys)) { + Log::Error("System is fully random, cannot load from file"); + return false; + } + + // NOTE: we don't run the PopulateSystem generator here, due to its + // reliance on filled-out Sector information + // As a result, population information will not be correct + + if (!system->GetRootBody()) { + Log::Error("Custom system doesn't have a root body"); + return false; + } + + m_system = system; + m_viewport->SetSystem(system); + + CustomSystemInfo::ExplorationState explored = csys->explored ? + CustomSystemInfo::EXPLORE_ExploredAtStart : + CustomSystemInfo::EXPLORE_Unexplored; + + m_systemInfo.explored = csys->want_rand_explored ? CustomSystemInfo::EXPLORE_Random : explored; + m_systemInfo.randomLawlessness = csys->want_rand_lawlessness; + m_systemInfo.randomFaction = csys->faction == nullptr; + m_systemInfo.faction = csys->faction ? csys->faction->name : ""; + + return true; +} + +void SystemEditor::LoadSystemFromGalaxy(RefCountedPtr system) +{ + if (!system->GetRootBody()) { + Log::Error("Randomly-generated system doesn't have a root body"); + return; + } + + ClearSystem(); + + StarSystem::EditorAPI::RemoveFromCache(system.Get()); + + m_system = system; + m_viewport->SetSystem(system); + + bool explored = system->GetExplored() == StarSystem::eEXPLORED_AT_START; + + m_systemInfo.explored = explored ? CustomSystemInfo::EXPLORE_ExploredAtStart : CustomSystemInfo::EXPLORE_Unexplored; + m_systemInfo.randomLawlessness = false; + m_systemInfo.randomFaction = system->GetFaction(); + m_systemInfo.faction = system->GetFaction() ? system->GetFaction()->name : ""; +} + +void SystemEditor::ClearSystem() +{ + GetUndo()->Clear(); + m_lastSavedUndoStack = GetUndo()->GetStateHash(); + + m_system.Reset(); + m_systemInfo = {}; + m_viewport->SetSystem(m_system); + SetSelectedBody(nullptr); + m_pendingOp = {}; + + m_filepath.clear(); + m_newSystemPath.systemIndex = 0; + + SDL_SetWindowTitle(m_app->GetRenderer()->GetSDLWindow(), "System Editor"); +} + +// Here to avoid needing to drag in the Galaxy header in SystemEditor.h +RefCountedPtr SystemEditor::GetGalaxy() { return m_galaxy; } + +void SystemEditor::SetSelectedBody(SystemBody *body) +{ + // note: using const_cast here to work with Projectables which store a const pointer + m_selectedBody = body; + m_contextBody = body; +} + +void SystemEditor::Start() +{ +} + +void SystemEditor::End() +{ +} + +void SystemEditor::RegisterMenuActions() +{ + m_menuBinder->BeginMenu("File"); + + m_menuBinder->AddAction("New", { + "New System", ImGuiKey_N | ImGuiKey_ModCtrl, + [&]() { + if (HasUnsavedChanges()) { + m_unsavedFileModal = m_app->PushModal(); + m_pendingFileReq = FileRequest_New; + return; + } + + ActivateNewSystemDialog(); + } + }); + + m_menuBinder->AddAction("Open", { + "Open File", ImGuiKey_O | ImGuiKey_ModCtrl, + [&]() { + if (HasUnsavedChanges()) { + m_unsavedFileModal = m_app->PushModal(); + m_pendingFileReq = FileRequest_Open; + return; + } + + ActivateOpenDialog(); + } + }); + + m_menuBinder->AddAction("Save", { + "Save", ImGuiKey_S | ImGuiKey_ModCtrl, + [&]() { return m_system.Valid(); }, + sigc::mem_fun(this, &SystemEditor::SaveCurrentFile) + }); + + m_menuBinder->AddAction("SaveAs", { + "Save As", ImGuiKey_S | ImGuiKey_ModCtrl | ImGuiKey_ModShift, + [&]() { return m_system.Valid(); }, + sigc::mem_fun(this, &SystemEditor::ActivateSaveDialog) + }); + + m_menuBinder->AddAction("Quit", { + "Quit", ImGuiKey_Q | ImGuiKey_ModCtrl, + [this]() { + if (HasUnsavedChanges()) { + m_unsavedFileModal = m_app->PushModal(); + m_pendingFileReq = FileRequest_Quit; + } + } + }); + + m_menuBinder->EndMenu(); + + m_menuBinder->BeginMenu("Edit"); + + auto hasSelectedBody = [&]() { return m_contextBody != nullptr; }; + auto hasParentBody = [&]() { return m_contextBody && m_contextBody->GetParent(); }; + + m_menuBinder->BeginGroup("Body"); + + m_menuBinder->AddAction("Center", { + "Center on Body", {}, hasSelectedBody, + [&]() { + Projectable p = { Projectable::OBJECT, Projectable::SYSTEMBODY, m_contextBody }; + m_viewport->GetMap()->SetViewedObject(p); + } + }); + + m_menuBinder->AddAction("AddChild", { + "Add Child", ImGuiKey_A | ImGuiKey_ModCtrl, hasSelectedBody, + [&]() { + m_pendingOp.type = BodyRequest::TYPE_Add; + m_pendingOp.parent = m_contextBody ? m_contextBody : m_system->GetRootBody().Get(); + m_pendingOp.newBodyType = SystemBody::BodyType::TYPE_GRAVPOINT; + } + }); + + m_menuBinder->AddAction("AddSibling", { + "Add Sibling", ImGuiKey_A | ImGuiKey_ModCtrl | ImGuiKey_ModShift, hasParentBody, + [&]() { + m_pendingOp.type = BodyRequest::TYPE_Add; + m_pendingOp.parent = m_contextBody->GetParent(); + m_pendingOp.idx = SystemBody::EditorAPI::GetIndexInParent(m_contextBody) + 1; + m_pendingOp.newBodyType = SystemBody::BodyType::TYPE_GRAVPOINT; + } + }); + + m_menuBinder->AddAction("Delete", { + "Delete Body", ImGuiKey_W | ImGuiKey_ModCtrl, hasParentBody, + [&]() { + m_pendingOp.type = BodyRequest::TYPE_Delete; + m_pendingOp.body = m_contextBody; + } + }); + + m_menuBinder->EndGroup(); + + m_menuBinder->AddAction("Sort", { + "Sort Bodies", ImGuiKey_None, + [&]() { return m_system.Valid(); }, + [&]() { m_pendingOp.type = BodyRequest::TYPE_Resort; } + }); + + m_menuBinder->EndGroup(); +} + +bool SystemEditor::HasUnsavedChanges() +{ + return (m_system && GetUndo()->GetStateHash() != m_lastSavedUndoStack); +} + +void SystemEditor::SaveCurrentFile() +{ + // Cannot write back .lua files + if (m_filepath.empty() || ends_with_ci(m_filepath, ".lua")) { + ActivateSaveDialog(); + } else { + WriteSystem(m_filepath); + } +} + +void SystemEditor::OnSaveComplete(bool success) +{ + if (!success) { + // Cancel any pending actions if we failed to save or cancelled the process + m_pendingFileReq = FileRequest_None; + return; + } + + m_lastSavedUndoStack = GetUndo()->GetStateHash(); + + if (m_pendingFileReq != FileRequest_None) { + HandlePendingFileRequest(); + } +} + +void SystemEditor::ActivateOpenDialog() +{ + // FIXME: need to handle loading files outside of game data dir + m_openFile.reset(new pfd::open_file( + "Open Custom System File", + FileSystem::JoinPath(FileSystem::GetDataDir(), m_filedir), + { + "All System Definition Files", "*.lua *.json", + "Lua System Definition (.lua)", "*.lua", + "JSON System Definition (.json)", "*.json" + }) + ); + + m_fileActionModal = m_app->PushModal(); +} + +void SystemEditor::ActivateSaveDialog() +{ + m_saveFile.reset(new pfd::save_file( + "Save Custom System File", + FileSystem::JoinPath(FileSystem::GetDataDir(), m_filedir), + { + "JSON System Definition (.json)", "*.json" + }) + ); + + m_fileActionModal = m_app->PushModal(); +} + +void SystemEditor::ActivateNewSystemDialog() +{ + m_newSystemModal = m_app->PushModal(this, &m_newSystemPath); +} + +// ─── Update Loop ───────────────────────────────────────────────────────────── + +void SystemEditor::HandleInput() +{ + +} + +void SystemEditor::Update(float deltaTime) +{ + ImGuiID editorID = ImGui::GetID("System Editor"); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, editorID, ImGuiInputFlags_RouteGlobal)) { + GetUndo()->Redo(); + } else if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, editorID, ImGuiInputFlags_RouteGlobal)) { + GetUndo()->Undo(); + } + + DrawInterface(); + + HandleBodyOperations(); + + if (m_openFile && m_openFile->ready(0)) { + std::vector resultFiles = m_openFile->result(); + m_openFile.reset(); + + if (!resultFiles.empty()) { + Log::Info("OpenFile: {}", resultFiles[0]); + LoadSystemFromDisk(resultFiles[0]); + } + } + + if (m_saveFile && m_saveFile->ready(0)) { + std::string filePath = m_saveFile->result(); + m_saveFile.reset(); + + if (!filePath.empty()) { + Log::Info("SaveFile: {}", filePath); + + // Update current file path and directory from new "save-as" path + if (WriteSystem(filePath)) { + m_filepath = filePath; + m_filedir = filePath.substr(0, filePath.find_last_of("/\\")); + } + } else { + // Signal cancellation/failure to save + OnSaveComplete(false); + } + } + + // User responded to the unsaved changes modal + if (m_unsavedFileModal && m_unsavedFileModal->Ready()) { + auto result = m_unsavedFileModal->Result(); + + if (result == UnsavedFileModal::Result_Cancel) { + m_pendingFileReq = FileRequest_None; + } else if (result == UnsavedFileModal::Result_No) { + // User doesn't want to save, lose unsaved state + HandlePendingFileRequest(); + } else { + // Trigger the save-file flow + m_menuBinder->TriggerAction("File.Save"); + } + + m_unsavedFileModal.Reset(); + } + + // Finished with all OS file dialogs, can close the file action modal + if (m_fileActionModal && !m_openFile && !m_saveFile) { + m_fileActionModal->Close(); + m_fileActionModal.Reset(); + } +} + +void SystemEditor::HandlePendingFileRequest() +{ + if (m_pendingFileReq == FileRequest_New) { + ActivateNewSystemDialog(); + } + + if (m_pendingFileReq == FileRequest_Open) { + ActivateOpenDialog(); + } + + if (m_pendingFileReq == FileRequest_Quit) { + ClearSystem(); + RequestEndLifecycle(); + } + + m_pendingFileReq = FileRequest_None; +} + +void SystemEditor::HandleBodyOperations() +{ + if (m_pendingOp.type == BodyRequest::TYPE_None) + return; + + if (m_pendingOp.type == BodyRequest::TYPE_Add) { + + // TODO: generate body parameters according to m_pendingOp.newBodyType + SystemBody *body; + + if (!m_pendingOp.parent) + body = StarSystem::EditorAPI::NewBody(m_system.Get()); + else + body = StarSystem::EditorAPI::NewBodyAround(m_system.Get(), GetRng(), m_pendingOp.parent, m_pendingOp.idx); + + if (!body) { + Log::Error("Body parameters could not be automatically generated for the new body."); + + body = StarSystem::EditorAPI::NewBody(m_system.Get()); + } + + GetUndo()->BeginEntry("Add Body"); + GetUndo()->AddUndoStep(m_system.Get()); + + // Mark the body for removal on undo + GetUndo()->AddUndoStep(m_system.Get(), body); + // Add the new body to its parent + GetUndo()->AddUndoStep(m_pendingOp.parent, body, m_pendingOp.idx); + + GetUndo()->AddUndoStep(m_system.Get(), true); + GetUndo()->AddUndoStep(this, body); + GetUndo()->EndEntry(); + + // Give the body a basic name based on its position in its parent + SystemBody::EditorAPI::GenerateDefaultName(body); + } + + if (m_pendingOp.type == BodyRequest::TYPE_Delete) { + + std::vector toDelete { m_pendingOp.body }; + size_t sliceBegin = 0; + + // Iterate over all child bodies of this system body and mark for deletion + while (sliceBegin < toDelete.size()) { + size_t sliceEnd = toDelete.size(); + for (size_t idx = sliceBegin; idx < sliceEnd; idx++) { + SystemBody *body = toDelete[idx]; + if (body->HasChildren()) + for (auto &child : body->GetChildren()) + toDelete.push_back(child); + } + sliceBegin = sliceEnd; + } + + GetUndo()->BeginEntry("Delete Body"); + GetUndo()->AddUndoStep(m_system.Get()); + + // Record deletion of each marked body in reverse order (ending with the topmost body to delete) + for (auto &child : reverse_container(toDelete)) { + SystemBody *parent = child->GetParent(); + size_t idx = SystemBody::EditorAPI::GetIndexInParent(child); + GetUndo()->AddUndoStep(parent, idx); + GetUndo()->AddUndoStep(m_system.Get(), nullptr, child, true); + } + + GetUndo()->AddUndoStep(m_system.Get(), true); + GetUndo()->AddUndoStep(this, nullptr); + GetUndo()->EndEntry(); + + } + + if (m_pendingOp.type == BodyRequest::TYPE_Reparent) { + + size_t sourceIdx = SystemBody::EditorAPI::GetIndexInParent(m_pendingOp.body); + + if (m_pendingOp.parent == m_pendingOp.body->GetParent() && m_pendingOp.idx > sourceIdx) { + m_pendingOp.idx -= 1; + } + + GetUndo()->BeginEntry("Reorder Body"); + GetUndo()->AddUndoStep(m_system.Get()); + GetUndo()->AddUndoStep(m_pendingOp.body->GetParent(), sourceIdx); + GetUndo()->AddUndoStep(m_pendingOp.parent, m_pendingOp.body, m_pendingOp.idx); + GetUndo()->AddUndoStep(m_system.Get(), true); + GetUndo()->EndEntry(); + + } + + if (m_pendingOp.type == BodyRequest::TYPE_Resort) { + + GetUndo()->BeginEntry("Sort by Semi-Major Axis"); + StarSystem::EditorAPI::SortBodyHierarchy(m_system.Get(), GetUndo()); + GetUndo()->EndEntry(); + + } + + // Clear the pending operation + m_pendingOp = {}; +} + +// ─── Interface Rendering ───────────────────────────────────────────────────── + +void SystemEditor::SetupLayout(ImGuiID dockspaceID) +{ + ImGuiID nodeID = ImGui::DockBuilderAddNode(dockspaceID); + + ImGui::DockBuilderSetNodePos(nodeID, ImGui::GetWindowPos()); + ImGui::DockBuilderSetNodeSize(nodeID, ImGui::GetWindowSize()); + + ImGuiID leftSide = ImGui::DockBuilderSplitNode(nodeID, ImGuiDir_Left, 0.2, nullptr, &nodeID); + ImGuiID rightSide = ImGui::DockBuilderSplitNode(nodeID, ImGuiDir_Right, 0.2 / (1.0 - 0.2), nullptr, &nodeID); + // ImGuiID bottom = ImGui::DockBuilderSplitNode(nodeID, ImGuiDir_Down, 0.2, nullptr, &nodeID); + + ImGui::DockBuilderDockWindow(OUTLINE_WND_ID, leftSide); + ImGui::DockBuilderDockWindow(PROPERTIES_WND_ID, rightSide); + ImGui::DockBuilderDockWindow(VIEWPORT_WND_ID, nodeID); + + ImGui::DockBuilderFinish(dockspaceID); + + m_binderWindowOpen = false; + m_debugWindowOpen = false; + m_metricsWindowOpen = false; + m_undoStackWindowOpen = false; + m_resetDockingLayout = false; +} + +void SystemEditor::DrawInterface() +{ + static bool isFirstRun = true; + + Draw::BeginHostWindow("HostWindow", nullptr, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar); + + if (ImGui::BeginMenuBar()) + DrawMenuBar(); + + ImGuiID dockspaceID = ImGui::GetID("DockSpace"); + + if (!ImGui::DockBuilderGetNode(dockspaceID) || m_resetDockingLayout) + SetupLayout(dockspaceID); + + ImGui::DockSpace(dockspaceID); + + // Needs to be inside host-context window + m_menuBinder->Update(); + + ImGui::End(); + + // BUG: Right-click on button can break undo handling if it happens after active InputText is submitted + // We work around it by rendering the viewport first + m_viewport->Update(m_app->DeltaTime()); + + if (ImGui::Begin(OUTLINE_WND_ID)) { + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 14)); + + DrawOutliner(); + + ImGui::PopFont(); + } + ImGui::End(); + + if (ImGui::Begin(PROPERTIES_WND_ID)) { + // Adjust default item label position + ImGui::PushItemWidth(ImFloor(ImGui::GetWindowSize().x * 0.6f)); + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 13)); + + if (m_selectedBody) + DrawBodyProperties(); + else + DrawSystemProperties(); + + ImGui::PopFont(); + ImGui::PopItemWidth(); + } + ImGui::End(); + +#if 0 + if (ImGui::Begin("ModList")) { + for (const auto &mod : ModManager::EnumerateMods()) { + if (!mod.enabled) + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 64, 64, 255)); + + ImGui::PushFont(m_app->GetPiGui()->GetFont("orbiteer", 14)); + ImGui::TextUnformatted(mod.name.c_str()); + ImGui::PopFont(); + + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 12)); + ImGui::TextUnformatted(mod.path.c_str()); + ImGui::PopFont(); + + if (!mod.enabled) + ImGui::PopStyleColor(); + + ImGui::Spacing(); + } + } + ImGui::End(); +#endif + + if (m_binderWindowOpen) + m_menuBinder->DrawOverview("Shortcut List", &m_binderWindowOpen); + + if (m_undoStackWindowOpen) + Draw::ShowUndoDebugWindow(GetUndo(), &m_undoStackWindowOpen); + + if (m_metricsWindowOpen) + ImGui::ShowMetricsWindow(&m_metricsWindowOpen); + + if (m_debugWindowOpen) + ImGui::ShowDebugLogWindow(&m_debugWindowOpen); + + if (isFirstRun) + isFirstRun = false; +} + +void SystemEditor::DrawMenuBar() +{ + m_menuBinder->DrawMenuBar(); + + if (ImGui::BeginMenu("Windows")) { + if (ImGui::MenuItem("Metrics Window", nullptr, m_metricsWindowOpen)) + m_metricsWindowOpen = !m_metricsWindowOpen; + + if (ImGui::MenuItem("Undo Stack", nullptr, m_undoStackWindowOpen)) + m_undoStackWindowOpen = !m_undoStackWindowOpen; + + if (ImGui::MenuItem("Shortcut List", nullptr, m_binderWindowOpen)) + m_binderWindowOpen = !m_binderWindowOpen; + + if (ImGui::MenuItem("ImGui Debug Log", nullptr, m_debugWindowOpen)) + m_debugWindowOpen = !m_debugWindowOpen; + + if (ImGui::MenuItem("Reset Layout")) + m_resetDockingLayout = true; + + ImGui::EndMenu(); + } + + if (HasUnsavedChanges()) { + ImGui::Text("*"); + } + + ImGui::EndMenuBar(); +} + +void SystemEditor::DrawOutliner() +{ + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 16)); + + std::string name = m_system.Valid() ? m_system->GetName() : ""; + std::string label = fmt::format("System: {}", name); + if (ImGui::Selectable(label.c_str(), !m_selectedBody)) { + m_selectedBody = nullptr; + } + + ImGui::Spacing(); + + if (!m_system) { + ImGui::PopFont(); + return; + } + + if (ImGui::BeginChild("OutlinerList")) { + std::vector> m_systemStack { + { m_system->GetRootBody().Get(), 0 } + }; + + if (!DrawBodyNode(m_system->GetRootBody().Get(), true)) { + ImGui::EndChild(); + ImGui::PopFont(); + return; + } + + while (!m_systemStack.empty()) { + auto &pair = m_systemStack.back(); + + if (pair.second >= pair.first->GetNumChildren()) { + m_systemStack.pop_back(); + ImGui::TreePop(); + continue; + } + + SystemBody *body = pair.first->GetChildren()[pair.second++]; + if (DrawBodyNode(body, false)) + m_systemStack.push_back({ body, 0 }); + } + } + ImGui::EndChild(); + + ImGui::PopFont(); +} + +void SystemEditor::HandleOutlinerDragDrop(SystemBody *refBody) +{ + // Handle drag-drop re-order/re-parent + SystemBody *dropBody = nullptr; + Draw::DragDropTarget dropTarget = Draw::HierarchyDragDrop("SystemBody", ImGui::GetID(refBody), &refBody, &dropBody, sizeof(SystemBody *)); + + if (dropTarget != Draw::DragDropTarget::DROP_NONE && refBody != dropBody) { + size_t targetIdx = SystemBody::EditorAPI::GetIndexInParent(refBody); + + m_pendingOp.type = BodyRequest::TYPE_Reparent; + m_pendingOp.body = dropBody; + m_pendingOp.parent = dropTarget == Draw::DROP_CHILD ? refBody : refBody->GetParent(); + m_pendingOp.idx = 0; + + if (dropTarget == Draw::DROP_BEFORE) + m_pendingOp.idx = targetIdx; + else if (dropTarget == Draw::DROP_AFTER) + m_pendingOp.idx = targetIdx + 1; + } +} + +bool SystemEditor::DrawBodyNode(SystemBody *body, bool isRoot) +{ + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_DefaultOpen | + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanFullWidth | + ImGuiTreeNodeFlags_FramePadding; + + if (body->GetNumChildren() == 0) + flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + + if (body == m_selectedBody) + flags |= ImGuiTreeNodeFlags_Selected; + + ImGuiID bodyId = ImGui::GetID(body); + std::string name = fmt::format("{} {}###{:x}", GetBodyIcon(body), body->GetName(), bodyId); + bool open = ImGui::TreeNodeEx(name.c_str(), flags); + + if (body == m_selectedBody) + ImGui::SetItemDefaultFocus(); + + if (!isRoot) { + HandleOutlinerDragDrop(body); + } + + if (ImGui::IsItemActivated()) { + m_viewport->GetMap()->SetSelectedObject({ Projectable::OBJECT, Projectable::SYSTEMBODY, body }); + SetSelectedBody(body); + } + + if (ImGui::IsItemClicked(0) && ImGui::IsMouseDoubleClicked(0)) { + m_viewport->GetMap()->SetViewedObject({ Projectable::OBJECT, Projectable::SYSTEMBODY, body }); + } + + // TODO: custom rendering on body entry, e.g. icon / contents etc. + + DrawBodyContextMenu(body); + + return open && body->GetNumChildren(); +} + +void SystemEditor::DrawBodyProperties() +{ + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 16)); + ImGui::Text("Body: %s (%d)", m_selectedBody->GetName().c_str(), m_selectedBody->GetPath().bodyIndex); + ImGui::PopFont(); + + ImGui::Spacing(); + + ImGui::PushID(m_selectedBody); + + SystemBody::EditorAPI::EditBodyName(m_selectedBody, GetRng(), m_nameGen.get(), GetUndo()); + + SystemBody::EditorAPI::EditProperties(m_selectedBody, GetRng(), GetUndo()); + + ImGui::PopID(); +} + +void SystemEditor::DrawSystemProperties() +{ + if (!m_system) { + ImGui::Text("No loaded system"); + return; + } + + SystemPath path = m_system->GetPath(); + + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 16)); + ImGui::Text("%s (%d, %d, %d : %d)", + m_system->GetName().c_str(), + path.sectorX, path.sectorY, path.sectorZ, path.systemIndex); + ImGui::PopFont(); + + ImGui::Spacing(); + + StarSystem::EditorAPI::EditName(m_system.Get(), GetRng(), GetUndo()); + + StarSystem::EditorAPI::EditProperties(m_system.Get(), m_systemInfo, m_galaxy->GetFactions(), GetUndo()); + +} + +void SystemEditor::DrawBodyContextMenu(SystemBody *body) +{ + if (ImGui::BeginPopupContextItem()) { + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 15)); + + m_contextBody = body; + m_menuBinder->DrawGroup("Edit.Body"); + m_contextBody = m_selectedBody; + + ImGui::PopFont(); + ImGui::EndPopup(); + } +} diff --git a/src/editor/system/SystemEditor.h b/src/editor/system/SystemEditor.h new file mode 100644 index 00000000000..dec605cf97a --- /dev/null +++ b/src/editor/system/SystemEditor.h @@ -0,0 +1,186 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "SystemEditorModals.h" + +#include "Input.h" +#include "Random.h" +#include "RefCounted.h" +#include "core/Application.h" +#include "galaxy/SystemPath.h" +#include "GalaxyEditAPI.h" + +#include + +// Forward declaration +typedef unsigned int ImGuiID; + +namespace pfd { + class open_file; + class save_file; +} // namespace pfd + +namespace FileSystem { + class FileInfo; +} // namespace FileSystem + +class LuaNameGen; + +class Galaxy; +class StarSystem; +class SystemBody; +class CustomSystem; +class CustomSystemsDatabase; + +namespace Editor { + +class EditorApp; +class UndoSystem; +class SystemEditorViewport; +class ActionBinder; + +const char *GetBodyIcon(const SystemBody *body); + +class SystemEditor : public Application::Lifecycle { +public: + SystemEditor(EditorApp *app); + ~SystemEditor(); + + void NewSystem(SystemPath path); + bool LoadSystem(SystemPath path); + bool LoadSystemFromDisk(const std::string &absolutePath); + + // Write the currently edited system out to disk as a JSON file + bool WriteSystem(const std::string &filepath); + + Random &GetRng() { return m_random; } + RefCountedPtr GetGalaxy(); + + void SetSelectedBody(SystemBody *body); + SystemBody *GetSelectedBody() { return m_selectedBody; } + + void DrawBodyContextMenu(SystemBody *body); + +protected: + void Start() override; + void Update(float deltaTime) override; + void End() override; + + void HandleInput(); + +private: + bool LoadSystemFromFile(const FileSystem::FileInfo &file); + bool LoadCustomSystem(const CustomSystem *system); + void LoadSystemFromGalaxy(RefCountedPtr system); + void ClearSystem(); + + void RegisterMenuActions(); + + bool HasUnsavedChanges(); + void SaveCurrentFile(); + void OnSaveComplete(bool success); + + void ActivateOpenDialog(); + void ActivateSaveDialog(); + void ActivateNewSystemDialog(); + + void HandlePendingFileRequest(); + void HandleBodyOperations(); + + void SetupLayout(ImGuiID dockspaceID); + void DrawInterface(); + + void DrawMenuBar(); + + bool DrawBodyNode(SystemBody *body, bool isRoot); + void HandleOutlinerDragDrop(SystemBody *refBody); + void DrawOutliner(); + + void DrawBodyProperties(); + void DrawSystemProperties(); + + void EditName(const char *undo_label, std::string *name); + + void DrawUndoDebug(); + + UndoSystem *GetUndo() { return m_undo.get(); } + +private: + class UndoSetSelection; + + // Pending file actions which triggered an unsaved changes modal + enum FileRequestType { + FileRequest_None, + FileRequest_Open, + FileRequest_New, + FileRequest_Quit + }; + + // Pending actions to the body tree hierarchy that should + // be handled at the end of the frame + struct BodyRequest { + enum Type { + TYPE_None, + TYPE_Add, + TYPE_Delete, + TYPE_Reparent, + TYPE_Resort + }; + + Type type = TYPE_None; + uint32_t newBodyType = 0; // SystemBody::BodyType + SystemBody *parent = nullptr; + SystemBody *body = nullptr; + size_t idx = 0; + }; + + EditorApp *m_app; + + RefCountedPtr m_galaxy; + RefCountedPtr m_system; + std::unique_ptr m_systemLoader; + + CustomSystemInfo m_systemInfo; + + std::unique_ptr m_viewport; + + Random m_random; + std::unique_ptr m_nameGen; + + std::unique_ptr m_undo; + size_t m_lastSavedUndoStack; + + std::string m_filepath; + std::string m_filedir; + + SystemBody *m_selectedBody; + SystemBody *m_contextBody; + + BodyRequest m_pendingOp; + + FileRequestType m_pendingFileReq; + + std::unique_ptr m_openFile; + std::unique_ptr m_saveFile; + + SystemPath m_openSystemPath; + + RefCountedPtr m_fileActionModal; + RefCountedPtr m_unsavedFileModal; + RefCountedPtr m_newSystemModal; + + SystemPath m_newSystemPath; + + std::unique_ptr m_menuBinder; + + bool m_metricsWindowOpen = false; + bool m_undoStackWindowOpen = false; + bool m_binderWindowOpen = false; + bool m_debugWindowOpen = false; + + bool m_resetDockingLayout = false; +}; + +} // namespace Editor diff --git a/src/editor/system/SystemEditorHelpers.cpp b/src/editor/system/SystemEditorHelpers.cpp new file mode 100644 index 00000000000..dbda7ef16aa --- /dev/null +++ b/src/editor/system/SystemEditorHelpers.cpp @@ -0,0 +1,298 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "SystemEditorHelpers.h" +#include "core/macros.h" +#include "gameconsts.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" + +using namespace Editor; + +enum DistanceUnits { + DISTANCE_AU, // Astronomical units + DISTANCE_LS, // Light-seconds + DISTANCE_KM, // Kilometers +}; + +const char *distance_labels[] = { + "AU", + "ls", + "km" +}; + +const char *distance_formats[] = { + "%.4f AU", + "%.4f ls", + "%.2f km" +}; + +const double distance_multipliers[] = { + 1.0, + 499.00478, + AU / 1000.0 +}; + +enum MassUnits { + MASS_SOLS, // Solar masses + MASS_EARTH, // Earth masses + MASS_PT, // Kilograms x 1e15 +}; + +const char *mass_labels[] = { + "Solar Masses", + "Earth Masses", + "Pt (× 10¹⁸ kg)", +}; + +const char *mass_formats[] = { + "%.4f Sol", + "%.4f Earth", + "%.4f Pt", +}; + +// KG->T = 1e3 + T->PT = 1e15 +const double KG_TO_PT = 1e18; + +const double SOL_MASS_PT = SOL_MASS / KG_TO_PT; +const double EARTH_MASS_PT = EARTH_MASS / KG_TO_PT; +const double SOL_TO_EARTH_MASS = SOL_MASS / EARTH_MASS; + +enum RadiusUnits { + RADIUS_SOLS, // Solar masses + RADIUS_EARTH, // Earth masses + RADIUS_KM, // Kilometers +}; + +const char *radius_labels[] = { + "Solar Radii", + "Earth Radii", + "km" +}; + +const char *radius_formats[] = { + "%.4f Sol", + "%.4f Earth", + "%.2f km" +}; + +const double SOL_RADIUS_KM = SOL_RADIUS / 1000.0; +const double EARTH_RADIUS_KM = EARTH_RADIUS / 1000.0; +const double SOL_TO_EARTH_RADIUS = SOL_RADIUS / EARTH_RADIUS; + +namespace ImGui { + void UnDisable() + { + ImGuiContext& g = *GImGui; + + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (was_disabled) { + g.Style.Alpha = g.DisabledAlphaBackup; + g.CurrentItemFlags &= ~ImGuiItemFlags_Disabled; + } + } + + void ReDisable() + { + ImGuiContext& g = *GImGui; + + bool was_disabled = (g.ItemFlagsStack.back() & ImGuiItemFlags_Disabled) != 0; + if (was_disabled) { + g.Style.Alpha *= g.Style.DisabledAlpha; + g.CurrentItemFlags |= ImGuiItemFlags_Disabled; + } + } + + bool IsDisabled() { return (GImGui->ItemFlagsStack.back() & ImGuiItemFlags_Disabled) != 0; } +} + +void Draw::SubtractItemWidth() +{ + ImGuiWindow *window = ImGui::GetCurrentWindow(); + float used_width = window->DC.CursorPos.x - IM_FLOOR(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + ImGui::SetNextItemWidth(ImGui::CalcItemWidth() - used_width); +} + +bool Draw::InputFixedSlider(const char *str, fixed *val, double val_min, double val_max, const char *format, ImGuiSliderFlags flags) +{ + double val_d = val->ToDouble(); + + bool changed = ImGui::SliderScalar(str, ImGuiDataType_Double, &val_d, &val_min, &val_max, format, flags | ImGuiSliderFlags_NoRoundToFormat); + // delay one frame before writing back the value for the undo system to push a value + if (changed && !ImGui::IsItemActivated()) + *val = fixed::FromDouble(val_d); + + return changed && !ImGui::IsItemActivated(); +} + +bool Draw::InputFixedDegrees(const char *str, fixed *val, double val_min, double val_max, ImGuiInputTextFlags flags) +{ + double val_d = RAD2DEG(val->ToDouble()); + + bool changed = ImGui::SliderScalar(str, ImGuiDataType_Double, &val_d, &val_min, &val_max, "%.3f°", ImGuiSliderFlags_NoRoundToFormat); + // bool changed = ImGui::InputDouble(str, &val_d, 1.0, 10.0, "%.3f°", flags | ImGuiInputTextFlags_EnterReturnsTrue); + // delay one frame before writing back the value for the undo system to push a value + if (changed && !ImGui::IsItemActivated()) + *val = fixed::FromDouble(DEG2RAD(val_d)); + + return changed && !ImGui::IsItemActivated(); +} + +bool Draw::InputFixedDistance(const char *str, fixed *val, ImGuiInputTextFlags flags) +{ + ImGuiStyle &style = ImGui::GetStyle(); + + ImGui::BeginGroup(); + ImGui::PushID(str); + + ImGuiID unit_type_id = ImGui::GetID("#UnitType"); + + int unit_type = DISTANCE_AU; + + double val_d = val->ToDouble(); + if (val_d < 0.001) + unit_type = DISTANCE_LS; + if (val_d < 0.00001) + unit_type = DISTANCE_KM; + + unit_type = ImGui::GetStateStorage()->GetInt(unit_type_id, unit_type); + + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::UnDisable(); + if (ImGui::Combo("##Unit", &unit_type, distance_labels, COUNTOF(distance_labels))) { + ImGui::GetStateStorage()->SetInt(unit_type_id, unit_type); + } + ImGui::ReDisable(); + + double val_step = unit_type == DISTANCE_KM ? 1.0 : unit_type == DISTANCE_LS ? 0.1 : 0.01; + val_d *= distance_multipliers[unit_type]; + + if (ImGui::IsDisabled()) val_step *= 0.0; + + ImGui::SameLine(0.f, 1.f); + Draw::SubtractItemWidth(); + bool changed = ImGui::InputDouble(str, &val_d, val_step, val_step * 10.0, distance_formats[unit_type], flags | ImGuiInputTextFlags_EnterReturnsTrue); + if (changed) + *val = fixed::FromDouble(val_d / distance_multipliers[unit_type]); + + if (ImGui::IsItemActivated()) + ImGui::GetStateStorage()->SetInt(unit_type_id, unit_type); + + ImGui::PopID(); + ImGui::EndGroup(); + + return changed; +} + +bool Draw::InputFixedMass(const char *str, fixed *val, bool is_solar, ImGuiInputTextFlags flags) +{ + ImGuiStyle &style = ImGui::GetStyle(); + + ImGui::BeginGroup(); + ImGui::PushID(str); + + ImGuiID unit_type_id = ImGui::GetID("#UnitType"); + + int unit_type = MASS_SOLS; + if (!is_solar) + unit_type = MASS_EARTH; + + double val_d = val->ToDouble(); + if (!is_solar && val_d < 0.0001) + unit_type = MASS_PT; + + unit_type = ImGui::GetStateStorage()->GetInt(unit_type_id, unit_type); + + ImGui::UnDisable(); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + if (ImGui::Combo("##Unit", &unit_type, mass_labels, COUNTOF(mass_labels))) { + ImGui::GetStateStorage()->SetInt(unit_type_id, unit_type); + } + ImGui::ReDisable(); + + double val_step = 0.01; + + if (ImGui::IsDisabled()) val_step *= 0.0; + + if (is_solar && unit_type != MASS_SOLS) + val_d *= unit_type == MASS_EARTH ? SOL_TO_EARTH_MASS : SOL_MASS_PT; + if (!is_solar && unit_type != MASS_EARTH) + val_d *= unit_type == MASS_SOLS ? (1.0 / SOL_TO_EARTH_MASS) : EARTH_MASS_PT; + + ImGui::SameLine(0.f, 1.f); + Draw::SubtractItemWidth(); + bool changed = ImGui::InputDouble(str, &val_d, val_step, val_step * 10.0, mass_formats[unit_type], flags | ImGuiInputTextFlags_EnterReturnsTrue); + + if (is_solar && unit_type != MASS_SOLS) + val_d /= unit_type == MASS_EARTH ? SOL_TO_EARTH_MASS : SOL_MASS_PT; + if (!is_solar && unit_type != MASS_EARTH) + val_d /= unit_type == MASS_SOLS ? (1.0 / SOL_TO_EARTH_MASS) : EARTH_MASS_PT; + + if (changed) + *val = fixed::FromDouble(val_d); + + if (ImGui::IsItemActivated()) + ImGui::GetStateStorage()->SetInt(unit_type_id, unit_type); + + ImGui::PopID(); + ImGui::EndGroup(); + + return changed; +} + +bool Draw::InputFixedRadius(const char *str, fixed *val, bool is_solar, ImGuiInputTextFlags flags) +{ + ImGuiStyle &style = ImGui::GetStyle(); + + ImGui::BeginGroup(); + ImGui::PushID(str); + + ImGuiID unit_type_id = ImGui::GetID("#UnitType"); + + int unit_type = RADIUS_SOLS; + if (!is_solar) + unit_type = RADIUS_EARTH; + + double val_d = val->ToDouble(); + if (!is_solar && val_d < 0.1) + unit_type = RADIUS_KM; + + unit_type = ImGui::GetStateStorage()->GetInt(unit_type_id, unit_type); + + ImGui::UnDisable(); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + if (ImGui::Combo("##Unit", &unit_type, radius_labels, COUNTOF(radius_labels))) { + ImGui::GetStateStorage()->SetInt(unit_type_id, unit_type); + } + ImGui::ReDisable(); + + double val_step = 0.01; + + if (ImGui::IsDisabled()) val_step *= 0.0; + + if (is_solar && unit_type != RADIUS_SOLS) + val_d *= unit_type == RADIUS_EARTH ? SOL_TO_EARTH_MASS : SOL_RADIUS_KM; + if (!is_solar && unit_type != RADIUS_EARTH) + val_d *= unit_type == RADIUS_SOLS ? (1.0 / SOL_TO_EARTH_MASS) : EARTH_RADIUS_KM; + + ImGui::SameLine(0.f, 1.f); + Draw::SubtractItemWidth(); + bool changed = ImGui::InputDouble(str, &val_d, val_step, val_step * 10.0, radius_formats[unit_type], flags | ImGuiInputTextFlags_EnterReturnsTrue); + + if (is_solar && unit_type != RADIUS_SOLS) + val_d /= unit_type == RADIUS_EARTH ? SOL_TO_EARTH_MASS : SOL_RADIUS_KM; + if (!is_solar && unit_type != RADIUS_EARTH) + val_d /= unit_type == RADIUS_SOLS ? (1.0 / SOL_TO_EARTH_MASS) : EARTH_RADIUS_KM; + + if (changed) + *val = fixed::FromDouble(val_d); + + if (ImGui::IsItemActivated()) + ImGui::GetStateStorage()->SetInt(unit_type_id, unit_type); + + ImGui::PopID(); + ImGui::EndGroup(); + + return changed; +} diff --git a/src/editor/system/SystemEditorHelpers.h b/src/editor/system/SystemEditorHelpers.h new file mode 100644 index 00000000000..839ccd69811 --- /dev/null +++ b/src/editor/system/SystemEditorHelpers.h @@ -0,0 +1,52 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "EditorIcons.h" +#include "MathUtil.h" +#include "imgui/imgui.h" +#include "imgui/imgui_stdlib.h" + +#include "fixed.h" + +namespace ImGui { + inline bool InputFixed(const char *str, fixed *val, double step = 0.01, double step_fast = 0.1, const char *format = "%.4f", ImGuiInputTextFlags flags = 0) + { + double val_d = val->ToDouble(); + bool changed = ImGui::InputDouble(str, &val_d, step, step_fast, format, flags | ImGuiInputTextFlags_EnterReturnsTrue); + if (changed) + *val = fixed::FromDouble(val_d); + + return changed; + } + + inline bool InputInt(const char* label, int* v, int step, int step_fast, const char *format, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags); + } +} + +namespace Editor::Draw { + + // Subtract the currently used space on this line and apply it to the next drawn item + void SubtractItemWidth(); + + inline bool RandomButton() + { + ImGuiStyle &style = ImGui::GetStyle(); + bool ret = ImGui::Button(EICON_RANDOM); + + ImGui::SameLine(0.f, style.ItemInnerSpacing.x); + Draw::SubtractItemWidth(); + + return ret; + } + + bool InputFixedSlider(const char *str, fixed *val, double val_min = 0.0, double val_max = 1.0, const char *format = "%.4f", ImGuiSliderFlags flags = ImGuiSliderFlags_AlwaysClamp); + bool InputFixedDegrees(const char *str, fixed *val, double val_min = -360.0, double val_max = 360.0, ImGuiInputTextFlags flags = 0); + bool InputFixedDistance(const char *str, fixed *val, ImGuiInputTextFlags flags = 0); + bool InputFixedMass(const char *str, fixed *val, bool is_solar, ImGuiInputTextFlags flags = 0); + bool InputFixedRadius(const char *str, fixed *val, bool is_solar, ImGuiInputTextFlags flags = 0); + +}; diff --git a/src/editor/system/SystemEditorModals.cpp b/src/editor/system/SystemEditorModals.cpp new file mode 100644 index 00000000000..1e76a119a60 --- /dev/null +++ b/src/editor/system/SystemEditorModals.cpp @@ -0,0 +1,181 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "SystemEditorModals.h" +#include "SystemEditor.h" + +#include "editor/EditorApp.h" +#include "editor/EditorDraw.h" +#include "editor/EditorIcons.h" +#include "editor/Modal.h" + +#include "galaxy/Galaxy.h" +#include "galaxy/Sector.h" +#include "galaxy/SystemPath.h" +#include "pigui/PiGui.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" + +using namespace Editor; + +FileActionOpenModal::FileActionOpenModal(EditorApp *app) : + Modal(app, "File Window Open", false) +{ +} + +void FileActionOpenModal::Draw() +{ + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(30, 30)); + + Modal::Draw(); + + ImGui::PopStyleVar(); +} + +void FileActionOpenModal::DrawInternal() +{ + ImGui::TextUnformatted("Waiting on a file action to complete..."); +} + +// ============================================================================= + +UnsavedFileModal::UnsavedFileModal(EditorApp *app) : + Modal(app, "Unsaved Changes", true) +{ +} + +void UnsavedFileModal::DrawInternal() +{ + ImGui::TextUnformatted("The current file has unsaved changes."); + ImGui::Spacing(); + ImGui::TextUnformatted("Do you want to save the current file before proceeding?"); + + ImGui::NewLine(); + + float width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; + + if (ImGui::Button("No", ImVec2(width, 0))) { + m_result = Result_No; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Yes", ImVec2(width, 0))) { + m_result = Result_Yes; + ImGui::CloseCurrentPopup(); + } +} + +// ============================================================================= + +NewSystemModal::NewSystemModal(EditorApp *app, SystemEditor *editor, SystemPath *path) : + Modal(app, "New System from Galaxy", true), + m_editor(editor), + m_path(path) +{ +} + +void NewSystemModal::Draw() +{ + ImVec2 vpSize = ImGui::GetMainViewport()->Size; + ImVec2 windSize = ImVec2(vpSize.x * 0.5, vpSize.y * 0.333); + ImVec2 windSizeMax = ImVec2(vpSize.x * 0.5, vpSize.y * 0.5); + ImGui::SetNextWindowSizeConstraints(windSize, windSizeMax); + + Modal::Draw(); +} + +void NewSystemModal::DrawInternal() +{ + if (Draw::LayoutHorizontal("Sector", 3, ImGui::GetFontSize())) { + bool changed = false; + changed |= ImGui::InputInt("X", &m_path->sectorX, 1, 0); + changed |= ImGui::InputInt("Y", &m_path->sectorY, 1, 0); + changed |= ImGui::InputInt("Z", &m_path->sectorZ, 1, 0); + + if (changed) + m_path->systemIndex = 0; + + Draw::EndLayout(); + } + + ImGui::Separator(); + + RefCountedPtr sec = m_editor->GetGalaxy()->GetSector(m_path->SectorOnly()); + + ImGui::BeginGroup(); + if (ImGui::BeginChild("Systems", ImVec2(ImGui::GetContentRegionAvail().x * 0.33, -ImGui::GetFrameHeightWithSpacing()))) { + + for (const Sector::System &system : sec->m_systems) { + std::string label = fmt::format("{} ({}x{})", system.GetName(), EICON_SUN, system.GetNumStars()); + + if (ImGui::Selectable(label.c_str(), system.idx == m_path->systemIndex)) + m_path->systemIndex = system.idx; + + if (ImGui::IsItemClicked() && ImGui::IsMouseDoubleClicked(0)) { + m_editor->LoadSystem(system.GetPath()); + ImGui::CloseCurrentPopup(); + } + + } + + } + ImGui::EndChild(); + + if (ImGui::Button("New System")) { + m_editor->NewSystem(m_path->SectorOnly()); + ImGui::CloseCurrentPopup(); + } + + ImGui::SetItemTooltip("Create a new empty system in this sector."); + + ImGui::SameLine(); + + if (ImGui::Button("Edit Selected")) { + m_editor->LoadSystem(m_path->SystemOnly()); + ImGui::CloseCurrentPopup(); + } + + ImGui::SetItemTooltip("Load the selected system as a template."); + + ImGui::EndGroup(); + + ImGui::SameLine(); + ImGui::BeginGroup(); + + if (m_path->systemIndex < sec->m_systems.size()) { + const Sector::System &system = sec->m_systems[m_path->systemIndex]; + + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 16)); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(system.GetName().c_str()); + + ImGui::PopFont(); + + ImGui::Spacing(); + + ImGui::TextUnformatted("Is Custom:"); + ImGui::SameLine(ImGui::CalcItemWidth()); + ImGui::TextUnformatted(system.GetCustomSystem() ? "yes" : "no"); + + ImGui::TextUnformatted("Is Explored:"); + ImGui::SameLine(ImGui::CalcItemWidth()); + ImGui::TextUnformatted(system.GetExplored() == StarSystem::eEXPLORED_AT_START ? "yes" : "no"); + + ImGui::TextUnformatted("Faction:"); + ImGui::SameLine(ImGui::CalcItemWidth()); + ImGui::TextUnformatted(system.GetFaction() ? system.GetFaction()->name.c_str() : ""); + + ImGui::TextUnformatted("Other Names:"); + ImGui::SameLine(ImGui::CalcItemWidth()); + + ImGui::BeginGroup(); + for (auto &name : system.GetOtherNames()) + ImGui::TextUnformatted(name.c_str()); + ImGui::EndGroup(); + } + ImGui::EndGroup(); +} diff --git a/src/editor/system/SystemEditorModals.h b/src/editor/system/SystemEditorModals.h new file mode 100644 index 00000000000..507a6eb5921 --- /dev/null +++ b/src/editor/system/SystemEditorModals.h @@ -0,0 +1,55 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "editor/Modal.h" + +using ImGuiID = unsigned int; + +class SystemPath; + +namespace Editor { + + class EditorApp; + class SystemEditor; + + class FileActionOpenModal : public Modal { + public: + FileActionOpenModal(EditorApp *app); + + protected: + void Draw() override; + void DrawInternal() override; + }; + + class UnsavedFileModal : public Modal { + public: + UnsavedFileModal(EditorApp *app); + + enum ResultType { + Result_Cancel, + Result_No, + Result_Yes + }; + + ResultType Result() { return m_result; } + + protected: + void DrawInternal() override; + ResultType m_result = Result_Cancel; + }; + + class NewSystemModal : public Modal { + public: + NewSystemModal(EditorApp *app, SystemEditor *editor, SystemPath *path); + + void Draw() override; + + protected: + void DrawInternal() override; + + SystemEditor *m_editor; + SystemPath *m_path; + }; +} diff --git a/src/editor/system/SystemEditorViewport.cpp b/src/editor/system/SystemEditorViewport.cpp new file mode 100644 index 00000000000..8a94c49327e --- /dev/null +++ b/src/editor/system/SystemEditorViewport.cpp @@ -0,0 +1,250 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "SystemEditorViewport.h" + +#include "EditorIcons.h" +#include "SystemEditor.h" + +#include "Background.h" +#include "SystemView.h" +#include "galaxy/Galaxy.h" +#include "galaxy/StarSystem.h" + +#include "editor/EditorApp.h" +#include "editor/ViewportWindow.h" +#include "pigui/PiGui.h" +#include "system/SystemEditor.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" + +using namespace Editor; + +SystemEditorViewport::SystemEditorViewport(EditorApp *app, SystemEditor *editor) : + ViewportWindow(app), + m_app(app), + m_editor(editor) +{ + m_map.reset(new SystemMapViewport(m_app)); + + m_map->SetShowGravpoints(true); + + m_map->svColor[SystemMapViewport::GRID] = Color(0x24242BFF); + m_map->svColor[SystemMapViewport::GRID_LEG] = Color(0x787878FF); + m_map->svColor[SystemMapViewport::SYSTEMBODY] = Color(0xB5BCE3FF).Shade(0.5); + m_map->svColor[SystemMapViewport::SYSTEMBODY_ORBIT] = Color(0x5ACC0AFF); + + m_background.reset(new Background::Container(m_app->GetRenderer(), m_editor->GetRng())); + m_background->SetDrawFlags(Background::Container::DRAW_SKYBOX); + m_map->SetBackground(m_background.get()); +} + +SystemEditorViewport::~SystemEditorViewport() +{ +} + +void SystemEditorViewport::SetSystem(RefCountedPtr system) +{ + m_map->SetReferenceTime(0.0); // Jan 1 3200 + m_map->SetCurrentSystem(system); +} + +bool SystemEditorViewport::OnCloseRequested() +{ + return false; +} + +void SystemEditorViewport::OnUpdate(float deltaTime) +{ + m_map->Update(deltaTime); + + m_map->AddObjectTrack({ Projectable::types(Projectable::_MAX + 1), Projectable::BODY, static_cast(nullptr), vector3d(1e12, 0.0, 0.0) }); +} + +void SystemEditorViewport::OnRender(Graphics::Renderer *r) +{ + m_map->Draw3D(); +} + +void SystemEditorViewport::OnHandleInput(bool clicked, bool released, ImVec2 mousePos) +{ + m_map->HandleInput(m_app->DeltaTime()); +} + +void SystemEditorViewport::OnDraw() +{ + ImDrawListSplitter split {}; + + split.Split(ImGui::GetWindowDrawList(), 2); + split.SetCurrentChannel(ImGui::GetWindowDrawList(), 0); + + split.SetCurrentChannel(ImGui::GetWindowDrawList(), 1); + + // Draw background + ImVec2 toolbar_bg_size = ImVec2(ImGui::GetWindowSize().x, ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y * 2.f); + ImColor toolbar_bg_color = ImGui::GetStyle().Colors[ImGuiCol_FrameBg]; + + ImGui::GetWindowDrawList()->PushClipRect(ImGui::GetWindowPos(), ImGui::GetWindowPos() + ImGui::GetWindowSize()); + ImGui::GetWindowDrawList()->AddRectFilled( + ImGui::GetWindowPos(), ImGui::GetWindowPos() + toolbar_bg_size, + toolbar_bg_color); + ImGui::GetWindowDrawList()->PopClipRect(); + + DrawTimelineControls(); + + // Render all GUI elements over top of viewport overlays + // (Items rendered earlier take priority when using IsItemHovered) + + split.SetCurrentChannel(ImGui::GetWindowDrawList(), 0); + + const auto &projected = m_map->GetProjected(); + std::vector groups = m_map->GroupProjectables({ 10.f, 10.f }, {}); + + // Depth sort groups (further groups drawn first / under closer groups) + std::sort(groups.begin(), groups.end(), [](const auto &a, const auto &b){ return a.screenpos.z > b.screenpos.z; }); + + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 16)); + + ImRect screen_rect = ImRect(ImVec2(0, 0), ImGui::GetWindowSize()); + + // Then draw "under" the GUI elements so we can use ImGui::IsItemHovered et al. + for (auto &group : groups) { + ImVec2 itempos = { group.screenpos.x, group.screenpos.y }; + ImVec2 iconSize = { ImGui::GetFontSize(), ImGui::GetFontSize() }; + + // Simple screen clipping rejection test + if (!screen_rect.Contains(itempos)) + continue; + + const Projectable &item = projected[group.tracks[0]]; + if (group.type == Projectable::OBJECT && item.base == Projectable::SYSTEMBODY) { + ImColor icon_col(0xFFC8C8C8); + + std::string label = group.tracks.size() > 1 ? + fmt::format("{} ({})", item.ref.sbody->GetName(), group.tracks.size()) : + item.ref.sbody->GetName(); + + bool clicked = DrawIcon(ImGui::GetID(item.ref.sbody), itempos, icon_col, GetBodyIcon(item.ref.sbody), label.c_str()); + + if (clicked) { + m_map->SetSelectedObject(item); + m_editor->SetSelectedBody(const_cast(item.ref.sbody)); + + if (ImGui::IsMouseDoubleClicked(0)) { + m_map->ViewSelectedObject(); + } + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", label.c_str()); + + m_editor->DrawBodyContextMenu(const_cast(item.ref.sbody)); + + } + + #if 0 // TODO: visual edit gizmos for body axes + if (int(group.type) == Projectable::_MAX + 1) { + DrawIcon(itempos, IM_COL32_WHITE, EICON_AXES); + } + #endif + } + + ImGui::PopFont(); + + split.Merge(ImGui::GetWindowDrawList()); +} + +void SystemEditorViewport::DrawTimelineControls() +{ + double timeAccel = 0.0; + + ImGui::PushFont(m_app->GetPiGui()->GetFont("icons", ImGui::GetFrameHeight())); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing); + + ImGui::Button(EICON_REWIND3); + if (ImGui::IsItemActive()) + timeAccel = -3600.0 * 24.0 * 730.0; // 2 years per second + + ImGui::Button(EICON_REWIND2); + if (ImGui::IsItemActive()) + timeAccel = -3600.0 * 24.0 * 60.0; // 2 months per second + + ImGui::Button(EICON_REWIND1); + if (ImGui::IsItemActive()) + timeAccel = -3600.0 * 24.0 * 5.0; // 5 days per second + + bool timeStop = ImGui::ButtonEx(EICON_TIMESTOP, ImVec2(0,0), ImGuiButtonFlags_PressedOnClick); + + ImGui::Button(EICON_FORWARD1); + if (ImGui::IsItemActive()) + timeAccel = 3600.0 * 24.0 * 5.0; // 5 days per second + + ImGui::Button(EICON_FORWARD2); + if (ImGui::IsItemActive()) + timeAccel = 3600.0 * 24.0 * 60.0; // 2 months per second + + ImGui::Button(EICON_FORWARD3); + if (ImGui::IsItemActive()) + timeAccel = 3600.0 * 24.0 * 730.0; // 2 years per second + + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + ImGui::AlignTextToFramePadding(); + + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) + timeAccel *= 0.1; + + if (!timeStop) + m_map->AccelerateTime(timeAccel); + else + m_map->SetRealTime(); + + ImGui::Text("%s", format_date(m_map->GetTime()).c_str()); +} + +bool SystemEditorViewport::DrawIcon(ImGuiID id, const ImVec2 &icon_pos, const ImColor &color, const char *icon, const char *label) +{ + ImVec2 icon_size = ImVec2(ImGui::GetFontSize(), ImGui::GetFontSize()); + ImVec2 draw_pos = ImGui::GetWindowPos() + icon_pos - icon_size * 0.5f; + + ImGui::GetWindowDrawList()->AddText(draw_pos, color, icon); + + ImRect hover_rect = ImRect(draw_pos, draw_pos + icon_size); + + if (label) { + ImGui::PushFont(m_app->GetPiGui()->GetFont("pionillium", 12)); + ImVec2 text_pos = ImGui::GetWindowPos() + icon_pos + ImVec2(icon_size.x, -ImGui::GetFontSize() * 0.5f); + ImVec2 text_size = ImGui::CalcTextSize(label); + // label shadow + ImGui::GetWindowDrawList()->AddText(text_pos + ImVec2(1.f, 1.f), IM_COL32_BLACK, label); + // body label + ImGui::GetWindowDrawList()->AddText(text_pos, color, label); + ImGui::PopFont(); + + hover_rect.Add(text_pos + text_size); + } + + ImGuiID prevHovered = ImGui::GetHoveredID(); + + ImGuiButtonFlags flags = + ImGuiButtonFlags_MouseButtonLeft | + ImGuiButtonFlags_PressedOnClickRelease | + ImGuiButtonFlags_PressedOnDoubleClick; + + ImGui::ItemAdd(hover_rect, id); + + // Allow interaction with this label + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(hover_rect, id, &hovered, &held, flags); + + // Reset hovered state so viewport ButtonBehavior receives middle-mouse clicks + // (otherwise this label "steals" hovered state and blocks all interaction) + // NOTE: this works with practically any button-like widget, not just ButtonBehavior + if (ImGui::IsItemHovered()) + ImGui::SetHoveredID(prevHovered); + + return pressed; +} diff --git a/src/editor/system/SystemEditorViewport.h b/src/editor/system/SystemEditorViewport.h new file mode 100644 index 00000000000..32351c133b7 --- /dev/null +++ b/src/editor/system/SystemEditorViewport.h @@ -0,0 +1,50 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "ViewportWindow.h" + +namespace Background { + class Container; +} + +class StarSystem; +class SystemMapViewport; + +namespace Editor { + + class SystemEditor; + + class SystemEditorViewport : public ViewportWindow { + public: + SystemEditorViewport(EditorApp *app, SystemEditor *editor); + ~SystemEditorViewport(); + + void SetSystem(RefCountedPtr system); + + SystemMapViewport *GetMap() { return m_map.get(); } + + protected: + void OnUpdate(float deltaTime) override; + void OnRender(Graphics::Renderer *renderer) override; + + void OnHandleInput(bool clicked, bool released, ImVec2 mousePos) override; + + void OnDraw() override; + bool OnCloseRequested() override; + + const char *GetWindowName() override { return "Viewport"; } + + private: + + void DrawTimelineControls(); + bool DrawIcon(ImGuiID id, const ImVec2 &iconPos, const ImColor &color, const char *icon, const char *label = nullptr); + + EditorApp *m_app; + SystemEditor *m_editor; + + std::unique_ptr m_map; + std::unique_ptr m_background; + }; +}; diff --git a/src/galaxy/StarSystem.h b/src/galaxy/StarSystem.h index 5f2d255677e..93ded4fcab5 100644 --- a/src/galaxy/StarSystem.h +++ b/src/galaxy/StarSystem.h @@ -30,6 +30,7 @@ class StarSystem : public RefCounted { friend class SystemBody; friend class GalaxyObjectCache; class GeneratorAPI; // Complete definition below + class EditorAPI; // Defined in editor module enum ExplorationState { eUNEXPLORED = 0, @@ -165,9 +166,10 @@ class StarSystem : public RefCounted { class StarSystem::GeneratorAPI : public StarSystem { private: friend class GalaxyGenerator; - GeneratorAPI(const SystemPath &path, RefCountedPtr galaxy, StarSystemCache *cache, Random &rand); public: + GeneratorAPI(const SystemPath &path, RefCountedPtr galaxy, StarSystemCache *cache, Random &rand); + bool HasCustomBodies() const { return m_hasCustomBodies; } void SetCustom(bool isCustom, bool hasCustomBodies) diff --git a/src/galaxy/StarSystemGenerator.cpp b/src/galaxy/StarSystemGenerator.cpp index 37379a40cc6..22ac93fbff3 100644 --- a/src/galaxy/StarSystemGenerator.cpp +++ b/src/galaxy/StarSystemGenerator.cpp @@ -1389,34 +1389,34 @@ bool StarSystemRandomGenerator::Apply(Random &rng, RefCountedPtr galaxy, * Position a surface starport anywhere. Space.cpp::MakeFrameFor() ensures it * is on dry land (discarding this position if necessary) */ -void PopulateStarSystemGenerator::PositionSettlementOnPlanet(SystemBody *sbody, std::vector &prevOrbits) +void PopulateStarSystemGenerator::PositionSettlementOnPlanet(SystemBody *sbody, std::vector &prevOrbits) { PROFILE_SCOPED() Random r(sbody->GetSeed()); // used for orientation on planet surface - double r2 = r.Double(); // function parameter evaluation order is implementation-dependent - double r1 = r.Double(); // can't put two rands in the same expression + + fixed longitude = r.Fixed(); // function parameter evaluation order is implementation-dependent + fixed latitude = r.NormFixed(); // can't put two rands in the same expression // try to ensure that stations are far enough apart for (size_t i = 0, iterations = 0; i < prevOrbits.size() && iterations < 128; i++, iterations++) { - const double &orev = prevOrbits[i]; - const double len = fabs(r1 - orev); - if (len < 0.05) { - r2 = r.Double(); - r1 = r.Double(); + const fixed &prev = prevOrbits[i]; + const fixed len = (latitude - prev).Abs(); + if (len < fixed(5, 100)) { + longitude = r.Fixed(); + latitude = r.NormFixed(); i = 0; // reset to start the checking from beginning as we're generating new values. } } - prevOrbits.push_back(r1); - - // pset the orbit - sbody->m_orbit.SetPlane(matrix3x3d::RotateZ(2 * M_PI * r1) * matrix3x3d::RotateY(2 * M_PI * r2)); + prevOrbits.push_back(latitude.ToDouble()); // store latitude and longitude to equivalent orbital parameters to // be accessible easier - sbody->m_inclination = fixed(r1 * 10000, 10000) + FIXED_PI / 2; // latitude - sbody->m_orbitalOffset = FIXED_PI / 2; // longitude + sbody->m_inclination = latitude * FIXED_PI * fixed(1, 2); + sbody->m_orbitalOffset = longitude * FIXED_PI * 2; + + sbody->SetOrbitFromParameters(); } /* @@ -1618,6 +1618,10 @@ void PopulateStarSystemGenerator::PopulateAddStations(SystemBody *sbody, StarSys RefCountedPtr namerand(new Random); namerand->seed(_init, 6); + // XXX: station placement code is in need of improvement; the code currently fails to appear "intelligent" + // with respect to well-spaced orbital shells (e.g. a "station belt" around a high-population planet) + // as well as generating station placement for e.g. research, industrial, or communications stations + if (sbody->GetPopulationAsFixed() < fixed(1, 1000)) return; fixed orbMaxS = fixed(1, 4) * fixed(CalcHillRadius(sbody)); fixed orbMinS = fixed().FromDouble((sbody->CalcAtmosphereParams().atmosRadius + +500000.0 / EARTH_RADIUS)) * AU_EARTH_RADIUS; @@ -1627,13 +1631,16 @@ void PopulateStarSystemGenerator::PopulateAddStations(SystemBody *sbody, StarSys // starports - orbital fixed pop = sbody->GetPopulationAsFixed() + rand.Fixed(); if (orbMinS < orbMaxS) { + // How many stations do we need? pop -= rand.Fixed(); Uint32 NumToMake = 0; while (pop >= 0) { ++NumToMake; - pop -= rand.Fixed(); + pop -= fixed(1, 1) - rand.NormFixed().Abs(); } + + // Always generate a station around a populated high-gravity world if ((NumToMake == 0) and (sbody->CalcSurfaceGravity() > 10.5)) { // 10.5 m/s2 = 1,07 g NumToMake = 1; } @@ -1661,13 +1668,21 @@ void PopulateStarSystemGenerator::PopulateAddStations(SystemBody *sbody, StarSys // I like to think that we'd fill several "shells" of orbits at once rather than fill one and move out further static const Uint32 MAX_ORBIT_SHELLS = 3; fixed shells[MAX_ORBIT_SHELLS]; + fixed shellIncl[MAX_ORBIT_SHELLS]; + if (innerOrbit != orbMaxS) { shells[0] = innerOrbit; // low shells[1] = innerOrbit + ((orbMaxS - innerOrbit) * fixed(1, 2)); // med shells[2] = orbMaxS; // high + + shellIncl[0] = rand.NormFixed() * FIXED_PI; + shellIncl[1] = rand.NormFixed() * FIXED_PI; + shellIncl[2] = rand.NormFixed() * FIXED_PI; } else { shells[0] = shells[1] = shells[2] = innerOrbit; + shellIncl[0] = shellIncl[1] = shellIncl[2] = rand.NormFixed() * FIXED_PI; } + Uint32 orbitIdx = 0; double orbitSlt = 0.0; const double orbitSeparation = (NumToMake > 1) ? ((M_PI * 2.0) / double(NumToMake - 1)) : M_PI; @@ -1675,6 +1690,7 @@ void PopulateStarSystemGenerator::PopulateAddStations(SystemBody *sbody, StarSys for (Uint32 i = 0; i < NumToMake; i++) { // Pick the orbit we've currently placing a station into. const fixed currOrbit = shells[orbitIdx]; + const fixed currOrbitIncl = shells[orbitIdx]; ++orbitIdx; if (orbitIdx >= MAX_ORBIT_SHELLS) // wrap it { @@ -1692,20 +1708,21 @@ void PopulateStarSystemGenerator::PopulateAddStations(SystemBody *sbody, StarSys sp->m_mass = 0; // place stations between min and max orbits to reduce the number of extremely close/fast orbits - sp->m_semiMajorAxis = currOrbit; - sp->m_eccentricity = fixed(); + // This avoids the worst-case of having 3-5 stations all in the exact orbit close to each other + sp->m_semiMajorAxis = currOrbit * rand.NormFixed(fixed(12, 10), fixed(2, 10)); + + // Generate slightly random orbits for each station in the orbital "shell" + // slightly random min/max orbital distance + sp->m_eccentricity = rand.NormFixed().Abs() * fixed(1, 8); + // perturb the orbital plane to avoid all stations falling in line with each other + sp->m_inclination = currOrbitIncl + rand.NormFixed() * fixed(1, 4) * FIXED_PI; + // station spacing around the primary body + sp->m_argOfPeriapsis = rand.Fixed() * FIXED_PI * 2; + // TODO: no axial tilt for stations / axial tilt in general is strangely modeled sp->m_axialTilt = fixed(); - sp->m_orbit.SetShapeAroundPrimary(sp->GetSemiMajorAxis() * AU, centralMass, 0.0); + sp->SetOrbitFromParameters(); - // The rotations around X & Y perturb the orbits just a little bit so that not all stations are exactly within the same plane - // The Z rotation is what gives them the separation in their orbit around the parent body as a whole. - sp->m_orbit.SetPlane( - matrix3x3d::RotateX(rand.Double(M_PI * 0.03125)) * - matrix3x3d::RotateY(rand.Double(M_PI * 0.03125)) * - matrix3x3d::RotateZ(orbitSlt * orbitSeparation)); - - sp->m_inclination = fixed(); sbody->m_children.insert(sbody->m_children.begin(), sp); system->AddSpaceStation(sp); sp->m_orbMin = sp->GetSemiMajorAxisAsFixed(); @@ -1718,11 +1735,11 @@ void PopulateStarSystemGenerator::PopulateAddStations(SystemBody *sbody, StarSys // starports - surface // give it a fighting chance of having a decent number of starports (*3) pop = sbody->GetPopulationAsFixed() + (rand.Fixed() * 3); - std::vector previousOrbits; + std::vector previousOrbits; previousOrbits.reserve(8); int max = 6; while (max-- > 0) { - pop -= rand.Fixed(); + pop -= (fixed(1, 1) - rand.NormFixed()); if (pop < 0) break; SystemBody *sp = system->NewBody(); diff --git a/src/galaxy/StarSystemGenerator.h b/src/galaxy/StarSystemGenerator.h index 676e3f190bf..61eea9cc173 100644 --- a/src/galaxy/StarSystemGenerator.h +++ b/src/galaxy/StarSystemGenerator.h @@ -73,7 +73,7 @@ class PopulateStarSystemGenerator : public StarSystemLegacyGeneratorBase { void SetEconType(RefCountedPtr system); void PopulateAddStations(SystemBody *sbody, StarSystem::GeneratorAPI *system); - void PositionSettlementOnPlanet(SystemBody *sbody, std::vector &prevOrbits); + void PositionSettlementOnPlanet(SystemBody *sbody, std::vector &prevOrbits); void PopulateStage1(SystemBody *sbody, StarSystem::GeneratorAPI *system, fixed &outTotalPop); }; diff --git a/src/galaxy/SystemBody.cpp b/src/galaxy/SystemBody.cpp index 3b74a27596c..c856f294d69 100644 --- a/src/galaxy/SystemBody.cpp +++ b/src/galaxy/SystemBody.cpp @@ -117,10 +117,16 @@ void SystemBodyData::LoadFromJson(const Json &obj) m_rings.baseColor = obj.value("ringsBaseColor", {}); } + // NOTE: the following parameters should be replaced with entries + // in a PropertyMap owned by the system body m_spaceStationType = obj.value("spaceStationType", ""); + // HACK: this is to support the current / legacy heightmap fractal system + // Should be replaced with PropertyMap entries and validation moved to Terrain.cpp m_heightMapFilename = obj.value("heightMapFilename", ""); m_heightMapFractal = obj.value("heightMapFractal", 0); + + m_heightMapFractal = std::min(m_heightMapFractal, uint32_t(1)); } SystemBody::SystemBody(const SystemPath &path, StarSystem *system) : diff --git a/src/graphics/Renderer.h b/src/graphics/Renderer.h index 90b5ae62531..5dbdecec142 100644 --- a/src/graphics/Renderer.h +++ b/src/graphics/Renderer.h @@ -68,6 +68,8 @@ namespace Graphics { int GetWindowHeight() const { return m_height; } virtual int GetMaximumNumberAASamples() const = 0; + virtual void SetVSyncEnabled(bool enabled) = 0; + //get supported minimum for z near and maximum for z far values virtual bool GetNearFarRange(float &near_, float &far_) const = 0; diff --git a/src/graphics/dummy/RendererDummy.h b/src/graphics/dummy/RendererDummy.h index 6886e77f7fd..ad57eef9893 100644 --- a/src/graphics/dummy/RendererDummy.h +++ b/src/graphics/dummy/RendererDummy.h @@ -34,6 +34,8 @@ namespace Graphics { virtual int GetMaximumNumberAASamples() const override final { return 0; } virtual bool GetNearFarRange(float &near_, float &far_) const override final { return true; } + virtual void SetVSyncEnabled(bool) override {} + virtual bool BeginFrame() override final { return true; } virtual bool EndFrame() override final { return true; } virtual bool SwapBuffers() override final { return true; } diff --git a/src/graphics/opengl/RendererGL.cpp b/src/graphics/opengl/RendererGL.cpp index 21561856c3e..8cc805e087d 100644 --- a/src/graphics/opengl/RendererGL.cpp +++ b/src/graphics/opengl/RendererGL.cpp @@ -126,7 +126,7 @@ namespace Graphics { SDL_SetWindowTitle(window, vs.title); SDL_ShowCursor(0); - SDL_GL_SetSwapInterval((vs.vsync != 0) ? 1 : 0); + SDL_GL_SetSwapInterval((vs.vsync != 0) ? -1 : 0); return new RendererOGL(window, vs, glContext); } @@ -465,6 +465,11 @@ namespace Graphics { return true; } + void RendererOGL::SetVSyncEnabled(bool enabled) + { + SDL_GL_SetSwapInterval(enabled ? -1 : 0); + } + bool RendererOGL::BeginFrame() { PROFILE_SCOPED() diff --git a/src/graphics/opengl/RendererGL.h b/src/graphics/opengl/RendererGL.h index 3991ae44a36..3942eada3f7 100644 --- a/src/graphics/opengl/RendererGL.h +++ b/src/graphics/opengl/RendererGL.h @@ -56,6 +56,8 @@ namespace Graphics { virtual int GetMaximumNumberAASamples() const override final; virtual bool GetNearFarRange(float &near_, float &far_) const override final; + virtual void SetVSyncEnabled(bool) override; + virtual bool BeginFrame() override final; virtual bool EndFrame() override final; virtual bool SwapBuffers() override final; diff --git a/src/lua/LuaEngine.cpp b/src/lua/LuaEngine.cpp index a48a62ff8b3..f905f4193c0 100644 --- a/src/lua/LuaEngine.cpp +++ b/src/lua/LuaEngine.cpp @@ -20,6 +20,7 @@ #include "Pi.h" #include "Player.h" #include "Random.h" +#include "SDL_video.h" #include "WorldView.h" #include "buildopts.h" #include "core/OS.h" @@ -419,9 +420,12 @@ static int l_engine_set_vsync_enabled(lua_State *l) { if (lua_isnone(l, 1)) return luaL_error(l, "SetVSyncEnabled takes one boolean argument"); + const bool vsync = lua_toboolean(l, 1); Pi::config->SetInt("VSync", (vsync ? 1 : 0)); Pi::config->Save(); + + Pi::renderer->SetVSyncEnabled(vsync); return 0; } diff --git a/src/pigui/PiGuiRenderer.cpp b/src/pigui/PiGuiRenderer.cpp index 23b668f1d20..f4cf1d83893 100644 --- a/src/pigui/PiGuiRenderer.cpp +++ b/src/pigui/PiGuiRenderer.cpp @@ -11,7 +11,6 @@ #include "graphics/VertexBuffer.h" #include "profiler/Profiler.h" -#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui/imgui.h" #include "imgui/imgui_internal.h" diff --git a/src/terrain/Terrain.cpp b/src/terrain/Terrain.cpp index 4647e5048d5..79f7afec74e 100644 --- a/src/terrain/Terrain.cpp +++ b/src/terrain/Terrain.cpp @@ -18,7 +18,7 @@ Terrain *Terrain::InstanceTerrain(const SystemBody *body) // special case for heightmaps // XXX this is terrible but will do for now until we get a unified // heightmap setup. if you add another height fractal, remember to change - // the check in CustomSystem::l_height_map + // the check in CustomSystem::l_height_map / SystemBodyData::LoadFromJson if (!body->GetHeightMapFilename().empty()) { const GeneratorInstancer choices[] = { InstanceGenerator,