osb/source/game/StarDungeonTMXPart.cpp
2023-06-20 14:33:09 +10:00

467 lines
16 KiB
C++

#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);
}
}
}
}