yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_catalog.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <set>
8#include <stdexcept>
9
10#include "nlohmann/json.hpp"
12
13namespace yaze::core {
14namespace {
15using nlohmann::json;
16constexpr size_t kMaxBytes = 1024 * 1024;
17
18void Fields(const json& object, std::initializer_list<const char*> fields) {
19 if (!object.is_object())
20 throw std::runtime_error("Expected an object");
21 for (const auto& item : object.items()) {
22 if (std::none_of(fields.begin(), fields.end(),
23 [&](const char* field) { return item.key() == field; }))
24 throw std::runtime_error("Unsupported field: " + item.key());
25 }
26}
27std::string Text(const json& object, const char* key) {
28 const auto& value = object.at(key);
29 if (!value.is_string() || value.get_ref<const std::string&>().empty() ||
30 value.get_ref<const std::string&>().size() > 4096 ||
31 value.get_ref<const std::string&>().find('\0') != std::string::npos)
32 throw std::runtime_error(std::string("Invalid text: ") + key);
33 return value.get<std::string>();
34}
35int Integer(const json& value, int maximum) {
36 if (!value.is_number_integer() || value < 0 || value > maximum)
37 throw std::runtime_error("Integer outside supported range");
38 return value.get<int>();
39}
40bool OneOf(std::string_view text, std::initializer_list<const char*> values) {
41 return std::any_of(values.begin(), values.end(),
42 [&](const char* value) { return text == value; });
43}
44bool ValidUse(std::string_view use) {
45 return OneOf(use, {"link_items", "npcs", "enemies", "bosses", "environment",
46 "hud", "effects", "mixed"});
47}
48std::string Lower(std::string_view text) {
49 std::string result(text);
50 std::transform(result.begin(), result.end(), result.begin(),
51 [](unsigned char c) { return std::tolower(c); });
52 return result;
53}
54} // namespace
55
56absl::StatusOr<PaletteCatalog> PaletteCatalog::Parse(std::string_view text) {
57 if (text.size() > kMaxBytes)
58 return absl::ResourceExhaustedError("Palette catalog exceeds 1 MiB");
59 try {
60 const auto document = json::parse(text);
61 Fields(document, {"schema_version", "game_id", "entries"});
62 if (Integer(document.at("schema_version"), 1) != 1)
63 throw std::runtime_error("Palette catalog requires schema_version 1");
64 Text(document, "game_id");
65 const auto& entries = document.at("entries");
66 if (!entries.is_array() || entries.size() > 4096)
67 throw std::runtime_error("Expected at most 4096 palette entries");
68 PaletteCatalog result;
69 std::set<std::string> keys;
70 std::set<std::pair<std::string, int>> identities;
71 for (const auto& item : entries) {
72 Fields(item, {"key", "name", "group", "index", "world", "use",
73 "description", "provenance", "ownership", "usages"});
75 entry.key = Text(item, "key");
76 entry.name = Text(item, "name");
77 entry.group = Text(item, "group");
78 entry.index = Integer(item.at("index"), 255);
79 const auto* group = zelda3::GetPaletteGroupMetadata(entry.group.c_str());
80 if (!group || entry.index >= group->palette_count)
81 throw std::runtime_error("Unknown palette group or index");
82 entry.world = Text(item, "world");
83 entry.use = Text(item, "use");
84 entry.description = Text(item, "description");
85 entry.provenance = Text(item, "provenance");
86 entry.ownership = Text(item, "ownership");
87 if (!OneOf(entry.world, {"light_world", "dark_world", "underworld",
88 "special", "shared"}) ||
89 !ValidUse(entry.use) || !OneOf(entry.ownership, {"rom", "asm"}))
90 throw std::runtime_error("Unsupported world, use or ownership");
91 if (!keys.insert(entry.key).second ||
92 !identities.insert({entry.group, entry.index}).second)
93 throw std::runtime_error("Duplicate palette key or group/index");
94 const auto& usages = item.at("usages");
95 if (!usages.is_array() || usages.size() > 256)
96 throw std::runtime_error("Expected at most 256 contextual usages");
97 for (const auto& usage : usages) {
98 Fields(usage, {"name", "context", "use", "obj_row", "sprite_sheets",
99 "source"});
100 PaletteCatalogUsage parsed;
101 parsed.name = Text(usage, "name");
102 parsed.context = Text(usage, "context");
103 parsed.use = Text(usage, "use");
104 if (!ValidUse(parsed.use))
105 throw std::runtime_error("Unsupported usage category");
106 parsed.obj_row = Integer(usage.at("obj_row"), 7);
107 parsed.source = Text(usage, "source");
108 const auto& sheets = usage.at("sprite_sheets");
109 if (!sheets.is_array() || sheets.size() > 8)
110 throw std::runtime_error("Expected at most 8 sprite sheet values");
111 for (const auto& sheet : sheets)
112 parsed.sprite_sheets.push_back(Integer(sheet, 0x6B));
113 entry.usages.push_back(std::move(parsed));
114 }
115 result.entries_.push_back(std::move(entry));
116 }
117 return result;
118 } catch (const std::exception& error) {
119 return absl::InvalidArgumentError(std::string("Invalid palette catalog: ") +
120 error.what());
121 }
122}
123
124absl::StatusOr<std::filesystem::path> PaletteCatalog::ResolvePathForProject(
125 const std::string& project_file, const std::string& catalog_file) {
126 namespace fs = std::filesystem;
127 const fs::path configured(catalog_file);
128 // Descriptor loading normalizes to native absolute paths, including Windows
129 // drive paths. These must still pass canonical project containment below.
130 if (project_file.empty() || catalog_file.empty() ||
131 catalog_file.find('\0') != std::string::npos ||
132 (!configured.is_absolute() &&
133 (catalog_file.find('\\') != std::string::npos ||
134 catalog_file.find(':') != std::string::npos)))
135 return absl::InvalidArgumentError(
136 "Catalog requires a project-relative path");
137 for (const auto& part : configured)
138 if (part == "..")
139 return absl::InvalidArgumentError("Catalog path contains traversal");
140 std::error_code ec;
141 const auto root = fs::canonical(fs::path(project_file).parent_path(), ec);
142 if (ec)
143 return absl::NotFoundError("Project directory is unavailable");
144 const auto path = fs::canonical(root / configured, ec);
145 if (ec || !fs::is_regular_file(path, ec))
146 return absl::NotFoundError("Palette catalog is unavailable");
147 const auto mismatch =
148 std::mismatch(root.begin(), root.end(), path.begin(), path.end());
149 if (mismatch.first != root.end())
150 return absl::InvalidArgumentError("Catalog path escapes the project");
151 return path;
152}
153
154absl::StatusOr<PaletteCatalog> PaletteCatalog::LoadForProject(
155 const std::string& project_file, const std::string& catalog_file) {
156 const auto path = ResolvePathForProject(project_file, catalog_file);
157 if (!path.ok())
158 return path.status();
159 std::ifstream stream(*path, std::ios::binary);
160 if (!stream)
161 return absl::NotFoundError("Cannot read palette catalog");
162 std::string text(kMaxBytes + 1, '\0');
163 stream.read(text.data(), text.size());
164 text.resize(static_cast<size_t>(stream.gcount()));
165 if (stream.bad())
166 return absl::DataLossError("Cannot read palette catalog");
167 return Parse(text);
168}
169
170std::vector<const PaletteCatalogEntry*> PaletteCatalog::Filter(
171 std::string_view query, std::string_view world,
172 std::string_view use) const {
173 std::vector<const PaletteCatalogEntry*> result;
174 const auto needle = Lower(query);
175 for (const auto& entry : entries_) {
176 if (!world.empty() && entry.world != world)
177 continue;
178 if (!use.empty() && entry.use != use &&
179 std::none_of(entry.usages.begin(), entry.usages.end(),
180 [&](const auto& usage) { return usage.use == use; }))
181 continue;
182 std::string text = entry.key + " " + entry.name + " " + entry.group + " " +
183 std::to_string(entry.index) + " " + entry.description +
184 " " + entry.provenance;
185 for (const auto& usage : entry.usages)
186 text += " " + usage.name + " " + usage.context + " " + usage.source;
187 if (needle.empty() || Lower(text).find(needle) != std::string::npos)
188 result.push_back(&entry);
189 }
190 return result;
191}
192} // namespace yaze::core
const std::vector< PaletteCatalogEntry > & entries() const
std::vector< const PaletteCatalogEntry * > Filter(std::string_view query, std::string_view world={}, std::string_view use={}) const
static absl::StatusOr< PaletteCatalog > Parse(std::string_view text)
static absl::StatusOr< std::filesystem::path > ResolvePathForProject(const std::string &project_file, const std::string &catalog_file)
std::vector< PaletteCatalogEntry > entries_
static absl::StatusOr< PaletteCatalog > LoadForProject(const std::string &project_file, const std::string &catalog_file)
bool OneOf(std::string_view text, std::initializer_list< const char * > values)
void Fields(const json &object, std::initializer_list< const char * > fields)
const PaletteGroupMetadata * GetPaletteGroupMetadata(const char *group_id)