/* 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 "lastexpress/debug.h" // Data #include "lastexpress/fight/fight.h" #include "lastexpress/game/beetle.h" #include "lastexpress/game/logic.h" #include "lastexpress/game/savegame.h" #include "lastexpress/menu/clock.h" #include "lastexpress/graphics.h" #include "lastexpress/lastexpress.h" #include "common/debug-channels.h" #include "common/md5.h" #include "backends/imgui/imgui.h" namespace LastExpress { #ifdef USE_IMGUI typedef struct ImGuiState { LastExpressEngine *_engine = nullptr; ImGuiTextFilter _filter; int _currentTab = 0; int _selectedCharacter = 0; bool _forceReturnToListView = false; float _rightPanelWidth = 0.0f; float _bottomPanelHeight = 0.0f; float _rightTopPanelHeight = 0.0f; int _ticksToAdvance = 0; ImTextureID _textureID = ImTextureID_Invalid; int _selectedGlobalVarRow = -1; } ImGuiState; ImGuiState *_state = nullptr; void onImGuiInit() { ImGuiIO &io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; _state = new ImGuiState(); _state->_engine = (LastExpressEngine *)g_engine; } void onImGuiRender() { if (_state->_engine->shouldQuit() || _state->_engine->_exitFromMenuButton) return; if (!debugChannelSet(-1, kDebugConsole)) { ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse; return; } ImGui::GetIO().ConfigFlags &= ~(ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse); ImGui::SetNextWindowSize(ImVec2(1400, 1000), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(30, 30), ImGuiCond_FirstUseEver); _state->_rightPanelWidth = 300.0f; _state->_bottomPanelHeight = 280.0f; _state->_rightTopPanelHeight = 200.0f; // Disable the debugger when the NIS engine is running... if (_state->_engine->getNISManager()->getNISFlag() & kNisFlagPlaying) return; // Disable the debugger when the fighting engine is running... if (_state->_engine->_fight) return; if (ImGui::Begin("Last Express Debugger")) { ImVec2 windowSize = ImGui::GetContentRegionAvail(); // Right panel splitter... { ImGui::SameLine(windowSize.x - _state->_rightPanelWidth - 8); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 0.3f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); ImGui::Button("##vsplitter", ImVec2(8, windowSize.y - _state->_bottomPanelHeight)); ImGui::PopStyleVar(); ImGui::PopStyleColor(); if (ImGui::IsItemActive()) _state->_rightPanelWidth += ImGui::GetIO().MouseDelta.x * -1.0f; _state->_rightPanelWidth = CLIP(_state->_rightPanelWidth, 150.0f, windowSize.x * 0.7f); } // Bottom panel splitter... { float splitterY = windowSize.y - _state->_bottomPanelHeight - 8; ImGui::SetCursorPosY(splitterY); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 0.3f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); ImGui::Button("##hsplitter", ImVec2(windowSize.x, 8)); ImGui::PopStyleVar(); ImGui::PopStyleColor(); if (ImGui::IsItemActive()) _state->_bottomPanelHeight += ImGui::GetIO().MouseDelta.y * -1.0f; _state->_bottomPanelHeight = CLIP(_state->_bottomPanelHeight, 150.0f, windowSize.y * 0.7f); } // Right panel splitter (between top and bottom sections)... { ImGui::SetCursorPos(ImVec2(windowSize.x - _state->_rightPanelWidth, _state->_rightTopPanelHeight)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 0.3f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); ImGui::Button("##rightsplitter", ImVec2(_state->_rightPanelWidth, 8)); ImGui::PopStyleVar(); ImGui::PopStyleColor(); if (ImGui::IsItemActive()) _state->_rightTopPanelHeight += ImGui::GetIO().MouseDelta.y; _state->_rightTopPanelHeight = CLIP(_state->_rightTopPanelHeight, 100.0f, windowSize.y - _state->_bottomPanelHeight - 100.0f); } float mainAreaWidth = windowSize.x - _state->_rightPanelWidth - 8; float mainAreaHeight = windowSize.y - _state->_bottomPanelHeight - 8; // Top-left: Character Debugger ImGui::SetCursorPos(ImVec2(0, 16)); ImGui::BeginChild("CharacterDebugger", ImVec2(mainAreaWidth, mainAreaHeight), true); { ImGuiTabItemFlags flags = 0; // Tab bar for different views... if (ImGui::BeginTabBar("CharacterViews")) { if (_state->_forceReturnToListView) { _state->_forceReturnToListView = false; flags |= ImGuiTabItemFlags_SetSelected; } if (ImGui::BeginTabItem("List View", nullptr, flags)) { _state->_currentTab = 0; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Grid View")) { _state->_currentTab = 1; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Pinned Characters")) { _state->_currentTab = 2; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Current Scene")) { _state->_currentTab = 3; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Global Vars")) { _state->_currentTab = 4; ImGui::EndTabItem(); } ImGui::EndTabBar(); } if (_state->_currentTab >= 0 && _state->_currentTab <= 2) { // Update the character filter... _state->_filter.Draw("Filter Characters", 180); ImGui::SameLine(); if (ImGui::Button("Clear")) { _state->_filter.Clear(); } } // Show corresponding view based on selected tab... switch (_state->_currentTab) { case 0: // List View _state->_engine->getLogicManager()->renderCharacterList(_state->_selectedCharacter); break; case 1: // Grid View _state->_engine->getLogicManager()->renderCharacterGrid(false, _state->_selectedCharacter); break; case 2: // Pinned Characters _state->_engine->getLogicManager()->renderCharacterGrid(true, _state->_selectedCharacter); break; case 3: // Current Scene _state->_engine->getLogicManager()->renderCurrentSceneDebugger(); break; case 4: // Global Vars _state->_engine->getLogicManager()->renderGlobalVars(); break; default: break; } } ImGui::EndChild(); // Top-right area // Top-right is Clock ImGui::SetCursorPos(ImVec2(mainAreaWidth + 8, 16)); ImGui::BeginChild("Clock", ImVec2(_state->_rightPanelWidth, _state->_rightTopPanelHeight), true); { _state->_engine->getClock()->showCurrentTime(); } ImGui::EndChild(); // Bottom-right is Where's Cath? ImGui::SetCursorPos(ImVec2(mainAreaWidth + 8, _state->_rightTopPanelHeight + 8)); ImGui::BeginChild("CathInfo", ImVec2(_state->_rightPanelWidth, mainAreaHeight - _state->_rightTopPanelHeight - 8), true); { ImGui::Text("Where's Cath?"); ImGui::Separator(); _state->_engine->getLogicManager()->showCurrentTrainNode(); } ImGui::EndChild(); // Bottom-right is Where's Cath? ImGui::SetCursorPos(ImVec2(mainAreaWidth + 8, _state->_rightTopPanelHeight * 2 - 16)); ImGui::BeginChild("EngineInfo", ImVec2(_state->_rightPanelWidth, mainAreaHeight - (_state->_rightTopPanelHeight * 2 - 16)), true); { ImGui::Text("Engine Info"); ImGui::Separator(); _state->_engine->showEngineInfo(); } ImGui::EndChild(); // Bottom panel: Train Map ImGui::SetCursorPos(ImVec2(0, windowSize.y - _state->_bottomPanelHeight)); ImGui::BeginChild("TrainMap", ImVec2(windowSize.x, _state->_bottomPanelHeight), true); { _state->_engine->getLogicManager()->showTrainMapWindow(); } ImGui::EndChild(); } ImGui::End(); } void onImGuiCleanup() { delete _state; _state = nullptr; } void Clock::showCurrentTime() { int timeSource = _engine->getMenu()->isShowingMenu() ? _engine->getClock()->getTimeShowing() : (int)_engine->getLogicManager()->getGameTime(); int hours = timeSource % 1296000 / 54000; int minutes = timeSource % 54000 / 900 % 60 % 60; int seconds = (timeSource % 900) / 15; char clockText[32]; Common::sprintf_s(clockText, "%02d:%02d:%02d", hours, minutes, seconds); ImDrawList *drawList = ImGui::GetWindowDrawList(); ImVec2 windowPos = ImGui::GetWindowPos(); ImVec2 windowCenter = ImVec2( windowPos.x + ImGui::GetWindowSize().x / 2, windowPos.y + ImGui::GetWindowSize().y / 2 ); drawList->AddText( ImGui::GetFont(), 4.0f * ImGui::GetFontSize(), ImVec2(windowPos.x + 40, windowCenter.y - 20), IM_COL32(255, 255, 255, 255), clockText ); char dayText[32]; int dayOffset = timeSource / 1296000; Common::sprintf_s(dayText, "July %d, 1914", 24 + dayOffset); // The game starts in July 24, 1914... drawList->AddText( ImGui::GetFont(), 2.0f * ImGui::GetFontSize(), ImVec2(windowPos.x + 60, windowCenter.y + 35), IM_COL32(255, 255, 255, 255), dayText ); ImGui::Text("Game time: %d", _engine->getLogicManager()->getGameTime()); ImGui::Text("Real time: %d", _engine->getLogicManager()->getRealTime()); ImGui::Text("Time speed: %d", _engine->getLogicManager()->getTimeSpeed()); ImGui::Text("Grace timer: %d", _engine->_gracePeriodTimer); ImGui::Separator(); } Common::String LogicManager::translateNodeProperty(int property) { Common::StringArray properties = { "No Property", "Has Door", "Has Item", "Has 2 Items", "Has Door Item", "Has 3 Items", "Model Pad", "Soft Point", "Soft Point Item", "Auto Walk", "Sleeping On Bed", "Beetle Area", "Pulling Emergency Stop", "Rebecca's Diary", "Stops Fast Walk" }; if (property >= 0 && property <= 9) { return properties[property]; } else if (property >= 128 && property <= 133) { return properties[property - 128 + 9]; } else { return "Unknown property " + Common::String(property); } } void LogicManager::showCurrentTrainNode() { ImGui::Text("Node property: %s", translateNodeProperty(_trainData[_activeNode].property).c_str()); ImGui::Text("Direction: %u", _trainData[_activeNode].cathDir); ImGui::Text("Node position:"); ImGui::BulletText("Car %u", _trainData[_activeNode].nodePosition.car); ImGui::BulletText("Location %u", _trainData[_activeNode].nodePosition.location); ImGui::BulletText("Position %u", _trainData[_activeNode].nodePosition.position); ImGui::Text("Parameters: %u %u %u", _trainData[_activeNode].parameter1, _trainData[_activeNode].parameter2, _trainData[_activeNode].parameter3); ImGui::Text("Scene filename: %s", _trainData[_activeNode].sceneFilename); } void LogicManager::showCharacterDebugger() { ImGuiTabItemFlags flags = 0; if (ImGui::BeginTabBar("CharacterViews")) { if (_state->_forceReturnToListView) { _state->_forceReturnToListView = false; flags |= ImGuiTabItemFlags_SetSelected; } if (ImGui::BeginTabItem("List View", nullptr, flags)) { _state->_currentTab = 0; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Grid View")) { _state->_currentTab = 1; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Pinned Characters")) { _state->_currentTab = 2; ImGui::EndTabItem(); } ImGui::EndTabBar(); } _state->_filter.Draw("Filter Characters", 180); ImGui::SameLine(); if (ImGui::Button("Clear")) { _state->_filter.Clear(); } switch (_state->_currentTab) { case 0: // List View renderCharacterList(_state->_selectedCharacter); break; case 1: // Grid View renderCharacterGrid(false, _state->_selectedCharacter); break; case 2: // Pinned Characters renderCharacterGrid(true, _state->_selectedCharacter); break; } } void LogicManager::renderCharacterList(int &selectedCharacter) { ImGui::BeginChild("CharacterList", ImVec2(200, 0), true); for (int i = 0; i < 40; i++) { Character *character = &getCharacter(i); if (!character) continue; char buffer[64]; Common::sprintf_s(buffer, "%s (%d)", getCharacterName(i), i); if (!_state->_filter.PassFilter(buffer)) continue; bool isPinned = isCharacterPinned(i); if (isPinned) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 220, 0, 255)); // Yellow for pinned... } if (ImGui::Selectable(buffer, selectedCharacter == i)) { selectedCharacter = i; } if (isPinned) { ImGui::PopStyleColor(); } if (ImGui::BeginPopupContextItem()) { if (ImGui::MenuItem(isPinned ? "Unpin Character" : "Pin Character")) { toggleCharacterPin(i); } ImGui::EndPopup(); } } ImGui::EndChild(); ImGui::SameLine(); // Right panel: Character details ImGui::BeginChild("CharacterDetails", ImVec2(0, 0), true); if (selectedCharacter < 40) { Character *character = &getCharacter(selectedCharacter); if (character) { renderCharacterDetails(character, selectedCharacter); } } else { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Select a character from the list to view details"); } ImGui::EndChild(); } void LogicManager::renderCurrentSceneDebugger() { if (_state->_textureID) g_system->freeImGuiTexture((void *)_state->_textureID); // Let's blit the current background on the ImGui window... Graphics::Surface temp; temp.create(640, 480, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); _engine->getGraphicsManager()->copy(_engine->getGraphicsManager()->_backBuffer, (PixMap *)temp.getPixels(), 0, 0, 640, 480); _state->_textureID = (ImTextureID)(intptr_t)g_system->getImGuiTexture(temp); temp.free(); ImVec2 imagePos = ImGui::GetCursorScreenPos(); ImVec2 imageSize(640, 480); ImGui::Image(_state->_textureID, imageSize); // Set hovering flag... ImVec2 mousePos = ImGui::GetMousePos(); ImVec2 relativeMousePos(mousePos.x - imagePos.x, mousePos.y - imagePos.y); bool mouseOverImage = (relativeMousePos.x >= 0 && relativeMousePos.x < 640 && relativeMousePos.y >= 0 && relativeMousePos.y < 480); ImDrawList *drawList = ImGui::GetWindowDrawList(); Link *hoveredLink = nullptr; int hoveredLinkIndex = -1; // Draw all the Link hotspots and collision lines... Node *currentNode = &_trainData[_activeNode]; if (currentNode && currentNode->link) { Link *currentLink = currentNode->link; int linkIndex = 0; while (currentLink) { // Calculate screen coordinates for this link's bounding box... ImVec2 topLeft(imagePos.x + currentLink->left, imagePos.y + currentLink->top); ImVec2 bottomRight(imagePos.x + currentLink->right, imagePos.y + currentLink->bottom); // Check if mouse is hovering over this link... bool isHovered = mouseOverImage && relativeMousePos.x >= currentLink->left && relativeMousePos.x <= currentLink->right && relativeMousePos.y >= currentLink->top && relativeMousePos.y <= currentLink->bottom; // Store the hovered link with highest priority, // if they have the same priority, the last one in list wins... if (isHovered) { if (hoveredLink == nullptr || currentLink->location > hoveredLink->location || currentLink->location == hoveredLink->location) { hoveredLink = currentLink; hoveredLinkIndex = linkIndex; } } ImU32 rectColor = IM_COL32(255, 0, 0, 100); // Semi-transparent red ImU32 borderColor = IM_COL32(255, 0, 0, 255); // Solid red border // Highlight hovered link... if (isHovered) { rectColor = IM_COL32(255, 255, 255, 150); } drawList->AddRectFilled(topLeft, bottomRight, rectColor); drawList->AddRect(topLeft, bottomRight, borderColor, 0.0f, 0, 2.0f); // Draw collision lines... Line7 *currentLine = currentLink->lineList; int lineIndex = 0; while (currentLine) { ImU32 lineColor = IM_COL32(0, 255, 255, 255); switch (currentLine->lineType) { case 0: lineColor = IM_COL32(0, 255, 255, 255); // Cyan break; case 1: default: lineColor = IM_COL32(255, 128, 0, 255); // Orange break; } if (currentLine->slope == 0) { // Horizontal line: y = -intercept / 1000 float y = -(float)currentLine->intercept / 1000.0f; if (y >= currentLink->top && y <= currentLink->bottom) { ImVec2 lineStart(imagePos.x + currentLink->left, imagePos.y + y); ImVec2 lineEnd(imagePos.x + currentLink->right, imagePos.y + y); drawList->AddLine(lineStart, lineEnd, lineColor, 2.0f); } } else { // Non-horizontal line float slope = (float)currentLine->slope; float intercept = (float)currentLine->intercept; // Calculate line endpoints across the entire screen bounds... Common::Array intersectionPoints; // Left edge (x = currentLink->left) float yLeft = -(slope * currentLink->left + intercept) / 1000.0f; if (yLeft >= currentLink->top && yLeft <= currentLink->bottom) { intersectionPoints.push_back(ImVec2(imagePos.x + currentLink->left, imagePos.y + yLeft)); } // Right edge (x = currentLink->right) float yRight = -(slope * currentLink->right + intercept) / 1000.0f; if (yRight >= currentLink->top && yRight <= currentLink->bottom) { intersectionPoints.push_back(ImVec2(imagePos.x + currentLink->right, imagePos.y + yRight)); } // Top edge (y = currentLink->top) float xTop = -(1000.0f * currentLink->top + intercept) / slope; if (xTop >= currentLink->left && xTop <= currentLink->right) { intersectionPoints.push_back(ImVec2(imagePos.x + xTop, imagePos.y + currentLink->top)); } // Bottom edge (y = currentLink->bottom) float xBottom = -(1000.0f * currentLink->bottom + intercept) / slope; if (xBottom >= currentLink->left && xBottom <= currentLink->right) { intersectionPoints.push_back(ImVec2(imagePos.x + xBottom, imagePos.y + currentLink->bottom)); } // Draw line if we have at least 2 intersection points... if (intersectionPoints.size() >= 2) { drawList->AddLine(intersectionPoints[0], intersectionPoints[1], lineColor, 2.0f); } } currentLine = currentLine->next; lineIndex++; } currentLink = currentLink->next; linkIndex++; } } // Show tooltip for hovered link... if (hoveredLink && mouseOverImage) { ImGui::BeginTooltip(); ImGui::Text("Link %d", hoveredLinkIndex); ImGui::Text("Bounds: (%d,%d) - (%d,%d)", hoveredLink->left, hoveredLink->top, hoveredLink->right, hoveredLink->bottom); if (hoveredLink->scene) { ImGui::Text("Leads to scene: %s (%d)", _trainData[hoveredLink->scene].sceneFilename, hoveredLink->scene); } else { ImGui::Text("Leads to scene: None (0)"); } ImGui::Text("Location: %d", hoveredLink->location); const char *actionName; if (_engine->getMenu()->isShowingMenu()) { const char *menuActionNames[] = { "None", "PlayGame", "Credits", "Quit", "Action4", "Action5", "SwitchEggs", "Rewind", "FastForward", "Action9", "GoToParis", "GoToStrasbourg", "GoToMunich", "GoToVienna", "GoToBudapest", "GoToBelgrad", "GoToCostantinople", "VolumeDown", "VolumeUp", "BrightnessDown", "BrightnessUp" }; actionName = (hoveredLink->action < 21) ? menuActionNames[hoveredLink->action] : "Unknown"; } else { const char *gameActionNames[] = { "None", "Inventory", "SendCathMessage", "PlaySound", "PlayMusic", "Knock", "Compartment", "PlaySounds", "PlayAnimation", "SetDoor", "SetModel", "SetItem", "KnockInside", "TakeItem", "DropItem", "LinkOnGlobal", "Rattle", "DummyAction1", "LeanOutWindow", "AlmostFall", "ClimbInWindow", "ClimbLadder", "ClimbDownTrain", "KronosSanctum", "EscapeBaggage", "EnterBaggage", "BombPuzzle", "Conductors", "KronosConcert", "LetterInAugustSuitcase", "CatchBeetle", "ExitCompartment", "OutsideTrain", "FirebirdPuzzle", "OpenMatchBox", "OpenBed", "DummyAction2", "HintDialog", "MusicEggBox", "FindEggUnderSink", "Bed", "PlayMusicChapter", "PlayMusicChapterSetupTrain", "SwitchChapter", "EasterEgg" }; actionName = (hoveredLink->action < 45) ? gameActionNames[hoveredLink->action] : "Unknown"; } ImGui::Text("Action: %d (%s)", hoveredLink->action, actionName); const char *cursorNames[] = { "Normal", "Forward", "Backward", "TurnRight", "TurnLeft", "Up", "Down", "Left", "Right", "Hand", "HandKnock", "Magnifier", "HandPointer", "Sleep", "Talk", "Talk2", "MatchBox", "Telegram", "PassengerList", "Article", "Scarf", "Paper", "Parchemin", "Match", "Whistle", "Key", "Bomb", "Firebird", "Briefcase", "Corpse", "PunchLeft", "PunchRight", "Portrait", "PortraitSelected", "PortraitGreen", "PortraitGreenSelected", "PortraitYellow", "PortraitYellowSelected", "HourGlass", "EggBlue", "EggRed", "EggGreen", "EggPurple", "EggTeal", "EggGold", "EggClock", "Normal2", "Blank" }; const char *cursorName; if (hoveredLink->cursor == 128) { cursorName = "Process"; } else if (hoveredLink->cursor == 255) { cursorName = "KeepValue"; } else if (hoveredLink->cursor < 48) { cursorName = cursorNames[hoveredLink->cursor]; } else { cursorName = "Unknown"; } ImGui::Text("Cursor: %d (%s)", hoveredLink->cursor, cursorName); ImGui::Text("Params: %d, %d, %d", hoveredLink->param1, hoveredLink->param2, hoveredLink->param3); // Show line information... if (hoveredLink->lineList) { ImGui::Separator(); Line7 *line = hoveredLink->lineList; int lineNum = 0; while (line) { ImGui::Text("Line %d: slope=%d, intercept=%d, type=%d", lineNum, line->slope, line->intercept, line->lineType); line = line->next; lineNum++; } } ImGui::EndTooltip(); } } void LogicManager::renderGlobalVars() { _state->_selectedGlobalVarRow = -1; if (ImGui::BeginTable("GlobalVars", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 400.0f); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); const char *globalNames[58] = { "", "Jacket", "CorpseMovedFromFloor", "ReadLetterInAugustSuitcase", "FoundCorpse", "CharacterSearchingForCath", "PhaseOfTheNight", "CathIcon", "CorpseHasBeenThrown", "FrancoisHasSeenCorpseThrown", "AnnaIsEating", "Chapter", "DoneSavePointAfterLeftCompWithNewJacket", "MetAugust", "IsDayTime", "PoliceHasBoardedAndGone", "ConcertIsHappening", "KahinaKillTimeoutActive", "MaxHasToStayInBaggage", "UnknownDebugFlag", "TrainIsRunning", "AnnaIsInBaggageCar", "DoneSavePointAfterLeavingSuitcaseInCathComp", "TatianaFoundOutEggStolen", "OverheardAugustInterruptingAnnaAtDinner", "MetTatianaAndVassili", "OverheardTatianaAndAlexeiAtBreakfast", "KnowAboutAugust", "KnowAboutKronos", "EggIsOpen", "CanPlayKronosSuitcaseLeftInCompMusic", "CanPlayEggSuitcaseMusic", "CanPlayEggUnderSinkMusic", "CathInSpecialState", "OverheardAlexeiTellingTatianaAboutBomb", "OverheardAlexeiTellingTatianaAboutWantingToKillVassili", "OverheardTatianaAndAlexeiPlayingChess", "OverheardMilosAndVesnaConspiring", "OverheardVesnaAndMilosDebatingAboutCath", "FrancoisSawABlackBeetle", "OverheardMadameAndFrancoisTalkingAboutWhistle", "MadameDemandedMaxInBaggage", "MadameComplainedAboutMax", "MetMadame", "KnowAboutRebeccaDiary", "OverheardSophieTalkingAboutCath", "MetSophieAndRebecca", "KnowAboutRebeccaAndSophieRelationship", "RegisteredTimeAtWhichCathGaveFirebirdToKronos", "MetMahmud", "AlmostFallActionIsAvailable", "MetMilos", "MetMonsieur", "MetHadija", "MetYasmin", "MetAlouan", "MetFatima", "TatianaScheduledToVisitCath" }; for (int i = 1; i < 58; i++) { ImGui::TableNextRow(); // Make the entire row selectable, so I don't have to buy new glasses to see the name of the variable... ImGui::TableSetColumnIndex(0); bool isSelected = (_state->_selectedGlobalVarRow == i); if (ImGui::Selectable(("##row" + Common::String(i)).c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) { _state->_selectedGlobalVarRow = i; } ImGui::SameLine(); ImGui::TableSetColumnIndex(0); ImGui::Text("%d", i); ImGui::TableSetColumnIndex(1); ImGui::Text("%s", globalNames[i]); ImGui::TableSetColumnIndex(2); ImGui::Text("%d", _globals[i]); } ImGui::EndTable(); } } Common::StringArray LogicManager::getCharacterFunctionNames(int character) { return _engine->isDemo() ? _demoFuncNames[character] : _funcNames[character]; } void LogicManager::showTrainMapWindow() { ImVec4 restaurantColor = ImVec4(0.95f, 0.6f, 0.6f, 1.0f); // Pink/red ImVec4 loungeColor = ImVec4(0.8f, 0.6f, 0.8f, 1.0f); // Violet ImVec4 redSleepingColor = ImVec4(0.95f, 0.7f, 0.7f, 1.0f); // Light red ImVec4 greenSleepingColor = ImVec4(0.7f, 0.95f, 0.7f, 1.0f); // Light green ImVec4 vestibuleColor = ImVec4(0.8f, 0.7f, 0.6f, 1.0f); // Brown ImVec4 kronosColor = ImVec4(0.95f, 0.9f, 0.6f, 1.0f); // Yellow ImVec4 characterColor = ImVec4(1.0f, 0.8f, 0.0f, 1.0f); // Yellow ImVec4 cathColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red ImVec4 voidColor = ImVec4(0.2f, 0.2f, 0.3f, 1.0f); // Dark blue/purple ImVec4 corridorColor = ImVec4(0.4f, 0.4f, 0.4f, 0.5f); // Gray Common::HashMap compartmentsPositions; compartmentsPositions[2740] = 7; compartmentsPositions[3050] = 6; compartmentsPositions[4070] = 5; compartmentsPositions[4840] = 4; compartmentsPositions[5790] = 3; compartmentsPositions[6470] = 2; compartmentsPositions[7500] = 1; compartmentsPositions[8200] = 0; // Train layout struct CarInfo { int id; const char *name; ImVec4 color; float width; int compartments; bool isSleeper; }; CarInfo carsChapter1[] = { { kCarRestaurant, "Restaurant Car", restaurantColor, 1.6f, 6, false }, { kCarRedSleeping, "Red Sleeping Car", redSleepingColor, 1.2f, 8, true }, { kCarGreenSleeping, "Green Sleeping Car", greenSleepingColor, 1.2f, 8, true }, { kCarVestibule, "Vestibule", vestibuleColor, 0.3f, 1, false }, { kCarKronos, "Kronos Car", kronosColor, 1.0f, 3, false } }; CarInfo carsChapters23[] = { { kCarLocomotive, "Locomotive", vestibuleColor, 0.5f, 1, false }, { kCarCoalTender, "Coal Tender", voidColor, 1.0f, 1, false }, { kCarBaggage, "Baggage Car", vestibuleColor, 1.0f, 1, false }, { kCarRestaurant, "Restaurant Car", restaurantColor, 1.6f, 6, false }, { kCarRedSleeping, "Red Sleeping Car", redSleepingColor, 1.2f, 8, true }, { kCarGreenSleeping, "Green Sleeping Car", greenSleepingColor, 1.2f, 8, true }, { kCarVestibule, "Vestibule", vestibuleColor, 0.3f, 1, false }, { kCarKronos, "Kronos Car", kronosColor, 1.0f, 3, false }, { kCarBaggageRear, "Rear Baggage Car", vestibuleColor, 1.0f, 1, false } }; CarInfo carsChapter4[] = { { kCarLocomotive, "Locomotive", vestibuleColor, 0.5f, 1, false }, { kCarCoalTender, "Coal Tender", voidColor, 1.0f, 1, false }, { kCarBaggage, "Baggage Car", vestibuleColor, 1.0f, 1, false }, { kCarRestaurant, "Restaurant Car", restaurantColor, 1.6f, 6, false }, { kCarRedSleeping, "Red Sleeping Car", redSleepingColor, 1.2f, 8, true }, { kCarGreenSleeping, "Green Sleeping Car", greenSleepingColor, 1.2f, 8, true }, { kCarVestibule, "Vestibule", vestibuleColor, 0.3f, 1, false }, { kCarBaggageRear, "Rear Baggage Car", vestibuleColor, 1.0f, 1, false } }; CarInfo carsChapter5[] = { { kCarLocomotive, "Locomotive", vestibuleColor, 0.5f, 1, false }, { kCarCoalTender, "Coal Tender", voidColor, 1.0f, 1, false }, { kCarBaggage, "Baggage Car", vestibuleColor, 1.0f, 1, false }, { kCarRestaurant, "Restaurant Car", restaurantColor, 1.6f, 6, false } }; CarInfo *cars = nullptr; int carCount = 0; switch (_state->_engine->getLogicManager()->_globals[kGlobalChapter]) { case 1: cars = carsChapter1; carCount = ARRAYSIZE(carsChapter1); break; case 2: case 3: default: cars = carsChapters23; carCount = ARRAYSIZE(carsChapters23); break; case 4: case 5: if (_state->_engine->getLogicManager()->_globals[kGlobalChapter] == 5 && (_state->_engine->getLogicManager()->_doneNIS[kEventAugustUnhookCars] || _state->_engine->getLogicManager()->_doneNIS[kEventAugustUnhookCarsBetrayal])) { cars = carsChapter5; carCount = ARRAYSIZE(carsChapter5); } else { cars = carsChapter4; carCount = ARRAYSIZE(carsChapter4); } break; } // Calculate total relative width to scale properly... float totalRelativeWidth = 0.2f; for (int i = 0; i < carCount; i++) { totalRelativeWidth += cars[i].width; } const float availableWidth = ImGui::GetContentRegionAvail().x - 20; // Subtract padding const float unitWidth = availableWidth / totalRelativeWidth; const float carHeight = 140; const float corridorHeight = 50; // Character position mapping... struct CharPos { int car; int compartment; bool inCorridor; int charIndex; int position; }; Common::Array charPositions; Common::Array voidCharPositions; // Collect character positions... for (int i = 0; i < 40; i++) { // These are basically invisible entities, not real characters... if (i == kCharacterClerk || i == kCharacterMaster || i == kCharacterMitchell) continue; Character *character = &getCharacter(i); if (!character) continue; CharPos pos; pos.car = character->characterPosition.car; pos.compartment = compartmentsPositions.getValOrDefault(character->characterPosition.position); pos.inCorridor = character->characterPosition.location != 1; pos.charIndex = i; pos.position = character->characterPosition.position; if (pos.car == 0) { voidCharPositions.push_back(pos); } else { charPositions.push_back(pos); } } // Draw train cars... float carX = 10; // Starting position! for (int c = 0; c < carCount; c++) { CarInfo &car = cars[c]; float carWidth = unitWidth * car.width; // Draw car outline... ImDrawList *drawList = ImGui::GetWindowDrawList(); ImVec2 carMin; carMin.x = ImGui::GetWindowPos().x + carX; carMin.y = ImGui::GetWindowPos().y + 30; ImVec2 carMax; carMax.x = carMin.x + carWidth; carMax.y = carMin.y + carHeight; // Draw car body... drawList->AddRectFilled( carMin, carMax, ImGui::ColorConvertFloat4ToU32(car.color), 5.0f ); // Corridor position - default is center for non-sleeper cars... float corridorY = carMin.y + (carHeight - corridorHeight) / 2; // For sleeper cars, special layout... if (car.isSleeper) { // Sleeping car with 8 compartments; // position 8600 (left) to 2000 (right) mark the start and // the end (empirical values ;-) )... // Calculate the actual start and end points for compartments... float leftmostPos = 8600.0f; float rightmostPos = 2000.0f; // Calculate what percentage of the car width the compartments occupy... float leftEdgeRatio = 1.0f - (leftmostPos / 10000.0f); float rightEdgeRatio = 1.0f - (rightmostPos / 10000.0f); // Calculate the actual pixel positions... float compartmentLeftEdge = carMin.x + (carWidth * leftEdgeRatio); float compartmentRightEdge = carMin.x + (carWidth * rightEdgeRatio); // Draw corridor at the bottom (full width of car)... corridorY = carMin.y + carHeight - corridorHeight; drawList->AddRectFilled( ImVec2(carMin.x, corridorY), ImVec2(carMax.x, corridorY + corridorHeight), ImGui::ColorConvertFloat4ToU32(corridorColor), 0.0f ); // Draw corridor-colored areas on both sides of the compartments // (where the compartements rooms end and the door to the next one is nearby) // Left side (beyond compartments) drawList->AddRectFilled( ImVec2(carMin.x, carMin.y), ImVec2(compartmentLeftEdge, corridorY), ImGui::ColorConvertFloat4ToU32(corridorColor), 0.0f ); // Right side (beyond compartments) drawList->AddRectFilled( ImVec2(compartmentRightEdge, carMin.y), ImVec2(carMax.x, corridorY), ImGui::ColorConvertFloat4ToU32(corridorColor), 0.0f ); // Calculate width of each compartment... float compartmentWidth = (compartmentRightEdge - compartmentLeftEdge) / 8; // Draw the 8 compartments between the calculated edges... for (int i = 0; i <= 8; i++) { float x = compartmentLeftEdge + (i * compartmentWidth); // Draw compartment vertical walls... drawList->AddLine( ImVec2(x, carMin.y), ImVec2(x, corridorY), IM_COL32(0, 0, 0, 255), 1.0f ); } } else if (car.id == kCarRestaurant) { // Restaurant car with two sections: lounge and restaurant // Position 0-3749 = lounge (right side), 3750-10000 = restaurant (left side) // Calculate bounds... float loungeRatio = 3750.0f / 10000.0f; float loungeWidth = carWidth * loungeRatio; float restaurantWidth = carWidth - loungeWidth; // Draw lounge section (right side)... drawList->AddRectFilled( ImVec2(carMin.x + restaurantWidth, carMin.y), ImVec2(carMax.x, carMax.y), ImGui::ColorConvertFloat4ToU32(loungeColor), 0.0f ); // Dividing wall between lounge and restaurant... drawList->AddLine( ImVec2(carMin.x + restaurantWidth, carMin.y), ImVec2(carMin.x + restaurantWidth, carMax.y), IM_COL32(0, 0, 0, 255), 2.0f ); // Center corridor for the entire car... corridorY = carMin.y + (carHeight - corridorHeight) / 2; // Draw corridor through both sections... drawList->AddRectFilled( ImVec2(carMin.x, corridorY), ImVec2(carMax.x, corridorY + corridorHeight), ImGui::ColorConvertFloat4ToU32(corridorColor), 0.0f ); // Draw restaurant tables... float tableWidth = restaurantWidth / 3; for (int i = 0; i < 3; i++) { float tableX = carMin.x + i * tableWidth; drawList->AddLine( ImVec2(tableX, carMin.y), ImVec2(tableX, corridorY), IM_COL32(0, 0, 0, 255), 1.0f ); drawList->AddLine( ImVec2(tableX, corridorY + corridorHeight), ImVec2(tableX, carMax.y), IM_COL32(0, 0, 0, 255), 1.0f ); drawList->AddRectFilled( ImVec2(tableX + 5, carMin.y + 5), ImVec2(tableX + tableWidth - 5, corridorY - 5), IM_COL32(255, 255, 255, 255), 3.0f ); drawList->AddRectFilled( ImVec2(tableX + 5, corridorY + corridorHeight + 5), ImVec2(tableX + tableWidth - 5, carMax.y - 5), IM_COL32(255, 255, 255, 255), 3.0f ); } } else if (car.id == kCarKronos) { // Kronos car with 3 sections float sectionWidth = carWidth / 3; // Draw corridor drawList->AddRectFilled( ImVec2(carMin.x, corridorY), ImVec2(carMax.x, corridorY + corridorHeight), ImGui::ColorConvertFloat4ToU32(corridorColor), 0.0f ); for (int i = 0; i < 2; i++) { drawList->AddLine( ImVec2(carMin.x + (i + 1) * sectionWidth, carMin.y), ImVec2(carMin.x + (i + 1) * sectionWidth, carMax.y), IM_COL32(0, 0, 0, 255), 1.0f ); } } else { // Default corridor for other cars... drawList->AddRectFilled( ImVec2(carMin.x, corridorY), ImVec2(carMax.x, corridorY + corridorHeight), ImGui::ColorConvertFloat4ToU32(corridorColor), 0.0f ); } // Draw car name and ID... char carName[32]; Common::sprintf_s(carName, "%s\n(%d)", car.name, car.id); ImVec2 textSize = ImGui::CalcTextSize(carName); drawList->AddText( ImVec2(carMin.x + (carWidth / 2) - (textSize.x / 2), carMax.y + 5), IM_COL32(255, 255, 255, 255), carName ); // Draw characters in this car... for (uint i = 0; i < charPositions.size(); i++) { if (charPositions[i].car == car.id) { float charX, charY; if (charPositions[i].inCorridor) { // Calculate position along the car based on the 0-10000 range // where 0 = rightmost edge and 10000 = leftmost edge... float positionRatio = (float)charPositions[i].position / 10000.0f; // Flip the ratio since 0 is right and 10000 is left... positionRatio = 1.0f - positionRatio; // Set horizontal position based on the ratio... charX = carMin.x + (carWidth * positionRatio); // Set vertical position in the corridor... charY = corridorY + corridorHeight / 2; // Let's see the player marker a little better, shall we? :-) if (charPositions[i].charIndex == kCharacterCath) { charY -= 10; } else { charY += 10; } } else { // For characters not in corridor... int compartment = charPositions[i].compartment; if (car.isSleeper) { // Calculate compartment positions based on the 8600-2000 range... float leftmostPos = 8600.0f; float rightmostPos = 2000.0f; float leftEdgeRatio = 1.0f - (leftmostPos / 10000.0f); float rightEdgeRatio = 1.0f - (rightmostPos / 10000.0f); float compartmentLeftEdge = carMin.x + (carWidth * leftEdgeRatio); float compartmentRightEdge = carMin.x + (carWidth * rightEdgeRatio); float compartmentWidth = (compartmentRightEdge - compartmentLeftEdge) / 8; compartment = compartment % 8; // All 16 compartments are consecutive, so we mod by 8 // Calculate character position based on compartment charX = compartmentLeftEdge + (compartment * compartmentWidth) + (compartmentWidth / 2); charY = carMin.y + (corridorY - carMin.y) / 2; } else if (car.id == kCarRestaurant) { // Restaurant car with lounge (0-3749) and restaurant (3750-10000) sections... float loungeRatio = 3750.0f / 10000.0f; float loungeWidth = carWidth * loungeRatio; float restaurantWidth = carWidth - loungeWidth; // Center corridor for restaurant car... float restCorridorY = carMin.y + (carHeight - corridorHeight) / 2; // Check if character is in lounge or restaurant section based on position... bool inLounge = (charPositions[i].position < 3750); if (inLounge) { // Lounge area (right side of car)... charX = carMin.x + restaurantWidth + (loungeWidth / 2); charY = carMin.y + carHeight / 2; } else { // Restaurant area (left side of car)... float tableWidth = restaurantWidth / 3; compartment = compartment % 6; if (compartment < 3) { // Top side charX = carMin.x + compartment * tableWidth + tableWidth / 2; charY = carMin.y + (restCorridorY - carMin.y) / 2; } else { // Bottom side charX = carMin.x + (compartment - 3) * tableWidth + tableWidth / 2; charY = restCorridorY + corridorHeight + (carMax.y - (restCorridorY + corridorHeight)) / 2; } } } else if (car.id == kCarKronos) { float sectionWidth = carWidth / 3; compartment = compartment % 3; charX = carMin.x + compartment * sectionWidth + sectionWidth / 2; charY = carMin.y + carHeight / 2; } else { // Vestibule car or whatever else... charX = carMin.x + carWidth / 2; charY = carMin.y + carHeight / 2; } } // Draw character marker... drawList->AddCircleFilled( ImVec2(charX, charY), 8.0f, ImGui::ColorConvertFloat4ToU32(charPositions[i].charIndex == kCharacterCath ? cathColor : characterColor), 12 ); char charId[8]; Common::sprintf_s(charId, "%d", charPositions[i].charIndex); drawList->AddText( ImVec2(charX - ImGui::CalcTextSize(charId).x / 2, charY - ImGui::CalcTextSize(charId).y / 2), charPositions[i].charIndex == kCharacterCath ? IM_COL32(255, 255, 255, 255) : IM_COL32(0, 0, 0, 255), charId ); // Tooltip! ImGui::SetCursorScreenPos(ImVec2(charX - 8, charY - 8)); ImGui::InvisibleButton(charId, ImVec2(16, 16)); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::Text("%s", getCharacterName(charPositions[i].charIndex)); ImGui::Text("Car: %d, Loc: %d, Pos: %d", charPositions[i].car, getCharacter(charPositions[i].charIndex).characterPosition.location, getCharacter(charPositions[i].charIndex).characterPosition.position ); ImGui::EndTooltip(); } } } // Move to next car... carX += carWidth + 5; } // Draw "The Void" section at the bottom... if (voidCharPositions.size() > 0) { ImDrawList *drawList = ImGui::GetWindowDrawList(); // Area... ImVec2 voidMin; voidMin.x = ImGui::GetWindowPos().x + 10; voidMin.y = ImGui::GetWindowPos().y + 205; // Below the train cars ImVec2 voidMax; voidMax.x = ImGui::GetWindowPos().x + ImGui::GetContentRegionAvail().x - 10; voidMax.y = voidMin.y + 60; // Rectangle... drawList->AddRectFilled( voidMin, voidMax, ImGui::ColorConvertFloat4ToU32(voidColor), 5.0f ); // Label... const char *voidLabel = "THE VOID (id: 0)"; drawList->AddText( ImVec2(voidMin.x + 10, voidMin.y + 5), IM_COL32(255, 255, 255, 255), voidLabel ); // Character markers... float charSpacing = (voidMax.x - voidMin.x - 20) / (voidCharPositions.size() + 1); for (uint i = 0; i < voidCharPositions.size(); i++) { float charX = voidMin.x + 10 + charSpacing * (i + 1); float charY = voidMin.y + 35; drawList->AddCircleFilled( ImVec2(charX, charY), 8.0f, ImGui::ColorConvertFloat4ToU32(characterColor), 12 ); char charId[16]; Common::sprintf_s(charId, "%d", voidCharPositions[i].charIndex); drawList->AddText( ImVec2(charX - ImGui::CalcTextSize(charId).x / 2, charY - ImGui::CalcTextSize(charId).y / 2), IM_COL32(0, 0, 0, 255), charId ); // Tooltip! :-) ImGui::SetCursorScreenPos(ImVec2(charX - 8, charY - 8)); ImGui::InvisibleButton(charId, ImVec2(16, 16)); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::Text("%s", getCharacterName(voidCharPositions[i].charIndex)); ImGui::Text("Car: 0, Loc: %d, Pos: %d", getCharacter(voidCharPositions[i].charIndex).characterPosition.location, getCharacter(voidCharPositions[i].charIndex).characterPosition.position ); ImGui::EndTooltip(); } } } } void LogicManager::renderCharacterGrid(bool onlyPinned, int &selectedCharacter) { const int charsPerRow = 3; int displayed = 0; float windowWidth = ImGui::GetContentRegionAvail().x + ImGui::GetCursorScreenPos().x - ImGui::GetWindowPos().x; float cardWidth = (windowWidth / charsPerRow) - 8; for (int i = 0; i < 40; i++) { // Get (and filter) character... Character *character = &getCharacter(i); if (!character) continue; if (onlyPinned && !isCharacterPinned(i)) continue; char buffer[64]; Common::sprintf_s(buffer, "%s (%d)", getCharacterName(i), i); if (!_state->_filter.PassFilter(buffer)) continue; if (displayed > 0 && displayed % charsPerRow == 0) { ImGui::NewLine(); } else if (displayed > 0) { ImGui::SameLine(displayed % charsPerRow * (cardWidth + 8)); } displayed++; // Create a card for the character... ImGui::PushID(i); ImGui::BeginChild(ImGui::GetID((void *)(intptr_t)i), ImVec2(cardWidth, 180), true); if (isCharacterPinned(i)) { ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "%s (%d)", getCharacterName(i), i); ImGui::SameLine(ImGui::GetWindowWidth() - 50); if (ImGui::SmallButton("Unpin")) { toggleCharacterPin(i); } } else { ImGui::Text("%s (%d)", getCharacterName(i), i); ImGui::SameLine(ImGui::GetWindowWidth() - 40); if (ImGui::SmallButton("Pin")) { toggleCharacterPin(i); } } ImGui::Separator(); Common::StringArray funcNames = getCharacterFunctionNames(i); Common::String funcCurrName = "NONE"; if ((uint)(character->callbacks[character->currentCall] - 1) < funcNames.size()) funcCurrName = funcNames[character->callbacks[character->currentCall] - 1]; // Cath (the player) doesn't have logic functions... if (i != 0) { ImGui::Text("Current logic function: %s (%d)", funcCurrName.c_str(), character->callbacks[character->currentCall]); } ImGui::Separator(); ImGui::Text("Position: Car %u, Loc %u, Pos %u", character->characterPosition.car, character->characterPosition.location, character->characterPosition.position ); ImGui::Text("Sequence: %s", character->sequenceName); ImGui::Text("Direction: %d", character->direction); ImGui::Text("Current Frame: Seq1 %d / Seq2 %d", character->currentFrameSeq1, character->currentFrameSeq2); if (ImGui::Button("View Details")) { _state->_selectedCharacter = i; _state->_forceReturnToListView = true; } ImGui::EndChild(); ImGui::PopID(); } if (displayed == 0) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), onlyPinned ? "No pinned characters match the filter" : "No characters match the filter" ); } } void LogicManager::renderCharacterDetails(Character *character, int index) { ImGui::Text("%s (Character ID: %d)", getCharacterName(index), index); ImGui::SameLine(ImGui::GetWindowWidth() - 100); if (ImGui::Button(isCharacterPinned(index) ? "Unpin Character" : "Pin Character")) { toggleCharacterPin(index); } ImGui::Separator(); ImGui::Text("Position: Car %u, Location %u, Position %u", character->characterPosition.car, character->characterPosition.location, character->characterPosition.position ); Common::StringArray funcNames = getCharacterFunctionNames(index); Common::String funcCurrName = "NONE"; if ((uint)(character->callbacks[character->currentCall] - 1) < funcNames.size()) funcCurrName = funcNames[character->callbacks[character->currentCall] - 1]; // Cath (the player) doesn't have logic functions... if (index != 0) { ImGui::Text("Current logic function: %s (%d)", funcCurrName.c_str(), character->callbacks[character->currentCall]); } if (ImGui::CollapsingHeader("Logic call stack", ImGuiTreeNodeFlags_DefaultOpen)) { // Start with the current call... int currentDepth = character->currentCall; if (currentDepth < 0 || character->callbacks[currentDepth] == 0) { ImGui::Text("No active logic functions"); } else { ImGui::Text("Call stack depth: %d", currentDepth + 1); // Display the call stack... for (int i = currentDepth; i >= 0; i--) { int functionId = character->callbacks[i]; float indentAmount = (currentDepth - i) * 20.0f; indentAmount = indentAmount > 0.0f ? indentAmount : 0.1f; ImGui::Indent(indentAmount); ImGui::BulletText("Level %d: %s (#%d)", currentDepth - i, funcNames[functionId - 1].c_str(), functionId ); ImGui::Unindent(indentAmount); } } } if (ImGui::CollapsingHeader("Animation State", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text("Sequence: %s", character->sequenceName); ImGui::Text("Sequence 2: %s", character->sequenceName2); ImGui::Text("Prefix: %s", character->sequenceNamePrefix); ImGui::Text("Copy: %s", character->sequenceNameCopy); ImGui::Text("Current Frame: Seq1 %d / Seq2 %d", character->currentFrameSeq1, character->currentFrameSeq2); ImGui::Text("Waited ticks until cycle restart: %d", character->waitedTicksUntilCycleRestart); ImGui::Text("Elapsed Frames: %d", character->elapsedFrames); } if (ImGui::CollapsingHeader("Movement", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text("Direction: %d", character->direction); ImGui::Text("Walk Counter: %d", character->walkCounter); ImGui::Text("Walk Step Size: %d", character->walkStepSize); ImGui::Text("Direction Switch: %d", character->directionSwitch); } if (ImGui::CollapsingHeader("State", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text("Current Call: %d", character->currentCall); ImGui::Text("Inventory Item: %d", character->inventoryItem); ImGui::Text("Clothes: %d", character->clothes); ImGui::Text("Attached Conductor: %d", character->attachedConductor); ImGui::Text("Process Entity: %d", character->doProcessEntity); ImGui::Text("Previous Car: %d", character->car2); ImGui::Text("Previous Position: %d", character->position2); ImGui::Text("Position fudge flag: %d", character->needsPosFudge); ImGui::Text("Position fudge flag (secondary): %d", character->needsSecondaryPosFudge); } if (ImGui::CollapsingHeader("Call Parameters", ImGuiTreeNodeFlags_DefaultOpen)) { for (int call = 0; call < 9; call++) { char label[32]; Common::sprintf_s(label, "Call %d", call); // Skip calls with all zero parameters... bool hasNonZeroParams = false; for (int param = 0; param < 32; param++) { if (character->callParams[call].parameters[param] != 0) { hasNonZeroParams = true; break; } } if (!hasNonZeroParams) { continue; } if (ImGui::TreeNode(label)) { for (int param = 0; param < 32; param++) { if (character->callParams[call].parameters[param] != 0) { ImGui::Text("Param %d: %d", param, character->callParams[call].parameters[param]); } } ImGui::TreePop(); } } } if (ImGui::CollapsingHeader("Callbacks")) { bool hasCallbacks = false; for (int i = 0; i < 16; i++) { if (character->callbacks[i] != 0) { ImGui::Text("Callback %d: %d", i, character->callbacks[i]); hasCallbacks = true; } } if (!hasCallbacks) { ImGui::Text("No active callbacks"); } } } const char *LogicManager::getCharacterName(int index) const { if (index < 0 || index >= 40) { return "Unknown"; } return _characterNames[index]; } bool LogicManager::isCharacterPinned(int index) const { if (index < 0 || index >= 40) { return false; } return _pinnedCharacters[index]; } void LogicManager::toggleCharacterPin(int index) { if (index >= 0 && index < 40) { _pinnedCharacters[index] = !_pinnedCharacters[index]; } } void LastExpressEngine::showEngineInfo() { ImGui::Text("Mouse status:"); ImGui::BulletText("Is drawn: %s", getGraphicsManager()->canDrawMouse() ? "yes" : "no"); ImGui::BulletText("Has left clicked: %s", mouseHasLeftClicked() ? "yes" : "no"); ImGui::BulletText("Has right clicked: %s", mouseHasRightClicked() ? "yes" : "no"); ImGui::BulletText("Fast walk active: %s", getLogicManager()->_doubleClickFlag ? "yes" : "no"); if (!getMenu()->isShowingMenu()) { ImGui::Separator(); ImGui::Text("Utilities (careful!):"); ImGui::Checkbox("Lock grace period", &_lockGracePeriod); ImGui::NewLine(); ImGui::Text("Advance time by:"); ImGui::InputInt("ticks", &_state->_ticksToAdvance, 100, 1000); ImGui::SameLine(); bool shouldAdvance = ImGui::Button("Go"); if (shouldAdvance) { getLogicManager()->_gameTime += _state->_ticksToAdvance; getLogicManager()->_realTime += _state->_ticksToAdvance; _state->_ticksToAdvance = 0; } } } #endif Debugger::Debugger(LastExpressEngine *engine) : _engine(engine) { ////////////////////////////////////////////////////////////////////////// // Register the debugger commands ////////////////////////////////////////////////////////////////////////// // General registerCmd("help", WRAP_METHOD(Debugger, cmdHelp)); } Debugger::~Debugger() { // Zero passed pointers _engine = nullptr; } ////////////////////////////////////////////////////////////////////////// // Debugger commands ////////////////////////////////////////////////////////////////////////// bool Debugger::cmdHelp(int, const char **) { debugPrintf("No commands"); debugPrintf("\n"); return true; } } // End of namespace LastExpress