28uint32_t asUInt32(
const Json &
v, uint32_t fallback) {
29 if (!
v)
return fallback;
30 if (
v.isInt64())
return uint32_t(uint64_t(
v.asInt64(0)) & 0xffffffffu);
31 const double d =
v.asDouble(
static_cast<double>(fallback));
32 if (!std::isfinite(
d) || d < 0.0 || d > 4294967295.0)
return fallback;
36bool readVec2(
const Json &o,
const char *
key,
float &
a,
float &
b) {
37 if (!o || !o.has(
key))
return false;
38 const Json arr = o.get(
key);
39 if (!arr.isArray() || arr.size() < 2)
return false;
40 a = arr.at(0).asFloat(
a);
41 b = arr.at(1).asFloat(
b);
45bool readVec4(
const Json &o,
const char *
key,
float &
a,
float &
b,
float &
c,
float &
d) {
46 if (!o || !o.has(
key))
return false;
47 const Json arr = o.get(
key);
48 if (!arr.isArray() || arr.size() < 4)
return false;
49 a = arr.at(0).asFloat(
a);
50 b = arr.at(1).asFloat(
b);
51 c = arr.at(2).asFloat(
c);
52 d = arr.at(3).asFloat(
d);
56int64_t fileModtime(
const std::string &
path) {
57 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
58 if (!fs) fs = eve::filesystem::Filesystem::create();
60 if (!fs->getInfo(
path,
info))
return -1;
64graphics::Texture *tryLoadTexture(
const std::string &
path) {
65 if (
path.empty())
return nullptr;
66 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>(
"Graphics");
67 if (!gfx)
return nullptr;
69 graphics::Texture *tex = gfx->newTextureFromFile(
path);
70 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
71 hot->bind(
path,
"texture");
87 std::vector<TileLayer::Tileset::Visual>
visuals;
93TileLayer::Tileset::Visual readTileVisual(
const Json &o,
int fallbackGid) {
94 TileLayer::Tileset::Visual visual;
95 if (!o || !o.isObject())
return visual;
96 visual.gid = o.has(
"gid") ? o.getInt(
"gid", fallbackGid) : fallbackGid;
97 float x = 0.f,
y = 0.f,
w = 0.f,
h = 0.f;
98 if (readVec4(o,
"region",
x,
y,
w,
h)) {
101 visual.width = int(
w);
102 visual.height = int(
h);
104 readVec2(o,
"pivot", visual.pivotX, visual.pivotY);
105 visual.sortBias = o.has(
"sortBias") ? o.getFloat(
"sortBias", 0.f) : 0.f;
106 float fw = 1.f, fh = 1.f;
107 if (readVec2(o,
"footprint", fw, fh)) {
108 visual.footprintW = std::max(1,
int(fw));
109 visual.footprintH = std::max(1,
int(fh));
111 visual.walkable = o.has(
"walkable") ? o.getBool(
"walkable",
true) :
true;
112 visual.cost = o.has(
"cost") ? std::max(0.001f, o.getFloat(
"cost", 1.f)) : 1.f;
113 if (o.has(
"properties")) {
117 if (!property.isObject() || !
property.has(
"name") || !
property.has(
"value"))
continue;
118 const std::string
name =
property.getString(
"name");
119 if (
name ==
"walkable")
120 visual.walkable =
property.get(
"value").asBool(
true);
121 else if (
name ==
"cost")
122 visual.cost = std::max(0.001f, property.get(
"value").asFloat(1.f));
123 else if (
name ==
"enterMask")
124 visual.enterMask = uint8_t(property.get(
"value").asInt(0xff) & 0xff);
125 else if (
name ==
"exitMask")
126 visual.exitMask = uint8_t(property.get(
"value").asInt(0xff) & 0xff);
127 else if (
name ==
"opaque")
128 visual.opaque =
property.get(
"value").asBool(
false);
129 else if (
name ==
"semanticFlags")
130 visual.semanticFlags = uint32_t(property.get(
"value").asInt(0));
133 if (o.has(
"objectgroup")) {
134 const Json
group = o.get(
"objectgroup");
135 const Json objects =
group.isObject() ?
group.get(
"objects") : Json{};
136 for (
size_t index = 0; objects.isArray() &&
index < objects.size(); ++
index) {
137 const Json
object = objects.at(
index);
138 if (!
object.isObject())
continue;
139 float ox =
object.has(
"x") ?
object.getFloat(
"x", 0.f) : 0.f;
140 float oy =
object.has(
"y") ?
object.getFloat(
"y", 0.f) : 0.f;
141 float width =
object.has(
"width") ?
object.getFloat(
"width", 0.f) : 0.f;
142 float height =
object.has(
"height") ?
object.getFloat(
"height", 0.f) : 0.f;
143 if (
object.has(
"polygon")) {
144 const Json polygon =
object.get(
"polygon");
145 float minX = 0.f, minY = 0.f, maxX = 0.f, maxY = 0.f;
146 for (
size_t pointIndex = 0; polygon.isArray() && pointIndex < polygon.size(); ++pointIndex) {
147 const Json
point = polygon.at(pointIndex);
148 if (!
point.isObject())
continue;
149 const float px =
point.getFloat(
"x", 0.f);
150 const float py =
point.getFloat(
"y", 0.f);
151 minX = std::min(minX,
px);
152 minY = std::min(minY,
py);
153 maxX = std::max(maxX,
px);
154 maxY = std::max(maxY,
py);
163 if (!visual.collisionShapes.empty()) visual.walkable =
false;
168TilesetInfo readTilesetObject(
const Json &o) {
170 if (!o || !o.isObject())
return info;
172 info.image = o.getString(
"image");
173 else if (o.has(
"texture"))
174 info.image = o.getString(
"texture");
175 if (o.has(
"firstgid"))
176 info.firstGid = o.getInt(
"firstgid", 1);
177 else if (o.has(
"firstGid"))
178 info.firstGid = o.getInt(
"firstGid", 1);
179 if (o.has(
"columns"))
info.columns = o.getInt(
"columns", 1);
180 if (o.has(
"tilewidth"))
181 info.tileW = o.getInt(
"tilewidth", 32);
182 else if (o.has(
"tileWidth"))
183 info.tileW = o.getInt(
"tileWidth", 32);
184 if (o.has(
"tileheight"))
185 info.tileH = o.getInt(
"tileheight", 32);
186 else if (o.has(
"tileHeight"))
187 info.tileH = o.getInt(
"tileHeight", 32);
188 if (o.has(
"margin"))
info.margin = o.getInt(
"margin", 0);
189 if (o.has(
"spacing"))
info.spacing = o.getInt(
"spacing", 0);
190 if (
info.columns <= 0 && o.has(
"imagewidth") &&
info.tileW > 0) {
191 const int iw = o.getInt(
"imagewidth", 0);
192 if (iw > 0)
info.columns = std::max(1, (iw -
info.margin) / (
info.tileW +
info.spacing));
194 if (o.has(
"tiles")) {
195 const Json arr = o.get(
"tiles");
196 for (
size_t i = 0; arr.isArray() && i < arr.size(); ++i) {
197 const Json tile = arr.at(i);
198 const int localId = tile.isObject() && tile.has(
"id") ? tile.getInt(
"id",
int(i)) : int(i);
199 const int gid = tile.isObject() && tile.has(
"gid") ? tile.getInt(
"gid",
info.firstGid + localId)
200 :
info.firstGid + localId;
201 auto visual = readTileVisual(tile, gid);
202 if (visual.gid > 0)
info.visuals.push_back(visual);
203 if (!tile.isObject())
continue;
204 if (tile.has(
"animation")) {
205 const Json frames = tile.get(
"animation");
206 TileLayer::Tileset::Animation animation;
208 for (
size_t frameIndex = 0; frames.isArray() && frameIndex < frames.size(); ++frameIndex) {
209 const Json frame = frames.at(frameIndex);
210 if (!frame.isObject())
continue;
211 const int frameLocal = frame.getInt(
"tileid", localId);
212 const int duration = frame.has(
"duration") ? frame.getInt(
"duration", 100) : 100;
213 animation.frames.push_back({
info.firstGid + frameLocal, std::max(1,
duration)});
215 if (!animation.frames.empty())
info.animations.push_back(std::move(animation));
217 if (tile.has(
"terrain") && tile.has(
"neighborMask")) {
218 info.terrainRules.push_back(
219 {gid, tile.getInt(
"terrain", 0), tile.getInt(
"neighborMask", 0) & 0xff});
221 if (tile.has(
"properties")) {
222 const Json
properties = tile.get(
"properties");
225 const Json
property =
properties.at(propertyIndex);
226 if (!property.isObject() || !
property.has(
"name") || !
property.has(
"value"))
continue;
227 const std::string
type =
property.has(
"type") ?
property.getString(
"type") :
"string";
228 info.customData.push_back(
229 {gid,
property.getString(
"name"),
type,
property.get(
"value").asString()});
234 if (o.has(
"wangsets")) {
235 const Json sets = o.get(
"wangsets");
236 for (
size_t setIndex = 0; sets.isArray() && setIndex < sets.size(); ++setIndex) {
237 const Json set = sets.at(setIndex);
238 if (!set.isObject() || !set.has(
"wangtiles"))
continue;
239 const Json wangTiles = set.get(
"wangtiles");
240 for (
size_t tileIndex = 0; wangTiles.isArray() && tileIndex < wangTiles.size(); ++tileIndex) {
241 const Json wangTile = wangTiles.at(tileIndex);
242 if (!wangTile.isObject() || !wangTile.has(
"tileid") || !wangTile.has(
"wangid"))
continue;
243 const Json wangId = wangTile.get(
"wangid");
244 if (!wangId.isArray() || wangId.size() != 8)
continue;
245 const int gid =
info.firstGid + wangTile.getInt(
"tileid", 0);
248 constexpr int tiledToTerrainBit[8] = {1, 2, 3, 4, 5, 6, 7, 0};
251 bool present =
false;
257 if (present)
info.terrainRules.push_back({gid, int(setIndex) * 256 +
color,
mask});
265std::string resolveAssetPath(
const std::string &ownerPath,
const std::string &referencedPath) {
266 if (ownerPath.empty() || referencedPath.empty())
return referencedPath;
267 const std::filesystem::path
reference(referencedPath);
269 return (std::filesystem::path(ownerPath).parent_path() /
reference).lexically_normal().generic_string();
275 std::unique_ptr<eve::filesystem::FileData>
bytes(fs->
read(
path));
277 text.assign(
static_cast<const char *
>(
bytes->getData()),
bytes->getSize());
283 std::ifstream
input(std::filesystem::path(
path), std::ios::binary);
284 if (!
input)
return false;
285 std::ostringstream stream;
286 stream <<
input.rdbuf();
288 return !
text.empty();
297 if (!readImportText(fs,
path,
text)) {
301 std::string decodeError;
303 if (!document.valid() || !document.root().isObject()) {
305 *
error =
"map.import.external-tileset-invalid-json: " +
path +
306 (decodeError.empty() ? std::string{} :
" (" + decodeError +
")");
313 std::string *
error) {
317 if (!readImportText(fs,
path,
text)) {
321 std::string parseError;
323 auto root = document.root();
324 if (!document.valid() || !
root ||
root.tagName() !=
"tileset") {
326 *
error =
"map.import.external-tsx-invalid: " +
path +
327 (parseError.empty() ?
" (root is not tileset)" :
" (" + parseError +
")");
330 info.columns =
root.getIntAttribute(
"columns", 1);
331 info.tileW =
root.getIntAttribute(
"tilewidth", 32);
332 info.tileH =
root.getIntAttribute(
"tileheight", 32);
333 info.margin =
root.getIntAttribute(
"margin", 0);
334 info.spacing =
root.getIntAttribute(
"spacing", 0);
335 const auto images =
root.elementsByTag(
"image");
336 if (!images.empty())
info.image = resolveAssetPath(
path, images.front().getAttribute(
"source"));
337 const auto tiles =
root.children(
"tile");
340 TileLayer::Tileset::Visual visual;
341 const int localId = tile.getIntAttribute(
"id",
int(
index));
343 for (
const auto &property : tile.elementsByTag(
"property")) {
344 const std::string
name =
property.getAttribute(
"name");
345 const std::string
type =
property.getAttribute(
"type");
346 const std::string
value =
property.getAttribute(
"value");
348 if (
name ==
"walkable")
349 visual.walkable =
value !=
"false" &&
value !=
"0";
350 else if (
name ==
"cost") {
352 visual.cost = std::max(0.001f, std::stof(
value));
355 }
else if (
name ==
"enterMask") {
356 visual.enterMask = uint8_t(property.getIntAttribute(
"value", 0xff));
357 }
else if (
name ==
"exitMask") {
358 visual.exitMask = uint8_t(property.getIntAttribute(
"value", 0xff));
359 }
else if (
name ==
"opaque") {
360 visual.opaque =
value !=
"false" &&
value !=
"0";
361 }
else if (
name ==
"semanticFlags") {
362 visual.semanticFlags = uint32_t(property.getIntAttribute(
"value", 0));
365 for (
const auto &
object : tile.elementsByTag(
"object")) {
366 const float x = float(
object.getIntAttribute(
"x", 0));
367 const float y = float(
object.getIntAttribute(
"y", 0));
368 const float width = float(
object.getIntAttribute(
"width", 0));
369 const float height = float(
object.getIntAttribute(
"height", 0));
372 if (!visual.collisionShapes.empty()) visual.walkable =
false;
373 info.visuals.push_back(visual);
374 TileLayer::Tileset::Animation animation;
375 animation.gid = visual.gid;
376 for (
const auto &frame : tile.elementsByTag(
"frame")) {
377 animation.frames.push_back({
firstGid + frame.getIntAttribute(
"tileid", localId),
378 std::max(1, frame.getIntAttribute(
"duration", 100))});
380 if (!animation.frames.empty())
info.animations.push_back(std::move(animation));
382 const auto wangSets =
root.elementsByTag(
"wangset");
383 for (
size_t setIndex = 0; setIndex < wangSets.size(); ++setIndex) {
384 const auto &set = wangSets[setIndex];
385 for (
const auto &wangTile : set.elementsByTag(
"wangtile")) {
386 std::array<int, 8>
values{};
387 std::stringstream stream(wangTile.getAttribute(
"wangid"));
390 while (
count < 8 && std::getline(stream,
token,
',')) {
398 if (
count != 8)
continue;
399 constexpr int tiledToTerrainBit[8] = {1, 2, 3, 4, 5, 6, 7, 0};
405 info.terrainRules.push_back(
406 {
firstGid + wangTile.getIntAttribute(
"tileid", 0),
int(setIndex) * 256 +
color,
mask});
413void applyTileset(TileLayer *
layer,
const TilesetInfo &
info) {
416 graphics::Texture *tex = tryLoadTexture(
info.image);
417 layer->resource()->texturePath =
info.image;
419 layer->tileset()->visuals =
info.visuals;
420 layer->tileset()->animations =
info.animations;
421 layer->tileset()->terrainRules =
info.terrainRules;
422 layer->tileset()->customData =
info.customData;
425void appendTileset(TileLayer *
layer,
const TilesetInfo &
info) {
427 auto tileset =
layer->tileset();
428 graphics::Texture *texture = tryLoadTexture(
info.image);
429 tileset->atlases.push_back({texture,
info.firstGid, std::max(1,
info.columns), std::max(1,
info.tileW),
430 std::max(1,
info.tileH), std::max(0,
info.margin), std::max(0,
info.spacing),
432 tileset->visuals.insert(tileset->visuals.end(),
info.visuals.begin(),
info.visuals.end());
433 tileset->animations.insert(tileset->animations.end(),
info.animations.begin(),
info.animations.end());
434 tileset->terrainRules.insert(tileset->terrainRules.end(),
info.terrainRules.begin(),
info.terrainRules.end());
435 tileset->customData.insert(tileset->customData.end(),
info.customData.begin(),
info.customData.end());
438void applyTilesets(TileLayer *
layer,
const std::vector<TilesetInfo> &infos) {
439 if (!
layer || infos.empty())
return;
440 applyTileset(
layer, infos.front());
444bool decodeLayerData(
const Json &layerObj,
size_t expectedCount, std::vector<uint32_t> &out, std::string *
error) {
445 if (!layerObj || !layerObj.has(
"data")) {
450 const Json data = layerObj.get(
"data");
451 if (data.isArray()) {
452 out.resize(data.size());
453 for (
size_t i = 0; i < data.size(); ++i) out[i] = asUInt32(data.at(i), 0);
454 if (expectedCount > 0 && out.size() != expectedCount) {
461 const std::string
encoding = layerObj.has(
"encoding") ? layerObj.getString(
"encoding") :
"";
462 const std::string compression = layerObj.has(
"compression") ? layerObj.getString(
"compression") :
"";
467 if (compression ==
"zstd") {
468 if (
error) *
error =
"zstd not supported; export as zlib";
472 const std::string b64 = data.asString();
473 size_t decodedLen = 0;
474 std::unique_ptr<char[]> decoded(
eve::data::decode(
"base64", b64.data(), b64.size(), decodedLen));
480 const char *
bytes = decoded.get();
481 size_t nbytes = decodedLen;
482 std::unique_ptr<char[]> inflated;
483 if (!compression.empty()) {
484 if (compression !=
"zlib" && compression !=
"gzip") {
485 if (
error) *
error =
"unsupported compression";
488 size_t rawsize = expectedCount * 4;
499 bytes = inflated.get();
503 if (expectedCount > 0 && nbytes != expectedCount * 4) {
504 if (
error) *
error =
"gid byte length mismatch";
507 const size_t count = nbytes / 4;
509 for (
size_t i = 0; i <
count; ++i) {
510 const unsigned char *
p =
reinterpret_cast<const unsigned char *
>(
bytes) + i * 4;
511 out[i] = uint32_t(
p[0]) | (uint32_t(
p[1]) << 8) | (uint32_t(
p[2]) << 16) | (uint32_t(
p[3]) << 24);
516void applyLayerDraw(TileLayer *
layer,
const Json &layerObj) {
517 if (!
layer || !layerObj)
return;
518 if (layerObj.has(
"visible"))
layer->
setVisible(layerObj.getBool(
"visible",
true));
519 if (layerObj.has(
"layer"))
521 else if (layerObj.has(
"id"))
523 float r = 1,
g = 1,
b = 1,
a = 1;
524 if (readVec4(layerObj,
"tint",
r,
g,
b,
a))
526 else if (layerObj.has(
"opacity")) {
527 a = layerObj.getFloat(
"opacity", 1.f);
531 if (layerObj.has(
"x"))
ox = layerObj.getFloat(
"x",
ox);
532 if (layerObj.has(
"offsetx"))
ox += layerObj.getFloat(
"offsetx", 0.f);
533 if (layerObj.has(
"y"))
oy = layerObj.getFloat(
"y",
oy);
534 if (layerObj.has(
"offsety"))
oy += layerObj.getFloat(
"offsety", 0.f);
535 if (layerObj.has(
"x") || layerObj.has(
"y") || layerObj.has(
"offsetx") || layerObj.has(
"offsety"))
539bool isTileLayerObject(
const Json &o) {
540 if (!o || !o.isObject())
return false;
542 const std::string
t = o.getString(
"type");
543 if (!
t.empty() &&
t !=
"tilelayer")
return false;
545 return o.has(
"data") || o.has(
"chunks");
548bool isObjectGroup(
const Json &o) {
549 if (!o || !o.isObject() || !o.has(
"type"))
return false;
550 return o.getString(
"type") ==
"objectgroup";
553bool parseOrientation(
const Json &
root, TileLayer::Config *cfg, std::string *
error) {
554 if (!
root || !cfg)
return false;
555 if (!
root.has(
"orientation"))
return true;
556 const std::string o =
root.getString(
"orientation");
557 if (o.empty() || o ==
"orthogonal")
559 else if (o ==
"isometric")
561 else if (o ==
"staggered")
563 else if (o ==
"hexagonal")
566 if (
error) *
error =
"unknown orientation: " + o;
569 if (
root.has(
"staggeraxis")) {
570 const std::string
a =
root.getString(
"staggeraxis");
573 if (
root.has(
"staggerindex")) {
574 const std::string i =
root.getString(
"staggerindex");
577 if (
root.has(
"hexsidelength")) cfg->hexSideLength =
root.getFloat(
"hexsidelength", 0.f);
581bool applyMapGlobals(TileLayer *
layer,
const Json &
root, std::string *
error) {
589 if (
root.has(
"width")) mapW =
root.getInt(
"width", mapW);
590 if (
root.has(
"height")) mapH =
root.getInt(
"height", mapH);
591 if (
root.has(
"tilewidth"))
593 else if (
root.has(
"tileWidth"))
595 if (
root.has(
"tileheight"))
597 else if (
root.has(
"tileHeight"))
605 if (readVec2(
root,
"cellGap", gapX, gapY)) {
610 if (readVec2(
root,
"renderSpacing",
spacingX, spacingY)) {
613 if (
root.has(
"cellGapX")) gapX =
root.getFloat(
"cellGapX", gapX);
614 if (
root.has(
"cellGapY")) gapY =
root.getFloat(
"cellGapY", gapY);
619 if (!parseOrientation(
root, &(*
layer->config()),
error))
return false;
622 if (readVec2(
root,
"origin",
ox,
oy))
632 if (
root.has(
"autoReload"))
layer->resource()->autoReload =
root.getBool(
"autoReload",
true);
634 float r = 1,
g = 1,
b = 1,
a = 1;
637 if (
root.has(
"tileset") && !
root.get(
"tileset").isArray()) {
638 const Json tileset =
root.get(
"tileset");
639 if (tileset.isObject() && !tileset.has(
"source")) applyTileset(
layer, readTilesetObject(tileset));
640 }
else if (
root.has(
"tilesets")) {
641 const Json arr =
root.get(
"tilesets");
642 for (
size_t i = 0; arr.isArray() && i < arr.size(); ++i) {
643 const Json o = arr.at(i);
644 if (!o.isObject() || o.has(
"source"))
continue;
645 applyTileset(
layer, readTilesetObject(o));
648 }
else if (
root.has(
"image") ||
root.has(
"texture")) {
649 applyTileset(
layer, readTilesetObject(
root));
654bool applyFlatLayerData(TileLayer *
layer,
const Json &
root, std::string *
error) {
657 const size_t need = size_t(std::max(0, mapW) * std::max(0, mapH));
662 std::vector<uint32_t> data;
663 if (!decodeLayerData(
root, need, data,
error))
return false;
664 auto &gids =
layer->tiles()->gids;
665 gids.assign(need, 0
u);
666 const size_t n = std::min(need, data.size());
667 for (
size_t i = 0; i <
n; ++i) gids[i] = data[i];
672bool applyOneLayerObject(TileLayer *
layer,
const Json &layerObj,
int mapW,
int mapH, std::string *
error) {
673 if (!
layer || !layerObj)
return false;
674 if (layerObj.has(
"chunks")) {
675 const Json chunks = layerObj.get(
"chunks");
676 if (!chunks.isArray() || chunks.size() == 0)
return false;
677 int minX = 0, minY = 0, maxX = 0, maxY = 0;
679 for (
size_t i = 0; i < chunks.size(); ++i) {
680 const Json chunk = chunks.at(i);
681 if (!chunk.isObject())
continue;
682 const int x = chunk.getInt(
"x", 0),
y = chunk.getInt(
"y", 0);
683 const int w = chunk.getInt(
"width", 0),
h = chunk.getInt(
"height", 0);
684 if (
w <= 0 ||
h <= 0)
continue;
692 minX = std::min(minX,
x);
693 minY = std::min(minY,
y);
694 maxX = std::max(maxX,
x +
w);
695 maxY = std::max(maxY,
y +
h);
698 if (
first)
return false;
699 float shiftX = 0.f, shiftY = 0.f;
703 auto &gids =
layer->tiles()->gids;
704 for (
size_t i = 0; i < chunks.size(); ++i) {
705 const Json chunk = chunks.at(i);
706 if (!chunk.isObject())
continue;
707 const int x = chunk.getInt(
"x", 0),
y = chunk.getInt(
"y", 0);
708 const int w = chunk.getInt(
"width", 0),
h = chunk.getInt(
"height", 0);
709 if (
w <= 0 ||
h <= 0)
continue;
710 std::vector<uint32_t> data;
711 if (!decodeLayerData(chunk,
size_t(
w *
h), data,
error))
return false;
715 data[size_t(
cy *
w +
cx)];
718 applyLayerDraw(
layer, layerObj);
721 int w = mapW,
h = mapH;
722 if (layerObj.has(
"width"))
w = layerObj.getInt(
"width",
w);
723 if (layerObj.has(
"height"))
h = layerObj.getInt(
"height",
h);
728 std::vector<uint32_t> data;
729 if (!decodeLayerData(layerObj, need, data,
error))
return false;
730 auto &gids =
layer->tiles()->gids;
731 gids.assign(need, 0
u);
732 const size_t n = std::min(need, data.size());
733 for (
size_t i = 0; i <
n; ++i) gids[i] = data[i];
736 applyLayerDraw(
layer, layerObj);
740void parseObjectGroup(
const Json &
group, std::vector<MapObject> &out) {
742 const Json arr =
group.get(
"objects");
743 if (!arr.isArray())
return;
744 for (
size_t i = 0; i < arr.size(); ++i) {
745 const Json o = arr.at(i);
746 if (!o.isObject())
continue;
748 if (o.has(
"name")) mo.name = o.getString(
"name");
750 mo.type = o.getString(
"type");
751 else if (o.has(
"class"))
752 mo.type = o.getString(
"class");
753 mo.x = o.has(
"x") ? o.getFloat(
"x", 0.f) : 0.f;
754 mo.y = o.has(
"y") ? o.getFloat(
"y", 0.f) : 0.f;
755 mo.width = o.has(
"width") ? o.getFloat(
"width", 0.f) : 0.f;
756 mo.height = o.has(
"height") ? o.getFloat(
"height", 0.f) : 0.f;
757 if (o.has(
"gid")) mo.gid = uint32_t(o.getInt(
"gid", 0));
758 if (o.has(
"properties")) {
762 const Json
property =
properties.at(propertyIndex);
763 if (!property.isObject() || !
property.has(
"name") || !
property.has(
"value"))
continue;
764 const std::string
name =
property.getString(
"name");
765 if (!
name.empty()) mo.properties.emplace(
name, property.get(
"value").asString());
768 out.push_back(std::move(mo));
772void abandonLayers(std::vector<TileLayer *> &layers) {
773 for (TileLayer *
layer : layers) {
774 if (!
layer)
continue;
789void flattenLayers(
const Json &
source, std::vector<LayerEntry> &out,
float parentX = 0.f,
float parentY = 0.f,
790 float parentOpacity = 1.f,
bool parentVisible =
true) {
793 if (!
object.isObject())
continue;
795 parentX + (
object.has(
"offsetx") ?
object.getFloat(
"offsetx", 0.f) : 0.f);
797 parentY + (
object.has(
"offsety") ?
object.getFloat(
"offsety", 0.f) : 0.f);
799 parentOpacity * (
object.has(
"opacity") ?
object.getFloat(
"opacity", 1.f) : 1.f);
801 parentVisible && (!
object.has(
"visible") ||
object.getBool(
"visible",
true));
802 if (
object.has(
"type") &&
object.getString(
"type") ==
"group" &&
object.has(
"layers")) {
810std::vector<TilesetInfo> readTilesets(
const Json &
root,
const std::string &mapPath,
812 std::vector<TilesetInfo> result;
815 std::vector<eve::json::Document> ownedDocs;
816 if (
root.has(
"tilesets")) {
817 const Json arr =
root.get(
"tilesets");
818 if (!arr.isArray()) {
819 if (
error) *
error =
"map.import.tilesets-invalid";
822 for (
size_t i = 0; i < arr.size(); ++i) {
823 const Json o = arr.at(i);
825 if (
error) *
error =
"map.import.tileset-entry-invalid: index=" + std::to_string(i);
828 const int firstGid = o.has(
"firstgid") ? o.getInt(
"firstgid", 1) : 1;
829 if (o.has(
"source")) {
830 const std::string
source = resolveAssetPath(mapPath, o.getString(
"source"));
835 result.push_back(std::move(
info));
838 ownedDocs.push_back(readJsonDocument(fs,
source,
error));
839 if (!ownedDocs.back().valid())
return {};
840 TilesetInfo
info = readTilesetObject(ownedDocs.back().root());
844 result.push_back(std::move(
info));
847 TilesetInfo
info = readTilesetObject(o);
848 info.image = resolveAssetPath(mapPath,
info.image);
849 result.push_back(std::move(
info));
851 }
else if (
root.has(
"tileset") && !
root.get(
"tileset").isArray()) {
852 const Json o =
root.get(
"tileset");
853 if (o.isObject() && !o.has(
"source")) {
854 TilesetInfo
info = readTilesetObject(o);
855 info.image = resolveAssetPath(mapPath,
info.image);
856 result.push_back(std::move(
info));
858 }
else if (
root.has(
"image") ||
root.has(
"texture")) {
859 TilesetInfo
info = readTilesetObject(
root);
860 info.image = resolveAssetPath(mapPath,
info.image);
861 result.push_back(std::move(
info));
863 std::sort(result.begin(), result.end(),
864 [](
const TilesetInfo &
left,
const TilesetInfo &
right) { return left.firstGid < right.firstGid; });
869 std::vector<MapObject> *objects, std::string *
error) {
870 std::vector<TileLayer *> out;
872 if (
error) *
error =
"config root must be object";
876 int mapW = 10, mapH = 10;
878 if (
root.has(
"width")) mapW =
root.getInt(
"width", mapW);
879 if (
root.has(
"height")) mapH =
root.getInt(
"height", mapH);
880 if (
root.has(
"tilewidth"))
882 else if (
root.has(
"tileWidth"))
884 if (
root.has(
"tileheight"))
886 else if (
root.has(
"tileHeight"))
889 TileLayer::Config orientCheck;
890 if (!parseOrientation(
root, &orientCheck,
error))
return out;
892 std::vector<TilesetInfo> tilesets = readTilesets(
root,
path, fs,
error);
893 if (
root.has(
"tilesets") && tilesets.empty() &&
error && !
error->empty())
return out;
894 if (objects) objects->clear();
896 auto bindResource = [&](TileLayer *
layer) {
897 auto res =
layer->resource();
899 res->dependencyPaths.
clear();
900 res->dependencyModtimes.clear();
902 res->modtime = fileModtime(
path);
906 eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
907 hot->bind(
path,
"tilemap");
910 for (
const TilesetInfo &tileset : tilesets) {
911 if (tileset.sourcePath.empty())
continue;
912 res->dependencyPaths.push_back(tileset.sourcePath);
913 res->dependencyModtimes.push_back(fileModtime(tileset.sourcePath));
914 if (fs) fs->
watch(tileset.sourcePath);
915 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
916 hot->bind(tileset.sourcePath,
"tilemap");
918 if (
root.has(
"autoReload")) res->autoReload =
root.getBool(
"autoReload",
true);
921 if (
root.has(
"layers")) {
922 const Json arr =
root.get(
"layers");
924 std::vector<LayerEntry>
entries;
927 for (
const LayerEntry &entry :
entries) {
928 const Json &lo = entry.object;
929 if (objects && isObjectGroup(lo)) {
930 parseObjectGroup(lo, *objects);
933 if (!isTileLayerObject(lo))
continue;
941 applyTilesets(
layer, tilesets);
942 if (!applyOneLayerObject(
layer, lo, mapW, mapH,
error)) {
948 if (!lo.has(
"layer") && !lo.has(
"id"))
layer->
setLayer(sort);
950 layer->draw()->visible =
layer->draw()->visible && entry.visible;
951 layer->draw()->tint.a *= entry.opacity;
954 out.push_back(
layer);
957 if (!out.empty())
return out;
966 applyTilesets(
layer, tilesets);
967 if (
root.has(
"data")) {
975 out.push_back(
layer);
992 if (
root.has(
"layers")) {
993 const Json arr =
root.get(
"layers");
994 for (
size_t i = 0; arr.isArray() && i < arr.size(); ++i) {
995 const Json lo = arr.at(i);
996 if (!isTileLayerObject(lo))
continue;
1003 if (
root.has(
"data")) {
1004 if (!applyFlatLayerData(
layer,
root, &err))
1015 if (
error) *
error = err.empty() ?
"invalid json" : err;
1018 const Json
root = doc.root();
1019 if (!
root.isObject()) {
1020 if (
error) *
error =
"config root must be object";
1028 auto rollback = [&]() {
1029 *
layer->config() = oldConfig;
1030 *
layer->tiles() = oldTiles;
1031 *
layer->tileset() = oldTileset;
1032 *
layer->draw() = oldDraw;
1033 *
layer->resource() = oldResource;
1039 if (
root.has(
"layers")) {
1040 const Json arr =
root.get(
"layers");
1041 for (
size_t i = 0; arr.isArray() && i < arr.size(); ++i) {
1042 const Json lo = arr.at(i);
1043 if (!isTileLayerObject(lo))
continue;
1049 if (
root.has(
"data")) {
1063 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
1064 if (!fs) fs = eve::filesystem::Filesystem::create();
1067 if (!readImportText(fs,
path,
text)) {
1072 std::string decodeError;
1074 if (!document.valid() || !document.root().isObject()) {
1075 if (
error) *
error = decodeError.empty() ?
"map.import.invalid-json" : decodeError;
1078 const Json
root = document.root();
1079 std::vector<TilesetInfo> tilesets = readTilesets(
root,
path, fs,
error);
1080 if (
root.has(
"tilesets") && tilesets.empty() &&
error && !
error->empty())
return false;
1088 *
layer->config() = oldConfig;
1089 *
layer->tiles() = oldTiles;
1090 *
layer->tileset() = oldTileset;
1091 *
layer->draw() = oldDraw;
1092 *
layer->resource() = oldResource;
1095 if (!tilesets.empty()) applyTilesets(
layer, tilesets);
1097 auto res =
layer->resource();
1099 res->modtime = fileModtime(
path);
1100 res->dependencyPaths.clear();
1101 res->dependencyModtimes.clear();
1102 for (
const auto &tileset : tilesets) {
1103 if (tileset.sourcePath.empty())
continue;
1104 res->dependencyPaths.push_back(tileset.sourcePath);
1105 res->dependencyModtimes.push_back(fileModtime(tileset.sourcePath));
1106 fs->
watch(tileset.sourcePath);
1107 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
1108 hot->bind(tileset.sourcePath,
"tilemap");
1111 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
1112 hot->bind(
path,
"tilemap");
1117 if (!
layer)
return false;
1118 const std::string &
path =
layer->resource()->path;
1131 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
1132 if (!fs) fs = eve::filesystem::Filesystem::create();
1133 std::unique_ptr<eve::filesystem::FileData> data;
1140 if (!data || data->getSize() == 0) {
1144 const std::string
text(
static_cast<const char *
>(data->getData()), data->getSize());
1145 std::string decodeError;
1147 if (!doc.valid() || !doc.root().isObject()) {
1148 if (
error) *
error = decodeError.empty() ?
"invalid tileset manifest" : decodeError;
1151 Json
root = doc.root();
1152 if (
root.has(
"tileset")) {
1154 if (!
root.isObject()) {
1155 if (
error) *
error =
"tileset must be an object";
1159 const TilesetInfo
info = readTilesetObject(
root);
1160 if (
info.image.empty()) {
1161 if (
error) *
error =
"tileset manifest has no image";
1166 if (
auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>(
"HotReload"))
1167 hot->bind(
path,
"tilemap");
1171std::vector<TileLayer *>
loadMapText(
const std::string &json, std::vector<MapObject> *objects,
1172 std::string *
error) {
1175 if (!doc.valid() || !doc.root().isObject()) {
1176 if (
error) *
error = err.empty() ?
"invalid json" : err;
1179 return loadMapObject(doc.root(), {},
nullptr, objects,
error);
1182std::vector<TileLayer *>
loadMapFile(
const std::string &
path, std::vector<MapObject> *objects,
1183 std::string *
error) {
1190 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
1191 if (!fs) fs = eve::filesystem::Filesystem::create();
1194 if (!readImportText(fs,
path,
text)) {
1200 if (!doc.valid() || !doc.root().isObject()) {
1201 if (
error) *
error = err.empty() ?
"invalid json" : err;
1204 return loadMapObject(doc.root(),
path, fs, objects,
error);
AuthorityStoreHandleRef reference
std::unordered_map< std::string, QuestRuntime > entries
building::EdgeCurveGroup group
std::map< std::string, Var > values
std::array< float, 3 > position
std::map< std::string, PropertyRule > properties
std::vector< TileLayer::Tileset::TerrainRule > terrainRules
std::vector< TileLayer::Tileset::CustomData > customData
std::vector< TileLayer::Tileset::Visual > visuals
std::vector< TileLayer::Tileset::Animation > animations
const UnitySourceAsset & source
std::vector< WfcTile > tiles
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
EVENGINE_API_FOUNDATION public API.
virtual FileData * read(std::string filename, int64_t size=File::ALL) const =0
Reads data from a file.
virtual bool watch(std::string path)=0
Watch a virtual or absolute OS path (file or directory). File watches monitor the parent directory an...
EVENGINE_API_FOUNDATION public API.
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
EVENGINE_API_FOUNDATION public API.
ECS tile layer entity. Script mutates tile GIDs / tileset / draw; TileRenderSystem batch-draws atlas ...
void clear()
Clears the layer to empty.
float getTileWidth()
Tile size in pixels.
void setCellGap(float gapX, float gapY)
Sets extra world-space distance between projected cells.
void rebuildSpatialIndex()
Rebuild chunk occupancy after bulk loader writes directly to gids.
void setOrigin(float x, float y)
World origin of the layer (pixels).
void setTileset(graphics::Texture *texture, int firstGid, int columns, int margin=0, int spacing=0)
Binds the atlas texture and tile table.
void resize(int mapW, int mapH)
Resizes the tile grid (existing GIDs preserved where possible).
void setRenderSpacing(float spacingX, float spacingY)
Sets projected cell pitch as a multiple of the logical tile size.
void setTilesetTileSize(int tileW, int tileH)
Tile size in pixels of the bound tileset.
void setLayer(int layer)
Draw order layer / visibility / tint.
static TileLayer * createLayer(int mapW, int mapH, float tileW=32.f, float tileH=32.f)
float getCellGapX()
Horizontal and vertical world-space gaps between projected cells.
float getRenderSpacingY()
void setTileSize(float tileW, float tileH)
void tileToWorld(int tx, int ty, float &wx, float &wy)
Tile-index ↔ world-pixel conversions (per layer orientation).
float getRenderSpacingX()
Horizontal and vertical projected pitch multipliers.
void setVisible(bool visible)
int getMapWidth()
Map dimensions in tiles.
void setTint(float r, float g, float b, float a=1.f)
static Document parse(const std::string &text, std::string *error=nullptr)
std::string extension(std::string_view name)
Extension.
char * decompress(CompressedData *data, size_t &decompressedsize)
Decompresses existing compressed data into raw bytes.
char * decode(std::string format, const char *src, size_t srclen, size_t &dstlen)
Decodes a text buffer (hex / base64) back into raw bytes.
放置世界:格子占用(多通道)+ 地形语义 + 已放置建筑实例。 行为由 PlacementSystem 提供;本类暴露便于脚本绑定的薄封装方法。 坐标换算统一走 eve::grid(支持 rectang...
eve::Result< void > applyConfigDocument(TileLayer *layer, eve::json::Value root)
Apply map/layer JSON onto an existing TileLayer (Config + Tiles + Tileset + Draw)....
std::vector< TileLayer * > loadMapText(const std::string &json, std::vector< MapObject > *objects, std::string *error)
Parse map JSON text (no filesystem). Same semantics as loadMapFile.
bool loadTilesetManifestFile(TileLayer *layer, const std::string &path, std::string *error)
Load a composable TileSet manifest onto an existing layer.
bool reloadConfigFile(TileLayer *layer, std::string *error)
Reloads config file.
bool applyConfigText(TileLayer *layer, const std::string &json, std::string *error)
Parse JSON text and apply onto one layer.
StaggerIndex
Which rows/columns are offset by half a tile.
std::vector< TileLayer * > loadMapFile(const std::string &path, std::vector< MapObject > *objects, std::string *error)
Loads map file.
StaggerAxis
Stagger axis for staggered/hex maps (shift every other row/column).
bool loadConfigFile(TileLayer *layer, const std::string &path, std::string *error)
Read path via Filesystem, apply onto layer, bind Resource.path + modtime for hot reload....
DiagnosticCode
Stable machine-readable diagnostic codes.