diff --git a/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.cpp b/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.cpp index a075aadd9..74b7b65e9 100644 --- a/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.cpp +++ b/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.cpp @@ -1,15 +1,19 @@ #include "assetBrowser.h" +#include #include #include #include #include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include @@ -48,6 +52,7 @@ namespace AssetBrowser AssetHandle handle; }; typedef std::vector AssetList; + typedef std::vector SelectionList; struct Palette { @@ -92,8 +97,10 @@ namespace AssetBrowser static s32 s_defaultPal = 0; static s32 s_hovered = -1; - static s32 s_selected = -1; static s32 s_menuHeight = 20; + + static SelectionList s_selected; + static s32 s_selectRange[2]; static ViewerInfo s_viewInfo = {}; static AssetList s_viewAssetList; @@ -102,6 +109,11 @@ namespace AssetBrowser // Forward Declarations void updateAssetList(); void reloadAsset(Asset* asset, s32 palId, s32 lightLevel = 32); + bool isSelected(s32 index); + void unselect(s32 index); + void select(s32 index); + void exportSelected(); + s32 getAssetPalette(const char* name); void init() { @@ -214,8 +226,10 @@ namespace AssetBrowser { listChanged = true; s_viewInfo.levelSource = levelSource; - s_selected = -1; + s_selected.clear(); s_hovered = -1; + s_selectRange[0] = -1; + s_selectRange[1] = -1; } s_viewInfo.prevGame = s_viewInfo.game; s_viewInfo.prevType = s_viewInfo.type; @@ -249,7 +263,9 @@ namespace AssetBrowser } } - void drawInfoPanel(Asset* asset, u32 infoWidth, u32 infoHeight) + static s32 s_selectedPalette = 0; + + void drawInfoPanel(Asset* asset, u32 infoWidth, u32 infoHeight, bool multiselect) { DisplayInfo displayInfo; TFE_RenderBackend::getDisplayInfo(&displayInfo); @@ -263,11 +279,57 @@ namespace AssetBrowser bool active = true; ImGui::Begin("Asset Info", &active, window_flags); - if (asset) + if (multiselect) + { + AssetType type = s_viewAssetList[s_selected[0]].type; + ImGui::LabelText("##SelectedCount", "Selected Count: %d", (s32)s_selected.size()); + ImGui::LabelText("##Type", "Type: %s", c_assetType[type]); + + if (type == TYPE_TEXTURE || type == TYPE_SPRITE || type == TYPE_FRAME) + { + listSelectionPalette("Palette", s_palettes, &s_selectedPalette); + if (ImGui::Button("Force Palette")) + { + const size_t count = s_selected.size(); + for (size_t i = 0; i < count; i++) + { + Asset* asset = &s_viewAssetList[s_selected[i]]; + reloadAsset(asset, s_selectedPalette, 32); + } + } + ImGui::SameLine(); + if (ImGui::Button("Reset To Default")) + { + const size_t count = s_selected.size(); + for (size_t i = 0; i < count; i++) + { + Asset* asset = &s_viewAssetList[s_selected[i]]; + s32 palId = getAssetPalette(asset->name.c_str()); + reloadAsset(asset, palId, 32); + } + } + ImGui::Separator(); + } + if (ImGui::Button("Export")) + { + exportSelected(); + } + } + else if (asset) { ImGui::LabelText("##Name", "Name: %s", asset->name.c_str()); ImGui::LabelText("##Type", "Type: %s", c_assetType[asset->type]); + if (!s_selected.empty()) + { + ImGui::Separator(); + if (ImGui::Button("Export")) + { + exportSelected(); + } + ImGui::Separator(); + } + if (asset->type == TYPE_TEXTURE) { EditorTexture* tex = (EditorTexture*)getAssetData(asset->handle); @@ -469,10 +531,78 @@ namespace AssetBrowser } ImGui::End(); } + + bool isSelected(s32 index) + { + if(s_selected.empty()) { return false; } + + const size_t count = s_selected.size(); + const s32* selected = s_selected.data(); + for (size_t i = 0; i < count; i++) + { + if (selected[i] == index) { return true; } + } + return false; + } + + void unselect(s32 index) + { + if (index < 0 || index >= s_viewAssetList.size()) { return; } + const size_t count = s_selected.size(); + const s32* selected = s_selected.data(); + size_t selectedIndex = count; + for (size_t i = 0; i < count; i++) + { + if (selected[i] == index) + { + selectedIndex = i; + break; + } + } + if (selectedIndex < count) + { + s_selected.erase(s_selected.begin() + selectedIndex); + } + } + + void select(s32 index) + { + if (index < 0 || index >= s_viewAssetList.size()) { return; } + const size_t count = s_selected.size(); + const s32* selected = s_selected.data(); + size_t selectedIndex = count; + for (size_t i = 0; i < count; i++) + { + if (selected[i] == index) + { + // Already selected. + return; + } + } + s_selected.push_back(index); + } + + void selectRange() + { + s_selected.clear(); + if (s_selectRange[1] < 0) + { + s_selected.push_back(s_selectRange[0]); + return; + } + if (s_selectRange[0] > s_selectRange[1]) + { + std::swap(s_selectRange[0], s_selectRange[1]); + } + for (s32 i = s_selectRange[0]; i <= s_selectRange[1]; i++) + { + s_selected.push_back(i); + } + } ImVec4 getBorderColor(s32 index) { - if (index == s_selected) + if (isSelected(index)) { return ImVec4(1.0f, 1.0f, 0.5f, 1.0f); } @@ -523,7 +653,12 @@ namespace AssetBrowser if (ImGui::IsMouseClicked(0) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) { mouseClicked = true; - s_selected = -1; + if (!TFE_Input::keyModDown(KEYMOD_CTRL) && !TFE_Input::keyModDown(KEYMOD_SHIFT)) + { + s_selected.clear(); + s_selectRange[0] = -1; + s_selectRange[1] = -1; + } } } else @@ -671,9 +806,40 @@ namespace AssetBrowser if (ImGui::IsWindowHovered()) { s_hovered = a; - if (mouseClicked) + if (mouseClicked && TFE_Input::keyModDown(KEYMOD_CTRL)) + { + if (isSelected(a)) + { + unselect(a); + } + else + { + select(a); + } + + s_selectRange[0] = a; + s_selectRange[1] = -1; + } + else if (mouseClicked && TFE_Input::keyModDown(KEYMOD_SHIFT)) + { + if (s_selectRange[0] < 0) + { + s_selectRange[0] = a; + s_selectRange[1] = -1; + } + else + { + s_selectRange[1] = a; + } + selectRange(); + } + else if (mouseClicked) { - s_selected = a; + s_selected.resize(1); + s_selected[0] = a; + + s_selectRange[0] = a; + s_selectRange[1] = -1; } } } @@ -685,15 +851,20 @@ namespace AssetBrowser // Info Panel Asset* selectedAsset = nullptr; - if (s_selected >= 0 && s_selected < s_viewAssetList.size()) + bool multiselect = false; + if (s_selected.size() == 1 && s_selected[0] < s_viewAssetList.size()) + { + selectedAsset = &s_viewAssetList[s_selected[0]]; + } + else if (s_selected.size() > 1) { - selectedAsset = &s_viewAssetList[s_selected]; + multiselect = true; } else if (s_hovered >= 0 && s_hovered < s_viewAssetList.size()) { selectedAsset = &s_viewAssetList[s_hovered]; } - drawInfoPanel(selectedAsset, infoWidth, infoHeight); + drawInfoPanel(selectedAsset, infoWidth, infoHeight, multiselect); } ImGui::End(); @@ -725,6 +896,42 @@ namespace AssetBrowser { } + void selectAll() + { + const s32 count = (s32)s_viewAssetList.size(); + s_selected.resize(count); + for (s32 i = 0; i < count; i++) + { + s_selected[i] = i; + } + + s_selectRange[0] = 0; + s_selectRange[1] = -1; + } + + void selectNone() + { + s_selected.clear(); + s_selectRange[0] = -1; + s_selectRange[1] = -1; + } + + void invertSelection() + { + const s32 count = (s32)s_viewAssetList.size(); + SelectionList list; + s_selectRange[0] = -1; + s_selectRange[1] = -1; + for (s32 i = 0; i < count; i++) + { + if (!isSelected(i)) + { + list.push_back(i); + } + } + s_selected = list; + } + //////////////////////////////////////////////// // Internal //////////////////////////////////////////////// @@ -1387,4 +1594,118 @@ namespace AssetBrowser } } } + + void exportSelected() + { + if (!FileUtil::directoryExits(s_editorConfig.exportPath)) + { + showMessageBox("ERROR", getErrorMsg(ERROR_INVALID_EXPORT_PATH), s_editorConfig.exportPath); + return; + } + + char path[TFE_MAX_PATH]; + strcpy(path, s_editorConfig.exportPath); + size_t len = strlen(path); + if (path[len - 1] != '/' && path[len - 1] != '\\') + { + path[len] = '\\'; + path[len + 1] = 0; + } + + const char* assetSubPath[] = + { + "Textures", // TYPE_TEXTURE + "Sprites", // TYPE_SPRITE + "Frames", // TYPE_FRAME + "Models", // TYPE_3DOBJ + "Levels", // TYPE_LEVEL + "Palettes", // TYPE_PALETTE + }; + + const s32 count = (s32)s_selected.size(); + const s32* index = s_selected.data(); + for (s32 i = 0; i < count; i++) + { + Asset* asset = &s_viewAssetList[index[i]]; + + char subDir[TFE_MAX_PATH]; + sprintf(subDir, "%s%s", path, assetSubPath[asset->type]); + if (!FileUtil::directoryExits(subDir)) + { + FileUtil::makeDirectory(subDir); + } + + // Read the full contents. + Archive* archive = getArchive(asset->archiveName.c_str(), asset->gameId); + if (!archive) { continue; } + if (!archive->openFile(asset->name.c_str())) { continue;} + + WorkBuffer& buffer = getWorkBuffer(); + size_t len = archive->getFileLength(); + buffer.resize(len); + archive->readFile(buffer.data(), len); + archive->closeFile(); + + char fullPath[TFE_MAX_PATH]; + sprintf(fullPath, "%s\\%s", subDir, asset->name.c_str()); + FileStream outFile; + if (outFile.open(fullPath, FileStream::MODE_WRITE)) + { + outFile.writeBuffer(buffer.data(), (u32)len); + outFile.close(); + } + + if (asset->type == TYPE_TEXTURE) + { + EditorTexture* texture = (EditorTexture*)getAssetData(asset->handle); + if (texture && texture->frameCount == 1) + { + TextureGpu* frame = texture->frames[0]; + buffer.resize(frame->getWidth() * frame->getHeight() * 4); + frame->readCpu(buffer.data()); + + char pngFile[TFE_MAX_PATH]; + FileUtil::replaceExtension(fullPath, "PNG", pngFile); + TFE_Image::writeImage(pngFile, texture->width, texture->height, (u32*)buffer.data()); + } + else if (texture && texture->frameCount > 1) + { + char pngFile[TFE_MAX_PATH]; + FileUtil::getFileNameFromPath(fullPath, pngFile); + + for (u32 f = 0; f < texture->frameCount; f++) + { + sprintf(fullPath, "%s\\%s_%d.png", subDir, pngFile, f); + + TextureGpu* frame = texture->frames[f]; + buffer.resize(frame->getWidth() * frame->getHeight() * 4); + frame->readCpu(buffer.data()); + TFE_Image::writeImage(fullPath, texture->width, texture->height, (u32*)buffer.data()); + } + } + } + else if (asset->type == TYPE_FRAME) + { + EditorFrame* frame = (EditorFrame*)getAssetData(asset->handle); + TextureGpu* tex = frame->texGpu; + buffer.resize(tex->getWidth() * tex->getHeight() * 4); + tex->readCpu(buffer.data()); + + char pngFile[TFE_MAX_PATH]; + FileUtil::replaceExtension(fullPath, "PNG", pngFile); + TFE_Image::writeImage(pngFile, tex->getWidth(), tex->getHeight(), (u32*)buffer.data()); + } + else if (asset->type == TYPE_SPRITE) + { + EditorSprite* sprite = (EditorSprite*)getAssetData(asset->handle); + TextureGpu* tex = sprite->texGpu; + buffer.resize(tex->getWidth() * tex->getHeight() * 4); + tex->readCpu(buffer.data()); + + char pngFile[TFE_MAX_PATH]; + FileUtil::replaceExtension(fullPath, "PNG", pngFile); + TFE_Image::writeImage(pngFile, tex->getWidth(), tex->getHeight(), (u32*)buffer.data()); + } + } + } } \ No newline at end of file diff --git a/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.h b/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.h index 7b9575140..b27631bf9 100644 --- a/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.h +++ b/TheForceEngine/TFE_Editor/AssetBrowser/assetBrowser.h @@ -15,4 +15,8 @@ namespace AssetBrowser void update(); void render(); + + void selectAll(); + void selectNone(); + void invertSelection(); } diff --git a/TheForceEngine/TFE_Editor/editor.cpp b/TheForceEngine/TFE_Editor/editor.cpp index dd6a947fe..7ef96de14 100644 --- a/TheForceEngine/TFE_Editor/editor.cpp +++ b/TheForceEngine/TFE_Editor/editor.cpp @@ -19,6 +19,13 @@ namespace TFE_Editor EDIT_ASSET, EDIT_LEVEL, }; + + struct MessageBox + { + bool active = false; + char id[512]; + char msg[TFE_MAX_PATH * 2] = ""; + }; static bool s_showPerf = true; static bool s_showEditor = true; @@ -28,6 +35,8 @@ namespace TFE_Editor static bool s_configView = false; static WorkBuffer s_workBuffer; + static MessageBox s_msgBox = {}; + static ImFont* s_fonts[FONT_COUNT * FONT_SIZE_COUNT] = { 0 }; void menu(); @@ -46,6 +55,7 @@ namespace TFE_Editor loadFonts(); loadConfig(); AssetBrowser::init(); + s_msgBox = {}; } void disable() @@ -53,12 +63,41 @@ namespace TFE_Editor AssetBrowser::destroy(); } + void messageBoxUi() + { + pushFont(FONT_SMALL); + if (ImGui::BeginPopupModal(s_msgBox.id, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGuiStyle& style = ImGui::GetStyle(); + f32 textWidth = ImGui::CalcTextSize(s_msgBox.msg).x + style.FramePadding.x; + f32 buttonWidth = ImGui::CalcTextSize("OK").x; + + ImGui::Text(s_msgBox.msg); + ImGui::Separator(); + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (textWidth - buttonWidth) * 0.5f); + if (ImGui::Button("OK")) + { + s_msgBox.active = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + popFont(); + } + bool update(bool consoleOpen) { TFE_RenderBackend::clearWindow(); + + if (s_msgBox.active) + { + ImGui::OpenPopup(s_msgBox.id); + } + menu(); - if (configSetupRequired()) + if (configSetupRequired() && !s_msgBox.active) { s_editorMode = EDIT_CONFIG; } @@ -75,9 +114,22 @@ namespace TFE_Editor AssetBrowser::update(); } + if (s_msgBox.active) + { + messageBoxUi(); + } + if (TFE_Input::keyPressed(KEY_ESCAPE)) { - s_exitEditor = true; + if (s_msgBox.active) + { + s_msgBox.active = false; + ImGui::CloseCurrentPopup(); + } + else + { + s_exitEditor = true; + } } return s_exitEditor; @@ -149,6 +201,31 @@ namespace TFE_Editor } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Select")) + { + if (ImGui::MenuItem("Select All", NULL, (bool*)NULL)) + { + if (s_editorMode == EDIT_ASSET) + { + AssetBrowser::selectAll(); + } + } + if (ImGui::MenuItem("Select None", NULL, (bool*)NULL)) + { + if (s_editorMode == EDIT_ASSET) + { + AssetBrowser::selectNone(); + } + } + if (ImGui::MenuItem("Invert Selection", NULL, (bool*)NULL)) + { + if (s_editorMode == EDIT_ASSET) + { + AssetBrowser::invertSelection(); + } + } + ImGui::EndMenu(); + } } endMenuBar(); @@ -185,4 +262,17 @@ namespace TFE_Editor { ImGui::PopFont(); } + + void showMessageBox(const char* type, const char* msg, ...) + { + char fullStr[TFE_MAX_PATH * 2]; + va_list arg; + va_start(arg, msg); + vsprintf(fullStr, msg, arg); + va_end(arg); + + s_msgBox.active = true; + strcpy(s_msgBox.msg, fullStr); + sprintf(s_msgBox.id, "%s##MessageBox", type); + } } diff --git a/TheForceEngine/TFE_Editor/editor.h b/TheForceEngine/TFE_Editor/editor.h index ac2414cf6..56e895bfc 100644 --- a/TheForceEngine/TFE_Editor/editor.h +++ b/TheForceEngine/TFE_Editor/editor.h @@ -26,6 +26,8 @@ namespace TFE_Editor void pushFont(FontType type); void popFont(); + void showMessageBox(const char* type, const char* msg, ...); + // Resizable temporary memory. WorkBuffer& getWorkBuffer(); } diff --git a/TheForceEngine/TFE_Editor/editorProject.cpp b/TheForceEngine/TFE_Editor/editorProject.cpp new file mode 100644 index 000000000..822906f8e --- /dev/null +++ b/TheForceEngine/TFE_Editor/editorProject.cpp @@ -0,0 +1,42 @@ +#include "editorProject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace TFE_Editor +{ + static Project* s_curProject = nullptr; + + Project* getProject() + { + return s_curProject; + } + + void closeProject() + { + // TODO: Clear project specific editor data. + s_curProject = nullptr; + } + + void saveProject() + { + } + + bool ui_loadProject() + { + return false; + } + + void ui_newProject() + { + } +} \ No newline at end of file diff --git a/TheForceEngine/TFE_Editor/editorProject.h b/TheForceEngine/TFE_Editor/editorProject.h new file mode 100644 index 000000000..49d711e4a --- /dev/null +++ b/TheForceEngine/TFE_Editor/editorProject.h @@ -0,0 +1,47 @@ +#pragma once +////////////////////////////////////////////////////////////////////// +// The Force Engine Editor +// A system built to view and edit Dark Forces data files. +// The viewing aspect needs to be put in place at the beginning +// in order to properly test elements in isolation without having +// to "play" the game as intended. +////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include + +namespace TFE_Editor +{ + enum ProjectType + { + PROJ_RESOURCE_ONLY = 0, + PROJ_LEVELS, + PROJ_COUNT + }; + + enum FeatureSet + { + FSET_VANILLA = 0, + FSET_TFE, + FSET_COUNT + }; + + struct Project + { + std::string name; + std::string path; + std::string desc; + std::string authors; + + ProjectType type; + GameID game; + FeatureSet featureSet; + }; + + Project* getProject(); + + bool ui_loadProject(); + void ui_closeProject(); + void ui_newProject(); +} diff --git a/TheForceEngine/TFE_Editor/errorMessages.cpp b/TheForceEngine/TFE_Editor/errorMessages.cpp new file mode 100644 index 000000000..b96a8b4c5 --- /dev/null +++ b/TheForceEngine/TFE_Editor/errorMessages.cpp @@ -0,0 +1,19 @@ +#include "errorMessages.h" +#include + +namespace TFE_Editor +{ + static const char* c_errorMsg[] = + { + // ERROR_INVALID_EXPORT_PATH + "Export Path '%s' is invalid, cannot export assets!\n" + "Please go to the 'Editor' menu, select 'Editor Config',\n" + "and setup a valid 'Export Path'.", + }; + + const char* getErrorMsg(EditorError err) + { + assert(err >= 0 && err < ERROR_COUNT); + return c_errorMsg[err]; + } +} diff --git a/TheForceEngine/TFE_Editor/errorMessages.h b/TheForceEngine/TFE_Editor/errorMessages.h new file mode 100644 index 000000000..f3bbe8240 --- /dev/null +++ b/TheForceEngine/TFE_Editor/errorMessages.h @@ -0,0 +1,21 @@ +#pragma once +////////////////////////////////////////////////////////////////////// +// The Force Engine Editor +// A system built to view and edit Dark Forces data files. +// The viewing aspect needs to be put in place at the beginning +// in order to properly test elements in isolation without having +// to "play" the game as intended. +////////////////////////////////////////////////////////////////////// +#include +#include + +namespace TFE_Editor +{ + enum EditorError + { + ERROR_INVALID_EXPORT_PATH = 0, + ERROR_COUNT, + }; + + const char* getErrorMsg(EditorError err); +} diff --git a/TheForceEngine/TFE_RenderBackend/Win32OpenGL/textureGpu.cpp b/TheForceEngine/TFE_RenderBackend/Win32OpenGL/textureGpu.cpp index 4fff44bc3..c307015c3 100644 --- a/TheForceEngine/TFE_RenderBackend/Win32OpenGL/textureGpu.cpp +++ b/TheForceEngine/TFE_RenderBackend/Win32OpenGL/textureGpu.cpp @@ -234,3 +234,10 @@ void TextureGpu::clearSlots(u32 count, u32 start/* = 0*/) glBindTexture(GL_TEXTURE_2D, 0); } } + +void TextureGpu::readCpu(u8* image) +{ + glBindTexture(GL_TEXTURE_2D, m_gpuHandle); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, image); + glBindTexture(GL_TEXTURE_2D, 0); +} \ No newline at end of file diff --git a/TheForceEngine/TFE_RenderBackend/textureGpu.h b/TheForceEngine/TFE_RenderBackend/textureGpu.h index f1c02f015..c6d2f8cac 100644 --- a/TheForceEngine/TFE_RenderBackend/textureGpu.h +++ b/TheForceEngine/TFE_RenderBackend/textureGpu.h @@ -47,6 +47,8 @@ class TextureGpu u32 getHeight() const { return m_height; } u32 getLayers() const { return m_layers; } + void readCpu(u8* image); + inline u32 getHandle() const { return m_gpuHandle; } private: diff --git a/TheForceEngine/TheForceEngine.vcxproj b/TheForceEngine/TheForceEngine.vcxproj index 453b4baab..b9e8ffb21 100644 --- a/TheForceEngine/TheForceEngine.vcxproj +++ b/TheForceEngine/TheForceEngine.vcxproj @@ -454,6 +454,8 @@ echo ^)"; + + @@ -811,6 +813,8 @@ echo ^)"; + + diff --git a/TheForceEngine/TheForceEngine.vcxproj.filters b/TheForceEngine/TheForceEngine.vcxproj.filters index ea230169b..62a03ec6a 100644 --- a/TheForceEngine/TheForceEngine.vcxproj.filters +++ b/TheForceEngine/TheForceEngine.vcxproj.filters @@ -1291,6 +1291,12 @@ Source\TFE_Editor\AssetBrowser + + Source\TFE_Editor + + + Source\TFE_Editor + @@ -2190,6 +2196,12 @@ Source\TFE_Editor\AssetBrowser + + Source\TFE_Editor + + + Source\TFE_Editor + diff --git a/TheForceEngine/gitVersion.h b/TheForceEngine/gitVersion.h index a57af2ec6..6e4fe3849 100644 --- a/TheForceEngine/gitVersion.h +++ b/TheForceEngine/gitVersion.h @@ -1,3 +1,3 @@ const char c_gitVersion[] = R"( -v1.09.530-11-gd9a077bd +v1.09.530-13-gd029fe2b )";