From 7f9c7daca90f946fce761a51dad1149eb5f4361a Mon Sep 17 00:00:00 2001 From: jorozcove Date: Sun, 1 Feb 2026 20:50:36 -0500 Subject: [PATCH 1/4] Minimap implementation inital commit - Added support into the level data for minimaps - Added minimap vram packing - Added minimaps presets - Added minimap UI - Solve merge conflicts --- CrashTeamEditor.vcxproj | 2 + CrashTeamEditor.vcxproj.filters | 6 + src/gui_render_settings.cpp | 1 + src/gui_render_settings.h | 2 +- src/io.cpp | 52 ++++++ src/io.h | 4 + src/level.cpp | 200 +++++++++++++++++++++- src/level.h | 7 +- src/levelui.cpp | 16 +- src/minimap.cpp | 285 ++++++++++++++++++++++++++++++++ src/minimap.h | 75 +++++++++ src/psx_types.h | 47 ++++++ 12 files changed, 686 insertions(+), 11 deletions(-) create mode 100644 src/minimap.cpp create mode 100644 src/minimap.h diff --git a/CrashTeamEditor.vcxproj b/CrashTeamEditor.vcxproj index b371610..38e8b96 100644 --- a/CrashTeamEditor.vcxproj +++ b/CrashTeamEditor.vcxproj @@ -48,6 +48,7 @@ + @@ -92,6 +93,7 @@ + diff --git a/CrashTeamEditor.vcxproj.filters b/CrashTeamEditor.vcxproj.filters index 2525aad..970edcc 100644 --- a/CrashTeamEditor.vcxproj.filters +++ b/CrashTeamEditor.vcxproj.filters @@ -78,6 +78,9 @@ Source Files + + Source Files + Source Files @@ -197,6 +200,9 @@ Header Files + + Header Files + Renderer diff --git a/src/gui_render_settings.cpp b/src/gui_render_settings.cpp index bfc064e..a105531 100644 --- a/src/gui_render_settings.cpp +++ b/src/gui_render_settings.cpp @@ -25,6 +25,7 @@ bool GuiRenderSettings::showStartpoints = false; bool GuiRenderSettings::showVisTree = false; bool GuiRenderSettings::filterActive = true; bool GuiRenderSettings::showSelectedQuadblockInfo = true; +bool GuiRenderSettings::showMinimapBounds = false; Color GuiRenderSettings::defaultFilterColor = Color(static_cast(255), static_cast(128), static_cast(0)); Color GuiRenderSettings::selectedCheckpointColor = Color(static_cast(0), static_cast < unsigned char>(255), static_cast < unsigned char>(255)); int GuiRenderSettings::renderType = 0; diff --git a/src/gui_render_settings.h b/src/gui_render_settings.h index 27323de..40769b6 100644 --- a/src/gui_render_settings.h +++ b/src/gui_render_settings.h @@ -19,7 +19,7 @@ struct GuiRenderSettings static float camFovDeg, camZoomMult, camRotateMult, camMoveMult, camSprintMult; static int camKeyForward, camKeyBack, camKeyLeft, camKeyRight, camKeyUp, camKeyDown, camKeySprint; static int camOrbitMouseButton; - static bool showLowLOD, showWireframe, showVerts, showBackfaces, showBspRectTree, showLevel, showCheckpoints, showStartpoints, showVisTree, filterActive, showSelectedQuadblockInfo; + static bool showLowLOD, showWireframe, showVerts, showBackfaces, showBspRectTree, showLevel, showCheckpoints, showStartpoints, showVisTree, filterActive, showSelectedQuadblockInfo, showMinimapBounds; static Color defaultFilterColor, selectedCheckpointColor; static const std::vector renderTypeLabels; }; diff --git a/src/io.cpp b/src/io.cpp index 5063ed3..7325f7c 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -85,6 +85,58 @@ void from_json(const nlohmann::json& json, Stars& stars) if (json.contains("depth")) { stars.zDepth = json["depth"]; } } +void to_json(nlohmann::json& json, const MinimapConfig& minimap) +{ + json = { + {"enabled", minimap.enabled}, + {"worldEndX", minimap.worldEndX}, + {"worldEndY", minimap.worldEndY}, + {"worldStartX", minimap.worldStartX}, + {"worldStartY", minimap.worldStartY}, + {"iconSizeX", minimap.iconSizeX}, + {"iconSizeY", minimap.iconSizeY}, + {"driverDotStartX", minimap.driverDotStartX}, + {"driverDotStartY", minimap.driverDotStartY}, + {"orientationMode", minimap.orientationMode}, + {"unk", minimap.unk}, + {"topTexturePath", minimap.topTexturePath.string()}, + {"bottomTexturePath", minimap.bottomTexturePath.string()} + }; +} + +void from_json(const nlohmann::json& json, MinimapConfig& minimap) +{ + if (json.contains("enabled")) { json.at("enabled").get_to(minimap.enabled); } + if (json.contains("worldEndX")) { json.at("worldEndX").get_to(minimap.worldEndX); } + if (json.contains("worldEndY")) { json.at("worldEndY").get_to(minimap.worldEndY); } + if (json.contains("worldStartX")) { json.at("worldStartX").get_to(minimap.worldStartX); } + if (json.contains("worldStartY")) { json.at("worldStartY").get_to(minimap.worldStartY); } + if (json.contains("iconSizeX")) { json.at("iconSizeX").get_to(minimap.iconSizeX); } + if (json.contains("iconSizeY")) { json.at("iconSizeY").get_to(minimap.iconSizeY); } + if (json.contains("driverDotStartX")) { json.at("driverDotStartX").get_to(minimap.driverDotStartX); } + if (json.contains("driverDotStartY")) { json.at("driverDotStartY").get_to(minimap.driverDotStartY); } + if (json.contains("orientationMode")) { json.at("orientationMode").get_to(minimap.orientationMode); } + else if (json.contains("mode")) { json.at("mode").get_to(minimap.orientationMode); } // backwards compatibility + if (json.contains("unk")) { json.at("unk").get_to(minimap.unk); } + + if (json.contains("topTexturePath")) + { + std::string path; + json.at("topTexturePath").get_to(path); + if (!path.empty()) { minimap.topTexturePath = path; } + } + + if (json.contains("bottomTexturePath")) + { + std::string path; + json.at("bottomTexturePath").get_to(path); + if (!path.empty()) { minimap.bottomTexturePath = path; } + } + + // Load textures after setting paths + minimap.LoadTextures(); +} + void ReadBinaryFile(std::vector& v, const std::filesystem::path& path) { std::ifstream file(path, std::ios::binary); diff --git a/src/io.h b/src/io.h index 6309561..85f2bfb 100644 --- a/src/io.h +++ b/src/io.h @@ -4,6 +4,7 @@ #include "path.h" #include "quadblock.h" #include "animtexture.h" +#include "minimap.h" #include @@ -26,6 +27,9 @@ void from_json(const nlohmann::json& json, ColorGradient& spawn); void to_json(nlohmann::json& json, const Stars& stars); void from_json(const nlohmann::json& json, Stars& stars); +void to_json(nlohmann::json& json, const MinimapConfig& minimap); +void from_json(const nlohmann::json& json, MinimapConfig& minimap); + void ReadBinaryFile(std::vector& v, const std::filesystem::path& path); template static inline void Read(std::ifstream& file, T& data) diff --git a/src/level.cpp b/src/level.cpp index f2e6aa6..32663b5 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -8,6 +8,7 @@ #include "renderer.h" #include "vistree.h" #include "text3d.h" +#include "minimap.h" #include #include @@ -77,6 +78,7 @@ void Level::Clear(bool clearErrors) m_saveScript = false; m_vrm.clear(); m_lastAnimTextureCount = 0; + m_minimapConfig.Clear(); DeleteMaterials(this); for (Model* model : m_models) @@ -115,6 +117,11 @@ const std::filesystem::path& Level::GetParentPath() const return m_parentPath; } +MinimapConfig& Level::GetMinimapConfig() +{ + return m_minimapConfig; +} + std::vector Level::GetMaterialNames() const { std::vector names; @@ -393,7 +400,7 @@ bool Level::GenerateCheckpoints() enum class PresetHeader : unsigned { - SPAWN, LEVEL, PATH, MATERIAL, TURBO_PAD, ANIM_TEXTURES, SCRIPT + SPAWN, LEVEL, PATH, MATERIAL, TURBO_PAD, ANIM_TEXTURES, SCRIPT, MINIMAP }; bool Level::LoadPreset(const std::filesystem::path& filename) @@ -527,6 +534,13 @@ bool Level::LoadPreset(const std::filesystem::path& filename) { m_pythonScript = json["script"]; } + else if (header == PresetHeader::MINIMAP) + { + if (json.contains("minimap")) + { + m_minimapConfig = json["minimap"]; + } + } else { m_logMessage += "\nFailed loaded preset: " + filename.string(); @@ -626,6 +640,14 @@ bool Level::SavePreset(const std::filesystem::path& path) scriptJson["script"] = m_pythonScript; SaveJSON(dirPath / "script.json", scriptJson); } + + if (m_minimapConfig.enabled) + { + nlohmann::json minimapJson = {}; + minimapJson["header"] = PresetHeader::MINIMAP; + minimapJson["minimap"] = m_minimapConfig; + SaveJSON(dirPath / "minimap.json", minimapJson); + } return true; } @@ -1130,13 +1152,8 @@ bool Level::SaveLEV(const std::filesystem::path& path) const size_t offOxideGhost = m_oxideGhost.empty() ? 0 : currOffset; currOffset += m_oxideGhost.size(); + // Note: extraHeader.offsets[MINIMAP] will be updated later after minimap data is serialized PSX::LevelExtraHeader extraHeader = {}; - if (offTropyGhost > 0) - { - if (offOxideGhost > 0) { extraHeader.count = PSX::LevelExtra::COUNT; } - else { extraHeader.count = PSX::LevelExtra::N_OXIDE_GHOST; } - } - else { extraHeader.count = 0; } extraHeader.offsets[PSX::LevelExtra::MINIMAP] = 0; extraHeader.offsets[PSX::LevelExtra::SPAWN] = 0; extraHeader.offsets[PSX::LevelExtra::CAMERA_END_OF_RACE] = 0; @@ -1145,6 +1162,12 @@ bool Level::SaveLEV(const std::filesystem::path& path) extraHeader.offsets[PSX::LevelExtra::N_OXIDE_GHOST] = static_cast(offOxideGhost); extraHeader.offsets[PSX::LevelExtra::CREDITS] = 0; + // Determine count based on what's enabled + if (offOxideGhost > 0) { extraHeader.count = PSX::LevelExtra::COUNT; } + else if (offTropyGhost > 0) { extraHeader.count = PSX::LevelExtra::N_OXIDE_GHOST; } + else if (m_minimapConfig.IsReady()) { extraHeader.count = PSX::LevelExtra::COUNT; } + else { extraHeader.count = 0; } + const size_t offExtraHeader = currOffset; currOffset += sizeof(extraHeader); @@ -1173,6 +1196,85 @@ bool Level::SaveLEV(const std::filesystem::path& path) const size_t offVisMem = currOffset; currOffset += sizeof(visMem); + // Minimap data serialization + size_t offMinimapStruct = 0; + size_t offSpawnType1 = 0; + size_t offLevTexLookup = 0; + size_t offMinimapIcons = 0; + std::vector minimapData; + std::vector minimapPtrMapOffsets; + + if (m_minimapConfig.IsReady()) + { + // Map struct + offMinimapStruct = currOffset; + PSX::Map mapStruct = m_minimapConfig.Serialize(); + size_t mapStructOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(PSX::Map)); + memcpy(&minimapData[mapStructOffset], &mapStruct, sizeof(PSX::Map)); + currOffset += sizeof(PSX::Map); + + // SpawnType1 header (count + pointer to map struct) + offSpawnType1 = currOffset; + PSX::SpawnType1 spawnType1Header = {}; + spawnType1Header.count = PSX::LevelExtra::COUNT; // Must have all entries for minimap + size_t st1HeaderOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(PSX::SpawnType1)); + memcpy(&minimapData[st1HeaderOffset], &spawnType1Header, sizeof(PSX::SpawnType1)); + currOffset += sizeof(PSX::SpawnType1); + + // SpawnType1 pointers array - pointer at index 0 points to Map struct + size_t st1PointersOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(uint32_t) * PSX::LevelExtra::COUNT); + uint32_t* st1Pointers = reinterpret_cast(&minimapData[st1PointersOffset]); + st1Pointers[PSX::LevelExtra::MINIMAP] = static_cast(offMinimapStruct); + for (size_t i = 1; i < PSX::LevelExtra::COUNT; i++) { st1Pointers[i] = 0; } + minimapPtrMapOffsets.push_back(currOffset); // Pointer to map struct needs patching + currOffset += sizeof(uint32_t) * PSX::LevelExtra::COUNT; + + // Icon structs (top and bottom minimap textures) + offMinimapIcons = currOffset; + + // Create default UV for full texture + QuadUV defaultUV = {{Vec2(0.0f, 0.0f), Vec2(1.0f, 0.0f), Vec2(0.0f, 1.0f), Vec2(1.0f, 1.0f)}}; + + // Top icon + PSX::Icon topIcon = {}; + strncpy_s(topIcon.name, sizeof(topIcon.name), "minimap-top", _TRUNCATE); + topIcon.globalIconArrayIndex = PSX::ICON_INDEX_MAP_TOP; + topIcon.texLayout = m_minimapConfig.topTexture.Serialize(defaultUV); + size_t topIconOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(PSX::Icon)); + memcpy(&minimapData[topIconOffset], &topIcon, sizeof(PSX::Icon)); + currOffset += sizeof(PSX::Icon); + + // Bottom icon + PSX::Icon bottomIcon = {}; + strncpy_s(bottomIcon.name, sizeof(bottomIcon.name), "minimap-bot", _TRUNCATE); + bottomIcon.globalIconArrayIndex = PSX::ICON_INDEX_MAP_BOTTOM; + bottomIcon.texLayout = m_minimapConfig.bottomTexture.Serialize(defaultUV); + size_t bottomIconOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(PSX::Icon)); + memcpy(&minimapData[bottomIconOffset], &bottomIcon, sizeof(PSX::Icon)); + currOffset += sizeof(PSX::Icon); + + // LevTexLookup struct + offLevTexLookup = currOffset; + PSX::LevTexLookup levTexLookup = {}; + levTexLookup.numIcon = 2; + levTexLookup.offFirstIcon = static_cast(offMinimapIcons); + levTexLookup.numIconGroup = 0; + levTexLookup.offFirstIconGroupPtr = 0; + size_t levTexLookupOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(PSX::LevTexLookup)); + memcpy(&minimapData[levTexLookupOffset], &levTexLookup, sizeof(PSX::LevTexLookup)); + minimapPtrMapOffsets.push_back(currOffset + offsetof(PSX::LevTexLookup, offFirstIcon)); // Pointer to first icon + currOffset += sizeof(PSX::LevTexLookup); + + // Update extraHeader to point to the minimap struct + extraHeader.offsets[PSX::LevelExtra::MINIMAP] = static_cast(offMinimapStruct); + } + const size_t offPointerMap = currOffset; header.offMeshInfo = static_cast(offMeshInfo); @@ -1197,6 +1299,13 @@ bool Level::SaveLEV(const std::filesystem::path& path) header.offVisMem = static_cast(offVisMem); header.offLevNavTable = static_cast(offNavHeaders); + // Set minimap pointers in header if enabled + if (m_minimapConfig.IsReady()) + { + header.offIconsLookup = static_cast(offLevTexLookup); + header.offIcons = static_cast(offMinimapIcons); + } + #define CALCULATE_OFFSET(s, m, b) static_cast(offsetof(s, m) + b) std::vector pointerMap = @@ -1215,8 +1324,16 @@ bool Level::SaveLEV(const std::filesystem::path& path) CALCULATE_OFFSET(PSX::VisualMem, offBSP[0], offVisMem), }; + // Add minimap header pointers to pointer map + if (m_minimapConfig.IsReady()) + { + pointerMap.push_back(CALCULATE_OFFSET(PSX::LevHeader, offIconsLookup, offHeader)); + pointerMap.push_back(CALCULATE_OFFSET(PSX::LevHeader, offIcons, offHeader)); + } + if (offTropyGhost != 0) { pointerMap.push_back(CALCULATE_OFFSET(PSX::LevelExtraHeader, offsets[PSX::LevelExtra::N_TROPY_GHOST], offExtraHeader)); } if (offOxideGhost != 0) { pointerMap.push_back(CALCULATE_OFFSET(PSX::LevelExtraHeader, offsets[PSX::LevelExtra::N_OXIDE_GHOST], offExtraHeader)); } + if (m_minimapConfig.IsReady()) { pointerMap.push_back(CALCULATE_OFFSET(PSX::LevelExtraHeader, offsets[PSX::LevelExtra::MINIMAP], offExtraHeader)); } for (size_t i = 0; i < animPtrMapOffsets.size(); i++) { @@ -1255,6 +1372,12 @@ bool Level::SaveLEV(const std::filesystem::path& path) offCurrVisibleSet += sizeof(PSX::VisibleSet); } + // Add minimap internal pointers to pointer map + for (size_t offset : minimapPtrMapOffsets) + { + pointerMap.push_back(static_cast(offset)); + } + const size_t pointerMapBytes = pointerMap.size() * sizeof(uint32_t); Write(file, &offPointerMap, sizeof(uint32_t)); @@ -1290,6 +1413,8 @@ bool Level::SaveLEV(const std::filesystem::path& path) Write(file, visMemQuadsP1.data(), visMemQuadsP1.size() * sizeof(uint32_t)); Write(file, visMemBSPP1.data(), visMemBSPP1.size() * sizeof(uint32_t)); Write(file, &visMem, sizeof(visMem)); + // Write minimap data if present + if (!minimapData.empty()) { Write(file, minimapData.data(), minimapData.size()); } Write(file, &pointerMapBytes, sizeof(uint32_t)); Write(file, pointerMap.data(), pointerMapBytes); file.close(); @@ -1779,6 +1904,27 @@ bool Level::UpdateVRM() } } + // Add minimap textures if enabled + if (m_minimapConfig.IsReady()) + { + std::vector minimapTextures = m_minimapConfig.GetTextures(); + for (Texture* tex : minimapTextures) + { + bool foundEqual = false; + for (Texture* addedTexture : textures) + { + if (*tex == *addedTexture) + { + copyTextureAttributes.push_back({addedTexture, tex}); + foundEqual = true; + break; + } + } + if (foundEqual) { continue; } + textures.push_back(tex); + } + } + m_vrm = PackVRM(textures); if (m_vrm.empty()) { return false; } @@ -1828,6 +1974,9 @@ void Level::InitModels(Renderer& renderer) m_models[LevelModels::FILTER] = m_models[LevelModels::LEVEL]->AddModel(); m_models[LevelModels::FILTER]->SetRenderCondition([]() { return GuiRenderSettings::filterActive; }); + + m_models[LevelModels::MINIMAP_BOUNDS] = m_models[LevelModels::LEVEL]->AddModel(); + m_models[LevelModels::MINIMAP_BOUNDS]->SetRenderCondition([]() { return GuiRenderSettings::showMinimapBounds; }); } void Level::GenerateRenderLevData() @@ -2012,6 +2161,43 @@ void Level::GenerateRenderStartpointData() m_models[LevelModels::SPAWN]->GetMesh().SetGeometry(spawnsTriangles, Mesh::RenderFlags::DrawBackfaces | Mesh::RenderFlags::DontOverrideRenderFlags); } +void Level::GenerateRenderMinimapBoundsData() +{ + if (!m_models[LevelModels::MINIMAP_BOUNDS]) { return; } + + if (!m_minimapConfig.enabled) + { + m_models[LevelModels::MINIMAP_BOUNDS]->GetMesh().Clear(); + return; + } + + // Convert from fixed-point to world coordinates + float minX = static_cast(m_minimapConfig.worldStartX) / static_cast(FP_ONE_GEO); + float maxX = static_cast(m_minimapConfig.worldEndX) / static_cast(FP_ONE_GEO); + float minZ = static_cast(m_minimapConfig.worldStartY) / static_cast(FP_ONE_GEO); + float maxZ = static_cast(m_minimapConfig.worldEndY) / static_cast(FP_ONE_GEO); + + // Add some height to the minimap bounds for better visibility + float minY = -10.0f; + float maxY = 10.0f; + + // Create bounding box for minimap bounds + BoundingBox bbox; + bbox.min = Vec3(minX, minY, minZ); + bbox.max = Vec3(maxX, maxY, maxZ); + + // Magenta color for minimap bounds + Color c = Color(static_cast(255), static_cast(0), static_cast(255)); + + std::vector triangles = bbox.ToGeometry(); + for (Primitive& primitive : triangles) + { + for (unsigned i = 0; i < primitive.pointCount; i++) { primitive.p[i].color = c; } + } + + m_models[LevelModels::MINIMAP_BOUNDS]->GetMesh().SetGeometry(triangles, Mesh::RenderFlags::DrawWireframe | Mesh::RenderFlags::DontOverrideRenderFlags); +} + void Level::GenerateRenderSelectedBlockData(const Quadblock& quadblock, const Vec3& queryPoint) { if (!m_models[LevelModels::SELECTED]) { return; } diff --git a/src/level.h b/src/level.h index edb697a..045babf 100644 --- a/src/level.h +++ b/src/level.h @@ -12,6 +12,7 @@ #include "animtexture.h" #include "model.h" #include "vistree.h" +#include "minimap.h" #include #include @@ -32,7 +33,8 @@ namespace LevelModels static constexpr size_t SELECTED = 4; static constexpr size_t MULTI_SELECTED = 5; static constexpr size_t FILTER = 6; - static constexpr size_t COUNT = 7; + static constexpr size_t MINIMAP_BOUNDS = 7; + static constexpr size_t COUNT = 8; }; class Level @@ -63,6 +65,7 @@ class Level void ResetFilter(); void ResetRendererSelection(); void UpdateRenderCheckpointData(); + MinimapConfig& GetMinimapConfig(); private: void ManageTurbopad(Quadblock& quadblock); @@ -86,6 +89,7 @@ class Level void UpdateFilterRenderData(const Quadblock& qb); void GenerateRenderBspData(); void GenerateRenderStartpointData(); + void GenerateRenderMinimapBoundsData(); void GenerateRenderSelectedBlockData(const Quadblock& quadblock, const Vec3& queryPoint); bool UpdateAnimTextures(float deltaTime); void ViewportClickHandleBlockSelection(int pixelX, int pixelY, bool appendSelection, const Renderer& rend); @@ -124,6 +128,7 @@ class Level std::vector m_animTextures; BitMatrix m_bspVis; std::vector m_vrm; + MinimapConfig m_minimapConfig; std::unordered_map> m_materialToQuadblocks; std::unordered_map m_materialToTexture; diff --git a/src/levelui.cpp b/src/levelui.cpp index 06671b6..86484a4 100644 --- a/src/levelui.cpp +++ b/src/levelui.cpp @@ -11,6 +11,7 @@ #include "texture.h" #include "ui.h" #include "script.h" +#include "minimap.h" #include #include @@ -501,6 +502,15 @@ void Level::RenderUI(Renderer& renderer) ImGui::TreePop(); } + + if (ImGui::TreeNode("Minimap")) + { + if (m_minimapConfig.RenderUI(m_quadblocks) && GuiRenderSettings::showMinimapBounds) + { + GenerateRenderMinimapBoundsData(); + } + ImGui::TreePop(); + } } ImGui::End(); } @@ -912,9 +922,9 @@ void Level::RenderUI(Renderer& renderer) unsigned ret = REND_FLAGS_NONE; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - if (ImGui::Checkbox(leftLabel, leftValue)) { ret |= REND_FLAGS_COLUMN_0; } + if (leftValue && ImGui::Checkbox(leftLabel, leftValue)) { ret |= REND_FLAGS_COLUMN_0; } ImGui::TableSetColumnIndex(1); - if (ImGui::Checkbox(rightLabel, rightValue)) { ret |= REND_FLAGS_COLUMN_1; } + if (rightValue && ImGui::Checkbox(rightLabel, rightValue)) { ret |= REND_FLAGS_COLUMN_1; } return ret; }; @@ -923,6 +933,8 @@ void Level::RenderUI(Renderer& renderer) unsigned cpStartPoints = checkboxPair("Show Checkpoints", &GuiRenderSettings::showCheckpoints, "Show Starting Positions", &GuiRenderSettings::showStartpoints); if (cpStartPoints & REND_FLAGS_COLUMN_1) { GenerateRenderStartpointData(); } checkboxPair("Show BSP", &GuiRenderSettings::showBspRectTree, "Show Vis Tree", &GuiRenderSettings::showVisTree); + unsigned minimapBoundsChanged = checkboxPair("Show Minimap Bounds", &GuiRenderSettings::showMinimapBounds, "", nullptr); + if (minimapBoundsChanged & REND_FLAGS_COLUMN_0) { GenerateRenderMinimapBoundsData(); } ImGui::EndTable(); } diff --git a/src/minimap.cpp b/src/minimap.cpp new file mode 100644 index 0000000..a8394fc --- /dev/null +++ b/src/minimap.cpp @@ -0,0 +1,285 @@ +#include "minimap.h" +#include "quadblock.h" + +#include +#include +#include +#include +#include + +void MinimapConfig::CalculateWorldBoundsFromQuadblocks(const std::vector& quadblocks) +{ + if (quadblocks.empty()) { return; } + + float minX = std::numeric_limits::max(); + float minZ = std::numeric_limits::max(); + float maxX = std::numeric_limits::lowest(); + float maxZ = std::numeric_limits::lowest(); + + bool found = false; + for (const Quadblock& qb : quadblocks) + { + // Only include quadblocks that have a checkpoint assigned + if (qb.GetCheckpoint() >= 0) + { + const BoundingBox& bbox = qb.GetBoundingBox(); + minX = std::min(minX, bbox.min.x); + minZ = std::min(minZ, bbox.min.z); + maxX = std::max(maxX, bbox.max.x); + maxZ = std::max(maxZ, bbox.max.z); + found = true; + } + } + + // Convert to fixed-point coordinates (FP_ONE_GEO = 64) + worldStartX = static_cast(minX * FP_ONE_GEO); + worldStartY = static_cast(minZ * FP_ONE_GEO); + worldEndX = static_cast(maxX * FP_ONE_GEO); + worldEndY = static_cast(maxZ * FP_ONE_GEO); +} + +PSX::Map MinimapConfig::Serialize() const +{ + PSX::Map map = {}; + map.worldEndX = worldEndX; + map.worldEndY = worldEndY; + map.worldStartX = worldStartX; + map.worldStartY = worldStartY; + // Icon size is the texture dimensions + map.iconSizeX = hasTopTexture ? static_cast(topTexture.GetWidth()) : iconSizeX; + map.iconSizeY = hasTopTexture ? static_cast(topTexture.GetHeight()) : iconSizeY; + map.driverDotStartX = driverDotStartX; + map.driverDotStartY = driverDotStartY; + map.mode = orientationMode; + map.unk = unk; + return map; +} + +void MinimapConfig::Deserialize(const PSX::Map& map) +{ + worldEndX = map.worldEndX; + worldEndY = map.worldEndY; + worldStartX = map.worldStartX; + worldStartY = map.worldStartY; + iconSizeX = map.iconSizeX; + iconSizeY = map.iconSizeY; + driverDotStartX = map.driverDotStartX; + driverDotStartY = map.driverDotStartY; + orientationMode = map.mode; + unk = map.unk; +} + +void MinimapConfig::LoadTextures() +{ + if (!topTexturePath.empty() && std::filesystem::exists(topTexturePath)) + { + topTexture = Texture(topTexturePath); + hasTopTexture = !topTexture.IsEmpty(); + } + if (!bottomTexturePath.empty() && std::filesystem::exists(bottomTexturePath)) + { + bottomTexture = Texture(bottomTexturePath); + hasBottomTexture = !bottomTexture.IsEmpty(); + } +} + +bool MinimapConfig::IsReady() const +{ + return enabled && hasTopTexture && hasBottomTexture; +} + +std::vector MinimapConfig::GetTextures() +{ + std::vector textures; + if (hasTopTexture) { textures.push_back(&topTexture); } + if (hasBottomTexture) { textures.push_back(&bottomTexture); } + return textures; +} + +void MinimapConfig::Clear() +{ + worldEndX = 0; + worldEndY = 0; + worldStartX = 0; + worldStartY = 0; + iconSizeX = 0; + iconSizeY = 0; + driverDotStartX = 450; + driverDotStartY = 180; + orientationMode = 0; + unk = 0; + topTexturePath.clear(); + bottomTexturePath.clear(); + topTexture = Texture(); + bottomTexture = Texture(); + hasTopTexture = false; + hasBottomTexture = false; + enabled = false; +} + +bool MinimapConfig::RenderUI(const std::vector& quadblocks) +{ + bool boundsChanged = false; + + ImGui::Checkbox("Enable Minimap", &enabled); + + if (!enabled) { return false; } + + ImGui::Separator(); + ImGui::Text("World Bounds:"); + + // Convert fixed-point to float for display (divide by 64) + float startX = static_cast(worldStartX) / static_cast(FP_ONE_GEO); + float startY = static_cast(worldStartY) / static_cast(FP_ONE_GEO); + float endX = static_cast(worldEndX) / static_cast(FP_ONE_GEO); + float endY = static_cast(worldEndY) / static_cast(FP_ONE_GEO); + + if (ImGui::InputFloat("World Start X", &startX, 1.0f, 10.0f, "%.2f")) + { + worldStartX = static_cast(startX * static_cast(FP_ONE_GEO)); + boundsChanged = true; + } + if (ImGui::InputFloat("World Start Y", &startY, 1.0f, 10.0f, "%.2f")) + { + worldStartY = static_cast(startY * static_cast(FP_ONE_GEO)); + boundsChanged = true; + } + if (ImGui::InputFloat("World End X", &endX, 1.0f, 10.0f, "%.2f")) + { + worldEndX = static_cast(endX * static_cast(FP_ONE_GEO)); + boundsChanged = true; + } + if (ImGui::InputFloat("World End Y", &endY, 1.0f, 10.0f, "%.2f")) + { + worldEndY = static_cast(endY * static_cast(FP_ONE_GEO)); + boundsChanged = true; + } + + if (ImGui::Button("Calculate from Quadblocks")) + { + CalculateWorldBoundsFromQuadblocks(quadblocks); + boundsChanged = true; + } + ImGui::SetItemTooltip("(Experimental) Automatically calculate world bounds from quadblocks with checkpoints"); + + ImGui::Separator(); + ImGui::Text("Driver Icon Start Position (screen %d x %d):", PSX::SCREEN_WIDTH, PSX::SCREEN_HEIGHT); + if (ImGui::InputScalar("Icon Start X", ImGuiDataType_S16, &driverDotStartX)) { + if (driverDotStartX < 0) driverDotStartX = 0; + if (driverDotStartX > PSX::SCREEN_WIDTH) driverDotStartX = PSX::SCREEN_WIDTH; + } + if (ImGui::InputScalar("Icon Start Y", ImGuiDataType_S16, &driverDotStartY)) { + if (driverDotStartY < 0) driverDotStartY = 0; + if (driverDotStartY > PSX::SCREEN_HEIGHT) driverDotStartY = PSX::SCREEN_HEIGHT; + } + + ImGui::Separator(); + ImGui::Text("Minimap Orientation:"); + + // Orientation mode dropdown + const char* orientationModes[] = { "0°", "90°", "180°", "270°" }; + int currentOrientation = orientationMode; + if (currentOrientation < 0 || currentOrientation > 3) { currentOrientation = 0; } + if (ImGui::Combo("Relative rotation", ¤tOrientation, orientationModes, 4)) + { + orientationMode = static_cast(currentOrientation); + } + ImGui::SetItemTooltip("Determines minimap clockwise rotation relative to the world\n It doesnt affect texture orientation, it affects how the driver icon moves on the minimap"); + + ImGui::Separator(); + ImGui::InputScalar("Unknown", ImGuiDataType_S16, &unk); + ImGui::SetItemTooltip("???"); + + ImGui::Separator(); + ImGui::Text("Textures (both halves must have the same dimensions):"); + + // Top texture selection + std::string topPath = topTexturePath.empty() ? "(none)" : topTexturePath.filename().string(); + ImGui::Text("Top:"); ImGui::SameLine(); + ImGui::SetNextItemWidth(200.0f); + ImGui::BeginDisabled(); + ImGui::InputText("##toptex", &topPath, ImGuiInputTextFlags_ReadOnly); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Browse##selecttop")) + { + auto selection = pfd::open_file("Select Top Minimap Texture", ".", + {"Image Files", "*.png *.bmp *.jpg *.jpeg", "All Files", "*"}).result(); + if (!selection.empty()) + { + topTexturePath = selection.front(); + topTexture = Texture(topTexturePath); + hasTopTexture = !topTexture.IsEmpty(); + } + } + ImGui::SameLine(); + if (ImGui::Button("Clear##cleartop")) + { + topTexturePath.clear(); + topTexture = Texture(); + hasTopTexture = false; + } + + // Bottom texture selection + std::string bottomPath = bottomTexturePath.empty() ? "(none)" : bottomTexturePath.filename().string(); + ImGui::Text("Bottom:"); ImGui::SameLine(); + ImGui::SetNextItemWidth(200.0f); + ImGui::BeginDisabled(); + ImGui::InputText("##bottomtex", &bottomPath, ImGuiInputTextFlags_ReadOnly); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Browse##selectbottom")) + { + auto selection = pfd::open_file("Select Bottom Minimap Texture", ".", + {"Image Files", "*.png *.bmp *.jpg *.jpeg", "All Files", "*"}).result(); + if (!selection.empty()) + { + bottomTexturePath = selection.front(); + bottomTexture = Texture(bottomTexturePath); + hasBottomTexture = !bottomTexture.IsEmpty(); + } + } + ImGui::SameLine(); + if (ImGui::Button("Clear##clearbottom")) + { + bottomTexturePath.clear(); + bottomTexture = Texture(); + hasBottomTexture = false; + } + + // Status display + ImGui::Separator(); + if (IsReady()) + { + // Show texture dimensions + ImGui::Text("Texture Size: %dx%d pixels", topTexture.GetWidth(), topTexture.GetHeight()); + + // Check if dimensions match + if (topTexture.GetWidth() != bottomTexture.GetWidth() || topTexture.GetHeight() != bottomTexture.GetHeight()) + { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Warning: Top and bottom textures have different dimensions!"); + } + else + { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Minimap ready!"); + } + } + else if (hasTopTexture || hasBottomTexture) + { + if (hasTopTexture) + { + ImGui::Text("Top texture: %dx%d pixels", topTexture.GetWidth(), topTexture.GetHeight()); + } + if (hasBottomTexture) + { + ImGui::Text("Bottom texture: %dx%d pixels", bottomTexture.GetWidth(), bottomTexture.GetHeight()); + } + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Need both top and bottom textures"); + } + else + { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "No minimap textures loaded"); + } + + return boundsChanged; +} diff --git a/src/minimap.h b/src/minimap.h new file mode 100644 index 0000000..c901691 --- /dev/null +++ b/src/minimap.h @@ -0,0 +1,75 @@ +#pragma once + +#include "geo.h" +#include "psx_types.h" +#include "texture.h" + +#include +#include +#include +#include + +class Quadblock; + +struct MinimapConfig +{ + // World coordinate bounds (fixed-point, FP_ONE_GEO = 64) + int16_t worldEndX = 0; + int16_t worldEndY = 0; + int16_t worldStartX = 0; + int16_t worldStartY = 0; + + // Icon size is automatically calculated from texture dimensions + // (stored for serialization, updated when textures are loaded) + int16_t iconSizeX = 0; + int16_t iconSizeY = 0; + + // Screen position for driver dots (screen size is 512x252) + int16_t driverDotStartX = 450; + int16_t driverDotStartY = 180; + + // Orientation mode - determines minimap orientation relative to the world + // 0 = Right, 1 = Down, 2 = Left, 3 = Up + int16_t orientationMode = 0; + + // Unknown field - used for drawing, needed for some levels like Crash Cove + int16_t unk = 0; + + // Texture paths + std::filesystem::path topTexturePath; + std::filesystem::path bottomTexturePath; + + // Texture objects + Texture topTexture; + Texture bottomTexture; + + // State flags + bool hasTopTexture = false; + bool hasBottomTexture = false; + bool enabled = false; + + // Calculate world bounds from all quadblocks in the level + void CalculateWorldBoundsFromQuadblocks(const std::vector& quadblocks); + + // Serialize to PSX Map struct + PSX::Map Serialize() const; + + // Deserialize from PSX Map struct + void Deserialize(const PSX::Map& map); + + // Load textures from paths (call after loading from preset) + void LoadTextures(); + + // Check if minimap is ready for export + bool IsReady() const; + + // Get textures for VRM packing + std::vector GetTextures(); + + // Render the ImGui UI for minimap configuration + // Returns true if world bounds were modified (for updating visualization) + bool RenderUI(const std::vector& quadblocks); + + // Clear all minimap data + void Clear(); +}; diff --git a/src/psx_types.h b/src/psx_types.h index 55d9efd..ed481e8 100644 --- a/src/psx_types.h +++ b/src/psx_types.h @@ -238,6 +238,53 @@ namespace PSX uint32_t offsets[LevelExtra::COUNT]; }; + // Minimap struct - stored within SpawnType1[ST1_MAP] + struct Map + { + int16_t worldEndX; // 0x0 - World coordinate bound + int16_t worldEndY; // 0x2 + int16_t worldStartX; // 0x4 + int16_t worldStartY; // 0x6 + int16_t iconSizeX; // 0x8 - Size in pixels of minimap icon (width) + int16_t iconSizeY; // 0xA - Size in pixels of minimap icon (height) + int16_t driverDotStartX; // 0xC - Screen position for driver markers (512x252 screen) + int16_t driverDotStartY; // 0xE + int16_t mode; // 0x10 - Orientation mode (0=0°, 1=90°, 2=180°, 3=270°) + int16_t unk; // 0x12 - Needed for some levels like Crash Cove (value different from 0 stops drawing top part) + }; + + // Icon struct for minimap textures + struct Icon + { + char name[16]; // 0x0 - Icon name + int32_t globalIconArrayIndex; // 0x10 - Index in global icon array (3=top, 4=bottom) + TextureLayout texLayout; // 0x14 - UV and texture page info + }; + + // LevTexLookup - Icon pack header, pointed to by Level::levTexLookup + struct LevTexLookup + { + int32_t numIcon; // 0x0 - Number of icons (2 for minimap: top and bottom) + uint32_t offFirstIcon; // 0x4 - Pointer to first Icon struct + int32_t numIconGroup; // 0x8 - Number of icon groups + uint32_t offFirstIconGroupPtr; // 0xC - Pointer to IconGroup pointer array + }; + + // SpawnType1 header - contains count and array of pointers + struct SpawnType1 + { + uint32_t count; // 0x0 - Number of pointers in the array + // uint32_t offsets[count]; // Variable-length array of pointers follows + }; + + // Global icon array indices for minimap + static constexpr int32_t ICON_INDEX_MAP_TOP = 3; + static constexpr int32_t ICON_INDEX_MAP_BOTTOM = 4; + + // Screen size + static constexpr int32_t SCREEN_WIDTH = 512; + static constexpr int32_t SCREEN_HEIGHT = 252; + struct Vertex { PSX::Vec3 pos; // 0x0 From 8442e8b7adc643b197c3e207039a34cc4bf341fe Mon Sep 17 00:00:00 2001 From: jorozcove Date: Sun, 1 Feb 2026 21:22:24 -0500 Subject: [PATCH 2/4] - Naming changes --- src/io.cpp | 1 - src/minimap.cpp | 4 ++-- src/psx_types.h | 15 ++++----------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/io.cpp b/src/io.cpp index 7325f7c..0cf18f0 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -116,7 +116,6 @@ void from_json(const nlohmann::json& json, MinimapConfig& minimap) if (json.contains("driverDotStartX")) { json.at("driverDotStartX").get_to(minimap.driverDotStartX); } if (json.contains("driverDotStartY")) { json.at("driverDotStartY").get_to(minimap.driverDotStartY); } if (json.contains("orientationMode")) { json.at("orientationMode").get_to(minimap.orientationMode); } - else if (json.contains("mode")) { json.at("mode").get_to(minimap.orientationMode); } // backwards compatibility if (json.contains("unk")) { json.at("unk").get_to(minimap.unk); } if (json.contains("topTexturePath")) diff --git a/src/minimap.cpp b/src/minimap.cpp index a8394fc..cdddf32 100644 --- a/src/minimap.cpp +++ b/src/minimap.cpp @@ -50,7 +50,7 @@ PSX::Map MinimapConfig::Serialize() const map.iconSizeY = hasTopTexture ? static_cast(topTexture.GetHeight()) : iconSizeY; map.driverDotStartX = driverDotStartX; map.driverDotStartY = driverDotStartY; - map.mode = orientationMode; + map.orientationMode = orientationMode; map.unk = unk; return map; } @@ -65,7 +65,7 @@ void MinimapConfig::Deserialize(const PSX::Map& map) iconSizeY = map.iconSizeY; driverDotStartX = map.driverDotStartX; driverDotStartY = map.driverDotStartY; - orientationMode = map.mode; + orientationMode = map.orientationMode; unk = map.unk; } diff --git a/src/psx_types.h b/src/psx_types.h index ed481e8..2d91fd4 100644 --- a/src/psx_types.h +++ b/src/psx_types.h @@ -238,7 +238,7 @@ namespace PSX uint32_t offsets[LevelExtra::COUNT]; }; - // Minimap struct - stored within SpawnType1[ST1_MAP] + // Minimap struct struct Map { int16_t worldEndX; // 0x0 - World coordinate bound @@ -249,7 +249,7 @@ namespace PSX int16_t iconSizeY; // 0xA - Size in pixels of minimap icon (height) int16_t driverDotStartX; // 0xC - Screen position for driver markers (512x252 screen) int16_t driverDotStartY; // 0xE - int16_t mode; // 0x10 - Orientation mode (0=0°, 1=90°, 2=180°, 3=270°) + int16_t orientationMode; // 0x10 - Orientation mode (0=0°, 1=90°, 2=180°, 3=270°) int16_t unk; // 0x12 - Needed for some levels like Crash Cove (value different from 0 stops drawing top part) }; @@ -261,8 +261,8 @@ namespace PSX TextureLayout texLayout; // 0x14 - UV and texture page info }; - // LevTexLookup - Icon pack header, pointed to by Level::levTexLookup - struct LevTexLookup + // LevelIconHeader - Icon pack header, pointed to by Level::levelIconHeader + struct LevelIconHeader { int32_t numIcon; // 0x0 - Number of icons (2 for minimap: top and bottom) uint32_t offFirstIcon; // 0x4 - Pointer to first Icon struct @@ -270,13 +270,6 @@ namespace PSX uint32_t offFirstIconGroupPtr; // 0xC - Pointer to IconGroup pointer array }; - // SpawnType1 header - contains count and array of pointers - struct SpawnType1 - { - uint32_t count; // 0x0 - Number of pointers in the array - // uint32_t offsets[count]; // Variable-length array of pointers follows - }; - // Global icon array indices for minimap static constexpr int32_t ICON_INDEX_MAP_TOP = 3; static constexpr int32_t ICON_INDEX_MAP_BOTTOM = 4; From cc547ff63323e2ab06162e103a65d629eb226636 Mon Sep 17 00:00:00 2001 From: jorozcove Date: Sun, 1 Feb 2026 21:22:39 -0500 Subject: [PATCH 3/4] - Naming changes - Small improvement for pointer count assigment --- src/level.cpp | 66 +++++++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/level.cpp b/src/level.cpp index 32663b5..71a0e97 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -1162,11 +1162,11 @@ bool Level::SaveLEV(const std::filesystem::path& path) extraHeader.offsets[PSX::LevelExtra::N_OXIDE_GHOST] = static_cast(offOxideGhost); extraHeader.offsets[PSX::LevelExtra::CREDITS] = 0; - // Determine count based on what's enabled - if (offOxideGhost > 0) { extraHeader.count = PSX::LevelExtra::COUNT; } - else if (offTropyGhost > 0) { extraHeader.count = PSX::LevelExtra::N_OXIDE_GHOST; } - else if (m_minimapConfig.IsReady()) { extraHeader.count = PSX::LevelExtra::COUNT; } - else { extraHeader.count = 0; } + // Determine count based on highest enabled entry index + 1 + // Count represents the number of valid entries in the offsets array + if (offOxideGhost > 0) { extraHeader.count = PSX::LevelExtra::N_OXIDE_GHOST + 1; } + else if (offTropyGhost > 0) { extraHeader.count = PSX::LevelExtra::N_TROPY_GHOST + 1; } + // Note: minimap count will be set later if enabled and no ghosts present const size_t offExtraHeader = currOffset; currOffset += sizeof(extraHeader); @@ -1198,15 +1198,14 @@ bool Level::SaveLEV(const std::filesystem::path& path) // Minimap data serialization size_t offMinimapStruct = 0; - size_t offSpawnType1 = 0; - size_t offLevTexLookup = 0; + size_t offLevelIconHeader = 0; size_t offMinimapIcons = 0; std::vector minimapData; std::vector minimapPtrMapOffsets; if (m_minimapConfig.IsReady()) { - // Map struct + // Map struct - this is what extraHeader.offsets[MINIMAP] will point to offMinimapStruct = currOffset; PSX::Map mapStruct = m_minimapConfig.Serialize(); size_t mapStructOffset = minimapData.size(); @@ -1214,24 +1213,6 @@ bool Level::SaveLEV(const std::filesystem::path& path) memcpy(&minimapData[mapStructOffset], &mapStruct, sizeof(PSX::Map)); currOffset += sizeof(PSX::Map); - // SpawnType1 header (count + pointer to map struct) - offSpawnType1 = currOffset; - PSX::SpawnType1 spawnType1Header = {}; - spawnType1Header.count = PSX::LevelExtra::COUNT; // Must have all entries for minimap - size_t st1HeaderOffset = minimapData.size(); - minimapData.resize(minimapData.size() + sizeof(PSX::SpawnType1)); - memcpy(&minimapData[st1HeaderOffset], &spawnType1Header, sizeof(PSX::SpawnType1)); - currOffset += sizeof(PSX::SpawnType1); - - // SpawnType1 pointers array - pointer at index 0 points to Map struct - size_t st1PointersOffset = minimapData.size(); - minimapData.resize(minimapData.size() + sizeof(uint32_t) * PSX::LevelExtra::COUNT); - uint32_t* st1Pointers = reinterpret_cast(&minimapData[st1PointersOffset]); - st1Pointers[PSX::LevelExtra::MINIMAP] = static_cast(offMinimapStruct); - for (size_t i = 1; i < PSX::LevelExtra::COUNT; i++) { st1Pointers[i] = 0; } - minimapPtrMapOffsets.push_back(currOffset); // Pointer to map struct needs patching - currOffset += sizeof(uint32_t) * PSX::LevelExtra::COUNT; - // Icon structs (top and bottom minimap textures) offMinimapIcons = currOffset; @@ -1258,21 +1239,24 @@ bool Level::SaveLEV(const std::filesystem::path& path) memcpy(&minimapData[bottomIconOffset], &bottomIcon, sizeof(PSX::Icon)); currOffset += sizeof(PSX::Icon); - // LevTexLookup struct - offLevTexLookup = currOffset; - PSX::LevTexLookup levTexLookup = {}; - levTexLookup.numIcon = 2; - levTexLookup.offFirstIcon = static_cast(offMinimapIcons); - levTexLookup.numIconGroup = 0; - levTexLookup.offFirstIconGroupPtr = 0; - size_t levTexLookupOffset = minimapData.size(); - minimapData.resize(minimapData.size() + sizeof(PSX::LevTexLookup)); - memcpy(&minimapData[levTexLookupOffset], &levTexLookup, sizeof(PSX::LevTexLookup)); - minimapPtrMapOffsets.push_back(currOffset + offsetof(PSX::LevTexLookup, offFirstIcon)); // Pointer to first icon - currOffset += sizeof(PSX::LevTexLookup); - - // Update extraHeader to point to the minimap struct + // LevelIconHeader struct (pointed to by header.offIconsLookup) + offLevelIconHeader = currOffset; + PSX::LevelIconHeader levelIconHeader = {}; + levelIconHeader.numIcon = 2; + levelIconHeader.offFirstIcon = static_cast(offMinimapIcons); + levelIconHeader.numIconGroup = 0; + levelIconHeader.offFirstIconGroupPtr = 0; + size_t levelIconHeaderOffset = minimapData.size(); + minimapData.resize(minimapData.size() + sizeof(PSX::LevelIconHeader)); + memcpy(&minimapData[levelIconHeaderOffset], &levelIconHeader, sizeof(PSX::LevelIconHeader)); + minimapPtrMapOffsets.push_back(currOffset + offsetof(PSX::LevelIconHeader, offFirstIcon)); // Pointer to first icon + currOffset += sizeof(PSX::LevelIconHeader); + + // Update extraHeader to point to the minimap Map struct directly extraHeader.offsets[PSX::LevelExtra::MINIMAP] = static_cast(offMinimapStruct); + + // Set count if no ghosts are present (minimap is at index 0, so count = 1) + if (extraHeader.count == 0) { extraHeader.count = PSX::LevelExtra::MINIMAP + 1; } } const size_t offPointerMap = currOffset; @@ -1302,7 +1286,7 @@ bool Level::SaveLEV(const std::filesystem::path& path) // Set minimap pointers in header if enabled if (m_minimapConfig.IsReady()) { - header.offIconsLookup = static_cast(offLevTexLookup); + header.offIconsLookup = static_cast(offLevelIconHeader); header.offIcons = static_cast(offMinimapIcons); } From eb356afc69d387853f0bb9104dbb0692d4e37e07 Mon Sep 17 00:00:00 2001 From: jorozcove Date: Mon, 2 Feb 2026 20:51:50 -0500 Subject: [PATCH 4/4] [WIP] attempt to handle strech and split automatically --- src/io.cpp | 18 ++--- src/minimap.cpp | 190 ++++++++++++++++++++++++++++-------------------- src/minimap.h | 10 ++- src/texture.cpp | 38 ++++++++++ src/texture.h | 2 + 5 files changed, 166 insertions(+), 92 deletions(-) diff --git a/src/io.cpp b/src/io.cpp index 0cf18f0..09ac349 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -99,8 +99,7 @@ void to_json(nlohmann::json& json, const MinimapConfig& minimap) {"driverDotStartY", minimap.driverDotStartY}, {"orientationMode", minimap.orientationMode}, {"unk", minimap.unk}, - {"topTexturePath", minimap.topTexturePath.string()}, - {"bottomTexturePath", minimap.bottomTexturePath.string()} + {"sourceTexturePath", minimap.sourceTexturePath.string()} }; } @@ -118,18 +117,19 @@ void from_json(const nlohmann::json& json, MinimapConfig& minimap) if (json.contains("orientationMode")) { json.at("orientationMode").get_to(minimap.orientationMode); } if (json.contains("unk")) { json.at("unk").get_to(minimap.unk); } - if (json.contains("topTexturePath")) + // New format: single source texture path + if (json.contains("sourceTexturePath")) { std::string path; - json.at("topTexturePath").get_to(path); - if (!path.empty()) { minimap.topTexturePath = path; } + json.at("sourceTexturePath").get_to(path); + if (!path.empty()) { minimap.sourceTexturePath = path; } } - - if (json.contains("bottomTexturePath")) + // Old format backwards compatibility: if topTexturePath exists but sourceTexturePath doesn't, use topTexturePath + else if (json.contains("topTexturePath")) { std::string path; - json.at("bottomTexturePath").get_to(path); - if (!path.empty()) { minimap.bottomTexturePath = path; } + json.at("topTexturePath").get_to(path); + if (!path.empty()) { minimap.sourceTexturePath = path; } } // Load textures after setting paths diff --git a/src/minimap.cpp b/src/minimap.cpp index cdddf32..4afa104 100644 --- a/src/minimap.cpp +++ b/src/minimap.cpp @@ -1,6 +1,7 @@ #include "minimap.h" #include "quadblock.h" +#include #include #include #include @@ -71,21 +72,95 @@ void MinimapConfig::Deserialize(const PSX::Map& map) void MinimapConfig::LoadTextures() { - if (!topTexturePath.empty() && std::filesystem::exists(topTexturePath)) - { - topTexture = Texture(topTexturePath); - hasTopTexture = !topTexture.IsEmpty(); + lastError.clear(); + hasTopTexture = false; + hasBottomTexture = false; + topTexture = Texture(); + bottomTexture = Texture(); + + if (sourceTexturePath.empty()) { + lastError = "No file selected"; + return; } - if (!bottomTexturePath.empty() && std::filesystem::exists(bottomTexturePath)) - { - bottomTexture = Texture(bottomTexturePath); - hasBottomTexture = !bottomTexture.IsEmpty(); + if (!std::filesystem::exists(sourceTexturePath)) { + lastError = "File does not exist"; + return; + } + + // Load the source image using stb_image + int width = 0, height = 0, channels = 0; + unsigned char* sourceImage = stbi_load(sourceTexturePath.string().c_str(), &width, &height, &channels, 4); // Force RGBA + if (sourceImage == nullptr) { + lastError = "Failed to load image (unsupported format or corrupted file)"; + return; + } + + if (width <= 0 || height <= 0) { + stbi_image_free(sourceImage); + lastError = "Image has invalid dimensions"; + return; + } + + // Image should be twice the height (top + bottom halves) + if (height % 2 != 0) { + stbi_image_free(sourceImage); + lastError = "Image height must be even (for splitting into top/bottom)"; + return; + } + + int halfHeight = height / 2; + int stretchedWidth = static_cast(width * 1.5f); + + if (stretchedWidth <= 0) { + stbi_image_free(sourceImage); + lastError = "Stretched width is invalid (check image width)"; + return; + } + + // Create buffers for stretched top and bottom halves + std::vector topPixels(stretchedWidth * halfHeight * 4); + std::vector bottomPixels(stretchedWidth * halfHeight * 4); + + // Stretch and split the image using nearest-neighbor sampling + for (int y = 0; y < halfHeight; y++) { + for (int x = 0; x < stretchedWidth; x++) { + int srcX = static_cast(x / 1.5f); + if (srcX >= width) srcX = width - 1; + + // Top half + int srcTopIdx = (y * width + srcX) * 4; + int dstTopIdx = (y * stretchedWidth + x) * 4; + topPixels[dstTopIdx + 0] = sourceImage[srcTopIdx + 0]; + topPixels[dstTopIdx + 1] = sourceImage[srcTopIdx + 1]; + topPixels[dstTopIdx + 2] = sourceImage[srcTopIdx + 2]; + topPixels[dstTopIdx + 3] = sourceImage[srcTopIdx + 3]; + + // Bottom half + int srcBottomIdx = ((y + halfHeight) * width + srcX) * 4; + int dstBottomIdx = (y * stretchedWidth + x) * 4; + bottomPixels[dstBottomIdx + 0] = sourceImage[srcBottomIdx + 0]; + bottomPixels[dstBottomIdx + 1] = sourceImage[srcBottomIdx + 1]; + bottomPixels[dstBottomIdx + 2] = sourceImage[srcBottomIdx + 2]; + bottomPixels[dstBottomIdx + 3] = sourceImage[srcBottomIdx + 3]; + } + } + + stbi_image_free(sourceImage); + + topTexture = Texture::CreateFromPixelData(topPixels.data(), stretchedWidth, halfHeight); + bottomTexture = Texture::CreateFromPixelData(bottomPixels.data(), stretchedWidth, halfHeight); + + hasTopTexture = !topTexture.IsEmpty(); + hasBottomTexture = !bottomTexture.IsEmpty(); + + if (!hasTopTexture || !hasBottomTexture) { + lastError = "Failed to create minimap textures from image data"; } } bool MinimapConfig::IsReady() const { - return enabled && hasTopTexture && hasBottomTexture; + return enabled && hasTopTexture && hasBottomTexture && lastError.empty(); } std::vector MinimapConfig::GetTextures() @@ -108,32 +183,32 @@ void MinimapConfig::Clear() driverDotStartY = 180; orientationMode = 0; unk = 0; - topTexturePath.clear(); - bottomTexturePath.clear(); + sourceTexturePath.clear(); topTexture = Texture(); bottomTexture = Texture(); hasTopTexture = false; hasBottomTexture = false; enabled = false; + lastError.clear(); } bool MinimapConfig::RenderUI(const std::vector& quadblocks) { bool boundsChanged = false; - + ImGui::Checkbox("Enable Minimap", &enabled); if (!enabled) { return false; } ImGui::Separator(); ImGui::Text("World Bounds:"); - + // Convert fixed-point to float for display (divide by 64) float startX = static_cast(worldStartX) / static_cast(FP_ONE_GEO); float startY = static_cast(worldStartY) / static_cast(FP_ONE_GEO); float endX = static_cast(worldEndX) / static_cast(FP_ONE_GEO); float endY = static_cast(worldEndY) / static_cast(FP_ONE_GEO); - + if (ImGui::InputFloat("World Start X", &startX, 1.0f, 10.0f, "%.2f")) { worldStartX = static_cast(startX * static_cast(FP_ONE_GEO)); @@ -175,7 +250,7 @@ bool MinimapConfig::RenderUI(const std::vector& quadblocks) ImGui::Separator(); ImGui::Text("Minimap Orientation:"); - + // Orientation mode dropdown const char* orientationModes[] = { "0°", "90°", "180°", "270°" }; int currentOrientation = orientationMode; @@ -185,101 +260,58 @@ bool MinimapConfig::RenderUI(const std::vector& quadblocks) orientationMode = static_cast(currentOrientation); } ImGui::SetItemTooltip("Determines minimap clockwise rotation relative to the world\n It doesnt affect texture orientation, it affects how the driver icon moves on the minimap"); - + ImGui::Separator(); ImGui::InputScalar("Unknown", ImGuiDataType_S16, &unk); ImGui::SetItemTooltip("???"); ImGui::Separator(); - ImGui::Text("Textures (both halves must have the same dimensions):"); + ImGui::Text("Minimap Texture:"); - // Top texture selection - std::string topPath = topTexturePath.empty() ? "(none)" : topTexturePath.filename().string(); - ImGui::Text("Top:"); ImGui::SameLine(); - ImGui::SetNextItemWidth(200.0f); + // Single texture selection + std::string sourcePath = sourceTexturePath.empty() ? "(none)" : sourceTexturePath.filename().string(); + ImGui::Text("Image:"); ImGui::SameLine(); + ImGui::SetNextItemWidth(250.0f); ImGui::BeginDisabled(); - ImGui::InputText("##toptex", &topPath, ImGuiInputTextFlags_ReadOnly); + ImGui::InputText("##sourcetex", &sourcePath, ImGuiInputTextFlags_ReadOnly); ImGui::EndDisabled(); ImGui::SameLine(); - if (ImGui::Button("Browse##selecttop")) + if (ImGui::Button("Browse##selectsource")) { - auto selection = pfd::open_file("Select Top Minimap Texture", ".", + auto selection = pfd::open_file("Select Minimap Texture (will be split into top/bottom)", ".", {"Image Files", "*.png *.bmp *.jpg *.jpeg", "All Files", "*"}).result(); if (!selection.empty()) { - topTexturePath = selection.front(); - topTexture = Texture(topTexturePath); - hasTopTexture = !topTexture.IsEmpty(); + sourceTexturePath = selection.front(); + LoadTextures(); // Process the image immediately } } ImGui::SameLine(); - if (ImGui::Button("Clear##cleartop")) + if (ImGui::Button("Clear##clearsource")) { - topTexturePath.clear(); + sourceTexturePath.clear(); topTexture = Texture(); - hasTopTexture = false; - } - - // Bottom texture selection - std::string bottomPath = bottomTexturePath.empty() ? "(none)" : bottomTexturePath.filename().string(); - ImGui::Text("Bottom:"); ImGui::SameLine(); - ImGui::SetNextItemWidth(200.0f); - ImGui::BeginDisabled(); - ImGui::InputText("##bottomtex", &bottomPath, ImGuiInputTextFlags_ReadOnly); - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Browse##selectbottom")) - { - auto selection = pfd::open_file("Select Bottom Minimap Texture", ".", - {"Image Files", "*.png *.bmp *.jpg *.jpeg", "All Files", "*"}).result(); - if (!selection.empty()) - { - bottomTexturePath = selection.front(); - bottomTexture = Texture(bottomTexturePath); - hasBottomTexture = !bottomTexture.IsEmpty(); - } - } - ImGui::SameLine(); - if (ImGui::Button("Clear##clearbottom")) - { - bottomTexturePath.clear(); bottomTexture = Texture(); + hasTopTexture = false; hasBottomTexture = false; + lastError.clear(); } // Status display ImGui::Separator(); if (IsReady()) { - // Show texture dimensions - ImGui::Text("Texture Size: %dx%d pixels", topTexture.GetWidth(), topTexture.GetHeight()); - - // Check if dimensions match - if (topTexture.GetWidth() != bottomTexture.GetWidth() || topTexture.GetHeight() != bottomTexture.GetHeight()) - { - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Warning: Top and bottom textures have different dimensions!"); - } - else - { - ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Minimap ready!"); - } + ImGui::Text("Processed Size (each half): %dx%d pixels", topTexture.GetWidth(), topTexture.GetHeight()); + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Minimap ready!"); } - else if (hasTopTexture || hasBottomTexture) + else if (!sourceTexturePath.empty()) { - if (hasTopTexture) - { - ImGui::Text("Top texture: %dx%d pixels", topTexture.GetWidth(), topTexture.GetHeight()); - } - if (hasBottomTexture) - { - ImGui::Text("Bottom texture: %dx%d pixels", bottomTexture.GetWidth(), bottomTexture.GetHeight()); - } - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Need both top and bottom textures"); + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Error: %s", lastError.empty() ? "Unknown error" : lastError.c_str()); } else { - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "No minimap textures loaded"); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "No minimap texture loaded"); } - + return boundsChanged; } diff --git a/src/minimap.h b/src/minimap.h index c901691..8789285 100644 --- a/src/minimap.h +++ b/src/minimap.h @@ -35,11 +35,10 @@ struct MinimapConfig // Unknown field - used for drawing, needed for some levels like Crash Cove int16_t unk = 0; - // Texture paths - std::filesystem::path topTexturePath; - std::filesystem::path bottomTexturePath; + // Texture paths - single source image that will be split and stretched + std::filesystem::path sourceTexturePath; - // Texture objects + // Texture objects (generated from source) Texture topTexture; Texture bottomTexture; @@ -48,6 +47,9 @@ struct MinimapConfig bool hasBottomTexture = false; bool enabled = false; + // Last error message from loading textures + std::string lastError; + // Calculate world bounds from all quadblocks in the level void CalculateWorldBoundsFromQuadblocks(const std::vector& quadblocks); diff --git a/src/texture.cpp b/src/texture.cpp index 7af379b..cbf8cdc 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -22,6 +22,44 @@ Texture::Texture(const std::filesystem::path& path) if (!CreateTexture()) { ClearTexture(); } } +Texture Texture::CreateFromPixelData(const unsigned char* pixels, int width, int height) +{ + Texture tex; + tex.m_width = width; + tex.m_height = height; + tex.m_blendMode = PSX::BlendMode::HALF_TRANSPARENT; + + // Convert pixel data to texture format + std::vector colorIndexes; + for (int i = 0; i < width * height; i++) + { + int px = i * 4; // RGBA format + uint16_t color = tex.ConvertColor(pixels[px + 0], pixels[px + 1], pixels[px + 2], pixels[px + 3]); + bool foundColor = false; + size_t clutIndex = tex.m_clut.size(); + for (size_t j = 0; j < tex.m_clut.size(); j++) + { + if (color == tex.m_clut[j]) { clutIndex = j; foundColor = true; break; } + } + if (!foundColor) { tex.m_clut.push_back(color); } + colorIndexes.push_back(clutIndex); + } + + Texture::BPP bpp = tex.GetBPP(); + if (tex.GetVRAMWidth() > TEXPAGE_WIDTH || tex.GetHeight() > TEXPAGE_HEIGHT) + { + tex.ClearTexture(); + return tex; + } + + if (bpp == Texture::BPP::BPP_4) { tex.ConvertPixels(colorIndexes, 4); } + else if (bpp == Texture::BPP::BPP_8) { tex.ConvertPixels(colorIndexes, 2); } + else { tex.m_image = tex.m_clut; } + + tex.FillShapes(colorIndexes); + return tex; +} + void Texture::UpdateTexture(const std::filesystem::path& path) { uint16_t blendMode = m_blendMode; diff --git a/src/texture.h b/src/texture.h index da5169a..f740637 100644 --- a/src/texture.h +++ b/src/texture.h @@ -21,6 +21,8 @@ class Texture Texture() : m_width(0), m_height(0), m_imageX(0), m_imageY(0), m_clutX(0), m_clutY(0), m_blendMode(0) {}; Texture(const std::filesystem::path& path); void UpdateTexture(const std::filesystem::path& path); + // Create texture from raw RGBA pixel data (for minimap processing) + static Texture CreateFromPixelData(const unsigned char* pixels, int width, int height); Texture::BPP GetBPP() const; int GetWidth() const; int GetVRAMWidth() const;