Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarDungeonTMXPart.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarDungeonTMXPart.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarDungeonTMXPart.cpp')
-rw-r--r--source/game/StarDungeonTMXPart.cpp466
1 files changed, 466 insertions, 0 deletions
diff --git a/source/game/StarDungeonTMXPart.cpp b/source/game/StarDungeonTMXPart.cpp
new file mode 100644
index 0000000..9058a38
--- /dev/null
+++ b/source/game/StarDungeonTMXPart.cpp
@@ -0,0 +1,466 @@
+#include "StarCompression.hpp"
+#include "StarEncode.hpp"
+#include "StarRoot.hpp"
+#include "StarGameTypes.hpp"
+#include "StarAssets.hpp"
+#include "StarCasting.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarRect.hpp"
+#include "StarObjectDatabase.hpp"
+#include "StarDungeonTMXPart.hpp"
+#include "StarJsonExtra.hpp"
+
+namespace Star {
+
+namespace Dungeon {
+
+ bool TMXMap::forEachTile(TileCallback const& callback) const {
+ for (auto const& layer : tileLayers()) {
+ if (layer->forEachTile(this, callback))
+ return true;
+ }
+
+ for (auto const& group : objectGroups()) {
+ if (group->forEachTile(this, callback))
+ return true;
+ }
+
+ return false;
+ }
+
+ bool TMXTileLayer::forEachTile(TMXMap const* map, TileCallback const& callback) const {
+ auto const& tilesets = map->tilesets();
+ unsigned height = map->height();
+
+ for (int y = m_rect.yMin(); y <= m_rect.yMax(); ++y) {
+ for (int x = m_rect.xMin(); x <= m_rect.xMax(); ++x) {
+ if (callback(Vec2I(x, height - 1 - y), getTile(tilesets, Vec2I(x, y))))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ String TMXObjectGroup::name() const {
+ return m_name;
+ }
+
+ bool TMXObjectGroup::forEachTile(TMXMap const* map, TileCallback const& callback) const {
+ for (auto const& object : objects()) {
+ if (object->forEachTile(map, callback))
+ return true;
+ }
+
+ return false;
+ }
+
+ bool TMXObject::forEachTile(TMXMap const* map, TileCallback const& callback) const {
+ if (m_kind == ObjectKind::Stagehand) {
+ auto cPos = Vec2I(rect().center()[0], ceilf(RectF(rect()).center()[1]));
+ return callback(Vec2I(cPos[0], map->height() - cPos[1]), tile());
+ }
+
+ if (m_kind == ObjectKind::Tile) {
+ // Used for placing Starbound-Tiles and Starbound-Objects
+ Vec2I position(pos().x(), map->height() - pos().y());
+ return callback(position, tile());
+ }
+
+ if (m_kind == ObjectKind::Rectangle) {
+ // Used for creating custom brushes and rules
+ for (int x = m_rect.min().x(); x < m_rect.max().x(); ++x) {
+ for (int y = m_rect.min().y(); y < m_rect.max().y(); ++y) {
+ Vec2I position(x, map->height() - 1 - y);
+ if (callback(position, tile()))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ starAssert(m_kind == ObjectKind::Polyline);
+ // Used for wiring. Treat each vertex in the polyline as a tile with the
+ // wire brush.
+ for (Vec2I point : m_polyline) {
+ Vec2I position(m_rect.min().x() + point.x(), map->height() - 1 - m_rect.min().y() - point.y());
+ if (callback(position, tile()))
+ return true;
+ }
+ return false;
+ }
+
+ bool TMXMap::forEachTileAt(Vec2I pos, TileCallback const& callback) const {
+ for (auto const& layer : tileLayers()) {
+ if (layer->forEachTileAt(pos, this, callback))
+ return true;
+ }
+
+ for (auto const& group : objectGroups()) {
+ if (group->forEachTileAt(pos, this, callback))
+ return true;
+ }
+
+ return false;
+ }
+
+ bool TMXTileLayer::forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const {
+ Vec2I tilePos(pos.x(), map->height() - 1 - pos.y());
+ if (!m_rect.contains(tilePos))
+ return false;
+
+ auto const& tile = getTile(map->tilesets(), tilePos);
+ if (callback(pos, tile))
+ return true;
+
+ return false;
+ }
+
+ bool TMXObjectGroup::forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const {
+ for (auto const& object : objects()) {
+ if (object->forEachTileAt(pos, map, callback))
+ return true;
+ }
+
+ return false;
+ }
+
+ bool TMXObject::forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const {
+ if (m_kind == ObjectKind::Stagehand) {
+ Vec2I cPos = Vec2I(rect().center()[0], ceilf(RectF(rect()).center()[1]));
+ if (pos == cPos)
+ return callback(Vec2I(pos[0], map->height() - 1 - pos[1]), tile());
+ return false;
+ }
+
+ if (m_kind == ObjectKind::Tile) {
+ Vec2I vertexPos(pos.x(), map->height() - pos.y());
+ if (vertexPos != m_rect.min())
+ return false;
+ return callback(pos, tile());
+ }
+
+ if (m_kind == ObjectKind::Rectangle) {
+ if (!m_rect.contains(Vec2I(pos.x(), map->height() - 1 - pos.y())))
+ return false;
+ return callback(pos, tile());
+ }
+
+ starAssert(m_kind == ObjectKind::Polyline);
+ for (Vec2I point : m_polyline) {
+ Vec2I pointPos(m_rect.min().x() + point.x(), map->height() - 1 - m_rect.min().y() - point.y());
+ if (pos == pointPos && callback(pos, tile()))
+ return true;
+ }
+ return false;
+ }
+
+ TMXTileLayer::TMXTileLayer(Json const& layer) {
+ unsigned width = layer.getInt("width"), height = layer.getInt("height");
+ int x = layer.getInt("x", 0), y = layer.getInt("y", 0);
+ m_rect = RectI({x, y}, {x + (int)width - 1, y + (int)height - 1});
+
+ m_name = layer.getString("name");
+ m_layer = Tiled::LayerNames.getLeft(m_name);
+
+ if (layer.optString("compression") == String("zlib")) {
+ ByteArray compressedData = base64Decode(layer.getString("data"));
+ ByteArray bytes = uncompressData(compressedData);
+ for (size_t i = 0; i + 3 < bytes.size(); i += 4) {
+ uint32_t gid = (uint8_t)bytes[i] | ((uint8_t)bytes[i + 1] << 8) | ((uint8_t)bytes[i + 2] << 16) | ((uint8_t)bytes[i + 3] << 24);
+ m_tileData.append(gid & ~TileFlip::AllBits);
+ }
+ } else if (!layer.contains("compression")) {
+ for (Json const& index : layer.getArray("data")) {
+ // Ignore flipped tiles. Tiled can flip selected regions with X, but
+ // this
+ // also flips individual tiles (setting the high bits on the GID).
+ // Starbound has no support for flipped tiles, but being able to flip
+ // regions is still useful.
+ m_tileData.append(index.toUInt() & ~TileFlip::AllBits);
+ }
+ } else {
+ throw StarException::format("TMXTileLayer does not support compression mode %s", layer.getString("compression"));
+ }
+
+ if (m_tileData.count() != width * height)
+ throw StarException("TMXTileLayer data length was inconsistent with width/height");
+ }
+
+ TMXMap::TMXMap(Json const& tmx) {
+ if (tmx.getUInt("tileheight") != 8 || tmx.getUInt("tilewidth") != 8)
+ throw StarException("Invalid tile size");
+
+ m_width = tmx.getUInt("width");
+ m_height = tmx.getUInt("height");
+
+ m_tilesets = make_shared<TMXTilesets>(tmx.getArray("tilesets"));
+
+ for (Json const& tmxLayer : tmx.get("layers").iterateArray()) {
+ String layerType = tmxLayer.getString("type");
+
+ if (layerType == "tilelayer") {
+ TMXTileLayerPtr layer = make_shared<TMXTileLayer>(tmxLayer);
+ m_tileLayers.append(layer);
+
+ } else if (layerType == "objectgroup") {
+ TMXObjectGroupPtr group = make_shared<TMXObjectGroup>(tmxLayer, m_tilesets);
+ m_objectGroups.append(group);
+
+ } else {
+ throw StarException(strf("Unknown layer type '%s'", layerType.utf8Ptr()));
+ }
+ }
+ }
+
+ Tiled::Tile const& TMXTileLayer::getTile(TMXTilesetsPtr const& tilesets, Vec2I pos) const {
+ if (!m_rect.contains(pos))
+ return tilesets->nullTile();
+
+ int dx = pos.x() - m_rect.xMin();
+ int dy = pos.y() - m_rect.yMin();
+ unsigned tileIndex = dx + dy * width();
+
+ return tilesets->getTile(m_tileData[tileIndex], m_layer);
+ }
+
+ String tilesetAssetPath(String const& relativePath) {
+ // Tiled stores tileset paths relative to the map file, which can go below
+ // the assets root if it's referencing a tileset in another asset package.
+ // The solution chosen here is to ignore everything in the path up until a
+ // known path segment, e.g.:
+ // "source" : "..\/..\/..\/..\/packed\/tilesets\/packed\/materials.json"
+ // We ignore everything up until the 'tilesets' path segment, and the asset
+ // we actually load is located at:
+ // /tilesets/packed/materials.json
+
+ size_t i = relativePath.findLast("/tilesets/", String::CaseInsensitive);
+ if (i == NPos)
+ // Couldn't extract the right path, try the one we were given in the Json.
+ return relativePath;
+ return relativePath.slice(i);
+ }
+
+ TMXTilesets::TMXTilesets(Json const& tmx) {
+ for (Json const& tilesetJson : tmx.iterateArray()) {
+ if (!tilesetJson.contains("source"))
+ throw StarException::format("Tiled map has embedded tileset %s", tilesetJson.optString("name"));
+
+ String sourcePath = tilesetAssetPath(tilesetJson.getString("source"));
+ Tiled::TilesetConstPtr tileset = Root::singleton().tilesetDatabase()->get(sourcePath);
+ m_tilesets.append(tileset);
+
+ size_t firstGid = tilesetJson.getUInt("firstgid");
+ for (size_t i = 0; i < tileset->size(); ++i) {
+ m_foregroundTilesByGid.set(firstGid + i, tileset->getTile(i, TileLayer::Foreground).get());
+ m_backgroundTilesByGid.set(firstGid + i, tileset->getTile(i, TileLayer::Background).get());
+ }
+ }
+
+ m_nullTile = make_shared<Tiled::Tile>(Tiled::Properties(), TileLayer::Foreground);
+
+ JsonObject emptyBackProperties;
+ emptyBackProperties["clear"] = "true";
+ m_emptyBackTile = make_shared<Tiled::Tile>(Tiled::Properties(emptyBackProperties), TileLayer::Background);
+ }
+
+ Tiled::Tile const& TMXTilesets::getTile(unsigned gid, TileLayer layer) const {
+ Tiled::Tile const* tilePtr;
+ if (layer == TileLayer::Foreground)
+ tilePtr = m_foregroundTilesByGid[gid];
+ else
+ tilePtr = m_backgroundTilesByGid[gid];
+
+ if (tilePtr)
+ return *tilePtr;
+ if (layer == TileLayer::Foreground)
+ return *m_nullTile;
+ return *m_emptyBackTile;
+ }
+
+ bool TMXTilesets::tilesetComparator(TilesetInfo const& a, TilesetInfo const& b) {
+ return a.firstGid > b.firstGid;
+ }
+
+ TMXObject::TMXObject(Maybe<Json> const& groupProperties, Json const& tmx, TMXTilesetsPtr tilesets) {
+ m_objectId = tmx.getUInt("id");
+
+ // convert object properties in array format to object format
+ Maybe<Json> objectProperties = tmx.opt("properties").apply([](Json const& properties) -> Json {
+ if (properties.type() == Json::Type::Array) {
+ JsonObject objectProperties;
+ for (auto& p : properties.toArray())
+ objectProperties.set(p.getString("name"), p.get("value"));
+ return objectProperties;
+ } else {
+ return properties.toObject();
+ }
+ });
+
+ m_layer = getLayer(groupProperties, objectProperties);
+
+ Maybe<TileObjectInfo> tileObjectInfo = getTileObjectInfo(tmx, tilesets, m_layer);
+
+ // Merge properties in this order:
+ // Object
+ // Tile (and tileset by proxy)
+ // ObjectGroup
+ Tiled::Properties properties;
+ if (objectProperties)
+ properties = properties.inherit(*objectProperties);
+ if (tileObjectInfo.isValid())
+ properties = properties.inherit(tileObjectInfo->tileProperties);
+ if (groupProperties.isValid())
+ properties = properties.inherit(*groupProperties);
+
+ // Check whether the object was flipped horizontally before creating this
+ // object's Tile
+ bool flipX = tileObjectInfo.isValid() && (tileObjectInfo->flipBits & TileFlip::Horizontal) != 0;
+
+ m_kind = getObjectKind(tmx, objectProperties);
+
+ Vec2I pos = getPos(tmx) - getImagePosition(properties);
+ Vec2I size = getSize(tmx);
+ m_rect = RectI(pos, Vec2I(pos.x() + size.x(), pos.y() + size.y()));
+
+ JsonObject computedProperties;
+ if (m_kind == ObjectKind::Stagehand) {
+ Vec2I cPos = rect().center();
+ RectI broadcastArea(rect().min() - cPos, rect().max() - cPos);
+ computedProperties["broadcastArea"] = jsonFromRectI(broadcastArea).repr();
+ }
+
+ Maybe<float> rotation = tmx.optFloat("rotation");
+ if (rotation.isValid() && *rotation != 0.0f)
+ throw tmxObjectError(tmx, "object is rotated, which is not supported");
+
+ Maybe<JsonArray> polyline = tmx.optArray("polyline");
+ if (polyline.isValid()) {
+ for (Json const& point : *polyline)
+ m_polyline.append(getPos(point));
+ computedProperties["wire"] = "_polylineWire" + toString(m_objectId);
+ computedProperties["local"] = "true";
+ }
+
+ properties = properties.inherit(computedProperties);
+ m_tile = make_shared<Tiled::Tile>(properties, m_layer, flipX);
+ }
+
+ Vec2I TMXObject::getSize(Json const& tmx) {
+ Vec2I size;
+ if (tmx.contains("width") && tmx.contains("height"))
+ size = Vec2I(tmx.getUInt("width"), tmx.getUInt("height")) / TilePixels;
+ return size;
+ }
+
+ Vec2I TMXObject::getImagePosition(Tiled::Properties const& properties) {
+ int x = (int)(properties.opt<float>("imagePositionX").value(0) / TilePixels);
+ int y = (int)(properties.opt<float>("imagePositionY").value(0) / TilePixels);
+ return Vec2I(x, -y);
+ }
+
+ ObjectKind TMXObject::getObjectKind(Json const& tmx, Maybe<Json> const& objectProperties) {
+ if (objectProperties && objectProperties->contains("stagehand"))
+ return ObjectKind::Stagehand;
+ else if (tmx.contains("gid"))
+ // Tile / object
+ return ObjectKind::Tile;
+ else if (tmx.contains("ellipse"))
+ throw tmxObjectError(tmx, "object has unsupported ellipse shape");
+ else if (tmx.contains("polygon"))
+ throw tmxObjectError(tmx, "object has unsupported polygon shape");
+ else if (tmx.contains("polyline"))
+ // Wiring
+ return ObjectKind::Polyline;
+ else
+ // Custom brush
+ return ObjectKind::Rectangle;
+ }
+
+ Maybe<TMXObject::TileObjectInfo> TMXObject::getTileObjectInfo(
+ Json const& tmx, TMXTilesetsPtr tilesets, TileLayer layer) {
+ Maybe<unsigned> optGid = tmx.optUInt("gid");
+ if (optGid.isNothing())
+ return {};
+
+ unsigned flipBits = *optGid & TileFlip::AllBits;
+ unsigned gid = *optGid & ~TileFlip::AllBits;
+
+ if (flipBits & (TileFlip::Vertical | TileFlip::Diagonal))
+ throw tmxObjectError(tmx, "object contains vertical or diagonal flips, which are not supported");
+
+ Tiled::Tile const& gidTile = tilesets->getTile(gid, layer);
+ return TileObjectInfo{gidTile.properties, flipBits};
+ }
+
+ TileLayer TMXObject::getLayer(Maybe<Json> const& groupProperties, Maybe<Json> const& objectProperties) {
+ if (objectProperties.isValid() && objectProperties->contains("layer"))
+ return Tiled::LayerNames.getLeft(objectProperties->getString("layer"));
+ if (groupProperties.isValid() && groupProperties->contains("layer"))
+ return Tiled::LayerNames.getLeft(groupProperties->getString("layer"));
+ return TileLayer::Foreground;
+ }
+
+ Vec2I TMXObject::getPos(Json const& tmx) {
+ return Vec2I(tmx.getInt("x"), tmx.getInt("y")) / TilePixels;
+ }
+
+ StarException TMXObject::tmxObjectError(Json const& tmx, String const& msg) {
+ Vec2I pos = getPos(tmx);
+ return StarException::format("At %d,%d: %s", pos[0], pos[1], msg);
+ }
+
+ TMXObjectGroup::TMXObjectGroup(Json const& tmx, TMXTilesetsPtr tilesets) {
+ m_name = tmx.getString("name");
+
+ // convert group properties in array format to object format
+ Maybe<JsonObject> groupProperties = tmx.opt("properties").apply([](Json const& properties) {
+ if (properties.type() == Json::Type::Array) {
+ JsonObject objectProperties;
+ for (auto& p : properties.toArray())
+ objectProperties.set(p.getString("name"), p.get("value"));
+ return objectProperties;
+ } else {
+ return properties.toObject();
+ }
+ });
+ for (Json const& tmxObject : tmx.getArray("objects")) {
+ TMXObjectPtr object = make_shared<TMXObject>(groupProperties, tmxObject, tilesets);
+ m_objects.append(object);
+ }
+ }
+
+ void TMXPartReader::readAsset(String const& asset) {
+ auto assets = Root::singleton().assets();
+ m_maps.append(make_pair(asset, make_shared<const TMXMap>(assets->json(asset))));
+ }
+
+ Vec2U TMXPartReader::size() const {
+ Vec2U size;
+ forEachMap([&size](TMXMapConstPtr const& map) {
+ size = Vec2U{map->width(), map->height()};
+ return true;
+ });
+ return size;
+ }
+
+ void TMXPartReader::forEachTile(TileCallback const& callback) const {
+ forEachMap([callback](TMXMapConstPtr const& map) {
+ return map->forEachTile(callback);
+ });
+ }
+
+ void TMXPartReader::forEachTileAt(Vec2I pos, TileCallback const& callback) const {
+ forEachMap([pos, callback](TMXMapConstPtr const& map) {
+ return map->forEachTileAt(pos, callback);
+ });
+ }
+
+ void TMXPartReader::forEachMap(function<bool(TMXMapConstPtr const&)> func) const {
+ for (auto const& map : m_maps) {
+ func(map.second);
+ }
+ }
+}
+
+}