Whatever The Fuck Man

This commit is contained in:
shweet committed 2026-07-04 00:15:26 -04:00
1 parent ce77175162
commit 2efed2a443
162 files changed
+13119 -7417

No files matched your search

+536
View File
@@ -0,0 +1,536 @@
#include "window.hpp"
#include <algorithm>
#include <format>
#include <fstream>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
#include "../log.hpp"
#include "../util/physfs.hpp"
#include "../util/preferences.hpp"
namespace game::developer
{
bool gIsDocumentUndoMenuRequested{};
bool gIsDocumentRedoMenuRequested{};
namespace window_detail
{
constexpr auto RECENT_FILES_MAX = 10;
constexpr auto CENTER_RATIO = 0.5f;
constexpr auto DEVELOPER_WINDOW_SIZE_RATIO = 0.75f;
constexpr auto INFO_SCHEMA_TABLE_COLUMNS = 4;
constexpr auto DEVELOPER_WINDOW_FLAGS = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings;
constexpr auto EDITOR_CHILD_FLAGS = ImGuiWindowFlags_MenuBar;
constexpr auto DIALOGUE_SCHEMA_FILENAME = "dialogue.xml";
using Schema = resource::xml::Schema;
using Element = Schema::Element;
#define GAME_DEVELOPER_SCHEMA_FILES(X) \
X(CHARACTER, "character.xml", "Character", Element::CHARACTER) \
X(DIALOGUE, "dialogue.xml", "Dialogue", Element::DIALOGUE) \
X(ITEMS, "items.xml", "Items", Element::ITEM_SCHEMA) \
X(AREAS, "areas.xml", "Areas", Element::AREAS) \
X(MENU, "menu.xml", "Menu", Element::MENU) \
X(CURSOR, "cursor.xml", "Cursor", Element::CURSOR) \
X(PARTICLES, "particles.xml", "Particles", Element::PARTICLES) \
X(STRINGS, "strings.xml", "Strings", Element::STRINGS)
struct SchemaInfo
{
std::string_view filename{};
std::string_view label{};
Element::Type type{};
};
constexpr SchemaInfo SCHEMA_INFOS[]{
#define GAME_DEVELOPER_SCHEMA_INFO(SYMBOL, FILENAME, LABEL, TYPE) {.filename = FILENAME, .label = LABEL, .type = TYPE},
GAME_DEVELOPER_SCHEMA_FILES(GAME_DEVELOPER_SCHEMA_INFO)
#undef GAME_DEVELOPER_SCHEMA_INFO
};
std::filesystem::path path_normalized_get(const std::filesystem::path& path)
{
std::error_code error{};
auto normalized = std::filesystem::weakly_canonical(path, error);
if (!error) return normalized;
return std::filesystem::absolute(path, error).lexically_normal();
}
std::filesystem::path recent_file_path_get() { return util::preferences::path() / "recent.txt"; }
std::string document_path_label_get(const std::filesystem::path& path)
{
auto filename = path.filename();
if (filename.empty()) return path.string();
return filename.string();
}
bool shortcut_is_pressed(ImGuiKeyChord chord)
{
auto& io = ImGui::GetIO();
return !io.WantTextInput && !ImGui::IsAnyItemActive() &&
ImGui::Shortcut(chord, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused);
}
#undef GAME_DEVELOPER_SCHEMA_FILES
}
using namespace window_detail;
void Window::recent_files_load()
{
if (isRecentFilesLoaded) return;
isRecentFilesLoaded = true;
recentFiles.clear();
std::ifstream file(recent_file_path_get());
if (!file.is_open()) return;
std::string line{};
while (std::getline(file, line))
{
if (line.empty()) continue;
recentFiles.push_back(path_normalized_get(line));
if ((int)recentFiles.size() >= RECENT_FILES_MAX) break;
}
}
void Window::recent_files_save()
{
auto recentPath = recent_file_path_get();
if (recentPath.empty()) return;
std::error_code error{};
if (!recentPath.parent_path().empty()) std::filesystem::create_directories(recentPath.parent_path(), error);
std::ofstream file(recentPath, std::ios::trunc);
if (!file.is_open())
{
logger.warning(std::format("Failed to write {}", recentPath.string()));
return;
}
for (auto& path : recentFiles)
file << path.string() << '\n';
}
void Window::recent_file_push(const std::filesystem::path& path)
{
recent_files_load();
auto normalized = path_normalized_get(path);
if (normalized.empty()) return;
std::erase_if(recentFiles, [&](const auto& recent) { return recent == normalized; });
recentFiles.insert(recentFiles.begin(), normalized);
if ((int)recentFiles.size() > RECENT_FILES_MAX) recentFiles.resize(RECENT_FILES_MAX);
recent_files_save();
}
Document Window::document_load(const std::filesystem::path& path)
{
Document document{};
document.path = path_normalized_get(path);
for (auto& info : SCHEMA_INFOS)
{
Document::SchemaFile schemaFile{};
schemaFile.filename = info.filename;
schemaFile.label = info.label;
schemaFile.type = info.type;
schemaFile.path = document.path / schemaFile.filename;
schemaFile.isExists = std::filesystem::is_regular_file(schemaFile.path);
if (schemaFile.isExists)
{
schemaFile.isValid = schemaFile.schema.load(schemaFile.path);
auto* root = schemaFile.schema.root();
if (!root || root->type != schemaFile.type) schemaFile.isValid = false;
}
document.schemas.emplace_back(std::move(schemaFile));
}
return document;
}
void Window::folder_open(const std::filesystem::path& path)
{
auto normalizedPath = path_normalized_get(path);
std::error_code error{};
if (!std::filesystem::is_directory(normalizedPath, error))
{
logger.warning(std::format("Ignored {}", normalizedPath.string()));
return;
}
auto it = std::ranges::find_if(documents, [&](const auto& document) { return document.path == normalizedPath; });
if (it != documents.end())
{
*it = document_load(normalizedPath);
it->history_clear();
it->isDirty = false;
selectedDocument = (int)std::distance(documents.begin(), it);
logger.info(std::format("Opened {}", normalizedPath.string()));
toasts.push(std::format("Opened {}", normalizedPath.string()));
}
else
{
documents.push_back(document_load(normalizedPath));
selectedDocument = (int)documents.size() - 1;
logger.info(std::format("Opened {}", normalizedPath.string()));
toasts.push(std::format("Opened {}", normalizedPath.string()));
}
recent_file_push(normalizedPath);
}
void Window::save(int index)
{
if (index < 0 || index >= (int)documents.size()) return;
auto& document = documents[index];
auto* dialogue = document.dialogue_schema_get();
if (!dialogue || !dialogue->isExists)
{
toasts.push("No dialogue.xml to save");
return;
}
logger.info(std::format("Wrote to {}", dialogue->path.string()));
dialogue->schema.serialize(dialogue->path);
documents[index].isDirty = false;
toasts.push(std::format("Wrote to {}", dialogue->path.string()));
recent_file_push(documents[index].path);
}
void Window::export_to(int index, const std::filesystem::path& path)
{
if (index < 0 || index >= (int)documents.size()) return;
if (documents[index].isDirty) save(index);
auto destination = path_normalized_get(path);
if (util::physfs::archive_write(documents[index].path, destination))
toasts.push(std::format("Exported {}", destination.string()));
else
toasts.push(std::format("Failed to export {}", destination.string()));
}
void Window::close(int index)
{
if (index < 0 || index >= (int)documents.size()) return;
auto label = document_path_label_get(documents[index].path);
documents.erase(documents.begin() + index);
selectedDocument = documents.empty() ? 0 : std::clamp(index, 0, (int)documents.size() - 1);
toasts.push(std::format("Closed {}", label));
}
void Window::undo(int index)
{
if (index < 0 || index >= (int)documents.size()) return;
auto& document = documents[index];
if (!document.can_undo()) return;
auto message = document.undo_apply();
if (!message.empty()) toasts.push(std::format("Undo: {}", message));
}
void Window::redo(int index)
{
if (index < 0 || index >= (int)documents.size()) return;
auto& document = documents[index];
if (!document.can_redo()) return;
auto message = document.redo_apply();
if (!message.empty()) toasts.push(std::format("Redo: {}", message));
}
void Window::dropped_file_open(const char* path)
{
if (!path) return;
folder_open(path);
}
void Window::editor_shortcuts_update()
{
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_O)) dialog.folder_open(util::Dialog::SCHEMA_FOLDER_OPEN);
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z))
redo(selectedDocument);
else if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_Z))
undo(selectedDocument);
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_S))
dialog.file_save(util::Dialog::DOCUMENT_EXPORT);
else if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_S))
save(selectedDocument);
if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_W)) close(selectedDocument);
}
void Window::dialog_selection_update()
{
if (dialog.is_selected(util::Dialog::SCHEMA_FOLDER_OPEN))
{
folder_open(dialog.path);
dialog.reset();
}
else if (dialog.is_selected(util::Dialog::DOCUMENT_EXPORT))
{
export_to(selectedDocument, dialog.path);
dialog.reset();
}
}
void Window::editor_menu_update()
{
if (!ImGui::BeginMenuBar()) return;
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Open Folder", "Ctrl+O")) dialog.folder_open(util::Dialog::SCHEMA_FOLDER_OPEN);
if (ImGui::BeginMenu("Open Recent", !recentFiles.empty()))
{
for (int recentIndex = 0; recentIndex < (int)recentFiles.size(); ++recentIndex)
{
ImGui::PushID(recentIndex);
auto label = recentFiles[recentIndex].string();
if (ImGui::MenuItem(label.c_str())) folder_open(recentFiles[recentIndex]);
ImGui::PopID();
}
ImGui::EndMenu();
}
if (ImGui::MenuItem("Save", "Ctrl+S", false, selectedDocument >= 0 && selectedDocument < (int)documents.size()))
save(selectedDocument);
if (ImGui::MenuItem("Export", "Ctrl+Shift+S", false,
selectedDocument >= 0 && selectedDocument < (int)documents.size()))
dialog.file_save(util::Dialog::DOCUMENT_EXPORT);
if (ImGui::MenuItem("Close", "Ctrl+W", false, selectedDocument >= 0 && selectedDocument < (int)documents.size()))
close(selectedDocument);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit"))
{
auto isCanUndo =
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_undo();
auto isCanRedo =
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_redo();
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, isCanUndo)) undo(selectedDocument);
if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, isCanRedo)) redo(selectedDocument);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
void Window::editor_documents_update(Resources& resources)
{
if (documents.empty() || !ImGui::BeginTabBar("schemas", ImGuiTabBarFlags_Reorderable)) return;
auto closeRequest = -1;
for (int i = 0; i < (int)documents.size(); ++i)
{
ImGui::PushID(i);
auto label = document_path_label_get(documents[i].path) + (documents[i].isDirty ? " *" : "");
label += std::format("###{}", documents[i].path.string());
auto isOpen = true;
auto isTabOpen = ImGui::BeginTabItem(label.c_str(), &isOpen);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal))
ImGui::SetTooltip("%s", documents[i].path.string().c_str());
if (isTabOpen)
{
selectedDocument = i;
if (ImGui::BeginTabBar("##Document Tabs"))
{
if (ImGui::BeginTabItem("Info"))
{
document_info_update(documents[i]);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Dialogue"))
{
document_dialogue_update(resources, documents[i]);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::EndTabItem();
}
if (!isOpen) closeRequest = i;
ImGui::PopID();
}
ImGui::EndTabBar();
if (closeRequest >= 0) close(closeRequest);
}
void Window::document_info_update(const Document& document)
{
if (!ImGui::BeginTable("##Schema Info", INFO_SCHEMA_TABLE_COLUMNS, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
return;
ImGui::TableSetupColumn("Schema");
ImGui::TableSetupColumn("File");
ImGui::TableSetupColumn("Exists");
ImGui::TableSetupColumn("Valid");
ImGui::TableHeadersRow();
for (auto& schema : document.schemas)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(schema.label.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(schema.filename.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(schema.isExists ? "Yes" : "No");
ImGui::TableNextColumn();
ImGui::TextUnformatted(schema.isValid ? "Yes" : "No");
}
ImGui::EndTable();
}
void Window::document_dialogue_update(Resources& resources, Document& document)
{
auto* dialogue = document.dialogue_schema_get();
if (!dialogue || !dialogue->isExists)
{
ImGui::TextUnformatted("No dialogue.xml found.");
return;
}
if (!dialogue->isValid)
{
ImGui::TextUnformatted("dialogue.xml is not valid.");
return;
}
if (ImGui::IsAnyItemActive()) document.edit_snapshot_begin();
std::vector<Editor::LoadedSchema> loadedSchemas{};
for (auto& schemaFile : document.schemas)
{
if (!schemaFile.isValid) continue;
loadedSchemas.push_back({.path = &schemaFile.path, .schema = &schemaFile.schema});
}
auto before = dialogue->schema;
if (document.editor.update(resources, dialogue->schema, dialogue->path, dialog, loadedSchemas))
{
before = document.edit_snapshot_take_or(std::move(before));
if (before.element_serialize(0) != dialogue->schema.element_serialize(0))
{
auto message = Document::change_message_get(before, dialogue->schema);
document.snapshot_push(std::move(before), message);
}
else
document.isDirty = true;
}
else if (!ImGui::IsAnyItemActive())
document.edit_snapshot_clear();
}
void Window::editor_update(Resources& resources)
{
if (ImGui::BeginChild("##Editor", ImVec2(), ImGuiChildFlags_None, EDITOR_CHILD_FLAGS))
{
editor_menu_update();
if (ImGui::BeginPopupContextWindow("developer-context",
ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
{
auto isCanUndo =
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_undo();
auto isCanRedo =
selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_redo();
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, isCanUndo)) isContextUndoRequested = true;
if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, isCanRedo)) isContextRedoRequested = true;
ImGui::EndPopup();
}
editor_documents_update(resources);
}
ImGui::EndChild();
}
void Window::update(Resources& resources, SDL_Window* window, state::Play* play)
{
dialog.window_set(window);
if (!resources.isDeveloper && shortcut_is_pressed(ImGuiKey_GraveAccent))
{
resources.isDeveloper = true;
if (selectedTab == Tab::CONSOLE) console.focus_request();
}
if (!resources.isDeveloper)
{
isDeveloperOpen = false;
return;
}
recent_files_load();
auto* viewport = ImGui::GetMainViewport();
auto windowSize =
ImVec2(viewport->WorkSize.x * DEVELOPER_WINDOW_SIZE_RATIO, viewport->WorkSize.y * DEVELOPER_WINDOW_SIZE_RATIO);
auto windowPosition = ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x * CENTER_RATIO),
viewport->WorkPos.y + (viewport->WorkSize.y * CENTER_RATIO));
ImGui::SetNextWindowPos(windowPosition, ImGuiCond_Appearing, ImVec2(CENTER_RATIO, CENTER_RATIO));
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Appearing);
if (!isDeveloperOpen)
{
ImGui::SetNextWindowFocus();
if (selectedTab == Tab::CONSOLE) console.focus_request();
}
isDeveloperOpen = true;
isContextUndoRequested = false;
isContextRedoRequested = false;
action = {};
console.action = {};
gIsDocumentUndoMenuRequested = false;
gIsDocumentRedoMenuRequested = false;
if (selectedTab == Tab::EDITOR) editor_shortcuts_update();
dialog_selection_update();
auto gameStyle = ImGui::GetStyle();
ImGui::GetStyle() = ImGuiStyle{};
ImGui::StyleColorsDark();
ImGui::PushFont(resources.font.get(), resource::Font::SMALL);
if (ImGui::Begin("Developer", &resources.isDeveloper, DEVELOPER_WINDOW_FLAGS))
{
auto isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
if (isFocused && (shortcut_is_pressed(ImGuiKey_GraveAccent) || shortcut_is_pressed(ImGuiKey_Escape)))
resources.isDeveloper = false;
if (ImGui::BeginTabBar("##Developer Tabs"))
{
auto isWasConsole = selectedTab == Tab::CONSOLE;
if (ImGui::BeginTabItem("Console"))
{
selectedTab = Tab::CONSOLE;
if (!isWasConsole) console.focus_request();
console.update(resources, play);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Editor"))
{
selectedTab = Tab::EDITOR;
editor_update(resources);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
if (gIsDocumentUndoMenuRequested) isContextUndoRequested = true;
if (gIsDocumentRedoMenuRequested) isContextRedoRequested = true;
action = console.action;
if (isContextUndoRequested) undo(selectedDocument);
if (isContextRedoRequested) redo(selectedDocument);
toasts.update();
ImGui::PopFont();
ImGui::GetStyle() = gameStyle;
}
}