yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_room_transfer_json.cc
Go to the documentation of this file.
2
3#include <initializer_list>
4#include <set>
5#include <stdexcept>
6#include <string>
7#include <vector>
8
9#include "nlohmann/json.hpp"
10#include "util/macro.h"
11
12namespace yaze::editor {
13namespace {
14
15using Json = nlohmann::json;
16
17void Keys(const Json& value, std::initializer_list<const char*> keys) {
18 if (!value.is_object() || value.size() != keys.size()) {
19 throw std::invalid_argument(
20 "Unexpected, missing, or unknown room document fields");
21 }
22 for (const char* key : keys) {
23 if (!value.contains(key)) {
24 throw std::invalid_argument(std::string("Missing required field: ") +
25 key);
26 }
27 }
28}
29
30int Integer(const Json& value, int minimum, int maximum) {
31 if (value.is_number_unsigned()) {
32 const uint64_t number = value.get<uint64_t>();
33 if (number <= static_cast<uint64_t>(maximum) &&
34 (minimum <= 0 || number >= static_cast<uint64_t>(minimum))) {
35 return static_cast<int>(number);
36 }
37 } else if (value.is_number_integer()) {
38 const int64_t number = value.get<int64_t>();
39 if (number >= minimum && number <= maximum)
40 return static_cast<int>(number);
41 }
42 throw std::invalid_argument(
43 "Expected an integer inside the field's ROM range");
44}
45
46bool Boolean(const Json& value) {
47 if (!value.is_boolean())
48 throw std::invalid_argument("Expected a boolean field");
49 return value.get<bool>();
50}
51
52const Json& Array(const Json& value, size_t maximum) {
53 if (!value.is_array() || value.size() > maximum) {
54 throw std::invalid_argument("Expected an array within its room limit");
55 }
56 return value;
57}
58
59template <size_t N>
60std::array<uint8_t, N> ByteArray(const Json& value, int maximum = 255) {
61 Array(value, N);
62 if (value.size() != N)
63 throw std::invalid_argument("Incorrect fixed array length");
64 std::array<uint8_t, N> result{};
65 for (size_t i = 0; i < N; ++i)
66 result[i] = Integer(value[i], 0, maximum);
67 return result;
68}
69
71 return {{"palette", m.palette},
72 {"blockset", m.blockset},
73 {"spriteset", m.spriteset},
74 {"layout", m.layout},
75 {"floor1", m.floor1},
76 {"floor2", m.floor2},
77 {"message", m.message},
78 {"bg2", static_cast<int>(m.bg2)},
79 {"layer2_mode", m.layer2_mode},
80 {"layer_merging",
81 {{"id", m.layer_merging.ID},
82 {"name", m.layer_merging.Name},
83 {"on_top", m.layer_merging.Layer2OnTop},
84 {"translucent", m.layer_merging.Layer2Translucent},
85 {"visible", m.layer_merging.Layer2Visible}}},
86 {"is_dark", m.is_dark},
87 {"is_light", m.is_light},
88 {"collision", static_cast<int>(m.collision)},
89 {"effect", static_cast<int>(m.effect)},
90 {"tag1", static_cast<int>(m.tag1)},
91 {"tag2", static_cast<int>(m.tag2)},
92 {"holewarp", m.holewarp},
93 {"pit_target_layer", m.pit_target_layer},
94 {"staircase_rooms", m.staircase_rooms},
95 {"staircase_planes", m.staircase_planes}};
96}
97
99 Keys(j, {"palette",
100 "blockset",
101 "spriteset",
102 "layout",
103 "floor1",
104 "floor2",
105 "message",
106 "bg2",
107 "layer2_mode",
108 "layer_merging",
109 "is_dark",
110 "is_light",
111 "collision",
112 "effect",
113 "tag1",
114 "tag2",
115 "holewarp",
116 "pit_target_layer",
117 "staircase_rooms",
118 "staircase_planes"});
120 m.palette = Integer(j.at("palette"), 0, 255);
121 m.blockset = Integer(j.at("blockset"), 0, 255);
122 m.spriteset = Integer(j.at("spriteset"), 0, 255);
123 m.layout = Integer(j.at("layout"), 0, 7);
124 m.floor1 = Integer(j.at("floor1"), 0, 15);
125 m.floor2 = Integer(j.at("floor2"), 0, 15);
126 m.message = Integer(j.at("message"), 0, 65535);
127 m.bg2 = static_cast<background2>(Integer(j.at("bg2"), 0, 8));
128 m.layer2_mode = Integer(j.at("layer2_mode"), 0, 7);
129 const auto& merging = j.at("layer_merging");
130 Keys(merging, {"id", "name", "on_top", "translucent", "visible"});
131 if (!merging.at("name").is_string() ||
132 merging.at("name").get_ref<const std::string&>().size() > 64) {
133 throw std::invalid_argument("Invalid layer merging name");
134 }
135 m.layer_merging = zelda3::LayerMergeType(
136 Integer(merging.at("id"), 0, 8), merging.at("name").get<std::string>(),
137 Boolean(merging.at("visible")), Boolean(merging.at("on_top")),
138 Boolean(merging.at("translucent")));
139 m.is_dark = Boolean(j.at("is_dark"));
140 m.is_light = Boolean(j.at("is_light"));
141 m.collision =
142 static_cast<zelda3::CollisionKey>(Integer(j.at("collision"), 0, 7));
143 m.effect = static_cast<zelda3::EffectKey>(Integer(j.at("effect"), 0, 255));
144 m.tag1 = static_cast<zelda3::TagKey>(Integer(j.at("tag1"), 0, 255));
145 m.tag2 = static_cast<zelda3::TagKey>(Integer(j.at("tag2"), 0, 255));
146 m.holewarp = Integer(j.at("holewarp"), 0, 255);
147 m.pit_target_layer = Integer(j.at("pit_target_layer"), 0, 3);
148 m.staircase_rooms = ByteArray<4>(j.at("staircase_rooms"));
149 m.staircase_planes = ByteArray<4>(j.at("staircase_planes"), 3);
150 return m;
151}
152
153} // namespace
154
155absl::StatusOr<std::string> SerializeDungeonRoomDocument(
156 const DungeonRoomDocument& document) {
158 try {
159 Json j{{"format", "yaze.room"},
160 {"version", 1},
161 {"source_room_id", document.source_room_id},
162 {"metadata", MetadataToJson(document.metadata)},
163 {"objects", Json::array()},
164 {"chests", Json::array()},
165 {"doors", Json::array()},
166 {"sprites", Json::array()},
167 {"pot_items", Json::array()},
168 {"collision",
169 {{"has_data", document.collision.has_data},
170 {"tiles", document.collision.tiles},
171 {"overrides", Json::array()}}},
172 {"water",
173 {{"has_data", document.water.has_data},
174 {"sram_bit_mask", document.water.sram_bit_mask},
175 {"tiles", document.water.tiles}}}};
176 // Nonzero collision bytes already imply an override in legacy v1 files.
177 // This optional field records explicit floor offsets that bytes alone lose.
178 for (size_t offset = 0; offset < document.collision.tiles.size();
179 ++offset) {
180 if (document.collision.tiles[offset] == 0 &&
181 document.collision.HasOverride(offset)) {
182 j["collision"]["overrides"].push_back(offset);
183 }
184 }
185 for (const auto& o : document.contents.objects) {
186 j["objects"].push_back(
187 {{"id", o.id_},
188 {"x", o.x_},
189 {"y", o.y_},
190 {"size", o.size_},
191 {"layer", o.GetLayerValue()},
192 {"options", static_cast<int>(o.options())},
193 {"all_bgs", o.all_bgs_},
194 {"lit", o.lit_},
195 // Physical table slots are save provenance, not portable content.
196 {"block_load_order", zelda3::RoomObject::kBlockLoadOrderNew},
197 {"block_behavior_layer", o.block_behavior_layer()},
198 {"torch_reserved_bit", o.torch_reserved_bit()}});
199 }
200 for (auto chest : document.contents.chests) {
201 j["chests"].push_back({{"id", chest.id}, {"big", chest.size}});
202 }
203 for (auto door : document.contents.doors) {
204 j["doors"].push_back({{"position", door.position},
205 {"type", static_cast<int>(door.type)},
206 {"direction", static_cast<int>(door.direction)},
207 {"byte1", door.byte1},
208 {"byte2", door.byte2}});
209 }
210 for (auto s : document.contents.sprites) {
211 j["sprites"].push_back({{"id", s.id},
212 {"x", s.x},
213 {"y", s.y},
214 {"subtype", s.subtype},
215 {"layer", s.layer},
216 {"key_drop", s.key_drop},
217 {"deleted", s.deleted}});
218 }
219 for (auto item : document.contents.items) {
220 j["pot_items"].push_back(
221 {{"position", item.position}, {"item", item.item}});
222 }
223 auto serialized = j.dump(2);
224 if (serialized.size() > kMaxDungeonRoomDocumentBytes) {
225 return absl::ResourceExhaustedError("Room document exceeds 1 MiB");
226 }
227 return serialized;
228 } catch (const Json::exception& error) {
229 return absl::InvalidArgumentError(
230 std::string("Cannot serialize room document: ") + error.what());
231 }
232}
233
234absl::StatusOr<DungeonRoomDocument> ParseDungeonRoomDocument(
235 const std::string& text) {
236 if (text.size() > kMaxDungeonRoomDocumentBytes) {
237 return absl::ResourceExhaustedError("Room document exceeds 1 MiB");
238 }
239 try {
240 std::vector<std::set<std::string>> object_keys;
241 const auto j = Json::parse(
242 text, [&](int depth, Json::parse_event_t event, Json& value) {
243 if (depth > 32)
244 throw std::invalid_argument("Room document nesting is too deep");
245 if (event == Json::parse_event_t::object_start)
246 object_keys.emplace_back();
247 if (event == Json::parse_event_t::key &&
248 !object_keys.back().insert(value.get<std::string>()).second) {
249 throw std::invalid_argument("Duplicate room document field");
250 }
251 if (event == Json::parse_event_t::object_end)
252 object_keys.pop_back();
253 return true;
254 });
255 Keys(j, {"format", "version", "source_room_id", "metadata", "objects",
256 "chests", "doors", "sprites", "pot_items", "collision", "water"});
257 if (!j.at("format").is_string() || j.at("format") != "yaze.room" ||
258 Integer(j.at("version"), 1, 1) != 1) {
259 throw std::invalid_argument(
260 "Unsupported room document format or version");
261 }
262 DungeonRoomDocument document;
263 document.source_room_id =
264 Integer(j.at("source_room_id"), 0, zelda3::kNumberOfRooms - 1);
265 document.metadata = MetadataFromJson(j.at("metadata"));
266 for (const auto& o :
267 Array(j.at("objects"), kMaxDungeonRoomDocumentObjects)) {
268 Keys(o,
269 {"id", "x", "y", "size", "layer", "options", "all_bgs", "lit",
270 "block_load_order", "block_behavior_layer", "torch_reserved_bit"});
271 zelda3::RoomObject object(
272 Integer(o.at("id"), 0, 4095), Integer(o.at("x"), 0, 63),
273 Integer(o.at("y"), 0, 63), Integer(o.at("size"), 0, 255),
274 Integer(o.at("layer"), 0, 2));
275 object.set_options(
276 static_cast<zelda3::ObjectOption>(Integer(o.at("options"), 0, 63)));
277 object.all_bgs_ = Boolean(o.at("all_bgs"));
278 object.lit_ = Boolean(o.at("lit"));
279 // Accept older v1 documents but never import a foreign physical slot.
280 (void)Integer(o.at("block_load_order"), -1, 65535);
281 object.set_block_load_order(zelda3::RoomObject::kBlockLoadOrderNew);
282 object.set_block_behavior_layer(
283 Integer(o.at("block_behavior_layer"), 0, 1));
284 object.set_torch_reserved_bit(Integer(o.at("torch_reserved_bit"), 0, 1));
285 document.contents.objects.push_back(std::move(object));
286 }
287 for (const auto& c :
288 Array(j.at("chests"), kMaxDungeonRoomDocumentChestRecords)) {
289 Keys(c, {"id", "big"});
290 document.contents.chests.push_back(
291 {static_cast<uint8_t>(Integer(c.at("id"), 0, 255)),
292 Boolean(c.at("big"))});
293 }
294 for (const auto& d : Array(j.at("doors"), zelda3::kMaxDoors)) {
295 Keys(d, {"position", "type", "direction", "byte1", "byte2"});
296 document.contents.doors.push_back(
297 {static_cast<uint8_t>(Integer(d.at("position"), 0, 15)),
298 static_cast<zelda3::DoorType>(Integer(d.at("type"), 0, 255)),
299 static_cast<zelda3::DoorDirection>(Integer(d.at("direction"), 0, 3)),
300 static_cast<uint8_t>(Integer(d.at("byte1"), 0, 255)),
301 static_cast<uint8_t>(Integer(d.at("byte2"), 0, 255))});
302 }
303 for (const auto& s : Array(j.at("sprites"), zelda3::kMaxTotalSprites)) {
304 Keys(s, {"id", "x", "y", "subtype", "layer", "key_drop", "deleted"});
305 document.contents.sprites.push_back(
306 {static_cast<uint8_t>(Integer(s.at("id"), 0, 255)),
307 Integer(s.at("x"), 0, 31), Integer(s.at("y"), 0, 31),
308 Integer(s.at("subtype"), 0, 31), Integer(s.at("layer"), 0, 1),
309 Integer(s.at("key_drop"), 0, 2), Boolean(s.at("deleted"))});
310 }
311 for (const auto& i : Array(j.at("pot_items"), 4096)) {
312 Keys(i, {"position", "item"});
313 document.contents.items.push_back(
314 {static_cast<uint16_t>(Integer(i.at("position"), 0, 65535)),
315 static_cast<uint8_t>(Integer(i.at("item"), 0, 255))});
316 }
317 const auto& collision = j.at("collision");
318 if (collision.contains("overrides")) {
319 Keys(collision, {"has_data", "tiles", "overrides"});
320 } else {
321 Keys(collision, {"has_data", "tiles"});
322 }
323 document.collision.has_data = Boolean(collision.at("has_data"));
324 document.collision.tiles = ByteArray<4096>(collision.at("tiles"));
325 if (collision.contains("overrides")) {
326 std::set<int> seen;
327 for (const auto& value : Array(collision.at("overrides"), 4096)) {
328 const int offset = Integer(value, 0, 4095);
329 if (!seen.insert(offset).second) {
330 throw std::invalid_argument("Duplicate collision override offset");
331 }
332 document.collision.overrides.set(static_cast<size_t>(offset));
333 }
334 }
335 const auto& water = j.at("water");
336 Keys(water, {"has_data", "sram_bit_mask", "tiles"});
337 document.water.has_data = Boolean(water.at("has_data"));
338 document.water.sram_bit_mask = Integer(water.at("sram_bit_mask"), 0, 255);
339 document.water.tiles = ByteArray<4096>(water.at("tiles"), 1);
341 return document;
342 } catch (const Json::exception& error) {
343 return absl::InvalidArgumentError(std::string("Invalid room document: ") +
344 error.what());
345 } catch (const std::invalid_argument& error) {
346 return absl::InvalidArgumentError(std::string("Invalid room document: ") +
347 error.what());
348 }
349}
350
351} // namespace yaze::editor
const char * what() const noexcept override
Definition json.h:96
static Json parse(const std::string &)
Definition json.h:36
static Json array()
Definition json.h:35
T get() const
Definition json.h:49
static constexpr int kBlockLoadOrderNew
zelda3_bg2_effect
Background layer 2 effects.
Definition zelda.h:369
std::array< uint8_t, N > ByteArray(const Json &value, int maximum=255)
void Keys(const Json &value, std::initializer_list< const char * > keys)
Editors are the view controllers for the application.
constexpr size_t kMaxDungeonRoomDocumentObjects
absl::Status ValidateDungeonRoomDocumentForInterchange(const DungeonRoomDocument &document)
absl::StatusOr< std::string > SerializeDungeonRoomDocument(const DungeonRoomDocument &document)
absl::StatusOr< DungeonRoomDocument > ParseDungeonRoomDocument(const std::string &json)
constexpr size_t kMaxDungeonRoomDocumentChestRecords
constexpr size_t kMaxDungeonRoomDocumentBytes
DoorType
Door types from ALTTP.
Definition door_types.h:33
constexpr size_t kMaxDoors
constexpr int kNumberOfRooms
DoorDirection
Door direction on room walls.
Definition door_types.h:18
constexpr size_t kMaxTotalSprites
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Treasure chest.
Definition zelda.h:425
zelda3::CustomCollisionMap collision
zelda3::Room::MetadataSnapshot metadata
std::vector< zelda3::Room::Door > doors
std::vector< DungeonSpriteSnapshot > sprites
std::vector< zelda3::RoomObject > objects
std::array< uint8_t, 64 *64 > tiles
bool HasOverride(size_t offset) const
std::array< uint8_t, 4 > staircase_planes
Definition room.h:252
std::array< uint8_t, 4 > staircase_rooms
Definition room.h:251
std::array< uint8_t, 64 *64 > tiles
Definition room.h:223