code:
#include "Core/DatabaseEditor/DBWeaponsTab.hpp"
#include "Core/Application.hpp"
#include "Database/Weapons.hpp"

#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "imgui.h"
#include "imgui_internal.h"

DBWeaponsTab::DBWeaponsTab(Weapons& weapons, DatabaseEditor* parent)
: IDBEditorTab(parent)
, m_weapons(weapons) {
  m_selectedWeapon = m_weapons.weapon(1);
  if (m_selectedWeapon) {
    m_traitsEditor.setTraits(&m_selectedWeapon->traits());
  }
  m_maxWeapons = m_weapons.count();
}

void DBWeaponsTab::draw() {
  if (!m_itemButtonTexture) {
    m_itemButtonTexture.emplace();
    m_itemButtonTexture->setSize(38, 38);
    if (!m_itemSheet) {
      m_itemSheet.emplace(*m_parent->getIconSheet());
    }
    const auto& [uv0, uv1] = m_itemSheet.value().rectForId(m_selectedWeapon->iconIndex());
    const Point offset{static_cast<int>(uv0.x() * m_itemSheet.value().texture().width()), static_cast<int>(uv0.y() * m_itemSheet.value().texture().height())};
    m_itemButtonTexture->setTexturesToComposite({{m_itemSheet.value().texture(), {m_itemSheet.value().iconWidth(), m_itemSheet.value().iconHeight()}, offset}});
  }
  if (m_animationPicker) {
    if (const auto [closed, confirmed] = m_animationPicker->draw(); closed) {
      if (confirmed) {
        m_selectedWeapon->setAnimationId(m_animationPicker->selection());
      }
      m_animationPicker.reset();
    }
  }
  ImGui::BeginChild("#orpg_weapons_editor");
  {
    const auto calc = ImGui::CalcTextSize("ABCDEFGHIJKLMNOPQRSTUV");
    ImGui::BeginChild("##orpg_weapons_editor_weapons", ImVec2{calc.x + (ImGui::GetStyle().ItemSpacing.x * 2), 0}, 0, ImGuiWindowFlags_NoScrollbar);
    {
      ImGui::BeginGroup();
      {
        ImGui::SeparatorText("Weapons");
        ImGui::BeginChild("##orpg_weapons_editor_weapons_list", ImVec2{0, ImGui::GetContentRegionMax().y - ((calc.y + ImGui::GetStyle().ItemSpacing.y) * 4)});
        {
          ImGui::BeginGroup();
          {
            for (auto& weapon : m_weapons.weapons()) {
              if (weapon.id() == 0) {
                continue;
              }

              if (ImGui::Selectable(Database::instance()->weaponNameAndId(weapon.id()).c_str(), &weapon == m_selectedWeapon) || (ImGui::IsItemFocused() && m_selectedWeapon != &weapon)) {
                m_selectedWeapon = &weapon;
                m_traitsEditor.setTraits(&m_selectedWeapon->traits());
              }
            }
          }
          ImGui::EndGroup();
        }
        ImGui::EndChild();
        char str[4096];
        snprintf(str, 4096, "Max Weapons %i", m_maxWeapons);
        ImGui::SeparatorText(str);
        if (ImGui::Button("Change Max", ImVec2{ImGui::GetContentRegionMax().x - 8, 0})) {
          m_changeIntDialogOpen = true;
          m_editMaxWeapons = m_maxWeapons;
        }
      }
      ImGui::EndGroup();
    }
    ImGui::EndChild();
    ImGui::SameLine();
    ImGui::BeginChild("##orpg_weapons_editor_weapons_weapon_properties");
    {
      if (m_selectedWeapon) {
        ImGui::BeginChild("##orpg_weapons_weapon_panel_left", ImVec2{ImGui::GetContentRegionMax().x / 2, 0.f});
        {
          ImGui::BeginGroup();
          {
            ImGui::SeparatorText("General Settings");
            // Name
            ImGui::BeginGroup();
            {
              char name[4096];
              strncpy(name, m_selectedWeapon->name().c_str(), 4096);
              if (ImGui::LabelOverLineEdit("##orpg_weapons_editor_name", "Name:", name, 4096, ImGui::GetContentRegionMax().x / 2 - 16)) {
                m_selectedWeapon->setName(name);
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            // Icon
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 6.f);
            ImGui::BeginGroup();
            {
              ImGui::TextUnformatted("Icon:");
              if (ImGui::ImageButtonEx(ImGui::GetID("##orpg_database_weapons_image"), m_itemButtonTexture->get(), ImVec2(m_itemButtonTexture->size()), {0.f, 0.f}, {1.f, 1.f}, {}, {1.f, 1.f, 1.f, 1.f},
                                       ImGuiButtonFlags_PressedOnDoubleClick)) {
                m_currentSheet = &m_itemSheet.value();
                // m_characterPicker->setCharacterInfo("", m_selectedItem->iconIndex);
                // m_characterPicker->setOpen(true);
              }
            }
            ImGui::EndGroup();
            // Description
            ImGui::BeginGroup();
            {
              char description[4096];
              strncpy(description, m_selectedWeapon->description().c_str(), 4096);
              ImGui::Text("Description:");
              ImGui::InputTextMultiline("##orpg_database_weapons_description", description, 4096, ImVec2{360, 60});
            }
            ImGui::EndGroup();
            // Weapon Type
            ImGui::BeginGroup();
            {
              ImGui::Text("Weapon Type:");
              ImGui::SetNextItemWidth(170);
              if (ImGui::BeginCombo("##orpg_database_weapons_wtype", Database::instance()->system.weaponType(m_selectedWeapon->wtypeId()).c_str())) {
                int index{0};
                for (auto v : Database::instance()->system.weaponTypes()) {
                  bool selected = index == m_selectedWeapon->wtypeId();
                  if (index == 0) {
                    if (ImGui::Selectable(std::format("None##_{}", index).c_str(), selected)) {
                      m_selectedWeapon->setWtypeId(index);
                    }
                  } else {
                    if (ImGui::Selectable(Database::instance()->system.weaponType(index) == "" ? std::format("#{:02}", index).c_str() : std::format("{}##_#{}", v, index).c_str(), selected)) {
                      m_selectedWeapon->setWtypeId(index);
                    }
                  }
                  index++;
                }
                ImGui::EndCombo();
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 6.f);
            // Price
            ImGui::BeginGroup();
            {
              ImGui::Text("Price:");
              ImGui::SetNextItemWidth(170);
              int price = m_selectedWeapon->price();
              if (ImGui::InputInt("##orpg_database_weapons_price", &price, 1, 100)) {
                m_selectedWeapon->setPrice(std::clamp(price, 0, 999999));
              }
            }
            ImGui::EndGroup();
            ImGui::BeginGroup();
            {
              ImGui::Text("Animation:");
              ImGui::SetNextItemWidth(160);
              // Animation Button
              ImGui::PushID("##orpg_database_weapon_animation");
              if (ImGui::Button(m_selectedWeapon->animationId() == -1  ? "Normal Attack"
                                : m_selectedWeapon->animationId() == 0 ? "None"
                                                                       : Database::instance()->animationName(m_selectedWeapon->animationId()).c_str(),
                                ImVec2{200 - 15, 0})) {
                m_animationPicker = ObjectPicker("Animation"sv, Database::instance()->animations.animations(), m_selectedWeapon->animationId());
                m_animationPicker->setOpen(true);
              }
              ImGui::PopID();
            }
            ImGui::EndGroup();
          }
          ImGui::EndGroup();
          ImGui::BeginGroup();
          {
            ImGui::EndGroup();
            ImGui::SeparatorText("Parameter Changes");
            ImGui::BeginGroup();
            {
              ImGui::Text("Attack:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(0);
              if (ImGui::InputInt("##orpg_database_weapon_attack", &param, 1, 100)) {
                m_selectedWeapon->setParam(0, std::clamp(param, -999, 999));
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 6.f);
            ImGui::BeginGroup();
            {
              ImGui::Text("Defense:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(1);
              if (ImGui::InputInt("##orpg_database_weapon_defense", &param, 1, 100)) {
                m_selectedWeapon->setParam(1, std::clamp(param, -999, 999));
              }
            }
            ImGui::EndGroup();
            ImGui::BeginGroup();
            {
              ImGui::Text("M.Attack:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(2);
              if (ImGui::InputInt("##orpg_database_weapon_mattack", &param, 1, 100)) {
                m_selectedWeapon->setParam(2, std::clamp(param, -999, 999));
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 6.f);
            ImGui::BeginGroup();
            {
              ImGui::Text("M.Defense:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(3);
              if (ImGui::InputInt("##orpg_database_weapon_mdefense", &param, 1, 100)) {
                m_selectedWeapon->setParam(3, std::clamp(param, -999, 999));
              }
            }
            ImGui::EndGroup();
            ImGui::BeginGroup();
            {
              ImGui::Text("Agility:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(4);
              if (ImGui::InputInt("##orpg_database_weapon_agility", &param, 1, 100)) {
                m_selectedWeapon->setParam(4, std::clamp(param, -999, 999));
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 6.f);
            ImGui::BeginGroup();
            {
              ImGui::Text("Luck:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(5);
              if (ImGui::InputInt("##orpg_database_weapon_luck", &param, 1, 100)) {
                m_selectedWeapon->setParam(5, std::clamp(param, -999, 999));
              }
            }
            ImGui::EndGroup();
            ImGui::BeginGroup();
            {
              ImGui::Text("Max HP:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(6);
              if (ImGui::InputInt("##orpg_database_weapon_maxhp", &param, 1, 100)) {
                m_selectedWeapon->setParam(6, std::clamp(param, -9999, 9999));
              }
            }
            ImGui::EndGroup();
            ImGui::SameLine();
            ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 6.f);
            ImGui::BeginGroup();
            {
              ImGui::Text("Max MP:");
              ImGui::SetNextItemWidth(170);
              int param = m_selectedWeapon->param(7);
              if (ImGui::InputInt("##orpg_database_weapon_maxmp", &param, 1, 100)) {
                m_selectedWeapon->setParam(7, std::clamp(param, -9999, 9999));
              }
            }
            ImGui::EndGroup();
          }
        }
        ImGui::EndChild();
        ImGui::SameLine();
        ImGui::BeginChild("##orpg_weapons_weapon_panel_right");
        { m_traitsEditor.draw(m_parent); }
        ImGui::EndChild();
        ImGui::BeginChild("##orpg_weapons_weapon_panel_bottomright");
        {
          ImGui::SeparatorText("Note:");
          char note[8192];
          strncpy(note, m_selectedWeapon->note().c_str(), IM_ARRAYSIZE(note));
          if (ImGui::InputTextMultiline(std::format("##orpg_database_weapon_note{}", m_selectedWeapon->id()).c_str(), note, IM_ARRAYSIZE(note), ImVec2{ImGui::GetContentRegionMax().x - 16, 400})) {
            m_selectedWeapon->setNote(note);
          }
        }
        ImGui::EndChild();
      }

      ImGui::EndChild();
    }
    ImGui::EndChild();

    if (m_changeIntDialogOpen) {
      if (ImGui::Begin("Change Max Weapons", &m_changeIntDialogOpen,
                       ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_Modal | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) {
        ImGui::Text(
            "Specify an amount to resize the weapon list to\n"
            "This cannot be undone!");
        ImGui::InputInt("##value_input", &m_editMaxWeapons);
        ImGui::SameLine();
        if (ImGui::Button("Accept")) {
          m_changeConfirmDialogOpen = true;
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) {
          m_changeIntDialogOpen = false;
        }
      }
      ImGui::End();

      if (m_changeConfirmDialogOpen) {
        if (ImGui::Begin("Confirm Change", &m_changeConfirmDialogOpen,
                         ImGuiWindowFlags_NoResize | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) {
          ImGui::Text("Are you sure?");
          if (ImGui::Button("Yes")) {
            const int tmpId = m_selectedWeapon->id();
            m_maxWeapons = m_editMaxWeapons;
            m_weapons.resize(m_maxWeapons);
            m_selectedWeapon = m_weapons.weapon(tmpId);
            m_changeIntDialogOpen = false;
            m_changeConfirmDialogOpen = false;
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel")) {
            m_editMaxWeapons = m_maxWeapons;
            m_changeIntDialogOpen = false;
            m_changeConfirmDialogOpen = false;
          }
        }
        ImGui::End();
      }
    }
  }
}


code:
#pragma once
#include "Core/CommonUI/TraitsEditor.hpp"
#include "Core/DatabaseEditor/IDBEditorTab.hpp"
#include "Core/Graphics/CheckeredCompositeTexture.hpp"
#include "Core/Graphics/IconSheet.hpp"
#include "DBCommonEventsTab.hpp"
#include "Database/Animation.hpp"
#include "Database/Weapons.hpp"
struct Weapons;
struct DBWeaponsTab : IDBEditorTab {
  DBWeaponsTab() = delete;
  explicit DBWeaponsTab(Weapons& weapons, DatabaseEditor* parent);
  void draw() override;

  [[nodiscard]] std::vector<Weapon>& weapons() { return m_weapons.weapons(); }
  [[nodiscard]] const std::vector<Weapon>& weapons() const { return m_weapons.weapons(); }
  [[nodiscard]] Weapon* weapon(int id) { return m_weapons.weapon(id); }
  [[nodiscard]] const Weapon* weapon(int id) const { return m_weapons.weapon(id); }
  std::vector<int>& getHeaders() override { return m_headers; }
  int getHeader(int index) override { return m_headers.at(index); }
  bool hasHeader() override { return !m_headers.empty(); }
  void setHeaderRange(int start, int end) override {
    m_categoryStart = start;
    m_categoryEnd = end;
  }
  std::string getName(int index) override { return m_weapons.weapon(index)->name(); }
  int getCount() override { return m_weapons.count(); }

private:
  int m_categoryStart;
  int m_categoryEnd;
  std::vector<int> m_headers;
  Weapons& m_weapons;
  Weapon* m_selectedWeapon{};
  int m_maxWeapons{};
  int m_editMaxWeapons{};
  float m_splitterWidth = 300.f;
  bool m_changeIntDialogOpen = false;
  bool m_changeConfirmDialogOpen = false;
  IconSheet* m_currentSheet = nullptr;
  std::optional<CheckeredCompositeTexture> m_itemButtonTexture;
  std::optional<IconSheet> m_itemSheet;
  std::optional<IconSheet> m_itemPicker;
  TraitsEditor m_traitsEditor;
  std::optional<ObjectPicker<Animation>> m_animationPicker;
};

repo_name: Antidote/openrpgeditor

license: mit


code:
#pragma once

#include <string>
#include <vector>

class DatabaseEditor;
class IDBEditorTab {
public:
  explicit IDBEditorTab(DatabaseEditor* parent)
  : m_parent(parent) {}
  virtual ~IDBEditorTab() = default;
  virtual void draw() = 0;
  virtual int getHeader(int index) = 0;
  virtual std::vector<int>& getHeaders() = 0;
  virtual bool hasHeader() = 0;
  virtual void setHeaderRange(int start, int end) = 0;
  virtual std::string getName(int index) = 0;
  virtual int getCount() = 0;

  constexpr std::vector<std::string_view> getUnicodeFormatters() { return {"\u0D9E", "\u21C4", "\u2318", "\u2248", "\u221E", "\u25bc"}; }
  bool hasUnicodeFormatting(const std::string& text) { return text.contains("\u25bc"); }

  /* TODO: Make pure */
  virtual bool isModified() { return false; };

protected:
  DatabaseEditor* m_parent;
};


code:
#pragma once

#include <algorithm>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>
#include <thread>
#include <utility>

#include "Core/Log.hpp"

namespace App::Debug {

using FloatingPointMicroseconds = std::chrono::duration<double, std::micro>;

struct ProfileResult {
  std::string name;
  FloatingPointMicroseconds start;
  std::chrono::microseconds elapsed_time;
  std::thread::id thread_id;
};

struct InstrumentationSession {
  const std::string name;
  explicit InstrumentationSession(std::string session_name)
  : name(std::move(session_name)) {}
};

class Instrumentor {
public:
  Instrumentor(const Instrumentor&) = delete;
  Instrumentor(Instrumentor&&) = delete;
  Instrumentor& operator=(Instrumentor other) = delete;
  Instrumentor& operator=(Instrumentor&& other) = delete;

  void begin_session(const std::string& name, const std::string& filepath = "results.json") {
    const std::lock_guard lock(m_mutex);

    if (m_current_session != nullptr) {
      // If there is already a current session, then close it before beginning new one.
      // Subsequent profiling output meant for the original session will end up in the
      // newly opened session instead.  That's better than having badly formatted
      // profiling output.
      APP_ERROR("Instrumentor::begin_session('{0}') when session '{1}' already open.", name, m_current_session->name);
      internal_end_session();
    }
    m_output_stream.open(filepath);

    if (m_output_stream.is_open()) {
      m_current_session = std::make_unique<InstrumentationSession>(name);
      write_header();
    } else {
      APP_ERROR("Instrumentor could not open results file '{0}'.", filepath);
    }
  }

  void end_session() {
    const std::lock_guard lock(m_mutex);
    internal_end_session();
  }

  void write_profile(const ProfileResult& result) {
    std::stringstream json;

    std::string name{result.name};
    std::replace(name.begin(), name.end(), '"', '\'');

    json << std::setprecision(3) << std::fixed;
    json << ",{";
    json << R"("cat":"function",)";
    json << "\"dur\":" << (result.elapsed_time.count()) << ',';
    json << R"("name":")" << name << "\",";
    json << R"("ph":"X",)";
    json << "\"pid\":0,";
    json << R"("tid":")" << result.thread_id << "\",";
    json << "\"ts\":" << result.start.count();
    json << "}";

    const std::lock_guard lock(m_mutex);
    if (m_current_session != nullptr) {
      m_output_stream << json.str();
      m_output_stream.flush();
    }
  }

  static Instrumentor& get() {
    static Instrumentor instance;
    return instance;
  }

private:
  Instrumentor()
  : m_current_session(nullptr) {}

  ~Instrumentor() { end_session(); }

  void write_header() {
    m_output_stream << R"({"otherData": {},"traceEvents":[{})";
    m_output_stream.flush();
  }

  void write_footer() {
    m_output_stream << "]}";
    m_output_stream.flush();
  }

  // Note: you must already own lock on m_Mutex before
  // calling InternalEndSession()
  void internal_end_session() {
    if (m_current_session != nullptr) {
      write_footer();
      m_output_stream.close();
    }
  }

  std::mutex m_mutex;
  std::unique_ptr<InstrumentationSession> m_current_session;
  std::ofstream m_output_stream;
};

class InstrumentationTimer {
public:
  explicit InstrumentationTimer(std::string name)
  : m_name(std::move(name))
  , m_start_time_point(std::chrono::steady_clock::now()) {}

  InstrumentationTimer(const InstrumentationTimer&) = delete;
  InstrumentationTimer(InstrumentationTimer&&) = delete;
  InstrumentationTimer& operator=(InstrumentationTimer other) = delete;
  InstrumentationTimer& operator=(InstrumentationTimer&& other) = delete;

  ~InstrumentationTimer() {
    if (!m_stopped) {
      stop();
    }
  }

  void stop() {
    const auto end_time_point{std::chrono::steady_clock::now()};
    const auto high_res_start{FloatingPointMicroseconds{m_start_time_point.time_since_epoch()}};
    const auto elapsed_time{std::chrono::time_point_cast<std::chrono::microseconds>(end_time_point).time_since_epoch() -
                            std::chrono::time_point_cast<std::chrono::microseconds>(m_start_time_point).time_since_epoch()};

    Instrumentor::get().write_profile({m_name, high_res_start, elapsed_time, std::this_thread::get_id()});

    m_stopped = true;
  }

private:
  const std::string m_name;
  bool m_stopped{false};
  const std::chrono::time_point<std::chrono::steady_clock> m_start_time_point;
};

} // namespace App::Debug

#if APP_PROFILE
// Resolve which function signature macro will be used. Note that this only
// is resolved when the (pre)compiler starts, so the syntax highlighting
// could mark the wrong one in your editor!
#if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || (defined(__ICC) && (__ICC >= 600)) || defined(__ghs__)
// There is an implicit decay of an array into a pointer.
// NOLINTNEXTLINE
#define APP_FUNC_SIG __PRETTY_FUNCTION__
#elif defined(__DMC__) && (__DMC__ >= 0x810)
#define APP_FUNC_SIG __PRETTY_FUNCTION__
#elif defined(__FUNCSIG__)
#define APP_FUNC_SIG __FUNCSIG__
#elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || (defined(__IBMCPP__) && (__IBMCPP__ >= 500))
#define APP_FUNC_SIG __FUNCTION__
#elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550)
#define APP_FUNC_SIG __FUNC__
#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901)
#define APP_FUNC_SIG __func__
#elif defined(__cplusplus) && (__cplusplus >= 201103)
#define APP_FUNC_SIG __func__
#else
#define APP_FUNC_SIG "APP_FUNC_SIG unknown!"
#endif

#define JOIN_AGAIN(x, y) x##y
#define JOIN(x, y) JOIN_AGAIN(x, y)
#define APP_PROFILE_BEGIN_SESSION(name) ::App::Debug::Instrumentor::get().begin_session(name)
#define APP_PROFILE_BEGIN_SESSION_WITH_FILE(name, file_path) ::App::Debug::Instrumentor::get().begin_session(name, file_path)
#define APP_PROFILE_END_SESSION() ::App::Debug::Instrumentor::get().end_session()
#define APP_PROFILE_SCOPE(name)                                                                                \
  const ::App::Debug::InstrumentationTimer JOIN(timer, __LINE__) { name }
#define APP_PROFILE_FUNCTION() APP_PROFILE_SCOPE(APP_FUNC_SIG)
#else
#define APP_PROFILE_BEGIN_SESSION(name)
#define APP_PROFILE_BEGIN_SESSION_WITH_FILE(name, file_path)
#define APP_PROFILE_END_SESSION()
#define APP_PROFILE_SCOPE(name)
#define APP_PROFILE_FUNCTION()
#endif


code:
#include "Core/DirectoryMonitor.hpp"
#include <filesystem>
#include <ranges>
#include <unordered_map>

void DirectoryMonitor::watchDirectory() {
  std::unordered_map<std::filesystem::path, std::filesystem::file_time_type> lastWriteTimes;

  while (m_running) {
    {
      std::unique_lock lock(m_mutex);
      m_cv.wait_for(lock, std::chrono::seconds(1), [this] { return !m_running; });
    }

    if (!m_running) {
      break;
    }

    if (!std::filesystem::exists(m_watchDirectory)) {
      continue;
    }

    for (const auto& entry : std::filesystem::recursive_directory_iterator(m_watchDirectory)) {
      auto lastWriteTime = last_write_time(entry);
      auto it = lastWriteTimes.find(entry);
      if (it == lastWriteTimes.end()) {
        if (is_directory(entry)) {
          emit_signal(m_directoryAdded, entry);
        } else if (is_regular_file(entry)) {
          emit_signal(m_fileAdded, entry);
        }
      } else if (it->second != lastWriteTime) {
        if (is_directory(entry)) {
          emit_signal(m_directoryChanged, entry.path());
        } else if (is_regular_file(entry)) {
          emit_signal(m_fileChanged, entry);
        }
      }

      lastWriteTimes[entry.path()] = lastWriteTime;
    }

    for (auto it = lastWriteTimes.begin(); it != lastWriteTimes.end();) {
      if (!is_regular_file(it->first) && !is_directory(it->first)) {
        if (!is_regular_file(it->first)) {
          emit_signal(m_fileDeleted, it->first);
          it = lastWriteTimes.erase(it);
        } else if (!is_directory(it->first)) {
          emit_signal(m_directoryDeleted, it->first);
          it = lastWriteTimes.erase(it);
        } else {
          ++it;
        }
      } else {
        ++it;
      }
    }
  }
}


code:
#pragma once
#include "Utils/SignalSlot.hpp"

#include <condition_variable>
#include <filesystem>
#include <string_view>

class DirectoryMonitor {
public:
  ~DirectoryMonitor() { stop(); }
  void setWatchDirectory(const std::string_view watchDirectory) {
    std::unique_lock __(m_mutex);
    m_watchDirectory = watchDirectory;
  }

  void start() {
    m_running = true;
    m_watchDirectoryThread = std::thread(&DirectoryMonitor::watchDirectory, this);
  }

  void stop() {
    m_running = false;
    m_cv.notify_one();

    if (m_watchDirectoryThread.joinable()) {
      m_watchDirectoryThread.join();
    }
  }

  rpgmutils::signal<void(const std::filesystem::path&)>& fileChanged() { return m_fileChanged; }
  rpgmutils::signal<void(const std::filesystem::path&)>& fileAdded() { return m_fileAdded; }
  rpgmutils::signal<void(const std::filesystem::path&)>& fileDeleted() { return m_fileDeleted; }
  rpgmutils::signal<void(const std::filesystem::path&)>& directoryChanged() { return m_directoryChanged; }
  rpgmutils::signal<void(const std::filesystem::path&)>& directoryAdded() { return m_directoryAdded; }
  rpgmutils::signal<void(const std::filesystem::path&)>& directoryDeleted() { return m_directoryDeleted; }

private:
  void watchDirectory();
  std::string m_watchDirectory;
  rpgmutils::signal<void(const std::filesystem::path&)> m_fileChanged;
  rpgmutils::signal<void(const std::filesystem::path&)> m_fileAdded;
  rpgmutils::signal<void(const std::filesystem::path&)> m_fileDeleted;
  rpgmutils::signal<void(const std::filesystem::path&)> m_directoryChanged;
  rpgmutils::signal<void(const std::filesystem::path&)> m_directoryAdded;
  rpgmutils::signal<void(const std::filesystem::path&)> m_directoryDeleted;
  std::mutex m_mutex;
  std::condition_variable m_cv;
  bool m_running = false;
  std::thread m_watchDirectoryThread;
};


code:
#include "Core/DownloadManager.hpp"

#include "EventCommands/Dialog_EnemyRecoverAll.hpp"

// TODO: For some reason this doesn't work when multiple downloads are active, there is also a random crash on exit related to the destination file handle
int writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
  const auto entry = static_cast<DownloadManager::DownloadEntry*>(userdata);
  const auto written = fwrite(ptr, size, nmemb, entry->fileHandle());
  fflush(entry->fileHandle());
  entry->addBytesDownloaded(written);
  return written;
}

DownloadManager::DownloadEntry::DownloadEntry(const std::string_view url, const std::string_view destination)
: m_url(url)
, m_destinationPath(destination) {
  m_destination = fopen(m_destinationPath.data(), "wb");
  m_destinationPath = destination;
  m_curlHandle = curl_easy_init();

  if (m_curlHandle) {
    curl_easy_setopt(m_curlHandle, CURLOPT_URL, m_url.data());
    curl_easy_setopt(m_curlHandle, CURLOPT_WRITEFUNCTION, writeCallback);
    curl_easy_setopt(m_curlHandle, CURLOPT_WRITEDATA, this);
    curl_easy_setopt(m_curlHandle, CURLOPT_NOPROGRESS, 0L);
    curl_easy_setopt(m_curlHandle, CURLOPT_XFERINFOFUNCTION, progressCallback);
    curl_easy_setopt(m_curlHandle, CURLOPT_XFERINFODATA, this);
  }
}

void DownloadManager::DownloadEntry::updateInfo(const curl_off_t dltotal, const curl_off_t dlnow) {
  if (!m_bytesTotal && dltotal) {
    m_bytesTotal = dltotal;
  }
  // printf("%s %ld %ld\n", m_url.data(), dltotal, dlnow);
}

int DownloadManager::DownloadEntry::progressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
  const auto self = static_cast<DownloadEntry*>(clientp);
  self->updateInfo(dltotal, dlnow);
  return CURLE_OK;
}

DownloadManager::DownloadEntry::~DownloadEntry() {
  curl_easy_cleanup(m_curlHandle);
  m_curlHandle = nullptr;
  if (m_destination) {
    fclose(m_destination);
  }
  m_destination = nullptr;
}

DownloadManager::DownloadManager() {
  curl_global_init(CURL_GLOBAL_DEFAULT);
  m_multiHandle = curl_multi_init();
}

DownloadManager::~DownloadManager() {
  for (const auto& handle : m_curlHandles) {
    curl_multi_remove_handle(m_multiHandle, handle.handle());
  }
  m_curlHandles.clear();
  curl_multi_cleanup(m_multiHandle);
  curl_global_cleanup();
}

int DownloadManager::addDownload(const std::string_view url, const std::string_view destination) {
  m_curlHandles.emplace_back(url, destination);
  curl_multi_add_handle(m_multiHandle, m_curlHandles.back().handle());
  return m_curlHandles.size() - 1;
}

void DownloadManager::processDownloads() {
  int stillRunning;
  do {
    curl_multi_perform(m_multiHandle, &stillRunning);
  } while (stillRunning);

  CURLMsg* msg = nullptr;
  while ((msg = curl_multi_info_read(m_multiHandle, &stillRunning))) {
    if (msg->msg == CURLMSG_DONE) {
      if (auto it = std::ranges::find_if(m_curlHandles, [&msg](const DownloadEntry& entry) { return msg->easy_handle == entry.handle(); }); it != m_curlHandles.end()) {
        it->setCompleted();
        curl_multi_remove_handle(m_multiHandle, it->handle());
      }
    }
  }

  if (std::ranges::all_of(m_curlHandles, [](const DownloadEntry& entry) { return entry.completed(); })) {
    m_curlHandles.clear();
  }
}


code:
#pragma once

#include <curl/curl.h>
#include <fstream>
#include <string>
#include <vector>

class DownloadManager {
public:
  class DownloadEntry {
  public:
    DownloadEntry(std::string_view url, std::string_view destination);
    ~DownloadEntry();

    CURL* handle() const { return m_curlHandle; }
    std::string_view url() const { return m_url; }
    FILE* fileHandle() const { return m_destination; }
    curl_off_t bytesDownloaded() const { return m_bytesDownloaded; }
    curl_off_t bytesTotal() const { return m_bytesTotal; }

    double percent() const { return m_bytesTotal > 0 ? static_cast<double>(m_bytesDownloaded) / static_cast<double>(m_bytesTotal) : 0.0; }

    void addBytesDownloaded(const curl_off_t bytesDownloaded) { m_bytesDownloaded += bytesDownloaded; }

    bool completed() const { return m_completed; }
    void setCompleted() { m_completed = true; }

  private:
    friend class DownloadManager;
    void updateInfo(curl_off_t dltotal, curl_off_t dlnow);
    static int progressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
    std::string m_url;
    std::string m_destinationPath;
    FILE* m_destination = nullptr;
    CURL* m_curlHandle = nullptr;
    curl_off_t m_bytesDownloaded{};
    curl_off_t m_bytesTotal{};
    bool m_completed{false};
  };

  DownloadManager();
  ~DownloadManager();

  int addDownload(std::string_view url, std::string_view destination);

  void processDownloads();

  bool transferComplete(const int handle) const {
    if (handle < 0 || handle >= m_curlHandles.size()) {
      return true;
    }
    return m_curlHandles[handle].completed();
  }

  const std::vector<DownloadEntry>& handles() const { return m_curlHandles; }

private:
  CURLM* m_multiHandle;
  std::vector<DownloadEntry> m_curlHandles;
  bool m_transferComplete{false};
};


code:
#include "Core/EditorPlugin/EditorPlugin.hpp"
#include "Core/Script/ScriptEngine.hpp"

#include "Core/Script/ByteCodeStream.hpp"
#include "angelscript.h"

#include <nlohmann/json.hpp>

bool EditorPlugin::load(const std::filesystem::path& pluginBasedir, const PluginInfo& info) {
  std::filesystem::path binFilename = info.mainScript;
  binFilename.replace_extension("plug");
  std::filesystem::path scriptFilename = info.mainScript;
  scriptFilename.replace_extension("as");

  bool saveBinary = true;
  // if (info.compiled || exists(pluginBasedir / "bin" / binFilename)) {
  //   ByteCodeReader reader((pluginBasedir / "bin" / binFilename).generic_string());
  //   m_module = ScriptEngine::instance()->createModule(info.identifier, reader);
  // } else {
  //   saveBinary = true;
  // }
  if (!m_module && exists(pluginBasedir / "src" / scriptFilename)) {
    m_module = ScriptEngine::instance()->createModule(info.identifier, pluginBasedir / "src" / scriptFilename);
    if (saveBinary && m_module) {
      ByteCodeWriter writer((pluginBasedir / "bin" / binFilename).generic_string());
      m_module->SaveByteCode(&writer);
    }
  }

  if (m_module) {
    m_initializeFunction = m_module->GetFunctionByDecl("void initialize()");
    m_drawFunction = m_module->GetFunctionByDecl("void draw()");
    m_shutdownFunction = m_module->GetFunctionByDecl("void shutdown()");
    return true;
  }
  return false;
}

void EditorPlugin::callInitialize() {
  if (!m_initializeFunction) {
    return;
  }
  m_module->BindAllImportedFunctions();
  if (!m_initialized) {
    if (const auto ctx = ScriptEngine::instance()->prepareContextFromPool(m_initializeFunction)) {
      ScriptEngine::executeCall(ctx);
      ScriptEngine::instance()->returnContextToPool(ctx);
    }
    m_initialized = true;
  }
}
void EditorPlugin::callDraw() const {
  if (!m_drawFunction || !m_initialized) {
    return;
  }
  if (const auto ctx = ScriptEngine::instance()->prepareContextFromPool(m_drawFunction)) {
    ScriptEngine::executeCall(ctx);
    ScriptEngine::instance()->returnContextToPool(ctx);
  }
}

void EditorPlugin::callShutdown() {
  m_initialized = false;
  if (!m_shutdownFunction) {
    return;
  }
  if (const auto ctx = ScriptEngine::instance()->prepareContextFromPool(m_shutdownFunction)) {
    ScriptEngine::executeCall(ctx);
    ScriptEngine::instance()->returnContextToPool(ctx);
  }
}

void to_json(nlohmann::ordered_json& j, const EditorPlugin::PluginInfo& p) {
  j = {
      {"name", p.name},
      {"author", p.author},
      {"email", p.email},
      {"identifier", p.identifier},
      {"license", p.license},
      {"version", p.version},
      {"description", p.description},
      {"compiled", p.compiled},
      {"created", p.dateCreated},
      {"last-modified", p.lastModified},
      {"main-script", p.mainScript},
      {"dependencies", p.dependencies},
  };
}

void from_json(const nlohmann::ordered_json& j, EditorPlugin::PluginInfo& p) {
  p.name = j.value("name", p.name);
  p.author = j.value("author", p.author);
  p.email = j.value("email", p.email);
  p.identifier = j.value("identifier", p.identifier);
  p.license = j.value("license", p.license);
  p.version = j.value("version", p.version);
  p.description = j.value("description", p.description);
  p.compiled = j.value("compiled", p.compiled);
  p.dateCreated = j.value("created", p.dateCreated);
  p.lastModified = j.value("last-modified", p.lastModified);
  p.mainScript = j.value("main-script", p.mainScript);
  p.dependencies = j.value("dependencies", p.dependencies);
}


code:
#pragma once

#include <filesystem>
#include <nlohmann/json_fwd.hpp>
#include <string>
#include <unordered_map>
class asITypeInfo;
class asIScriptFunction;
class asIScriptModule;
class asIScriptObject;
class EditorPlugin {
public:
  struct PluginInfo {
    friend void to_json(nlohmann::ordered_json& j, const PluginInfo& p);
    friend void from_json(const nlohmann::ordered_json& j, PluginInfo& p);
    std::string name;
    std::string author;
    std::string email;
    std::string identifier;
    std::string license;
    std::string description;
    bool compiled;
    std::string dateCreated;
    std::string lastModified;
    std::string mainScript;
    std::string version;
    std::vector<std::string> dependencies;

    std::unordered_map<std::filesystem::path, std::filesystem::path> files; // absolute->relative
  };

  EditorPlugin() = default;

  bool load(const std::filesystem::path& pluginBasedir, const PluginInfo& info);

  void callInitialize();
  void callDraw() const;
  void callShutdown();

private:
  asIScriptModule* m_module = nullptr;
  asITypeInfo* m_typeInfo = nullptr;
  asIScriptObject* m_object = nullptr;
  asIScriptFunction* m_initializeFunction = nullptr;
  asIScriptFunction* m_drawFunction = nullptr;
  asIScriptFunction* m_shutdownFunction = nullptr;
  bool m_initialized = false;
};


code:
#include "Core/EditorPlugin/EditorPluginManager.hpp"

#include "Core/Application.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "Core/Script/ScriptEngine.hpp"

#include "misc/cpp/imgui_stdlib.h"

#include <iostream>

static const std::vector<std::filesystem::path> kFileBlacklist = {
    ".gitkeep",
    "meta.json",
};

EditorPluginManager* EditorPluginManager::instance() {
  static EditorPluginManager pluginManager;
  return &pluginManager;
}

EditorPluginManager::EditorPluginManager() {
  m_pluginBaseDir = std::filesystem::path(App::APP->userConfigPath()) / "plugins";
  m_directoryMonitor.setWatchDirectory(m_pluginBaseDir.generic_string());
  m_directoryMonitor.directoryAdded().connect<&EditorPluginManager::onDirectoryAdded>(this);
  m_directoryMonitor.directoryChanged().connect<&EditorPluginManager::onDirectoryModified>(this);
  m_directoryMonitor.directoryDeleted().connect<&EditorPluginManager::onDirectoryDeleted>(this);
  m_directoryMonitor.fileAdded().connect<&EditorPluginManager::onFileAdded>(this);
  m_directoryMonitor.fileChanged().connect<&EditorPluginManager::onFileModified>(this);
  m_directoryMonitor.fileDeleted().connect<&EditorPluginManager::onFileDeleted>(this);
  m_directoryMonitor.start();
  // populatePlugins();
}

static bool PluginLoaded(const std::string& identifier) { return EditorPluginManager::instance()->pluginLoaded(identifier); }

void EditorPluginManager::draw() {
  if (!m_open) {
    return;
  }

  const auto calc = ImGui::CalcTextSize("ABCDEFGHIJKLMNOPQRSTUV");
  // ImGui::SetNextWindowSizeConstraints({640.f, 640.f}, {FLT_MAX, FLT_MAX});
  if (ImGui::Begin(trNOOP("Plugin Manager"), &m_open)) {
    ImGui::BeginChild("##plugins_left_panel", {calc.x, 0}, ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX);
    {
      ImGui::TextUnformatted(trNOOP("Plugins"));
      ImGui::Separator();
      for (const auto& plugin : m_plugins | std::views::values) {
        if (ImGui::Selectable(std::format("{}##plugin_{}", plugin.name, plugin.identifier).c_str(), plugin.identifier == m_selectedIdentifier)) {
          m_selectedIdentifier = plugin.identifier;
        }
        ImGui::SetItemTooltip("%s", plugin.identifier.c_str());
      }
    }
    ImGui::EndChild();
    ImGui::SameLine();
    ImGui::BeginChild("##plugins_middle_panel", {calc.x, 0}, ImGuiChildFlags_Border | ImGuiChildFlags_ResizeX);
    {
      ImGui::TextUnformatted(trNOOP("File Listing"));
      ImGui::Separator();
      if (m_pluginInfoIdentifierMapping.contains(m_selectedIdentifier)) {
        const auto& plugin = m_pluginInfoIdentifierMapping.at(m_selectedIdentifier);
        for (const auto& file : plugin->files | std::views::values) {
          ImGui::Selectable(file.generic_string().c_str());
        }
      }
    }
    ImGui::EndChild();
    ImGui::SameLine();
    ImGui::BeginChild("##plugins_right_panel");
    {
      ImGui::TextUnformatted(trNOOP("Information"));
      ImGui::Separator();
      if (m_pluginInfoIdentifierMapping.contains(m_selectedIdentifier)) {
        auto& plugin = m_pluginInfoIdentifierMapping.at(m_selectedIdentifier);
        char tmp[4096]{};
        strncpy(tmp, plugin->name.c_str(), 4096);
        if (ImGui::LabelOverLineEdit("##plugin_name", trNOOP("Name"), tmp, 4096, ImGui::CalcTextSize("ABCDEFGKLMNOPQRSTUVWXYZ01").x)) {
          plugin->name = tmp;
        }

        memset(tmp, 0, sizeof(tmp));
        strncpy(tmp, plugin->author.c_str(), 4096);
        if (ImGui::LabelOverLineEdit("##plugin_author", trNOOP("Author"), tmp, 4096, ImGui::CalcTextSize("ABCDEFGKLMNOPQRSTUVWXYZ01").x)) {
          plugin->author = tmp;
        }

        memset(tmp, 0, sizeof(tmp));
        strncpy(tmp, plugin->email.c_str(), 4096);
        if (ImGui::LabelOverLineEdit("##plugin_author_email", trNOOP("Email"), tmp, 4096, ImGui::CalcTextSize("ABCDEFGKLMNOPQRSTUVWXYZ01").x)) {
          plugin->email = tmp;
        }

        // TODO(phil): Properly display dates
        ImGui::TextUnformatted(trNOOP("Date Created"));
        ImGui::TextUnformatted(plugin->dateCreated.c_str());
        ImGui::TextUnformatted(trNOOP("Last Modified"));
        ImGui::TextUnformatted(plugin->lastModified.c_str());

        ImGui::TextUnformatted(trNOOP("Description"));
        ImGui::InputTextMultiline("##plugin_description", &plugin->description, ImGui::GetContentRegionAvail() - ImGui::GetStyle().FramePadding);
      } else {
        ImGui::TextUnformatted(trNOOP("Select a Plugin"));
      }
    }
    ImGui::EndChild();
  }
  ImGui::End();
}
bool EditorPluginManager::pluginLoaded(const std::string& identifier) {
  auto it = m_plugins.begin();
  for (; it != m_plugins.end(); ++it) {
    if (it->second.identifier == identifier) {
      break;
    }
  }
  if (it != m_plugins.end()) {
    return m_pluginInstances.contains(it->first);
  }
  return false;
}
bool EditorPluginManager::initialize() {
  int ret = ScriptEngine::instance()->engine()->RegisterGlobalFunction("bool pluginAvailable(const string& in)", asFUNCTION(PluginLoaded), asCALL_CDECL);
  return ret >= asSUCCESS;
}

void EditorPluginManager::initializeAllPlugins() {
  for (auto& plugin : m_pluginInstances | std::views::values) {
    plugin.callInitialize();
  }
}

void EditorPluginManager::initializePluginById(const std::string_view identifier) {
  if (m_pluginInstanceIdentifierMapping.contains(identifier.data())) {
    m_pluginInstanceIdentifierMapping[identifier.data()]->callInitialize();
  }
}

void EditorPluginManager::callDraws() {
  for (auto& plugin : m_pluginInstances | std::views::values) {
    plugin.callDraw();
  }
}

void EditorPluginManager::shutdownAllPlugins() {
  for (auto& plugin : m_pluginInstances | std::views::values) {
    plugin.callShutdown();
  }
}

void EditorPluginManager::shutdownPluginById(const std::string_view identifier, bool dependencies) {
  if (m_pluginInstanceIdentifierMapping.contains(identifier.data())) {
    auto plugin = m_pluginInstanceIdentifierMapping[identifier.data()];
    /* Remove ourselves from the list so we don't have an infinite loop in case of cyclical dependencies */
    m_pluginInstanceIdentifierMapping.erase(identifier.data());
    if (dependencies) {
      for (const auto& dependency : m_pluginInfoIdentifierMapping[identifier.data()]->dependencies) {
        shutdownPluginById(dependency);
      }
    }
    plugin->callShutdown();
  }
}

void EditorPluginManager::onDirectoryAdded(const std::filesystem::path& path) {
  if (!is_regular_file(path / "meta.json")) {
    return;
  }

  try {
    std::ifstream jsonFile(path / "meta.json");
    nlohmann::ordered_json json = nlohmann::ordered_json::parse(jsonFile);
    EditorPlugin::PluginInfo plugin;
    json.get_to(plugin);

    if (!m_plugins.contains(plugin.identifier)) {
      m_plugins[path] = plugin;
      m_pluginInfoIdentifierMapping[plugin.identifier] = &m_plugins[path];
      Settings::instance()->plugins[plugin.identifier].compiled = plugin.compiled;
      APP_INFO("Added plugin: {}", plugin.name);
      APP_INFO("Path: {}", path.string());
      APP_INFO("Meta json: {}", json.dump());
      registerPluginFiles(path, m_plugins[path]);
      // if (Settings::instance()->plugins[plugin.identifier].enabled)
      {
        m_pluginInstances[path] = EditorPlugin();
        if (!m_pluginInstances[path].load(path, plugin)) {
          APP_ERROR("Failed to load plugin: {}", plugin.identifier);
        }
      }
    } else {
      APP_ERROR("Duplicate plugin identifier: {}", plugin.identifier);
    }
  } catch (std::exception& e) { APP_ERROR("Could not parse meta.json"); }
}

void EditorPluginManager::onDirectoryModified(const std::filesystem::path& path) {}

void EditorPluginManager::onDirectoryDeleted(const std::filesystem::path& path) {}

void EditorPluginManager::onFileAdded(const std::filesystem::path& path) {}

void EditorPluginManager::registerPluginFiles(const std::filesystem::path& path, EditorPlugin::PluginInfo& plugin) {
  for (const auto& entry : std::filesystem::recursive_directory_iterator(path)) {
    const auto& rel = relative(entry.path(), path);
    if (entry.is_regular_file() && std::ranges::find(kFileBlacklist, rel.filename()) == kFileBlacklist.end()) {
      APP_INFO("Registering plugin file: {}", rel.generic_string());
      plugin.files[entry.path()] = rel;
    }
  }
}

void EditorPluginManager::onFileModified(const std::filesystem::path& path) {
  if (path.filename() == "meta.json") {
    try {
      std::ifstream jsonFile(path);
      nlohmann::ordered_json json = nlohmann::ordered_json::parse(jsonFile);
      EditorPlugin::PluginInfo plugin;
      json.get_to(plugin);
      if (m_plugins.contains(path.parent_path())) {
        auto& entry = m_plugins[path.parent_path()];
        const std::string oldIdentifier = entry.identifier;
        entry = plugin;
        if (plugin.identifier != oldIdentifier) {
          if (!m_pluginInfoIdentifierMapping.contains(plugin.identifier)) {
            m_pluginInfoIdentifierMapping.erase(oldIdentifier);
            m_pluginInfoIdentifierMapping[entry.identifier] = &entry;
            Settings::instance()->plugins[entry.identifier].compiled = entry.compiled;
            Settings::instance()->plugins[entry.identifier].enabled = Settings::instance()->plugins[oldIdentifier].enabled;
            Settings::instance()->plugins.erase(oldIdentifier);
            APP_INFO("Added plugin changed identifier: {} {} {} -> {}", path.parent_path().generic_string(), plugin.name, oldIdentifier, plugin.identifier);
          } else if (m_pluginInfoIdentifierMapping.contains(plugin.identifier)) {
            entry.identifier = oldIdentifier;
            Settings::instance()->plugins[entry.identifier].compiled = entry.compiled;
            APP_ERROR("Duplicate plugin meta detected on change! Keeping old identifier");
          }
        } else {
          Settings::instance()->plugins[entry.identifier].compiled = entry.compiled;
        }

        registerPluginFiles(path.parent_path(), entry);
        std::cout << entry.identifier << " modified!" << std::endl;
      }

    } catch (const std::exception& e) { APP_ERROR("Could not parse meta.json"); }
  }
}

void EditorPluginManager::onFileDeleted(const std::filesystem::path& path) {}


code:
#pragma once
#include "Core/DirectoryMonitor.hpp"
#include "Core/EditorPlugin/EditorPlugin.hpp"

#include "angelscript.h"

#include <filesystem>
#include <nlohmann/json_fwd.hpp>
#include <string>
#include <unordered_map>

class EditorPluginManager {
public:
  static EditorPluginManager* instance();

  const std::unordered_map<std::filesystem::path, EditorPlugin::PluginInfo>& plugins() const { return m_plugins; }

  bool initialize();

  bool pluginLoaded(const std::string& identifier);
  void initializeAllPlugins();
  void initializePluginById(const std::string_view identifier);
  void shutdownAllPlugins();
  void shutdownPluginById(const std::string_view identifier, bool dependencies = false);
  void callDraws();

  void draw();

  void setOpen(const bool open = true) { m_open = open; }

private:
  EditorPluginManager();
  EditorPluginManager(const EditorPluginManager&) = delete;
  EditorPluginManager& operator=(const EditorPluginManager&) = delete;
  EditorPluginManager(EditorPluginManager&&) = delete;
  EditorPluginManager& operator=(EditorPluginManager&&) = delete;

  DirectoryMonitor m_directoryMonitor;
  std::unordered_map<std::filesystem::path, EditorPlugin::PluginInfo> m_plugins;
  std::unordered_map<std::string, EditorPlugin::PluginInfo*> m_pluginInfoIdentifierMapping; // identifier->plugin
  std::unordered_map<std::filesystem::path, EditorPlugin> m_pluginInstances;
  std::unordered_map<std::string, EditorPlugin*> m_pluginInstanceIdentifierMapping;
  std::filesystem::path m_pluginBaseDir;
  void onDirectoryAdded(const std::filesystem::path& path);
  void onDirectoryModified(const std::filesystem::path& path);
  void onDirectoryDeleted(const std::filesystem::path& path);
  void onFileAdded(const std::filesystem::path& path);
  void registerPluginFiles(const std::filesystem::path& path, EditorPlugin::PluginInfo& plugin);
  void onFileModified(const std::filesystem::path& path);
  void onFileDeleted(const std::filesystem::path& path);

  bool m_open = false;
  std::string m_selectedIdentifier;
};


code:
#include "Core/EventCommands/Dialog_AbortBattle.hpp"
#include <tuple>

std::tuple<bool, bool> Dialog_AbortBattle::draw() {
  if (isOpen()) {
    setOpen(false);
    m_confirmed = true;
  }
  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/EventCommands/AbortBattle.hpp"

struct Dialog_AbortBattle : IEventDialogController {
  Dialog_AbortBattle() = delete;
  explicit Dialog_AbortBattle(const std::string& name, const std::shared_ptr<AbortBattleCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , command(cmd) {
    if (cmd == nullptr) {
      command.reset(new AbortBattleCommand());
    }
    m_open = false;
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return command; };

private:
  bool m_confirmed{true};
  std::shared_ptr<AbortBattleCommand> command;
  std::tuple<bool, bool> result;
};


code:
#include "Core/EventCommands/Dialog_BattleProcessing.hpp"
#include "Core/CommonUI/GroupBox.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"

#include <Database/Database.hpp>

#include <imgui.h>
#include <imgui_internal.h>
#include <tuple>

std::tuple<bool, bool> Dialog_BattleProcessing::draw() {
  if (isOpen()) {
    ImGui::OpenPopup("###BattleProcessing");
  }
  const auto maxSize = ImVec2{(ImGui::CalcTextSize("#").x * 40) + (ImGui::GetStyle().FramePadding.x * 2), (ImGui::GetTextLineHeightWithSpacing() * 12) + (ImGui::GetStyle().FramePadding.y * 2)};
  ImGui::SetNextWindowSize(maxSize, ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(maxSize, {FLT_MAX, FLT_MAX});
  if (ImGui::BeginPopupModal(std::format("{}###BattleProcessing", m_name).c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {
    drawPickers();
    ImGui::BeginVertical("##battle_processing_main_layout", ImGui::GetContentRegionAvail(), 0);
    {
      GroupBox troopGroupBox(trNOOP("Troop"), "##battle_processing_troop_group_box", {-1, 0});
      if (troopGroupBox.begin()) {
        ImGui::RadioButton(trNOOP("Direct designation"), &m_type, 0);
        ImGui::Dummy({ImGui::GetFrameHeightWithSpacing(), ImGui::GetFrameHeightWithSpacing()});
        ImGui::SameLine();
        ImGui::BeginDisabled(m_type != 0);
        {
          if (ImGui::EllipsesButton(m_type == 0 ? Database::instance()->troopNameOrId(m_id).c_str() : "##battle_processing_troop_direct", {-1, 0})) {
            m_enemyPicker = ObjectPicker(trNOOP("Troop"), Database::instance()->troops.troops(), m_id);
            m_enemyPicker->setOpen(true);
          }
        }
        ImGui::EndDisabled();
        ImGui::RadioButton(trNOOP("Designation with variable"), &m_type, 1);
        ImGui::Dummy({ImGui::GetFrameHeightWithSpacing(), ImGui::GetFrameHeightWithSpacing()});
        ImGui::SameLine();
        ImGui::BeginDisabled(m_type != 1);
        {
          if (ImGui::EllipsesButton(m_type == 1 ? Database::instance()->variableNameAndId(m_varSelection).c_str() : "##battle_processing_troop_variable", {-1, 0})) {
            m_variablePicker = VariableSwitchPicker(trNOOP("Variables"), Database::instance()->system.variables(), m_varSelection);
            m_variablePicker->setOpen(true);
          }
        }
        ImGui::EndDisabled();
        ImGui::RadioButton(trNOOP("Same as Random Encounter"), &m_type, 2);
      }
      troopGroupBox.end();
      ImGui::Spring();
      ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ImGui::GetDPIScaledValue(1.5f));
      ImGui::BeginHorizontal("##battle_processing_button_layout", {-1, 0}, 0);
      {
        ImGui::Checkbox(trNOOP("Can Escape"), &m_canEscape);
        ImGui::Spring();
        ImGui::Checkbox(trNOOP("Can Lose"), &m_canLose);
        ImGui::Spring();
        if (const auto ret = ImGui::ButtonGroup("##battle_processing_buttons", {trNOOP("OK"), trNOOP("Cancel")}); ret == 0) {
          m_confirmed = true;
          m_command->type = static_cast<BattleProcessType>(m_type);
          m_command->id = m_id;
          m_command->canEscape = m_canEscape;
          m_command->canLose = m_canLose;
          ImGui::CloseCurrentPopup();
          setOpen(false);
        } else if (ret == 1) {
          ImGui::CloseCurrentPopup();
          setOpen(false);
        }
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndVertical();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}

void Dialog_BattleProcessing::drawPickers() {
  if (m_enemyPicker) {
    if (const auto [closed, confirmed] = m_enemyPicker->draw(); closed) {
      if (confirmed) {
        m_id = m_enemyPicker->selection();
      }
      m_enemyPicker.reset();
    }
  }

  if (m_variablePicker) {
    if (const auto [closed, confirmed] = m_variablePicker->draw(); closed) {
      if (confirmed) {
        m_varSelection = m_variablePicker->selection();
      }
      m_variablePicker.reset();
    }
  }
}


code:
#pragma once
#include "Core/CommonUI/ObjectPicker.hpp"
#include "Core/CommonUI/VariableSwitchPicker.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/EventCommands/BattleProcessing.hpp"
#include "Database/Troops.hpp"

struct Dialog_BattleProcessing : IEventDialogController {
  Dialog_BattleProcessing() = delete;
  explicit Dialog_BattleProcessing(const std::string& name, const std::shared_ptr<BattleProcessingCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , m_command(cmd) {
    if (cmd == nullptr) {
      m_command.reset(new BattleProcessingCommand());
    }

    m_type = static_cast<int>(m_command->type);
    m_id = m_command->id;
    m_canEscape = m_command->canEscape;
    m_canLose = m_command->canLose;
  }
  std::tuple<bool, bool> draw() override;
  std::shared_ptr<IEventCommand> getCommand() override { return m_command; };

private:
  void drawPickers();
  int m_type;
  int m_id;
  bool m_canEscape;
  bool m_canLose;

  int m_varSelection{1};

  bool m_confirmed{false};
  std::optional<ObjectPicker<Troop>> m_enemyPicker;
  std::optional<VariableSwitchPicker> m_variablePicker;
  std::shared_ptr<BattleProcessingCommand> m_command;
};


code:
#include "Core/EventCommands/Dialog_BreakLoop.hpp"
#include <tuple>

std::tuple<bool, bool> Dialog_BreakLoop::draw() {
  if (isOpen()) {
    setOpen(false);
    m_confirmed = true;
  }
  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/EventCommands/BreakLoop.hpp"

struct Dialog_BreakLoop : IEventDialogController {
  Dialog_BreakLoop() = delete;
  explicit Dialog_BreakLoop(const std::string& name, const std::shared_ptr<BreakLoopCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , command(cmd) {
    if (cmd == nullptr) {
      command.reset(new BreakLoopCommand());
    }
    m_open = false;
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return command; };

private:
  bool m_confirmed{true};
  std::shared_ptr<BreakLoopCommand> command;
  std::tuple<bool, bool> result;
};


code:
#include "Core/EventCommands/Dialog_ChangeActorImages.hpp"
#include "Core/Application.hpp"

#include "Core/Log.hpp"
#include "imgui.h"
#include <tuple>

std::tuple<bool, bool> Dialog_ChangeActorImages::draw() {
  if (isOpen()) {
    ImGui::OpenPopup(m_name.c_str());
  }
  ImVec2 center = ImGui::GetMainViewport()->GetCenter();
  ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
  ImGui::SetNextWindowSize(ImVec2{400, 225}, ImGuiCond_Appearing);
  if (ImGui::BeginPopupModal(m_name.c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {

    if (const auto [closed, confirmed] = m_characterPicker.draw(); closed) {
      if (confirmed) {
        m_characterPicker.accept();
        if (m_image_selection == 0) {
          m_faceIndex = m_characterPicker.character();
          m_facePicture = m_characterPicker.selectedSheet();
          m_faceSheet = FaceSheet(m_facePicture);
        } else if (m_image_selection == 1) {
          m_charIndex = m_characterPicker.character();
          m_charPicture = m_characterPicker.selectedSheet();
          m_characterSheet = CharacterSheet(m_charPicture);
        } else {
          m_battlerPicture = m_characterPicker.selectedSheet();
          m_battlerSheet = SideViewBattlerSheet(m_battlerPicture);
        }
      }
    }
    if (actor_picker) {
      auto [closed, confirmed] = actor_picker->draw();
      if (closed) {
        if (confirmed) {
          m_actor = actor_picker->selection();
        }
        actor_picker.reset();
      }
    }
    ImGui::PushID("##actor_selection");
    if (ImGui::Button((std::format("{:04} ", m_actor) + Database::instance()->actorName(m_actor)).c_str(), {(160), 0})) {

      actor_picker = ObjectPicker<Actor>("Actor"sv, Database::instance()->actors.actorList(), m_actor);
      actor_picker->setOpen(true);
    }
    ImGui::PopID();
    ImGui::BeginGroup();
    {
      ImGui::SeparatorText("Images");
      const auto buttonSize = ImVec2{144, 144};
      const auto buttonCenter = (buttonSize / 2);
      ImGui::BeginGroup();
      {

        ImGui::Text("Face:");
        auto cursorPos = ImGui::GetCursorPos();
        if (ImGui::ImageButton("##face_image", static_cast<ImTextureID>(m_buttonBack), ImVec2{80.f, 102.f})) {
          m_image_selection = 0;
          m_characterPicker.setCharacterInfo(m_charPicture, m_actor);
          m_characterPicker.setOpen(true);
        }
        if (m_faceSheet && m_faceSheet->texture()) {
          const auto faceRect = ImVec2{static_cast<float>(m_faceSheet->faceWidth()), static_cast<float>(m_faceSheet->faceHeight())};
          ImGui::SetCursorPos(((cursorPos + buttonCenter) - (faceRect / 2)) + (ImGui::GetStyle().ItemInnerSpacing - ImVec2{0.f, 1.f}));
          const auto rect = m_faceSheet->getFaceRect(m_faceIndex);
          ImGui::Image(m_faceSheet->texture(), faceRect, rect.min, rect.max);
        }
      }
      ImGui::EndGroup();
      ImGui::SameLine();
      ImGui::BeginGroup();
      {
        ImGui::Text("Character:");
        auto cursorPos = ImGui::GetCursorPos();
        if (ImGui::ImageButton("##char_image", static_cast<ImTextureID>(m_buttonBack), ImVec2{80.f, 102.f})) {
          m_image_selection = 1;
          m_characterPicker.setCharacterInfo(m_facePicture, m_charIndex);
          m_characterPicker.setOpen(true);
        }
        if (m_characterSheet && m_characterSheet->texture()) {
          if (m_characterSheet->characterWidth() < 72 || m_characterSheet->characterHeight() < 96) {
            ImGui::SetCursorPos(cursorPos + (ImVec2{m_characterSheet->characterWidth() / 2.f, m_characterSheet->characterHeight() / 2.f}));
          } else {
            ImGui::SetCursorPos(cursorPos);
          }

          const auto [min, max] = m_characterSheet->getRectForCharacter(m_charIndex);

          ImGui::Image(m_characterSheet->texture(), ImVec2{static_cast<float>(m_characterSheet->characterWidth()), static_cast<float>(m_characterSheet->characterHeight())}, min, max);
        }
      }
      ImGui::EndGroup();
      ImGui::SameLine();
      ImGui::BeginGroup();
      {
        ImGui::Text("[SV] Battler:");
        auto cursorPos = ImGui::GetCursorPos();
        if (ImGui::ImageButton("##svbattler_image", static_cast<ImTextureID>(m_buttonBack), ImVec2{80.f, 102.f})) {
          m_image_selection = 2;
          m_characterPicker.setCharacterInfo(m_battlerPicture, m_faceIndex);
          m_characterPicker.setOpen(true);
        }
        if (m_battlerSheet && m_battlerSheet->texture()) {
          const auto battlerRect = ImVec2{static_cast<float>(m_battlerSheet->characterWidth()) * 2, static_cast<float>(m_battlerSheet->characterHeight()) * 2};
          ImGui::SetCursorPos((cursorPos + buttonCenter) - (battlerRect / 2));
          const auto rect = m_battlerSheet->getAction(SideViewActionType::StepForward);
          ImGui::Image(m_battlerSheet->texture(), battlerRect, rect.frames[1].min, rect.frames[1].max);
        }
      }
      ImGui::EndGroup();
    }
    ImGui::EndGroup();
    ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 2.f);
    ImGui::BeginGroup();
    {
      if (ImGui::Button("OK")) {
        m_confirmed = true;
        command->actor = m_actor;
        command->charPicture = m_charPicture;
        command->charIndex = m_charIndex;
        command->facePicture = m_facePicture;
        command->faceIndex = m_faceIndex;
        command->battlerPicture = m_battlerPicture;
        ImGui::CloseCurrentPopup();
        setOpen(false);
      }
      ImGui::SameLine();
      if (ImGui::Button("Cancel")) {
        ImGui::CloseCurrentPopup();
        setOpen(false);
      }
    }
    ImGui::EndGroup();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/CommonUI/CharacterPicker.hpp"
#include "Core/CommonUI/ObjectPicker.hpp"
#include "Core/CommonUI/TextEditor.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Core/Graphics/CharacterSheet.hpp"
#include "Core/Graphics/FaceSheet.hpp"
#include "Core/Graphics/SideViewBattlerSheet.hpp"
#include "Database/Actors.hpp"
#include "Database/EventCommands/ChangeActorImage.hpp"

struct Dialog_ChangeActorImages : IEventDialogController {
  Dialog_ChangeActorImages() = delete;
  explicit Dialog_ChangeActorImages(const std::string& name, const std::shared_ptr<ChangeActorImageCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , command(cmd) {
    if (cmd == nullptr) {
      command.reset(new ChangeActorImageCommand());
    }
    m_actor = command->actor;
    m_charPicture = command->charPicture;
    m_charIndex = command->charIndex;
    m_facePicture = command->facePicture;
    m_faceIndex = command->faceIndex;
    m_battlerPicture = command->battlerPicture;
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return command; };

private:
  int m_actor;
  std::string m_charPicture;
  int m_charIndex;
  std::string m_facePicture;
  int m_faceIndex;
  std::string m_battlerPicture;

  int m_image_selection{0};

  CheckerboardTexture m_buttonBack{80, 102, CellSizes::_64, 255, 200};
  CharacterPicker m_characterPicker{CharacterPicker::PickerMode::Character};

  std::optional<CharacterSheet> m_characterSheet;
  std::optional<FaceSheet> m_faceSheet;
  std::optional<SideViewBattlerSheet> m_battlerSheet;

  bool m_confirmed{false};
  std::optional<ObjectPicker<Actor>> actor_picker;
  std::shared_ptr<ChangeActorImageCommand> command;
  std::tuple<bool, bool> result;
};


code:
#include "Core/EventCommands/Dialog_ChangeArmors.hpp"

#include "Database/Armors.hpp"
#include "Database/Database.hpp"

#include "Core/CommonUI/GroupBox.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"

#include <imgui.h>
#include <imgui_internal.h>

#include <tuple>

std::tuple<bool, bool> Dialog_ChangeArmors::draw() {
  if (isOpen()) {
    ImGui::OpenPopup("###ChangeArmors");
  }
  ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
  const auto maxSize =
      ImVec2{ImGui::CalcTextSize("#############################").x + (ImGui::GetStyle().FramePadding.x * 2), (ImGui::GetFrameHeightWithSpacing() * 10) + (ImGui::GetStyle().FramePadding.y * 2)};
  ImGui::SetNextWindowSize(maxSize, ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(maxSize, {FLT_MAX, FLT_MAX});

  if (ImGui::BeginPopupModal(std::format("{}###ChangeArmors", m_name).c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {
    drawPickers();
    ImGui::BeginVertical("##change_armors_main_layout", ImGui::GetContentRegionAvail());
    {
      GroupBox armorGroupBox(trNOOP("Armor"), "##change_armors_item_group", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (armorGroupBox.begin()) {
        ImGui::PushID("##change_armors_item");
        if (ImGui::EllipsesButton(Database::instance()->armorNameAndId(m_item).c_str(), ImVec2{-1, 0})) {
          m_armorPicker = ObjectPicker(trNOOP("Armors"), Database::instance()->armors.armorList(), m_item);
          m_armorPicker->setOpen(true);
        }
        ImGui::PopID();
      }
      armorGroupBox.end();

      GroupBox operationGroupBox(trNOOP("Operation"), "##change_armors_operation_group", {-1, 0}, nullptr,
                                 ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (operationGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_armors_operation_group_layout", {ImGui::GetContentRegionAvail().x, -1});
        {
          ImGui::Spring(.33);
          ImGui::RadioButton("Increase", &m_operation, 0);
          ImGui::Spring(.33);
          ImGui::RadioButton("Decrease", &m_operation, 1);
          ImGui::Spring(.33);
        }
        ImGui::EndHorizontal();
      }
      operationGroupBox.end();

      GroupBox operandGroupBox(trNOOP("Operand"), "##change_armors_operand_group", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (operandGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_armors_operand_group_layout", {ImGui::GetContentRegionAvail().x, -1});
        {
          ImGui::BeginVertical("##change_armors_operand_source", {-1, -1});
          {
            ImGui::RadioButton(trNOOP("Constant"), &m_operandSource, 0);
            ImGui::RadioButton(trNOOP("Variable"), &m_operandSource, 1);
          }
          ImGui::EndVertical();
          ImGui::BeginVertical("##change_armors_operand_value", {-1, -1});
          {
            ImGui::BeginDisabled(m_operandSource != 0);
            ImGui::SetNextItemWidth(-1);
            if (ImGui::SpinInt("##change_armors_constant", &m_quantity, 1, 100, m_operandSource == 0 ? nullptr : "")) {
              m_quantity = std::clamp(m_quantity, 1, 9999);
            }
            ImGui::EndDisabled();

            ImGui::BeginDisabled(m_operandSource != 1);
            ImGui::PushID("##change_armors_quant_var");
            if (ImGui::EllipsesButton(m_operandSource == 1 ? Database::instance()->variableNameAndId(m_quantityVar).c_str() : "", ImVec2{-1, 0})) {
              m_variablePicker.emplace(trNOOP("Variables"), Database::instance()->system.variables(), m_quantityVar);
              m_variablePicker->setOpen(true);
            }
            ImGui::PopID();
            ImGui::EndDisabled();
          }
          ImGui::EndVertical();
        }
        ImGui::EndHorizontal();
      }
      operandGroupBox.end();
      ImGui::Spring();
      ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ImGui::GetDPIScaledValue(1.5));
      ImGui::BeginHorizontal("##change_armors_buttons", {-1, -1});
      {
        ImGui::BeginDisabled(m_operation == 0);
        ImGui::Checkbox(trNOOP("Include Equipment"), &m_includeEquipment);
        ImGui::EndDisabled();
        ImGui::Spring();
        if (const auto ret = ImGui::ButtonGroup("##change_armors_buttons", {trNOOP("OK"), trNOOP("Cancel")}); ret == 0) {
          m_confirmed = true;

          m_command->item = m_item;
          m_command->operation = static_cast<QuantityChangeOp>(m_operation);
          m_command->operandSource = static_cast<QuantityChangeSource>(m_operandSource);
          m_command->includeEquipment = m_includeEquipment;

          if (m_command->operandSource == QuantityChangeSource::Variable)
            m_command->operand = m_quantityVar;
          else
            m_command->operand = m_quantity;

          ImGui::CloseCurrentPopup();
          setOpen(false);
        } else if (ret == 1) {
          ImGui::CloseCurrentPopup();
          setOpen(false);
        }
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndVertical();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}

void Dialog_ChangeArmors::drawPickers() {
  if (m_variablePicker) {
    if (const auto [closed, confirmed] = m_variablePicker->draw(); closed) {
      if (confirmed) {
        m_quantityVar = m_variablePicker->selection();
      }
      m_variablePicker.reset();
    }
  }

  if (m_armorPicker) {
    if (const auto [closed, confirmed] = m_armorPicker->draw(); closed) {
      if (confirmed) {
        m_item = m_armorPicker->selection();
      }
      m_armorPicker.reset();
    }
  }
}


code:
#pragma once
#include "Core/CommonUI/ObjectPicker.hpp"
#include "Core/CommonUI/VariableSwitchPicker.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/EventCommands/ChangeArmors.hpp"
#include "Database/EventCommands/ChangeEnemyHP.hpp"
#include "Database/Troops.hpp"

struct Armor;
struct Dialog_ChangeArmors : IEventDialogController {
  Dialog_ChangeArmors() = delete;
  explicit Dialog_ChangeArmors(const std::string& name, const std::shared_ptr<ChangeArmorsCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , m_command(cmd) {
    if (cmd == nullptr) {
      m_command.reset(new ChangeArmorsCommand());
    }

    m_item = m_command->item;
    m_operation = static_cast<int>(m_command->operation);
    m_operandSource = static_cast<int>(m_command->operandSource);
    m_includeEquipment = m_command->includeEquipment;

    if (m_command->operandSource == QuantityChangeSource::Variable)
      m_quantityVar = m_command->operand;
    else
      m_quantity = m_command->operand;
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return m_command; };

private:
  void drawPickers();
  int m_item;
  int m_operation;
  int m_operandSource;
  int m_quantity{1};
  int m_quantityVar{1};
  bool m_includeEquipment{false};

  bool m_confirmed{false};
  std::optional<ObjectPicker<Armor>> m_armorPicker;
  std::optional<VariableSwitchPicker> m_variablePicker;
  std::shared_ptr<ChangeArmorsCommand> m_command;
};


code:
#include "Core/EventCommands/Dialog_ChangeBattleBGM.hpp"

std::tuple<bool, bool> Dialog_ChangeBattleBGM::draw() {
  if (const auto [closed, confirmed] = m_audioRenderer.value().draw(m_name, "ChangeBattleBGM"); closed) {
    m_confirmed = confirmed;
    setOpen(false);
  }

  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Core/Log.hpp"
#include "Core/ResourceManager.hpp"
#include "Core/Settings.hpp"

#include "Core/CommonUI/AudioEditor.hpp"
#include "Core/Sound.hpp"
#include "Database/Database.hpp"
#include "Database/EventCommands/ChangeBattleBGM.hpp"

#include <iostream>

namespace fs = std::filesystem;
struct Dialog_ChangeBattleBGM : IEventDialogController {
  Dialog_ChangeBattleBGM() = delete;
  explicit Dialog_ChangeBattleBGM(const std::string& name, const std::shared_ptr<ChangeBattleBGMCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , m_command(cmd) {
    if (cmd == nullptr) {
      m_command.reset(new ChangeBattleBGMCommand());
    }
    m_audioRenderer.emplace(m_command->code(), m_command->bgm);
  }
  std::tuple<bool, bool> draw() override;
  [[nodiscard]] std::shared_ptr<IEventCommand> getCommand() override { return m_command; }

  void setOpen(const bool open) override {
    IEventDialogController::setOpen(open);
    m_audioRenderer->setOpen(open);
  }

private:
  bool m_confirmed{false};
  std::optional<AudioEditor> m_audioRenderer;
  std::shared_ptr<ChangeBattleBGMCommand> m_command;
};


code:
#include "Core/EventCommands/Dialog_ChangeBattleBack.hpp"

#include "Core/CommonUI/GroupBox.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "imgui.h"
#include "imgui_internal.h"

#include <tuple>

std::tuple<bool, bool> Dialog_ChangeBattleBack::draw() {
  if (isOpen()) {
    ImGui::OpenPopup("###ChangeBattleback");
  }
  const auto maxSize =
      ImVec2{ImGui::CalcTextSize("#############################").x + (ImGui::GetStyle().FramePadding.x * 2), (ImGui::GetFrameHeightWithSpacing() * 5) + (ImGui::GetStyle().FramePadding.y * 2)};
  ImGui::SetNextWindowSize(maxSize, ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(maxSize, {FLT_MAX, FLT_MAX});
  if (ImGui::BeginPopupModal(std::format("{}###ChangeBattleback", m_name).c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {

    if (const auto [closed, confirmed] = m_imagePicker->draw(); closed) {
      if (confirmed) {
        m_imagePicker->accept();
        m_image = m_imagePicker->selectedImage();
        m_image_2 = m_imagePicker->selectedImage2();
      }
    }

    ImGui::BeginVertical("##change_battleback_main_layout", ImGui::GetContentRegionAvail());
    {
      GroupBox operationGroupBox(trNOOP("Battle Background"), "##change_battleback_layout", {-1, 0}, nullptr,
                                 ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (operationGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_battleback_group_horizontal", {ImGui::GetContentRegionAvail().x, -1});
        {
          ImGui::Spring(.5);
          ImGui::PushID("#battleback_image_selection");
          if (ImGui::EllipsesButton(m_image_2.empty() ? m_image.c_str() : (m_image + " & " + m_image_2).c_str(), ImVec2{-1, 0})) {
            m_imagePicker->setOpen(true);
          }
          ImGui::PopID();
          ImGui::Spring(.5);
        }
        ImGui::EndHorizontal();
      }
      operationGroupBox.end();
      ImGui::Spring();
      ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ImGui::GetDPIScaledValue(1.5));
      ImGui::BeginHorizontal("##change_battleback_dialog_buttons", {-1, -1});
      {
        ImGui::Spring();
        if (const auto ret = ImGui::ButtonGroup("##change_battleback_dialog_buttons", {trNOOP("OK"), trNOOP("Cancel")}); ret == 0) {
          m_confirmed = true;
          command->battleBack1Name = m_image;
          command->battleBack2Name = m_image_2;
          ImGui::CloseCurrentPopup();
          setOpen(false);
        } else if (ret == 1) {
          ImGui::CloseCurrentPopup();
          setOpen(false);
        }
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndVertical();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/CommonUI/ImagePicker.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/EventCommands/ChangeBattleback.hpp"

struct Dialog_ChangeBattleBack : IEventDialogController {
  Dialog_ChangeBattleBack() = delete;
  explicit Dialog_ChangeBattleBack(const std::string& name, const std::shared_ptr<ChangeBattlebackCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , command(cmd) {
    if (cmd == nullptr) {
      command.reset(new ChangeBattlebackCommand());
    }
    m_image = command->battleBack1Name;
    m_image_2 = command->battleBack2Name;
    m_imagePicker.emplace(ImagePicker::PickerMode::Battleback, m_image, m_image_2);
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return command; };

private:
  std::string m_image;
  std::string m_image_2;

  bool m_confirmed{false};
  std::shared_ptr<ChangeBattlebackCommand> command;
  std::tuple<bool, bool> result;
  std::optional<ImagePicker> m_imagePicker;
};


code:
#include "Core/EventCommands/Dialog_ChangeClass.hpp"

#include "Database/Database.hpp"

#include "Core/CommonUI/GroupBox.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"

#include <imgui.h>
#include <imgui_internal.h>

#include <tuple>

std::tuple<bool, bool> Dialog_ChangeClass::draw() {
  if (isOpen()) {
    ImGui::OpenPopup("###ChangeClass");
  }
  ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
  const auto maxSize =
      ImVec2{ImGui::CalcTextSize("#############################").x + (ImGui::GetStyle().FramePadding.x * 2), (ImGui::GetFrameHeightWithSpacing() * 7) + (ImGui::GetStyle().FramePadding.y * 2)};
  ImGui::SetNextWindowSize(maxSize, ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(maxSize, {FLT_MAX, FLT_MAX});

  if (ImGui::BeginPopupModal(std::format("{}###ChangeClass", m_name).c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {
    drawPickers();

    ImGui::BeginVertical("##change_class_main_layout", ImGui::GetContentRegionAvail());
    {
      GroupBox actorGroupBox(trNOOP("Actor"), "##change_class_actor_group", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (actorGroupBox.begin()) {
        ImGui::PushID("##change_class_actor");
        if (ImGui::EllipsesButton(Database::instance()->actorNameAndId(m_actor).c_str(), ImVec2{-1, 0})) {
          m_actorPicker = ObjectPicker(trNOOP("Actor"), Database::instance()->actors.actorList(), m_actor);
          m_actorPicker->setOpen(true);
        }
        ImGui::PopID();
      }
      actorGroupBox.end();

      GroupBox classGroupBox(trNOOP("Class"), "##change_class_class_group", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (classGroupBox.begin()) {
        ImGui::PushID("##change_class_classid");
        if (ImGui::EllipsesButton(Database::instance()->classNameAndId(m_class).c_str(), ImVec2{-1, 0})) {
          m_classPicker = ObjectPicker(trNOOP("Class"), Database::instance()->classes.classes(), m_class);
          m_classPicker->setOpen(true);
        }
        ImGui::PopID();
      }
      classGroupBox.end();
      ImGui::Spring();
      ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ImGui::GetDPIScaledValue(1.5));
      ImGui::BeginHorizontal("##change_class_buttons");
      {
        // saveLevel checkbox
        ImGui::Checkbox("Save Level", &m_saveLevel);
        ImGui::Spring();
        if (const auto ret = ImGui::ButtonGroup("##change_class_buttons", {trNOOP("OK"), trNOOP("Cancel")}); ret == 0) {
          m_confirmed = true;
          m_command->actor = m_actor;
          m_command->classId = m_class;
          m_command->saveLevel = m_saveLevel;
          ImGui::CloseCurrentPopup();
          setOpen(false);
        } else if (ret == 1) {
          ImGui::CloseCurrentPopup();
          setOpen(false);
        }
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndVertical();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}

void Dialog_ChangeClass::drawPickers() {
  if (m_actorPicker) {
    if (const auto [closed, confirmed] = m_actorPicker->draw(); closed) {
      if (confirmed) {
        m_actor = m_actorPicker->selection();
      }
      m_actorPicker.reset();
    }
  }
  if (m_classPicker) {
    if (const auto [closed, confirmed] = m_classPicker->draw(); closed) {
      if (confirmed) {
        m_class = m_classPicker->selection();
      }
      m_classPicker.reset();
    }
  }
}


code:
#pragma once
#include "Core/CommonUI/ObjectPicker.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"

#include "Database/Actor.hpp"
#include "Database/Class.hpp"
#include "Database/EventCommands/ChangeClass.hpp"

struct Dialog_ChangeClass : IEventDialogController {
  Dialog_ChangeClass() = delete;
  explicit Dialog_ChangeClass(const std::string& name, const std::shared_ptr<ChangeClassCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , m_command(cmd) {
    if (cmd == nullptr) {
      m_command.reset(new ChangeClassCommand());
    }
    m_actor = m_command->actor;
    m_class = m_command->classId;
    m_saveLevel = m_command->saveLevel;
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return m_command; };

private:
  void drawPickers();
  int m_actor;
  int m_class;
  bool m_saveLevel;

  bool m_confirmed{false};
  std::optional<ObjectPicker<Actor>> m_actorPicker;
  std::optional<ObjectPicker<Class>> m_classPicker;
  std::shared_ptr<ChangeClassCommand> m_command;
};


code:
#include "Core/EventCommands/Dialog_ChangeDefeatME.hpp"

std::tuple<bool, bool> Dialog_ChangeDefeatME::draw() {
  if (const auto [closed, confirmed] = m_audioRenderer.value().draw(m_name, "ChangeDefeatME"); closed) {
    m_confirmed = confirmed;
    setOpen(false);
  }

  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/CommonUI/AudioEditor.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/Database.hpp"
#include "Database/EventCommands/ChangeDefeatME.hpp"
#include <iostream>
#include <SFML/Audio.hpp>

namespace fs = std::filesystem;
struct Dialog_ChangeDefeatME : IEventDialogController {
  Dialog_ChangeDefeatME() = delete;
  explicit Dialog_ChangeDefeatME(const std::string& name, const std::shared_ptr<ChangeDefeatMECommand>& cmd = nullptr)
  : IEventDialogController(name)
  , m_command(cmd) {
    if (cmd == nullptr) {
      m_command.reset(new ChangeDefeatMECommand());
    }
    m_audioRenderer.emplace(m_command->code(), m_command->me);
  }
  std::tuple<bool, bool> draw() override;
  [[nodiscard]] std::shared_ptr<IEventCommand> getCommand() override { return m_command; }

  void setOpen(const bool open) override {
    IEventDialogController::setOpen(open);
    m_audioRenderer->setOpen(open);
  }

private:
  bool m_confirmed{false};
  std::shared_ptr<ChangeDefeatMECommand> m_command;
  std::optional<AudioEditor> m_audioRenderer;
};


code:
#include "Core/EventCommands/Dialog_ChangeEXP.hpp"

#include "Database/Database.hpp"
#include "imgui.h"
#include <tuple>

#include "Core/CommonUI/GroupBox.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"

#include "Database/Database.hpp"

#include <imgui.h>
#include <imgui_internal.h>

#include <tuple>

std::tuple<bool, bool> Dialog_ChangeEXP::draw() {
  if (isOpen()) {
    ImGui::OpenPopup("###ChangeEXP");
  }
  ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
  const auto maxSize =
      ImVec2{ImGui::CalcTextSize("#############################").x + (ImGui::GetStyle().FramePadding.x * 2), (ImGui::GetFrameHeightWithSpacing() * 11) + (ImGui::GetStyle().FramePadding.y * 2)};
  ImGui::SetNextWindowSize(maxSize, ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(maxSize, {FLT_MAX, FLT_MAX});
  if (ImGui::BeginPopupModal(std::format("{}###ChangeEXP", m_name).c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {
    drawPickers();
    ImGui::BeginVertical("##change_exp_main_layout", ImGui::GetContentRegionAvail());
    {
      GroupBox actorGroupBox(trNOOP("Actor"), "##change_exp_actor_group", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (actorGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_exp_actor_group_horizontal", {-1, -1});
        {
          ImGui::BeginVertical("##change_exp_actor_group_radios", {-1, -1});
          {
            ImGui::RadioButton(trNOOP("Fixed"), &m_comparison, 0);
            ImGui::RadioButton(trNOOP("Variable"), &m_comparison, 1);
          }
          ImGui::EndVertical();
          ImGui::Spring();
          ImGui::BeginVertical("##change_exp_actor_group_buttons", {-1, -1});
          {
            ImGui::BeginDisabled(m_comparison != 0);
            ImGui::PushID("##change_exp_actor_group_fixed_button");
            if (ImGui::EllipsesButton(m_comparison == 0 ? Database::instance()->actorNameAndId(m_value).c_str() : "", {-1, 0})) {
              m_actorPicker = ObjectPicker(trNOOP("Actor"), Database::instance()->actors.actorList(), m_value);
              m_actorPicker->setOpen(true);
            }
            ImGui::PopID();
            ImGui::EndDisabled();
            ImGui::BeginDisabled(m_comparison != 1);
            ImGui::PushID("##change_exp_actor_group_var");
            if (ImGui::EllipsesButton(m_comparison == 1 ? Database::instance()->variableNameAndId(m_value).c_str() : "", {-1, 0})) {
              m_isOperand = false;
              m_variablePicker.emplace(trNOOP("Variables"), Database::instance()->system.variables(), m_value);
              m_variablePicker->setOpen(true);
            }
            ImGui::PopID();
            ImGui::EndDisabled();
          }
          ImGui::EndVertical();
        }
        ImGui::EndHorizontal();
      }
      actorGroupBox.end();
      GroupBox operationGroupBox(trNOOP("Operation"), "##change_exp_operation_group", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (operationGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_exp_operation_group_horizontal", {ImGui::GetContentRegionAvail().x, -1});
        {
          ImGui::Spring(0.33f);
          ImGui::RadioButton(trNOOP("Increase"), &m_quantityOp, 0);
          ImGui::Spring(0.33f);
          ImGui::RadioButton(trNOOP("Decrease"), &m_quantityOp, 1);
          ImGui::Spring(0.33f);
        }
        ImGui::EndHorizontal();
      }
      operationGroupBox.end();
      GroupBox operandGroupBox(trNOOP("Operand"), "##change_exp_operand", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (operandGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_exp_operand_horizontal", {-1, -1});
        {
          ImGui::BeginVertical("##change_exp_operand_radios", {-1, -1});
          {
            ImGui::RadioButton(trNOOP("Constant"), &m_quantitySource, 0);
            ImGui::RadioButton(std::format("{}##operand", tr("Variable")).c_str(), &m_quantitySource, 1);
          }
          ImGui::EndVertical();
          ImGui::Spring();
          ImGui::BeginVertical("##change_exp_operand_buttons", {-1, -1});
          {
            ImGui::BeginDisabled(m_quantitySource != 0);
            ImGui::SetNextItemWidth(-1);
            if (ImGui::SpinInt("##change_exp_operand_constant", &m_quantity, 1, 100, m_quantitySource == 0 ? nullptr : "")) {
              if (m_quantity > 100)
                m_quantity = 100;
              if (m_quantity < 1)
                m_quantity = 1;
            }
            ImGui::EndDisabled();

            ImGui::BeginDisabled(m_quantitySource != 1);
            ImGui::PushID("##change_exp_operand_quantity_variable");
            if (ImGui::EllipsesButton(m_quantitySource == 1 ? Database::instance()->variableNameAndId(m_quantityVar).c_str() : "", ImVec2{-1, 0})) {
              m_isOperand = true;
              m_variablePicker.emplace(trNOOP("Variables"), Database::instance()->system.variables(), m_quantityVar);
              m_variablePicker->setOpen(true);
            }
            ImGui::PopID();
            ImGui::EndDisabled();
          }
          ImGui::EndVertical();
        }
        ImGui::EndHorizontal();
      }
      operandGroupBox.end();
      ImGui::Spring();
      ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ImGui::GetDPIScaledValue(1.5));
      ImGui::BeginHorizontal("##change_exp_dialog_buttons", {-1, -1});
      {
        ImGui::BeginDisabled(m_quantityOp != 0);
        ImGui::Checkbox(trNOOP("Show Level Up"), &m_showLevelUp);
        ImGui::EndDisabled();
        ImGui::Spring();
        if (const auto ret = ImGui::ButtonGroup("##change_exp_dialog_buttons", {trNOOP("OK"), trNOOP("Cancel")}); ret == 0) {
          m_confirmed = true;
          m_command->comparison = static_cast<ActorComparisonSource>(m_comparison);
          m_command->quantityOp = static_cast<QuantityChangeOp>(m_quantityOp);
          m_command->quantitySource = static_cast<QuantityChangeSource>(m_quantitySource);
          if (m_command->comparison == ActorComparisonSource::Fixed) {
            m_command->value = m_value;
          } else {
            m_command->value = m_valueVar;
          }
          if (m_command->quantitySource == QuantityChangeSource::Constant) {
            m_command->quantity = m_quantity;
          } else {
            m_command->quantity = m_quantityVar;
          }

          ImGui::CloseCurrentPopup();
          setOpen(false);
        } else if (ret == 1) {
          ImGui::CloseCurrentPopup();
          setOpen(false);
        }
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndVertical();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}

void Dialog_ChangeEXP::drawPickers() {
  if (m_actorPicker) {
    if (const auto [closed, confirmed] = m_actorPicker->draw(); closed) {
      if (confirmed) {
        m_value = m_actorPicker->selection();
      }
      m_actorPicker.reset();
    }
  }
  if (m_variablePicker) {
    if (const auto [closed, confirmed] = m_variablePicker->draw(); closed) {
      if (confirmed) {
        if (m_isOperand) {
          m_quantityVar = m_variablePicker->selection();
        } else {
          m_valueVar = m_variablePicker->selection();
        }
      }
      m_variablePicker.reset();
    }
  }
}


code:
#pragma once
#include "Core/CommonUI/ObjectPicker.hpp"
#include "Core/CommonUI/VariableSwitchPicker.hpp"
#include "Core/EventCommands/IEventDialogController.hpp"

#include "Database/Actor.hpp"
#include "Database/EventCommands/ChangeEXP.hpp"

struct Dialog_ChangeEXP : IEventDialogController {
  Dialog_ChangeEXP() = delete;
  explicit Dialog_ChangeEXP(const std::string& name, const std::shared_ptr<ChangeEXPCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , m_command(cmd) {
    if (cmd == nullptr) {
      m_command.reset(new ChangeEXPCommand());
    }

    m_comparison = static_cast<int>(m_command->comparison);
    m_quantityOp = static_cast<int>(m_command->quantityOp);
    m_quantitySource = static_cast<int>(m_command->quantitySource);
    m_showLevelUp = m_command->showLevelUp;

    if (m_command->comparison == ActorComparisonSource::Variable)
      m_valueVar = m_command->value;
    else
      m_value = m_command->value;

    if (m_command->quantitySource == QuantityChangeSource::Variable)
      m_quantityVar = m_command->quantity;
    else
      m_quantity = m_command->quantity;
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return m_command; };

private:
  void drawPickers();
  int m_comparison;
  int m_value;
  int m_valueVar{1};
  int m_quantityOp;
  int m_quantitySource;
  int m_quantity;
  int m_quantityVar{1};
  bool m_showLevelUp{false};
  bool m_isOperand{false};

  bool m_confirmed{false};
  std::optional<ObjectPicker<Actor>> m_actorPicker;
  std::optional<VariableSwitchPicker> m_variablePicker;
  std::shared_ptr<ChangeEXPCommand> m_command;
};


code:
#include "Core/EventCommands/Dialog_ChangeEncounter.hpp"
#include "Core/CommonUI/GroupBox.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "Core/Log.hpp"
#include "imgui.h"
#include "imgui_internal.h"

#include <tuple>

std::tuple<bool, bool> Dialog_ChangeEncounter::draw() {
  if (isOpen()) {
    ImGui::OpenPopup(m_name.c_str());
  }

  const auto maxSize =
      ImVec2{ImGui::CalcTextSize("#############################").x + (ImGui::GetStyle().FramePadding.x * 2), (ImGui::GetFrameHeightWithSpacing() * 5) + (ImGui::GetStyle().FramePadding.y * 2)};
  ImGui::SetNextWindowSize(maxSize, ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(maxSize, {FLT_MAX, FLT_MAX});
  if (ImGui::BeginPopupModal(m_name.c_str(), &m_open, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) {

    ImGui::BeginVertical("##change_encounter_vert_layout", ImGui::GetContentRegionAvail());
    {
      GroupBox operationGroupBox(trNOOP("Encounter"), "##change_encounter_layout", {-1, 0}, nullptr, ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize);
      if (operationGroupBox.begin()) {
        ImGui::BeginHorizontal("##change_encounter_group_horizontal", {ImGui::GetContentRegionAvail().x, -1});
        {
          ImGui::Spring(.5);
          ImGui::RadioButton(trNOOP("OFF"), &isDisabled, 0);
          ImGui::RadioButton(trNOOP("ON"), &isDisabled, 1);
          ImGui::Spring(.5);
        }
        ImGui::EndHorizontal();
      }
      operationGroupBox.end();
      ImGui::Spring();
      ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, ImGui::GetDPIScaledValue(1.5));
      ImGui::BeginHorizontal("##change_encounter_dialog_buttons", {-1, -1});
      {
        ImGui::Spring();
        if (const auto ret = ImGui::ButtonGroup("##change_encounter_dialog_buttons", {trNOOP("OK"), trNOOP("Cancel")}); ret == 0) {

          command->access = isDisabled == 0 ? AccessMode::Disable : AccessMode::Enable;
          ImGui::CloseCurrentPopup();
          setOpen(false);
          m_confirmed = true;
        } else if (ret == 1) {
          ImGui::CloseCurrentPopup();
          setOpen(false);
        }
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndVertical();
    ImGui::EndPopup();
  }

  return std::make_tuple(!m_open, m_confirmed);
}


code:
#pragma once
#include "Core/EventCommands/IEventDialogController.hpp"
#include "Database/EventCommands/ChangeEncounterDisable.hpp"

struct Dialog_ChangeEncounter : IEventDialogController {
  Dialog_ChangeEncounter() = delete;
  explicit Dialog_ChangeEncounter(const std::string& name, const std::shared_ptr<ChangeEncounterDisableCommand>& cmd = nullptr)
  : IEventDialogController(name)
  , command(cmd) {
    if (cmd == nullptr) {
      command.reset(new ChangeEncounterDisableCommand());
    }
    isDisabled = static_cast<int>(command->access);
  }
  std::tuple<bool, bool> draw() override;

  std::shared_ptr<IEventCommand> getCommand() override { return command; };

private:
  int isDisabled = 0;
  bool m_confirmed{false};
  std::shared_ptr<ChangeEncounterDisableCommand> command;
  std::tuple<bool, bool> result;
};