载入中...
搜索中...
未找到
TileConfig.cpp
浏览该文件的文档.
1#include "map/TileConfig.h"
3
4#include "common/Json.h"
5#include "common/Module.h"
6#include "common/Xml.h"
7#include "data/DataModule.h"
11#include "graphics/Graphics.h"
12
13#include <algorithm>
14#include <array>
15#include <cmath>
16#include <cstdint>
17#include <filesystem>
18#include <fstream>
19#include <memory>
20#include <sstream>
21#include <stdexcept>
22
23namespace eve::map {
24namespace {
25
26using Json = eve::json::Value;
27
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;
33 return uint32_t(d);
34}
35
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);
42 return true;
43}
44
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);
53 return true;
54}
55
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;
61 return info.modtime;
62}
63
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;
68 try {
69 graphics::Texture *tex = gfx->newTextureFromFile(path);
70 if (auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload"))
71 hot->bind(path, "texture");
72 return tex;
73 } catch (...) {
74 return nullptr;
75 }
76}
77
78struct TilesetInfo {
79 std::string image;
80 std::string sourcePath;
81 int firstGid = 1;
82 int columns = 1;
83 int tileW = 32;
84 int tileH = 32;
85 int margin = 0;
86 int spacing = 0;
87 std::vector<TileLayer::Tileset::Visual> visuals;
88 std::vector<TileLayer::Tileset::Animation> animations;
89 std::vector<TileLayer::Tileset::TerrainRule> terrainRules;
90 std::vector<TileLayer::Tileset::CustomData> customData;
91};
92
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)) {
99 visual.x = int(x);
100 visual.y = int(y);
101 visual.width = int(w);
102 visual.height = int(h);
103 }
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));
110 }
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")) {
114 const Json properties = o.get("properties");
115 for (size_t index = 0; properties.isArray() && index < properties.size(); ++index) {
116 const Json property = properties.at(index);
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));
131 }
132 }
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);
155 }
156 ox += minX;
157 oy += minY;
158 width = maxX - minX;
159 height = maxY - minY;
160 }
161 if (width > 0.f && height > 0.f) visual.collisionShapes.push_back({ox, oy, width, height});
162 }
163 if (!visual.collisionShapes.empty()) visual.walkable = false;
164 }
165 return visual;
166}
167
168TilesetInfo readTilesetObject(const Json &o) {
169 TilesetInfo info;
170 if (!o || !o.isObject()) return info;
171 if (o.has("image"))
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));
193 }
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;
207 animation.gid = gid;
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)});
214 }
215 if (!animation.frames.empty()) info.animations.push_back(std::move(animation));
216 }
217 if (tile.has("terrain") && tile.has("neighborMask")) {
218 info.terrainRules.push_back(
219 {gid, tile.getInt("terrain", 0), tile.getInt("neighborMask", 0) & 0xff});
220 }
221 if (tile.has("properties")) {
222 const Json properties = tile.get("properties");
223 for (size_t propertyIndex = 0; properties.isArray() && propertyIndex < properties.size();
224 ++propertyIndex) {
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()});
230 }
231 }
232 }
233 }
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);
246 // Tiled orders Wang positions N, NE, E, SE, S, SW, W, NW.
247 // EVEngine terrain masks order NW, N, NE, E, SE, S, SW, W.
248 constexpr int tiledToTerrainBit[8] = {1, 2, 3, 4, 5, 6, 7, 0};
249 for (int color = 1; color <= 255; ++color) {
250 int mask = 0;
251 bool present = false;
252 for (int position = 0; position < 8; ++position) {
253 if (wangId.at(size_t(position)).asInt(0) != color) continue;
254 present = true;
255 mask |= 1 << tiledToTerrainBit[position];
256 }
257 if (present) info.terrainRules.push_back({gid, int(setIndex) * 256 + color, mask});
258 }
259 }
260 }
261 }
262 return info;
263}
264
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);
268 if (reference.is_absolute()) return reference.lexically_normal().generic_string();
269 return (std::filesystem::path(ownerPath).parent_path() / reference).lexically_normal().generic_string();
270}
271
272bool readImportText(eve::filesystem::Filesystem *fs, const std::string &path, std::string &text) {
273 if (fs) {
274 try {
275 std::unique_ptr<eve::filesystem::FileData> bytes(fs->read(path));
276 if (bytes && bytes->getSize() > 0) {
277 text.assign(static_cast<const char *>(bytes->getData()), bytes->getSize());
278 return true;
279 }
280 } catch (...) {
281 }
282 }
283 std::ifstream input(std::filesystem::path(path), std::ios::binary);
284 if (!input) return false;
285 std::ostringstream stream;
286 stream << input.rdbuf();
287 text = stream.str();
288 return !text.empty();
289}
290
291eve::json::Document readJsonDocument(eve::filesystem::Filesystem *fs, const std::string &path, std::string *error) {
292 if (!fs) {
293 if (error) *error = "map.import.filesystem-unavailable: " + path;
294 return {};
295 }
296 std::string text;
297 if (!readImportText(fs, path, text)) {
298 if (error) *error = "map.import.external-tileset-empty: " + path;
299 return {};
300 }
301 std::string decodeError;
302 auto document = eve::json::Document::parse(text, &decodeError);
303 if (!document.valid() || !document.root().isObject()) {
304 if (error)
305 *error = "map.import.external-tileset-invalid-json: " + path +
306 (decodeError.empty() ? std::string{} : " (" + decodeError + ")");
307 return {};
308 }
309 return document;
310}
311
312TilesetInfo readTsxTileset(eve::filesystem::Filesystem *fs, const std::string &path, int firstGid,
313 std::string *error) {
314 TilesetInfo info;
315 info.firstGid = firstGid;
316 std::string text;
317 if (!readImportText(fs, path, text)) {
318 if (error) *error = "map.import.external-tileset-empty: " + path;
319 return info;
320 }
321 std::string parseError;
322 auto document = eve::xml::Document::parse(text, &parseError);
323 auto root = document.root();
324 if (!document.valid() || !root || root.tagName() != "tileset") {
325 if (error)
326 *error = "map.import.external-tsx-invalid: " + path +
327 (parseError.empty() ? " (root is not tileset)" : " (" + parseError + ")");
328 return {};
329 }
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");
338 for (size_t index = 0; index < tiles.size(); ++index) {
339 const auto &tile = tiles[index];
340 TileLayer::Tileset::Visual visual;
341 const int localId = tile.getIntAttribute("id", int(index));
342 visual.gid = firstGid + localId;
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");
347 info.customData.push_back({visual.gid, name, type.empty() ? "string" : type, value});
348 if (name == "walkable")
349 visual.walkable = value != "false" && value != "0";
350 else if (name == "cost") {
351 try {
352 visual.cost = std::max(0.001f, std::stof(value));
353 } catch (...) {
354 }
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));
363 }
364 }
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));
370 if (width > 0.f && height > 0.f) visual.collisionShapes.push_back({x, y, width, height});
371 }
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))});
379 }
380 if (!animation.frames.empty()) info.animations.push_back(std::move(animation));
381 }
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"));
388 std::string token;
389 int count = 0;
390 while (count < 8 && std::getline(stream, token, ',')) {
391 try {
392 values[size_t(count)] = std::stoi(token);
393 } catch (...) {
394 values[size_t(count)] = 0;
395 }
396 ++count;
397 }
398 if (count != 8) continue;
399 constexpr int tiledToTerrainBit[8] = {1, 2, 3, 4, 5, 6, 7, 0};
400 for (int color = 1; color <= 255; ++color) {
401 int mask = 0;
402 for (int position = 0; position < 8; ++position)
403 if (values[size_t(position)] == color) mask |= 1 << tiledToTerrainBit[position];
404 if (mask != 0)
405 info.terrainRules.push_back(
406 {firstGid + wangTile.getIntAttribute("tileid", 0), int(setIndex) * 256 + color, mask});
407 }
408 }
409 }
410 return info;
411}
412
413void applyTileset(TileLayer *layer, const TilesetInfo &info) {
414 if (!layer) return;
415 layer->setTilesetTileSize(info.tileW, info.tileH);
416 graphics::Texture *tex = tryLoadTexture(info.image);
417 layer->resource()->texturePath = info.image;
418 layer->setTileset(tex, info.firstGid, info.columns, info.margin, info.spacing);
419 layer->tileset()->visuals = info.visuals;
420 layer->tileset()->animations = info.animations;
421 layer->tileset()->terrainRules = info.terrainRules;
422 layer->tileset()->customData = info.customData;
423}
424
425void appendTileset(TileLayer *layer, const TilesetInfo &info) {
426 if (!layer) return;
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),
431 info.image});
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());
436}
437
438void applyTilesets(TileLayer *layer, const std::vector<TilesetInfo> &infos) {
439 if (!layer || infos.empty()) return;
440 applyTileset(layer, infos.front());
441 for (size_t index = 1; index < infos.size(); ++index) appendTileset(layer, infos[index]);
442}
443
444bool decodeLayerData(const Json &layerObj, size_t expectedCount, std::vector<uint32_t> &out, std::string *error) {
445 if (!layerObj || !layerObj.has("data")) {
446 if (error) *error = "missing data";
447 return false;
448 }
449
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) {
455 if (error) *error = "gid count mismatch";
456 return false;
457 }
458 return true;
459 }
460
461 const std::string encoding = layerObj.has("encoding") ? layerObj.getString("encoding") : "";
462 const std::string compression = layerObj.has("compression") ? layerObj.getString("compression") : "";
463 if (encoding != "base64") {
464 if (error) *error = "unsupported encoding";
465 return false;
466 }
467 if (compression == "zstd") {
468 if (error) *error = "zstd not supported; export as zlib";
469 return false;
470 }
471
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));
475 if (!decoded) {
476 if (error) *error = "base64 decode failed";
477 return false;
478 }
479
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";
486 return false;
487 }
488 size_t rawsize = expectedCount * 4;
489 try {
490 inflated.reset(eve::data::decompress(compression, bytes, nbytes, rawsize));
491 } catch (...) {
492 if (error) *error = "decompress failed";
493 return false;
494 }
495 if (!inflated) {
496 if (error) *error = "decompress failed";
497 return false;
498 }
499 bytes = inflated.get();
500 nbytes = rawsize;
501 }
502
503 if (expectedCount > 0 && nbytes != expectedCount * 4) {
504 if (error) *error = "gid byte length mismatch";
505 return false;
506 }
507 const size_t count = nbytes / 4;
508 out.resize(count);
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);
512 }
513 return true;
514}
515
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"))
520 layer->setLayer(layerObj.getInt("layer", layer->getLayer()));
521 else if (layerObj.has("id"))
522 layer->setLayer(layerObj.getInt("id", layer->getLayer()));
523 float r = 1, g = 1, b = 1, a = 1;
524 if (readVec4(layerObj, "tint", r, g, b, a))
525 layer->setTint(r, g, b, a);
526 else if (layerObj.has("opacity")) {
527 a = layerObj.getFloat("opacity", 1.f);
528 layer->setTint(1.f, 1.f, 1.f, a);
529 }
530 float ox = layer->getX(), oy = layer->getY();
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"))
536 layer->setOrigin(ox, oy);
537}
538
539bool isTileLayerObject(const Json &o) {
540 if (!o || !o.isObject()) return false;
541 if (o.has("type")) {
542 const std::string t = o.getString("type");
543 if (!t.empty() && t != "tilelayer") return false;
544 }
545 return o.has("data") || o.has("chunks");
546}
547
548bool isObjectGroup(const Json &o) {
549 if (!o || !o.isObject() || !o.has("type")) return false;
550 return o.getString("type") == "objectgroup";
551}
552
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")
558 cfg->orientation = MapOrientation::Orthogonal;
559 else if (o == "isometric")
560 cfg->orientation = MapOrientation::Isometric;
561 else if (o == "staggered")
562 cfg->orientation = MapOrientation::Staggered;
563 else if (o == "hexagonal")
564 cfg->orientation = MapOrientation::Hexagonal;
565 else {
566 if (error) *error = "unknown orientation: " + o;
567 return false;
568 }
569 if (root.has("staggeraxis")) {
570 const std::string a = root.getString("staggeraxis");
571 cfg->staggerAxis = (a == "x") ? StaggerAxis::X : StaggerAxis::Y;
572 }
573 if (root.has("staggerindex")) {
574 const std::string i = root.getString("staggerindex");
575 cfg->staggerIndex = (i == "even") ? StaggerIndex::Even : StaggerIndex::Odd;
576 }
577 if (root.has("hexsidelength")) cfg->hexSideLength = root.getFloat("hexsidelength", 0.f);
578 return true;
579}
580
581bool applyMapGlobals(TileLayer *layer, const Json &root, std::string *error) {
582 if (!layer || !root) return false;
583
584 int mapW = layer->getMapWidth();
585 int mapH = layer->getMapHeight();
586 float tileW = layer->getTileWidth();
587 float tileH = layer->getTileHeight();
588
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"))
592 tileW = root.getFloat("tilewidth", tileW);
593 else if (root.has("tileWidth"))
594 tileW = root.getFloat("tileWidth", tileW);
595 if (root.has("tileheight"))
596 tileH = root.getFloat("tileheight", tileH);
597 else if (root.has("tileHeight"))
598 tileH = root.getFloat("tileHeight", tileH);
599
600 if (mapW != layer->getMapWidth() || mapH != layer->getMapHeight()) layer->resize(mapW, mapH);
602
603 float gapX = layer->getCellGapX();
604 float gapY = layer->getCellGapY();
605 if (readVec2(root, "cellGap", gapX, gapY)) {
606 layer->setCellGap(gapX, gapY);
607 } else {
609 float spacingY = layer->getRenderSpacingY();
610 if (readVec2(root, "renderSpacing", spacingX, spacingY)) {
611 layer->setRenderSpacing(spacingX, spacingY);
612 } else {
613 if (root.has("cellGapX")) gapX = root.getFloat("cellGapX", gapX);
614 if (root.has("cellGapY")) gapY = root.getFloat("cellGapY", gapY);
615 if (root.has("cellGapX") || root.has("cellGapY")) layer->setCellGap(gapX, gapY);
616 }
617 }
618
619 if (!parseOrientation(root, &(*layer->config()), error)) return false;
620
621 float ox = layer->getX(), oy = layer->getY();
622 if (readVec2(root, "origin", ox, oy))
623 layer->setOrigin(ox, oy);
624 else {
625 if (root.has("x")) ox = root.getFloat("x", ox);
626 if (root.has("y")) oy = root.getFloat("y", oy);
627 if (root.has("x") || root.has("y")) layer->setOrigin(ox, oy);
628 }
629
630 if (root.has("layer")) layer->setLayer(root.getInt("layer", layer->getLayer()));
631 if (root.has("visible")) layer->setVisible(root.getBool("visible", layer->isVisible()));
632 if (root.has("autoReload")) layer->resource()->autoReload = root.getBool("autoReload", true);
633
634 float r = 1, g = 1, b = 1, a = 1;
635 if (readVec4(root, "tint", r, g, b, a)) layer->setTint(r, g, b, a);
636
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));
646 break;
647 }
648 } else if (root.has("image") || root.has("texture")) {
649 applyTileset(layer, readTilesetObject(root));
650 }
651 return true;
652}
653
654bool applyFlatLayerData(TileLayer *layer, const Json &root, std::string *error) {
655 const int mapW = layer->getMapWidth();
656 const int mapH = layer->getMapHeight();
657 const size_t need = size_t(std::max(0, mapW) * std::max(0, mapH));
658 if (need == 0) {
659 if (error) *error = "empty map size";
660 return false;
661 }
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, 0u);
666 const size_t n = std::min(need, data.size());
667 for (size_t i = 0; i < n; ++i) gids[i] = data[i];
669 return true;
670}
671
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;
678 bool first = true;
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;
685 if (first) {
686 minX = x;
687 minY = y;
688 maxX = x + w;
689 maxY = y + h;
690 first = false;
691 } else {
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);
696 }
697 }
698 if (first) return false;
699 float shiftX = 0.f, shiftY = 0.f;
700 layer->tileToWorld(minX, minY, shiftX, shiftY);
701 layer->resize(maxX - minX, maxY - minY);
702 layer->setOrigin(shiftX, shiftY);
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;
712 for (int cy = 0; cy < h; ++cy)
713 for (int cx = 0; cx < w; ++cx)
714 gids[size_t((y - minY + cy) * layer->getMapWidth() + x - minX + cx)] =
715 data[size_t(cy * w + cx)];
716 }
718 applyLayerDraw(layer, layerObj);
719 return true;
720 }
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);
724 if (w != layer->getMapWidth() || h != layer->getMapHeight()) layer->resize(w, h);
725
726 const size_t need =
727 size_t(std::max(0, layer->getMapWidth()) * std::max(0, layer->getMapHeight()));
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, 0u);
732 const size_t n = std::min(need, data.size());
733 for (size_t i = 0; i < n; ++i) gids[i] = data[i];
735
736 applyLayerDraw(layer, layerObj);
737 return true;
738}
739
740void parseObjectGroup(const Json &group, std::vector<MapObject> &out) {
741 if (!group || !group.has("objects")) return;
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;
747 MapObject mo;
748 if (o.has("name")) mo.name = o.getString("name");
749 if (o.has("type"))
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")) {
759 const Json properties = o.get("properties");
760 for (size_t propertyIndex = 0; properties.isArray() && propertyIndex < properties.size();
761 ++propertyIndex) {
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());
766 }
767 }
768 out.push_back(std::move(mo));
769 }
770}
771
772void abandonLayers(std::vector<TileLayer *> &layers) {
773 for (TileLayer *layer : layers) {
774 if (!layer) continue;
775 layer->clear();
776 layer->setVisible(false);
777 }
778 layers.clear();
779}
780
781struct LayerEntry {
782 Json object;
783 float offsetX = 0.f;
784 float offsetY = 0.f;
785 float opacity = 1.f;
786 bool visible = true;
787};
788
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) {
791 for (size_t index = 0; source.isArray() && index < source.size(); ++index) {
792 const Json object = source.at(index);
793 if (!object.isObject()) continue;
794 const float offsetX =
795 parentX + (object.has("offsetx") ? object.getFloat("offsetx", 0.f) : 0.f);
796 const float offsetY =
797 parentY + (object.has("offsety") ? object.getFloat("offsety", 0.f) : 0.f);
798 const float opacity =
799 parentOpacity * (object.has("opacity") ? object.getFloat("opacity", 1.f) : 1.f);
800 const bool visible =
801 parentVisible && (!object.has("visible") || object.getBool("visible", true));
802 if (object.has("type") && object.getString("type") == "group" && object.has("layers")) {
803 flattenLayers(object.get("layers"), out, offsetX, offsetY, opacity, visible);
804 continue;
805 }
806 out.push_back({object, offsetX, offsetY, opacity, visible});
807 }
808}
809
810std::vector<TilesetInfo> readTilesets(const Json &root, const std::string &mapPath,
811 eve::filesystem::Filesystem *fs, std::string *error) {
812 std::vector<TilesetInfo> result;
813 // Keep external JSON documents alive: TilesetInfo reads image paths as
814 // strings (copied), but we still need the Document while reading fields.
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";
820 return {};
821 }
822 for (size_t i = 0; i < arr.size(); ++i) {
823 const Json o = arr.at(i);
824 if (!o.isObject()) {
825 if (error) *error = "map.import.tileset-entry-invalid: index=" + std::to_string(i);
826 return {};
827 }
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"));
831 if (std::filesystem::path(source).extension() == ".tsx") {
832 TilesetInfo info = readTsxTileset(fs, source, firstGid, error);
833 if (error && !error->empty()) return {};
834 info.sourcePath = source;
835 result.push_back(std::move(info));
836 continue;
837 }
838 ownedDocs.push_back(readJsonDocument(fs, source, error));
839 if (!ownedDocs.back().valid()) return {};
840 TilesetInfo info = readTilesetObject(ownedDocs.back().root());
841 info.firstGid = firstGid;
842 info.sourcePath = source;
843 info.image = resolveAssetPath(source, info.image);
844 result.push_back(std::move(info));
845 continue;
846 }
847 TilesetInfo info = readTilesetObject(o);
848 info.image = resolveAssetPath(mapPath, info.image);
849 result.push_back(std::move(info));
850 }
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));
857 }
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));
862 }
863 std::sort(result.begin(), result.end(),
864 [](const TilesetInfo &left, const TilesetInfo &right) { return left.firstGid < right.firstGid; });
865 return result;
866}
867
868std::vector<TileLayer *> loadMapObject(const Json &root, const std::string &path, eve::filesystem::Filesystem *fs,
869 std::vector<MapObject> *objects, std::string *error) {
870 std::vector<TileLayer *> out;
871 if (!root || !root.isObject()) {
872 if (error) *error = "config root must be object";
873 return out;
874 }
875
876 int mapW = 10, mapH = 10;
877 float tileW = 32.f, tileH = 32.f;
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"))
881 tileW = root.getFloat("tilewidth", tileW);
882 else if (root.has("tileWidth"))
883 tileW = root.getFloat("tileWidth", tileW);
884 if (root.has("tileheight"))
885 tileH = root.getFloat("tileheight", tileH);
886 else if (root.has("tileHeight"))
887 tileH = root.getFloat("tileHeight", tileH);
888
889 TileLayer::Config orientCheck;
890 if (!parseOrientation(root, &orientCheck, error)) return out;
891
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();
895
896 auto bindResource = [&](TileLayer *layer) {
897 auto res = layer->resource();
898 res->path = path;
899 res->dependencyPaths.clear();
900 res->dependencyModtimes.clear();
901 if (!path.empty()) {
902 res->modtime = fileModtime(path);
903 if (fs) {
904 fs->watch(path);
905 if (auto *hot =
906 eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload"))
907 hot->bind(path, "tilemap");
908 }
909 }
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");
917 }
918 if (root.has("autoReload")) res->autoReload = root.getBool("autoReload", true);
919 };
920
921 if (root.has("layers")) {
922 const Json arr = root.get("layers");
923 if (arr.isArray()) {
924 std::vector<LayerEntry> entries;
925 flattenLayers(arr, entries);
926 int sort = 0;
927 for (const LayerEntry &entry : entries) {
928 const Json &lo = entry.object;
929 if (objects && isObjectGroup(lo)) {
930 parseObjectGroup(lo, *objects);
931 continue;
932 }
933 if (!isTileLayerObject(lo)) continue;
934 TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH);
935 if (!applyMapGlobals(layer, root, error)) {
936 abandonLayers(out);
937 layer->clear();
938 layer->setVisible(false);
939 return {};
940 }
941 applyTilesets(layer, tilesets);
942 if (!applyOneLayerObject(layer, lo, mapW, mapH, error)) {
943 abandonLayers(out);
944 layer->clear();
945 layer->setVisible(false);
946 return {};
947 }
948 if (!lo.has("layer") && !lo.has("id")) layer->setLayer(sort);
949 layer->setOrigin(layer->getX() + entry.offsetX, layer->getY() + entry.offsetY);
950 layer->draw()->visible = layer->draw()->visible && entry.visible;
951 layer->draw()->tint.a *= entry.opacity;
952 ++sort;
953 bindResource(layer);
954 out.push_back(layer);
955 }
956 }
957 if (!out.empty()) return out;
958 }
959
960 TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH);
961 if (!applyMapGlobals(layer, root, error)) {
962 layer->clear();
963 layer->setVisible(false);
964 return {};
965 }
966 applyTilesets(layer, tilesets);
967 if (root.has("data")) {
968 if (!applyFlatLayerData(layer, root, error)) {
969 layer->clear();
970 layer->setVisible(false);
971 return {};
972 }
973 }
974 bindResource(layer);
975 out.push_back(layer);
976 return out;
977}
978
979} // namespace
980
982 auto fail = [](eve::DiagnosticCode code, const std::string& message) {
984 };
985 if (!layer || !root || !root.isObject())
986 return fail(eve::DiagnosticCode::InvalidArgument, "config root must be an object");
987
988 std::string err;
989 if (!applyMapGlobals(layer, root, &err))
990 return fail(eve::DiagnosticCode::Failed, err.empty() ? "map globals rejected" : err);
991
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;
997 if (!applyOneLayerObject(layer, lo, layer->getMapWidth(), layer->getMapHeight(), &err))
998 return fail(eve::DiagnosticCode::Failed, err.empty() ? "tile layer apply failed" : err);
1000 }
1001 }
1002
1003 if (root.has("data")) {
1004 if (!applyFlatLayerData(layer, root, &err))
1005 return fail(eve::DiagnosticCode::Failed, err.empty() ? "layer data apply failed" : err);
1007 }
1009}
1010
1011bool applyConfigText(TileLayer *layer, const std::string &json, std::string *error) {
1012 std::string err;
1013 auto doc = eve::json::Document::parse(json, &err);
1014 if (!doc.valid()) {
1015 if (error) *error = err.empty() ? "invalid json" : err;
1016 return false;
1017 }
1018 const Json root = doc.root();
1019 if (!root.isObject()) {
1020 if (error) *error = "config root must be object";
1021 return false;
1022 }
1023 const TileLayer::Config oldConfig = *layer->config();
1024 const TileLayer::Tiles oldTiles = *layer->tiles();
1025 const TileLayer::Tileset oldTileset = *layer->tileset();
1026 const TileLayer::Draw oldDraw = *layer->draw();
1027 const TileLayer::Resource oldResource = *layer->resource();
1028 auto rollback = [&]() {
1029 *layer->config() = oldConfig;
1030 *layer->tiles() = oldTiles;
1031 *layer->tileset() = oldTileset;
1032 *layer->draw() = oldDraw;
1033 *layer->resource() = oldResource;
1034 };
1035 if (!applyMapGlobals(layer, root, error)) {
1036 rollback();
1037 return false;
1038 }
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;
1044 if (applyOneLayerObject(layer, lo, layer->getMapWidth(), layer->getMapHeight(), error)) return true;
1045 rollback();
1046 return false;
1047 }
1048 }
1049 if (root.has("data")) {
1050 if (applyFlatLayerData(layer, root, error)) return true;
1051 rollback();
1052 return false;
1053 }
1054 return true;
1055}
1056
1057bool loadConfigFile(TileLayer *layer, const std::string &path, std::string *error) {
1058 if (error) error->clear();
1059 if (!layer || path.empty()) {
1060 if (error) *error = "empty path";
1061 return false;
1062 }
1063 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
1064 if (!fs) fs = eve::filesystem::Filesystem::create();
1065
1066 std::string text;
1067 if (!readImportText(fs, path, text)) {
1068 if (error) *error = "empty file: " + path;
1069 return false;
1070 }
1071
1072 std::string decodeError;
1073 auto document = eve::json::Document::parse(text, &decodeError);
1074 if (!document.valid() || !document.root().isObject()) {
1075 if (error) *error = decodeError.empty() ? "map.import.invalid-json" : decodeError;
1076 return false;
1077 }
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;
1081
1082 const TileLayer::Config oldConfig = *layer->config();
1083 const TileLayer::Tiles oldTiles = *layer->tiles();
1084 const TileLayer::Tileset oldTileset = *layer->tileset();
1085 const TileLayer::Draw oldDraw = *layer->draw();
1086 const TileLayer::Resource oldResource = *layer->resource();
1087 if (!applyConfigText(layer, text, error)) {
1088 *layer->config() = oldConfig;
1089 *layer->tiles() = oldTiles;
1090 *layer->tileset() = oldTileset;
1091 *layer->draw() = oldDraw;
1092 *layer->resource() = oldResource;
1093 return false;
1094 }
1095 if (!tilesets.empty()) applyTilesets(layer, tilesets);
1096
1097 auto res = layer->resource();
1098 res->path = path;
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");
1109 }
1110 fs->watch(path);
1111 if (auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload"))
1112 hot->bind(path, "tilemap");
1113 return true;
1114}
1115
1117 if (!layer) return false;
1118 const std::string &path = layer->resource()->path;
1119 if (path.empty()) {
1120 if (error) *error = "no config path";
1121 return false;
1122 }
1123 return loadConfigFile(layer, path, error);
1124}
1125
1126bool loadTilesetManifestFile(TileLayer *layer, const std::string &path, std::string *error) {
1127 if (!layer || path.empty()) {
1128 if (error) *error = "empty path";
1129 return false;
1130 }
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;
1134 try {
1135 data.reset(fs->read(path));
1136 } catch (...) {
1137 if (error) *error = "read failed: " + path;
1138 return false;
1139 }
1140 if (!data || data->getSize() == 0) {
1141 if (error) *error = "empty file: " + path;
1142 return false;
1143 }
1144 const std::string text(static_cast<const char *>(data->getData()), data->getSize());
1145 std::string decodeError;
1146 auto doc = eve::json::Document::parse(text, &decodeError);
1147 if (!doc.valid() || !doc.root().isObject()) {
1148 if (error) *error = decodeError.empty() ? "invalid tileset manifest" : decodeError;
1149 return false;
1150 }
1151 Json root = doc.root();
1152 if (root.has("tileset")) {
1153 root = root.get("tileset");
1154 if (!root.isObject()) {
1155 if (error) *error = "tileset must be an object";
1156 return false;
1157 }
1158 }
1159 const TilesetInfo info = readTilesetObject(root);
1160 if (info.image.empty()) {
1161 if (error) *error = "tileset manifest has no image";
1162 return false;
1163 }
1164 applyTileset(layer, info);
1165 fs->watch(path);
1166 if (auto *hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload"))
1167 hot->bind(path, "tilemap");
1168 return true;
1169}
1170
1171std::vector<TileLayer *> loadMapText(const std::string &json, std::vector<MapObject> *objects,
1172 std::string *error) {
1173 std::string err;
1174 auto doc = eve::json::Document::parse(json, &err);
1175 if (!doc.valid() || !doc.root().isObject()) {
1176 if (error) *error = err.empty() ? "invalid json" : err;
1177 return {};
1178 }
1179 return loadMapObject(doc.root(), {}, nullptr, objects, error);
1180}
1181
1182std::vector<TileLayer *> loadMapFile(const std::string &path, std::vector<MapObject> *objects,
1183 std::string *error) {
1184 if (error) error->clear();
1185 if (path.empty()) {
1186 if (error) *error = "empty path";
1187 return {};
1188 }
1189
1190 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
1191 if (!fs) fs = eve::filesystem::Filesystem::create();
1192
1193 std::string text;
1194 if (!readImportText(fs, path, text)) {
1195 if (error) *error = "empty file: " + path;
1196 return {};
1197 }
1198 std::string err;
1199 auto doc = eve::json::Document::parse(text, &err);
1200 if (!doc.valid() || !doc.root().isObject()) {
1201 if (error) *error = err.empty() ? "invalid json" : err;
1202 return {};
1203 }
1204 return loadMapObject(doc.root(), path, fs, objects, error);
1205}
1206
1207std::vector<TileLayer *> loadMapFile(const std::string &path, std::string *error) {
1208 return loadMapFile(path, nullptr, error);
1209}
1210
1212
1213} // namespace eve::map
double value
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
int root
Definition AnimSmr.cpp:119
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
std::unordered_map< std::string, QuestRuntime > entries
int mask
building::EdgeCurveGroup group
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float py
glm::vec4 p[6]
std::map< std::string, Var > values
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
DiagnosticCode code
tensor::Graph g
Definition GpuGraph.cpp:7
std::uint32_t key
vk::UniqueImage image
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
double r
float v
HexVec3 left
HexVec3 right
std::int32_t c
std::int32_t first
int h
std::vector< Colorf > px
std::string encoding
std::uint32_t height
std::uint32_t width
std::string text
std::array< float, 3 > position
std::map< std::string, PropertyRule > properties
std::uint64_t bytes
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string error
Definition Package.cpp:60
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
float d
float t
uint64_t token
std::uint32_t count
double spacingX
int spacing
int firstGid
std::vector< TileLayer::Tileset::TerrainRule > terrainRules
int columns
std::vector< TileLayer::Tileset::CustomData > customData
float offsetX
std::vector< TileLayer::Tileset::Visual > visuals
float offsetY
int tileW
float opacity
int margin
int tileH
std::string sourcePath
bool visible
Json object
std::vector< TileLayer::Tileset::Animation > animations
uint32_t index
const UnitySourceAsset & source
double oy
double ox
glm::vec3 point
std::vector< WfcTile > tiles
Definition WfcSimple.cpp:22
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
EVENGINE_API_FOUNDATION public API.
Definition Filesystem.h:43
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.
Definition Json.h:136
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
ECS tile layer entity. Script mutates tile GIDs / tileset / draw; TileRenderSystem batch-draws atlas ...
Definition TileLayer.h:32
void clear()
Clears the layer to empty.
float getTileWidth()
Tile size in pixels.
Definition TileLayer.cpp:37
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).
Definition TileLayer.cpp:27
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).
Definition TileLayer.cpp:46
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)
Definition TileLayer.cpp:14
float getCellGapX()
Horizontal and vertical world-space gaps between projected cells.
void setTileSize(float tileW, float tileH)
Definition TileLayer.cpp:40
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.
Definition TileLayer.cpp:35
void setTint(float r, float g, float b, float a=1.f)
static Document parse(const std::string &text, std::string *error=nullptr)
Definition Xml.cpp:304
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.
Definition Diagnostic.h:47
Config public API.
Definition TileLayer.h:41
glm::uvec4 info
glm::vec4 color