#include "TmxMap.h" #include #include #include #include #include #include namespace { constexpr std::uint32_t FlippedHorizontally = 0x80000000u; constexpr std::uint32_t FlippedVertically = 0x40000000u; constexpr std::uint32_t FlippedDiagonally = 0x20000000u; constexpr std::uint32_t FlippedHex120 = 0x10000000u; std::string Trim(std::string value) { auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); }); auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base(); if (first >= last) { return {}; } return std::string(first, last); } std::string LowerCopy(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return value; } std::filesystem::path NormalizePath(const std::filesystem::path& base, const char* relative) { return std::filesystem::weakly_canonical(base / relative); } std::map ReadProperties(const pugi::xml_node& node) { std::map result; for (pugi::xml_node property : node.child("properties").children("property")) { const std::string name = property.attribute("name").as_string(); const char* valueAttr = property.attribute("value").as_string(nullptr); if (valueAttr) { result[name] = valueAttr; } else { result[name] = property.text().as_string(); } } return result; } std::vector ParsePoints(const std::string& points); std::vector ParseCollisionShapes(const pugi::xml_node& tile) { std::vector shapes; for (pugi::xml_node object : tile.child("objectgroup").children("object")) { TmxCollisionShape shape; shape.x = object.attribute("x").as_float(); shape.y = object.attribute("y").as_float(); shape.width = object.attribute("width").as_float(); shape.height = object.attribute("height").as_float(); if (pugi::xml_node polygon = object.child("polygon")) { shape.polygon = ParsePoints(polygon.attribute("points").as_string()); } else if (pugi::xml_node polyline = object.child("polyline")) { shape.polygon = ParsePoints(polyline.attribute("points").as_string()); } if (!shape.polygon.empty() || shape.width > 0.0f || shape.height > 0.0f) { shapes.push_back(std::move(shape)); } } return shapes; } TmxTileset ParseTilesetNode(const pugi::xml_node& node, std::uint32_t firstGid, const std::filesystem::path& baseDir) { TmxTileset tileset; tileset.firstGid = firstGid; tileset.name = node.attribute("name").as_string(); tileset.tileWidth = node.attribute("tilewidth").as_int(); tileset.tileHeight = node.attribute("tileheight").as_int(); tileset.tileCount = node.attribute("tilecount").as_int(); tileset.columns = node.attribute("columns").as_int(); pugi::xml_node image = node.child("image"); if (!image) { throw std::runtime_error("Tileset has no image: " + tileset.name); } tileset.imagePath = NormalizePath(baseDir, image.attribute("source").as_string()); for (pugi::xml_node tile : node.children("tile")) { const auto tileId = static_cast(tile.attribute("id").as_uint()); std::vector collisionShapes = ParseCollisionShapes(tile); if (!collisionShapes.empty()) { tileset.collisionShapes[tileId] = std::move(collisionShapes); } pugi::xml_node animation = tile.child("animation"); if (!animation) { continue; } std::vector frames; for (pugi::xml_node frame : animation.children("frame")) { frames.push_back(TmxAnimationFrame{ static_cast(frame.attribute("tileid").as_uint()), frame.attribute("duration").as_int(), }); } if (!frames.empty()) { tileset.animations[tileId] = std::move(frames); } } return tileset; } TmxTileset ParseTileset(const pugi::xml_node& node, const std::filesystem::path& mapDir) { const auto firstGid = static_cast(node.attribute("firstgid").as_uint()); const char* source = node.attribute("source").as_string(nullptr); if (!source) { return ParseTilesetNode(node, firstGid, mapDir); } const std::filesystem::path tsxPath = NormalizePath(mapDir, source); pugi::xml_document doc; pugi::xml_parse_result ok = doc.load_file(tsxPath.c_str()); if (!ok) { throw std::runtime_error("Could not parse TSX: " + tsxPath.string()); } return ParseTilesetNode(doc.child("tileset"), firstGid, tsxPath.parent_path()); } std::vector ParseCsvGids(const std::string& csv) { std::vector gids; std::stringstream stream(csv); std::string item; while (std::getline(stream, item, ',')) { item = Trim(item); if (!item.empty()) { gids.push_back(static_cast(std::stoul(item))); } } return gids; } std::vector ParsePoints(const std::string& points) { std::vector out; std::stringstream stream(points); std::string pair; while (stream >> pair) { const std::size_t comma = pair.find(','); if (comma == std::string::npos) { continue; } TmxPoint point; point.x = std::stof(pair.substr(0, comma)); point.y = std::stof(pair.substr(comma + 1)); out.push_back(point); } return out; } } // namespace namespace { std::map& TmxMapCache() { static std::map cache; return cache; } } // namespace std::uint32_t CleanGid(std::uint32_t gid) { return gid & ~(FlippedHorizontally | FlippedVertically | FlippedDiagonally | FlippedHex120); } TmxMap LoadTmxMap(const std::filesystem::path& path) { pugi::xml_document doc; pugi::xml_parse_result ok = doc.load_file(path.c_str()); if (!ok) { throw std::runtime_error("Could not parse TMX: " + path.string()); } pugi::xml_node root = doc.child("map"); if (!root) { throw std::runtime_error("TMX file has no map root: " + path.string()); } TmxMap map; map.sourcePath = std::filesystem::weakly_canonical(path); map.width = root.attribute("width").as_int(); map.height = root.attribute("height").as_int(); map.tileWidth = root.attribute("tilewidth").as_int(); map.tileHeight = root.attribute("tileheight").as_int(); map.properties = ReadProperties(root); const std::filesystem::path mapDir = map.sourcePath.parent_path(); for (pugi::xml_node child : root.children()) { const std::string nodeName = child.name(); if (nodeName == "tileset") { map.tilesets.push_back(ParseTileset(child, mapDir)); } else if (nodeName == "layer") { pugi::xml_node data = child.child("data"); const std::string encoding = data.attribute("encoding").as_string(); if (encoding != "csv") { continue; } TmxLayer layer; layer.name = child.attribute("name").as_string(); layer.width = child.attribute("width").as_int(); layer.height = child.attribute("height").as_int(); layer.visible = child.attribute("visible").as_int(1) != 0; layer.opacity = child.attribute("opacity").as_float(1.0f); layer.gids = ParseCsvGids(data.text().as_string()); map.layers.push_back(std::move(layer)); } else if (nodeName == "objectgroup") { for (pugi::xml_node object : child.children("object")) { TmxObject out; out.id = object.attribute("id").as_int(); out.name = object.attribute("name").as_string(); out.type = object.attribute("type").as_string(); out.x = object.attribute("x").as_float(); out.y = object.attribute("y").as_float(); out.width = object.attribute("width").as_float(); out.height = object.attribute("height").as_float(); if (pugi::xml_node polygon = object.child("polygon")) { out.polygon = ParsePoints(polygon.attribute("points").as_string()); } else if (pugi::xml_node polyline = object.child("polyline")) { out.polygon = ParsePoints(polyline.attribute("points").as_string()); } out.properties = ReadProperties(object); map.objects.push_back(std::move(out)); } } } std::sort(map.tilesets.begin(), map.tilesets.end(), [](const TmxTileset& a, const TmxTileset& b) { return a.firstGid < b.firstGid; }); return map; } const TmxMap& LoadTmxMapCached(const std::filesystem::path& path) { const std::filesystem::path canonical = std::filesystem::weakly_canonical(path); const std::string key = canonical.string(); std::map& cache = TmxMapCache(); auto [it, inserted] = cache.try_emplace(key); if (inserted) { it->second = LoadTmxMap(canonical); } return it->second; } void ClearTmxMapCache() { TmxMapCache().clear(); } const TmxLayer* FindLayer(const TmxMap& map, const std::string& name) { auto it = std::find_if(map.layers.begin(), map.layers.end(), [&](const TmxLayer& layer) { return layer.name == name; }); return it == map.layers.end() ? nullptr : &*it; } const TmxObject* FindObject(const TmxMap& map, const std::string& name) { auto it = std::find_if(map.objects.begin(), map.objects.end(), [&](const TmxObject& object) { return object.name == name; }); return it == map.objects.end() ? nullptr : &*it; } const TmxTileset* FindTilesetForGid(const TmxMap& map, std::uint32_t gid) { gid = CleanGid(gid); const TmxTileset* found = nullptr; for (const TmxTileset& tileset : map.tilesets) { if (gid >= tileset.firstGid) { found = &tileset; } else { break; } } return found; } namespace { std::uint32_t LayerGidAt(const TmxLayer& layer, int x, int y) { if (x < 0 || y < 0 || x >= layer.width || y >= layer.height) { return 0; } const std::size_t index = static_cast(y * layer.width + x); return index < layer.gids.size() ? CleanGid(layer.gids[index]) : 0; } bool PointInRect(float x, float y, const TmxCollisionShape& shape) { return x >= shape.x && y >= shape.y && x < shape.x + shape.width && y < shape.y + shape.height; } bool PointOnSegment(float px, float py, TmxPoint a, TmxPoint b) { const float cross = (px - a.x) * (b.y - a.y) - (py - a.y) * (b.x - a.x); if (std::abs(cross) > 0.001f) { return false; } return px >= std::min(a.x, b.x) - 0.001f && px <= std::max(a.x, b.x) + 0.001f && py >= std::min(a.y, b.y) - 0.001f && py <= std::max(a.y, b.y) + 0.001f; } bool PointInPolygon(float x, float y, const TmxCollisionShape& shape) { if (shape.polygon.size() < 3) { return false; } std::vector points; points.reserve(shape.polygon.size()); for (const TmxPoint& point : shape.polygon) { points.push_back({shape.x + point.x, shape.y + point.y}); } bool inside = false; for (std::size_t i = 0, j = points.size() - 1; i < points.size(); j = i++) { const TmxPoint a = points[i]; const TmxPoint b = points[j]; if (PointOnSegment(x, y, a, b)) { return true; } const bool crosses = ((a.y > y) != (b.y > y)) && (x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x); if (crosses) { inside = !inside; } } return inside; } bool PointInCollisionShape(float x, float y, const TmxCollisionShape& shape) { if (!shape.polygon.empty()) { return PointInPolygon(x, y, shape); } return PointInRect(x, y, shape); } } // namespace bool IsPointBlockedByCollision(const TmxMap& map, float worldX, float worldY) { if (worldX < 0.0f || worldY < 0.0f || worldX >= static_cast(map.width * map.tileWidth) || worldY >= static_cast(map.height * map.tileHeight)) { return true; } if (map.tileWidth <= 0 || map.tileHeight <= 0) { return false; } const int tileX = static_cast(std::floor(worldX / static_cast(map.tileWidth))); const int tileY = static_cast(std::floor(worldY / static_cast(map.tileHeight))); for (const TmxLayer& layer : map.layers) { if (!layer.visible || !IsTmxCollisionLayer(layer.name)) { continue; } const std::uint32_t gid = LayerGidAt(layer, tileX, tileY); if (gid == 0) { continue; } const TmxTileset* tileset = FindTilesetForGid(map, gid); if (!tileset) { continue; } const std::uint32_t localTileId = gid - tileset->firstGid; const auto shapes = tileset->collisionShapes.find(localTileId); if (shapes == tileset->collisionShapes.end() || shapes->second.empty()) { return true; } const float localX = (worldX - static_cast(tileX * map.tileWidth)) * static_cast(std::max(1, tileset->tileWidth)) / static_cast(map.tileWidth); const float localY = (worldY - static_cast(tileY * map.tileHeight)) * static_cast(std::max(1, tileset->tileHeight)) / static_cast(map.tileHeight); for (const TmxCollisionShape& shape : shapes->second) { if (PointInCollisionShape(localX, localY, shape)) { return true; } } } return false; } bool IsTmxCollisionLayer(const std::string& layerName) { return LowerCopy(layerName).find("collision") != std::string::npos; } bool IsTmxOverlayLayer(const std::string& layerName) { const std::string lower = LowerCopy(layerName); return lower.find("fringe") != std::string::npos || lower.find("over") != std::string::npos; } std::uint32_t AnimatedLocalTileId(const TmxTileset& tileset, std::uint32_t localTileId, int elapsedMs) { const auto it = tileset.animations.find(localTileId); if (it == tileset.animations.end() || it->second.empty()) { return localTileId; } int totalDuration = 0; for (const TmxAnimationFrame& frame : it->second) { totalDuration += std::max(0, frame.durationMs); } if (totalDuration <= 0) { return localTileId; } int cursor = std::max(0, elapsedMs) % totalDuration; for (const TmxAnimationFrame& frame : it->second) { const int duration = std::max(0, frame.durationMs); if (cursor < duration) { return frame.tileId; } cursor -= duration; } return it->second.back().tileId; } TileBounds VisibleTileBounds( const TmxMap& map, float cameraTargetX, float cameraTargetY, float screenWidth, float screenHeight, float zoom, int paddingTiles) { const float safeZoom = std::max(0.1f, zoom); const float halfWorldWidth = screenWidth / safeZoom * 0.5f; const float halfWorldHeight = screenHeight / safeZoom * 0.5f; const float minWorldX = cameraTargetX - halfWorldWidth; const float maxWorldX = cameraTargetX + halfWorldWidth; const float minWorldY = cameraTargetY - halfWorldHeight; const float maxWorldY = cameraTargetY + halfWorldHeight; TileBounds bounds; bounds.minX = std::clamp(static_cast(std::floor(minWorldX / static_cast(map.tileWidth))) - paddingTiles, 0, std::max(0, map.width - 1)); bounds.maxX = std::clamp(static_cast(std::ceil(maxWorldX / static_cast(map.tileWidth))) + paddingTiles, 0, std::max(0, map.width - 1)); bounds.minY = std::clamp(static_cast(std::floor(minWorldY / static_cast(map.tileHeight))) - paddingTiles, 0, std::max(0, map.height - 1)); bounds.maxY = std::clamp(static_cast(std::ceil(maxWorldY / static_cast(map.tileHeight))) + paddingTiles, 0, std::max(0, map.height - 1)); return bounds; }