/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "backends/imgui/IconsMaterialSymbols.h" #define IMGUI_DEFINE_MATH_OPERATORS #include "backends/imgui/imgui.h" #include "backends/imgui/imgui_fonts.h" #include "common/archive.h" #include "common/compression/unzip.h" #include "common/debug.h" #include "common/path.h" #include "common/stack.h" #include "common/system.h" #include "graphics/managed_surface.h" #include "qdengine/debugger/dt-internal.h" #include "qdengine/qd_fwd.h" #include "qdengine/qdcore/qd_animation.h" #include "qdengine/qdcore/qd_animation_frame.h" #include "qdengine/qdcore/qd_file_manager.h" #include "qdengine/qdcore/qd_game_dispatcher.h" #include "qdengine/qdcore/qd_game_object.h" #include "qdengine/qdcore/qd_game_object_moving.h" #include "qdengine/qdcore/qd_game_scene.h" #include "qdengine/qdengine.h" #include "qdengine/system/graphics/gr_dispatcher.h" namespace QDEngine { const int TILES_ID = -1337; ImGuiState *_state = nullptr; ImGuiImage getImageID(Common::Path filename, int frameNum) { Common::String key = Common::String::format("%s:%d", filename.toString().c_str(), frameNum); if (_state->_frames.contains(key)) return _state->_frames[key]; int sx = 10, sy = 10; Graphics::ManagedSurface *surface = nullptr; if (_state->_displayMode == kDisplayQDA) { // Load the animation qdAnimation *animation = new qdAnimation(); animation->qda_load(filename); _state->_qdaToDisplayFrameCount = animation->num_frames(); if (frameNum == TILES_ID) { if (animation->tileAnimation()) { surface = animation->tileAnimation()->dumpTiles(25); sx = surface->w; sy = surface->h; } } else if (frameNum < 0) { // Tiles if (animation->tileAnimation()) { surface = animation->tileAnimation()->dumpFrameTiles(-frameNum + 1, 0.91670f); sx = surface->w; sy = surface->h; } } else { if (animation->tileAnimation()) { Vect2i size = animation->tileAnimation()->frameSize(); sx = size.x; sy = size.y; } else { qdAnimationFrame *frame = animation->get_frame(frameNum); if (frame) { sx = frame->size_x(); sy = frame->size_y(); } } surface = new Graphics::ManagedSurface(sx, sy, g_engine->_pixelformat); animation->set_cur_frame(frameNum); grDispatcher::instance()->surfaceOverride(surface); animation->redraw(sx / 2, sy / 2, 0, 0.91670f, 0); grDispatcher::instance()->resetSurfaceOverride(); } delete animation; } else if (_state->_displayMode == kDisplayTGA) { qdSprite *sprite = new qdSprite(); if (sprite->load(filename)) { sx = sprite->size_x(); sy = sprite->size_y(); surface = new Graphics::ManagedSurface(sx, sy, g_engine->_pixelformat); grDispatcher::instance()->surfaceOverride(surface); sprite->redraw(sx / 2, sy / 2, 0); grDispatcher::instance()->resetSurfaceOverride(); } else { warning("Error loading TGA file '%s'", transCyrillic(filename.toString())); } delete sprite; } if (surface) _state->_frames[key] = { (ImTextureID)g_system->getImGuiTexture(*surface->surfacePtr()), sx, sy }; delete surface; return _state->_frames[key]; } void showImage(const ImGuiImage &image, const char *name, float scale) { ImVec2 size = { (float)image.width * scale, (float)image.height * scale }; ImGui::BeginGroup(); ImVec2 screenPos = ImGui::GetCursorScreenPos(); ImGui::GetWindowDrawList()->AddRect(screenPos, screenPos + ImVec2(size.x, size.y), 0xFFFFFFFF); ImGui::Image(image.id, size); ImGui::EndGroup(); //setToolTipImage(image, name); } FileTree::FileTree(Common::Path *p, Common::String n, bool node, int i) { id = i; if (!node) { name = (char *)transCyrillic(n); } else { path = *p; name = (char *)transCyrillic(p->baseName()); } } void populateFileList() { Common::Array files; // Iterate through the 3 resource pak files for (int i = 0; i < qdFileManager::instance().get_num_packages(); i++) { Common::Archive *archive = qdFileManager::instance().get_package(i); Common::ArchiveMemberList members; if (archive) archive->listMembers(members); for (auto &it : members) files.push_back(it->getPathInArchive()); } Common::sort(files.begin(), files.end()); // Now build a tree Common::Path curr; Common::Stack treeStack; _state->_files.name = "Resource"; treeStack.push(&_state->_files); int id = 0; for (unsigned int f = 0; f < files.size(); f++) { // Skip duplicates between the archives if (f && files[f] == files[f - 1]) continue; Common::Path parent = files[f].getParent(); if (parent != curr) { Common::StringArray curArr = curr.splitComponents(); Common::StringArray newArr = parent.splitComponents(); if (curArr.back().empty()) curArr.pop_back(); if (newArr.back().empty()) newArr.pop_back(); unsigned int pos = 0; while (pos < curArr.size() && pos < newArr.size() && curArr[pos] == newArr[pos]) pos++; // if we need to close directories if (pos < curArr.size()) { for (unsigned int i = pos; i < curArr.size(); i++) (void)treeStack.pop(); } for (; pos < newArr.size(); pos++) { if (id == 0 && newArr[pos] == "Resource") // Skip the root node continue; treeStack.top()->children.push_back(new FileTree(nullptr, newArr[pos], false, ++id)); treeStack.push(treeStack.top()->children.back()); } curr = parent; } treeStack.top()->children.push_back(new FileTree(&files[f], "", true, ++id)); id++; } } static void displayQDA() { int totalFrames = _state->_qdaToDisplayFrameCount; ImGuiImage imgID; if (!_state->_fileToDisplay.empty()) { imgID = getImageID(_state->_fileToDisplay, _state->_qdaToDisplayFrame); ImGui::Text("Frame %s: %d of %d [%d x %d]", transCyrillic(_state->_fileToDisplay.toString()), _state->_qdaToDisplayFrame + 1, totalFrames, imgID.width, imgID.height); } else { ImGui::Text("Frame "); } ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_None; if (ImGui::BeginTabBar("FrameTabBar", tab_bar_flags)) { if (ImGui::BeginTabItem("Animation")) { if (ImGui::Button(ICON_MS_FAST_REWIND)) { _state->_qdaToDisplayFrame = 0; _state->_qdaIsPlaying = false; } ImGui::SameLine(); if (ImGui::Button(ICON_MS_SKIP_PREVIOUS)) { _state->_qdaToDisplayFrame = _state->_qdaToDisplayFrame + totalFrames - 1; _state->_qdaToDisplayFrame %= totalFrames; } ImGui::SameLine(); if (ImGui::Button(ICON_MS_PLAY_ARROW)) _state->_qdaIsPlaying = !_state->_qdaIsPlaying; ImGui::SameLine(); if (ImGui::Button(ICON_MS_SKIP_NEXT)) { _state->_qdaToDisplayFrame += 1; _state->_qdaToDisplayFrame %= totalFrames; } ImGui::SameLine(); if (ImGui::Button(ICON_MS_FAST_FORWARD)) { _state->_qdaToDisplayFrame = totalFrames - 1; _state->_qdaIsPlaying = false; } ImGui::SameLine(); // Frame Count char buf[6]; snprintf(buf, 6, "%d", _state->_qdaToDisplayFrame); ImGui::SetNextItemWidth(35); ImGui::InputText("##frame", buf, 5, ImGuiInputTextFlags_CharsDecimal); ImGui::SetItemTooltip("Frame"); ImGui::Separator(); if (!_state->_fileToDisplay.empty()) { showImage(imgID, (char *)transCyrillic(_state->_fileToDisplay.toString()), 1.0); } else { ImGui::InvisibleButton("##canvas", ImVec2(32.f, 32.f)); } ImGui::SameLine(); imgID = getImageID(_state->_fileToDisplay, -_state->_qdaToDisplayFrame - 1); showImage(imgID, (char *)transCyrillic(_state->_fileToDisplay.toString()), 1.0); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Tiles")) { if (!_state->_fileToDisplay.empty()) { imgID = getImageID(_state->_fileToDisplay, TILES_ID); showImage(imgID, (char *)transCyrillic(_state->_fileToDisplay.toString()), 1.0); } else { ImGui::InvisibleButton("##canvas", ImVec2(32.f, 32.f)); } ImGui::EndTabItem(); } ImGui::EndTabBar(); } } static void displayTGA() { ImGuiImage imgID; imgID = getImageID(_state->_fileToDisplay, 0); ImGui::Text("TGA %s: [%d x %d]", transCyrillic(_state->_fileToDisplay.toString()), imgID.width, imgID.height); ImGui::Separator(); showImage(imgID, (char *)transCyrillic(_state->_fileToDisplay.toString()), 1.0); } void displayTree(FileTree *tree) { if (tree->children.empty()) { // It is a file if (ImGui::Selectable(tree->name.c_str(), _state->_fileToDisplay == tree->path)) { _state->_fileToDisplay = tree->path; if (tree->name.hasSuffixIgnoreCase(".qda")) { _state->_qdaToDisplayFrame = 0; _state->_qdaIsPlaying = false; _state->_displayMode = kDisplayQDA; } else if (tree->name.hasSuffixIgnoreCase(".tga")) { _state->_displayMode = kDisplayTGA; } else { _state->_displayMode = -1; } } return; } static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_DrawLinesFull; if (ImGui::TreeNodeEx((void*)(intptr_t)(tree->id), base_flags, tree->name.c_str())) { for (auto &it : tree->children) displayTree(it); ImGui::TreePop(); } } void showArchives() { if (!_state->_showArchives) return; // Calculate the viewport size ImVec2 viewportSize = ImGui::GetMainViewport()->Size; // Calculate the window size ImVec2 windowSize = ImVec2( viewportSize.x * 0.9f, viewportSize.y * 0.9f ); // Calculate the centered position ImVec2 centeredPosition = ImVec2( (viewportSize.x - windowSize.x) * 0.5f, (viewportSize.y - windowSize.y) * 0.5f ); // Set the next window position and size ImGui::SetNextWindowPos(centeredPosition, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(windowSize, ImGuiCond_FirstUseEver); if (ImGui::Begin("Archives", &_state->_showArchives)) { ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.4f, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_None); ImGui::Button(ICON_MS_FILTER_ALT); ImGui::SameLine(); _state->_nameFilter.Draw(); ImGui::Separator(); if (_state->_files.children.empty()) populateFileList(); displayTree(&_state->_files); ImGui::EndChild(); ImGui::SameLine(); { // Right pane ImGui::BeginChild("ChildR", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_Borders); if (_state->_displayMode == kDisplayQDA) { displayQDA(); } else if (_state->_displayMode == kDisplayTGA) { displayTGA(); } ImGui::EndChild(); } } ImGui::End(); } void showSceneObjects() { if (!_state->_showSceneObjects) return; ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(300, 250), ImGuiCond_FirstUseEver); if (ImGui::Begin("Scene Objects", &_state->_showSceneObjects)) { qdGameScene *scene; qdGameDispatcher *dp = qdGameDispatcher::get_dispatcher(); if (dp && ((scene = dp->get_active_scene()))) { if (!scene->object_list().empty()) { for (auto &it : g_engine->_visible_objects) { if (ImGui::Selectable((char *)transCyrillic(it->name()), _state->_objectToDisplay == it->name())) { _state->_objectToDisplay = it->name(); } } } } } ImGui::End(); } void showScenePersonages() { if (!_state->_showScenePersonages) return; ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(300, 250), ImGuiCond_FirstUseEver); if (ImGui::Begin("Scene Personages", &_state->_showScenePersonages)) { qdGameScene *scene; qdGameDispatcher *dp = qdGameDispatcher::get_dispatcher(); if (dp && ((scene = dp->get_active_scene()))) { if (!scene->getPersonages()->empty()) { if (ImGui::BeginTable("Personages", 8, ImGuiTableFlags_Borders)) { ImGuiTableFlags flags = ImGuiTableColumnFlags_WidthFixed; ImGui::TableSetupColumn("Name", flags); ImGui::TableSetupColumn("Flags", flags); ImGui::TableSetupColumn("Control", flags); ImGui::TableSetupColumn("Movement", flags); ImGui::TableSetupColumn("Frame", flags); ImGui::TableSetupColumn("Time", flags); ImGui::TableSetupColumn("Anim Flags", flags); ImGui::TableSetupColumn("Anim Status", flags); ImGui::TableHeadersRow(); for (auto &it : *scene->getPersonages()) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text((char *)transCyrillic(it->name())); qdGameObjectState *st = it->get_state(it->cur_state()); ImGui::TableNextColumn(); ImGui::Text("%s", st ? qdGameObjectState::flag2str(st->flags(), true, true).c_str() : ""); ImGui::SetItemTooltip("%s", st ? qdGameObjectState::flag2str(st->flags(), true).c_str() : ""); ImGui::TableNextColumn(); ImGui::Text(qdGameObjectMoving::control2str(it->get_control_types(), true).c_str()); ImGui::TableNextColumn(); ImGui::Text(qdGameObjectMoving::movement2str(it->get_movement_mode(), true).c_str()); qdAnimation *anim = it->get_animation(); ImGui::TableNextColumn(); ImGui::Text("%d / %d", anim->get_cur_frame_number(), anim->num_frames()); ImGui::TableNextColumn(); ImGui::Text("%f / %f", anim->cur_time(), anim->length()); ImGui::TableNextColumn(); ImGui::Text(qdAnimation::flag2str(anim->flags(), true, true).c_str()); ImGui::SetItemTooltip(qdAnimation::flag2str(anim->flags(), true).c_str()); ImGui::TableNextColumn(); ImGui::Text(qdAnimation::status2str(anim->status(), true).c_str()); } ImGui::EndTable(); } } } } ImGui::End(); } void onImGuiInit() { ImGuiIO &io = ImGui::GetIO(); io.Fonts->AddFontDefault(); static const ImWchar cyrillic_ranges[] = { 0x0020, 0x00FF, // Basic Latin + Latin Supplement 0x0400, 0x04FF, // Cyrillic 0 }; io.FontDefault = ImGui::addTTFFontFromArchive("LiberationSans-Regular.ttf", 16.0f, nullptr, cyrillic_ranges);; ImFontConfig icons_config; icons_config.MergeMode = true; icons_config.PixelSnapH = false; icons_config.OversampleH = 3; icons_config.OversampleV = 3; icons_config.GlyphOffset = {0, 4}; static const ImWchar icons_ranges[] = {ICON_MIN_MS, ICON_MAX_MS, 0}; ImGui::addTTFFontFromArchive("MaterialSymbolsSharp.ttf", 16.f, &icons_config, icons_ranges); _state = new ImGuiState(); } void onImGuiRender() { if (!debugChannelSet(-1, kDebugImGui)) { ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse; return; } if (!_state) return; if (_state->_qdaIsPlaying && (int)g_system->getMillis() > _state->_qdaNextFrameTimestamp) { _state->_qdaToDisplayFrame++; _state->_qdaToDisplayFrame %= _state->_qdaToDisplayFrameCount; _state->_qdaNextFrameTimestamp = g_system->getMillis() + 50; // 20 fps } ImGui::GetIO().ConfigFlags &= ~(ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse); if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("View")) { ImGui::SeparatorText("Windows"); ImGui::MenuItem("Archives", NULL, &_state->_showArchives); ImGui::MenuItem("Scene Objects", NULL, &_state->_showSceneObjects); ImGui::MenuItem("Scene Personages", NULL, &_state->_showScenePersonages); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } showArchives(); showSceneObjects(); showScenePersonages(); } void onImGuiCleanup() { delete _state; _state = nullptr; } } // namespace QDEngine