113 return absl::ResourceExhaustedError(
"Wall profile source exceeds 1 MiB");
115 if (source.substr(0, 3) ==
"\xEF\xBB\xBF") {
116 return absl::InvalidArgumentError(
117 "Wall profile JSON must not contain a UTF-8 BOM");
121 std::vector<std::set<std::string>> keys;
122 const auto callback = [&](
int depth, Json::parse_event_t event,
125 throw JsonSourceError(
126 "Wall profile JSON nesting exceeds the v1 schema");
128 if (event == Json::parse_event_t::object_start) {
130 }
else if (event == Json::parse_event_t::key) {
131 const auto& key = parsed.get_ref<
const std::string&>();
132 if (keys.empty() || !keys.back().insert(key).second) {
133 throw JsonSourceError(
"Duplicate wall profile JSON key: " + key);
135 }
else if (event == Json::parse_event_t::object_end) {
140 root =
Json::parse(source.begin(), source.end(), callback);
141 }
catch (
const JsonSourceError& error) {
142 return absl::InvalidArgumentError(error.what());
144 return absl::InvalidArgumentError(
145 std::string(
"Invalid wall profile JSON: ") + error.what());
148 auto status = CheckObject(root, {
"schema_version",
"profiles",
"bindings"},
149 {
"schema_version",
"profiles",
"bindings"},
"root");
152 auto version = ReadUnsigned(root[
"schema_version"], 1,
"schema_version");
154 return version.status();
156 return absl::InvalidArgumentError(
157 "Only wall profile schema_version 1 is supported");
162 if (root[
"profiles"].empty()) {
163 return absl::InvalidArgumentError(
"Profiles must not be empty");
169 std::vector<DungeonWallProfile> profiles;
170 std::set<std::string> ids;
171 for (
const auto& item : root[
"profiles"]) {
173 CheckObject(item, {
"id",
"patterns"}, {
"id",
"patterns"},
"profile");
176 auto id = ReadIdentifier(item[
"id"],
"profile.id");
179 if (!ids.insert(*id).second) {
180 return absl::InvalidArgumentError(
"Duplicate wall profile id: " + *
id);
186 if (item[
"patterns"].empty()) {
187 return absl::InvalidArgumentError(
"Profile patterns must not be empty");
190 std::set<std::string> names;
191 std::set<uint32_t> addresses;
192 size_t profile_words = 0;
193 for (
const auto& entry : item[
"patterns"]) {
194 status = CheckObject(entry, {
"name",
"pc_offset",
"words"},
195 {
"name",
"pc_offset",
"words"},
"pattern");
198 auto name = ReadIdentifier(entry[
"name"],
"pattern.name");
200 return name.status();
201 if (!names.insert(*name).second) {
202 return absl::InvalidArgumentError(
"Duplicate pattern name: " + *
name);
204 auto pc = ReadUnsigned(entry[
"pc_offset"], kPatternBankEnd - 2,
205 "pattern.pc_offset");
208 if (*pc < kFirstPatternPc || (*pc & 1) != 0) {
209 return absl::InvalidArgumentError(
210 "Pattern PC must be aligned in 0x1B52-0x7FFE");
216 if (entry[
"words"].empty()) {
217 return absl::InvalidArgumentError(
"Pattern words must not be empty");
219 profile_words += entry[
"words"].size();
221 return absl::ResourceExhaustedError(
222 "Wall profile word budget exceeded");
224 if (*pc + entry[
"words"].size() * 2 > kPatternBankEnd) {
225 return absl::InvalidArgumentError(
226 "Pattern words cross bank-00 data boundary");
229 for (
const auto& word_json : entry[
"words"]) {
230 auto word = ReadUnsigned(word_json, 0xFFFF,
"pattern.words[]");
232 return word.status();
233 const uint32_t address = pattern.pc_offset + pattern.words.size() * 2;
234 if (!addresses.insert(address).second) {
235 return absl::InvalidArgumentError(
236 "Wall profile pattern spans overlap");
238 pattern.words.push_back(
static_cast<uint16_t
>(*word));
240 profile.patterns.push_back(std::move(pattern));
242 profiles.push_back(std::move(profile));
245 std::vector<DungeonWallProfileBinding> bindings;
246 for (
const auto& item : root[
"bindings"]) {
247 status = CheckObject(item, {
"room",
"profile",
"world",
"entrance"},
248 {
"room",
"profile"},
"binding");
251 auto room = ReadUnsigned(item[
"room"], 0x127,
"binding.room");
253 return room.status();
254 auto profile = ReadIdentifier(item[
"profile"],
"binding.profile");
256 return profile.status();
257 if (!ids.contains(*profile)) {
258 return absl::InvalidArgumentError(
"Binding names an unknown profile: " +
262 std::nullopt, std::nullopt};
263 if (item.contains(
"world")) {
264 auto world = ReadUnsigned(item[
"world"], 1,
"binding.world");
266 return world.status();
267 binding.world =
static_cast<uint8_t
>(*world);
269 if (item.contains(
"entrance")) {
270 auto entrance = ReadUnsigned(item[
"entrance"], 0x84,
"binding.entrance");
272 return entrance.status();
273 binding.entrance =
static_cast<uint16_t
>(*entrance);
275 const int rank = binding.world.has_value() + binding.entrance.has_value();
276 for (
const auto& other : bindings) {
277 const int other_rank =
278 other.world.has_value() + other.entrance.has_value();
279 const bool world_overlaps =
280 !binding.world || !other.world || binding.world == other.world;
281 const bool entrance_overlaps = !binding.entrance || !other.entrance ||
282 binding.entrance == other.entrance;
283 if (other.room == binding.room && rank == other_rank && world_overlaps &&
285 return absl::InvalidArgumentError(
286 "Ambiguous equal-specificity wall profile bindings");
289 bindings.push_back(std::move(binding));
291 return std::shared_ptr<const DungeonWallProfileCollection>(
293 std::string(source),
""));
298 std::error_code error;
299 const bool exists = std::filesystem::exists(path, error);
301 return absl::InternalError(
"Cannot inspect wall profile source: " + path);
304 return absl::NotFoundError(
"Wall profile source is missing: " + path);
305 const auto size = std::filesystem::file_size(path, error);
307 return absl::FailedPreconditionError(
"Cannot size wall profile source: " +
310 return absl::ResourceExhaustedError(
"Wall profile source exceeds 1 MiB: " +
313 std::ifstream input(path, std::ios::binary);
315 return absl::PermissionDeniedError(
"Cannot open wall profile source: " +
318 input.read(source.data(),
static_cast<std::streamsize
>(source.size()));
319 source.resize(
static_cast<size_t>(input.gcount()));
321 return absl::DataLossError(
"Cannot read wall profile source: " + path);
324 return parsed.status();
325 const auto canonical = std::filesystem::canonical(path, error);
327 return absl::NotFoundError(
"Cannot resolve wall profile source: " + path);
328 return std::shared_ptr<const DungeonWallProfileCollection>(
330 (*parsed)->bindings(), std::move(source),
331 canonical.string()));
337 if (context.
room < 0 || context.
room > 0x127 ||
341 return absl::InvalidArgumentError(
"Wall profile context is out of range");
344 int best_specificity = -1;
345 bool ambiguous =
false;
346 for (
const auto& binding : collection.
bindings()) {
347 if (binding.room != context.
room ||
349 (!context.
world || *binding.world != *context.
world)) ||
354 const int specificity =
355 binding.
world.has_value() + binding.entrance.has_value();
356 if (specificity > best_specificity) {
358 best_specificity = specificity;
360 }
else if (specificity == best_specificity) {
365 return absl::FailedPreconditionError(
366 "Ambiguous equally specific wall profile bindings");
368 if (selected ==
nullptr)
370 const auto profile = std::find_if(
372 [&](
const auto& value) { return value.id == selected->profile; });
373 if (profile == collection.
profiles().end()) {
374 return absl::InternalError(
375 "Validated wall profile binding lost its profile");
381 for (
const auto& pattern : profile->patterns) {
382 for (
size_t i = 0; i < pattern.words.size(); ++i) {
383 merged.
words[pattern.pc_offset + i * 2] = pattern.words[i];