yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <set>
6#include <string>
7
8#include "absl/status/status.h"
9#include "absl/strings/numbers.h"
10#include "absl/strings/str_format.h"
13#include "util/macro.h"
14#include "zelda3/game_data.h"
15
16namespace yaze {
17namespace cli {
18namespace handlers {
19
20namespace {
21
22// All valid palette group names for iteration and validation.
23constexpr const char* kAllGroupNames[] = {
24 "ow_main", "ow_aux", "ow_animated", "hud",
25 "global_sprites", "armors", "swords", "shields",
26 "sprites_aux1", "sprites_aux2", "sprites_aux3", "dungeon_main",
27 "grass", "3d_object", "ow_mini_map",
28};
29constexpr int kNumGroups = sizeof(kAllGroupNames) / sizeof(kAllGroupNames[0]);
30
31// Load game data (palettes only) from the ROM.
32absl::Status LoadPalettes(Rom* rom, zelda3::GameData& game_data) {
33 game_data.set_rom(rom);
35 opts.load_graphics = false;
36 opts.load_palettes = true;
37 opts.load_gfx_groups = false;
38 opts.expand_rom = false;
39 opts.populate_metadata = false;
40 return zelda3::LoadGameData(*rom, game_data, opts);
41}
42
43// Format a single SnesColor as a hex RGB string (#RRGGBB).
44std::string FormatColorHex(const gfx::SnesColor& color) {
45 auto rgb = color.rgb(); // 0-255 stored in ImVec4
46 return absl::StrFormat("#%02X%02X%02X", static_cast<int>(rgb.x),
47 static_cast<int>(rgb.y), static_cast<int>(rgb.z));
48}
49
50// Compute perceptual brightness (0-255) from an SnesColor.
52 auto rgb = color.rgb();
53 // ITU-R BT.601 luma
54 return static_cast<int>(0.299 * rgb.x + 0.587 * rgb.y + 0.114 * rgb.z);
55}
56
57// Emit the colors of a single SnesPalette to the formatter.
59 resources::OutputFormatter& formatter) {
60 formatter.BeginArray("colors");
61 for (size_t c = 0; c < palette.size(); ++c) {
62 const auto& color = palette[c];
63 auto rgb = color.rgb();
64 std::string entry = absl::StrFormat(
65 "%zu: %s (R=%d G=%d B=%d, SNES=0x%04X)", c, FormatColorHex(color),
66 static_cast<int>(rgb.x), static_cast<int>(rgb.y),
67 static_cast<int>(rgb.z), color.snes());
68 formatter.AddArrayItem(entry);
69 }
70 formatter.EndArray();
71}
72
73// Analyze a single palette group and emit stats.
74void AnalyzeGroup(const std::string& group_name, const gfx::PaletteGroup& group,
75 resources::OutputFormatter& formatter) {
76 formatter.BeginObject(group_name);
77 formatter.AddField("group", group_name);
78 formatter.AddField("palette_count", static_cast<int>(group.size()));
79
80 int total_colors = 0;
81 std::set<uint16_t> unique_snes;
82 int brightness_sum = 0;
83 int darkest = 255;
84 int brightest = 0;
85
86 for (size_t p = 0; p < group.size(); ++p) {
87 const auto& pal = group.palette_ref(p);
88 total_colors += static_cast<int>(pal.size());
89 for (size_t c = 0; c < pal.size(); ++c) {
90 uint16_t snes_val = pal[c].snes();
91 unique_snes.insert(snes_val);
92 int br = ColorBrightness(pal[c]);
93 brightness_sum += br;
94 darkest = std::min(darkest, br);
95 brightest = std::max(brightest, br);
96 }
97 }
98
99 formatter.AddField("total_colors", total_colors);
100 formatter.AddField("unique_colors", static_cast<int>(unique_snes.size()));
101
102 if (total_colors > 0) {
103 formatter.BeginObject("brightness");
104 formatter.AddField("average", brightness_sum / total_colors);
105 formatter.AddField("darkest", darkest);
106 formatter.AddField("brightest", brightest);
107 formatter.EndObject();
108 }
109
110 // ROM address range for the group
111 try {
112 uint32_t first_addr = gfx::GetPaletteAddress(group_name, 0, 0);
113 formatter.AddHexField("rom_address_start", first_addr, 6);
114 } catch (...) {
115 // GetPaletteAddress may throw for grass (single-color group).
116 // Gracefully omit the address.
117 }
118
119 formatter.EndObject();
120}
121
122} // namespace
123
124// ---------------------------------------------------------------------------
125// palette-get-colors
126// ---------------------------------------------------------------------------
128 Rom* rom, const resources::ArgumentParser& parser,
129 resources::OutputFormatter& formatter) {
130 auto group_name = parser.GetString("group").value();
131
132 // Load palette data from ROM.
133 zelda3::GameData game_data;
134 RETURN_IF_ERROR(LoadPalettes(rom, game_data));
135
136 const auto* group = game_data.palette_groups.get_group(group_name);
137 if (!group) {
138 return absl::NotFoundError(
139 absl::StrFormat("Unknown palette group: '%s'. Valid groups: ow_main, "
140 "ow_aux, ow_animated, hud, global_sprites, armors, "
141 "swords, shields, sprites_aux1, sprites_aux2, "
142 "sprites_aux3, dungeon_main, grass, 3d_object, "
143 "ow_mini_map",
144 group_name));
145 }
146
147 // Optional: narrow to a single palette index within the group.
148 auto index_result = parser.GetInt("index");
149 if (!index_result.ok() && !absl::IsNotFound(index_result.status())) {
150 return index_result.status();
151 }
152
153 formatter.BeginObject("Palette Colors");
154 formatter.AddField("group", group_name);
155 formatter.AddField("palette_count", static_cast<int>(group->size()));
156
157 if (index_result.ok()) {
158 int idx = index_result.value();
159 if (idx < 0 || idx >= static_cast<int>(group->size())) {
160 return absl::InvalidArgumentError(absl::StrFormat(
161 "Palette index %d out of range [0, %d)", idx, group->size()));
162 }
163
164 const auto& pal = group->palette_ref(idx);
165 formatter.AddField("palette_index", idx);
166 formatter.AddField("color_count", static_cast<int>(pal.size()));
167 FormatPaletteColors(pal, formatter);
168 } else {
169 // Dump all palettes in the group.
170 formatter.BeginArray("palettes");
171 for (size_t p = 0; p < group->size(); ++p) {
172 const auto& pal = group->palette_ref(p);
173 std::string label =
174 absl::StrFormat("palette %zu (%zu colors)", p, pal.size());
175 formatter.AddArrayItem(label);
176 }
177 formatter.EndArray();
178
179 // If the group is small enough, also dump all colors inline.
180 if (group->size() <= 6) {
181 for (size_t p = 0; p < group->size(); ++p) {
182 const auto& pal = group->palette_ref(p);
183 formatter.BeginObject(absl::StrFormat("palette_%zu", p));
184 formatter.AddField("index", static_cast<int>(p));
185 formatter.AddField("color_count", static_cast<int>(pal.size()));
186 FormatPaletteColors(pal, formatter);
187 formatter.EndObject();
188 }
189 }
190 }
191
192 formatter.EndObject();
193 return absl::OkStatus();
194}
195
196// ---------------------------------------------------------------------------
197// palette-set-color
198// ---------------------------------------------------------------------------
200 Rom* rom, const resources::ArgumentParser& parser,
201 resources::OutputFormatter& formatter) {
202 auto group_name = parser.GetString("group").value();
203 auto palette_str = parser.GetString("palette").value();
204 auto index_str = parser.GetString("index").value();
205 auto color_str = parser.GetString("color").value();
206 bool write = parser.HasFlag("write");
207 if (write) {
208 return absl::FailedPreconditionError(
209 "palette-set-color --write is disabled because it cannot guarantee "
210 "durable, manifest-safe persistence; use "
211 "dungeon-set-palette-color for dungeon palette writes");
212 }
213
214 int palette_idx;
215 if (!absl::SimpleAtoi(palette_str, &palette_idx)) {
216 return absl::InvalidArgumentError(
217 absl::StrFormat("Invalid palette index: '%s'", palette_str));
218 }
219
220 int color_idx;
221 if (!absl::SimpleAtoi(index_str, &color_idx)) {
222 return absl::InvalidArgumentError(
223 absl::StrFormat("Invalid color index: '%s'", index_str));
224 }
225
226 // Parse RGB hex color (RRGGBB, with or without leading # or 0x).
227 std::string hex = color_str;
228 if (!hex.empty() && hex[0] == '#')
229 hex = hex.substr(1);
230 if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
231 hex = hex.substr(2);
232
233 if (hex.size() != 6) {
234 return absl::InvalidArgumentError(absl::StrFormat(
235 "Color must be a 6-digit hex RGB value (e.g., FF0000). Got: '%s'",
236 color_str));
237 }
238
239 unsigned int r_val, g_val, b_val;
240 if (sscanf(hex.c_str(), "%02x%02x%02x", &r_val, &g_val, &b_val) != 3) {
241 return absl::InvalidArgumentError(
242 absl::StrFormat("Failed to parse color hex: '%s'", color_str));
243 }
244
245 uint8_t r = static_cast<uint8_t>(r_val);
246 uint8_t g = static_cast<uint8_t>(g_val);
247 uint8_t b = static_cast<uint8_t>(b_val);
248
249 // Load palette data from ROM.
250 zelda3::GameData game_data;
251 RETURN_IF_ERROR(LoadPalettes(rom, game_data));
252
253 auto* group = game_data.palette_groups.get_group(group_name);
254 if (!group) {
255 return absl::NotFoundError(
256 absl::StrFormat("Unknown palette group: '%s'", group_name));
257 }
258
259 if (palette_idx < 0 || palette_idx >= static_cast<int>(group->size())) {
260 return absl::InvalidArgumentError(absl::StrFormat(
261 "Palette index %d out of range [0, %d)", palette_idx, group->size()));
262 }
263
264 const auto& pal = group->palette_ref(palette_idx);
265 if (color_idx < 0 || color_idx >= static_cast<int>(pal.size())) {
266 return absl::InvalidArgumentError(absl::StrFormat(
267 "Color index %d out of range [0, %d)", color_idx, pal.size()));
268 }
269
270 // Build the new SNES color.
271 gfx::SnesColor new_color(r, g, b);
272
273 // Read the old color for reporting.
274 auto old_color = group->GetColor(palette_idx, color_idx);
275 std::string old_hex = FormatColorHex(old_color);
276
277 formatter.BeginObject("Palette Color Set");
278 formatter.AddField("group", group_name);
279 formatter.AddField("palette_index", palette_idx);
280 formatter.AddField("color_index", color_idx);
281 formatter.AddField("old_color", old_hex);
282 formatter.AddField("new_color", FormatColorHex(new_color));
283 formatter.AddHexField("old_snes", old_color.snes(), 4);
284 formatter.AddHexField("new_snes", new_color.snes(), 4);
285
286 formatter.AddField("status", "dry_run");
287 formatter.AddField(
288 "message",
289 "Legacy preview only; use a domain-specific manifest-safe command for "
290 "persistent writes");
291
292 formatter.EndObject();
293 return absl::OkStatus();
294}
295
296// ---------------------------------------------------------------------------
297// palette-analyze
298// ---------------------------------------------------------------------------
300 Rom* rom, const resources::ArgumentParser& parser,
301 resources::OutputFormatter& formatter) {
302 auto group_name_opt = parser.GetString("group");
303
304 // Load palette data from ROM.
305 zelda3::GameData game_data;
306 RETURN_IF_ERROR(LoadPalettes(rom, game_data));
307
308 formatter.BeginObject("Palette Analysis");
309
310 if (group_name_opt.has_value()) {
311 // Analyze a single group.
312 const std::string& group_name = group_name_opt.value();
313 const auto* group = game_data.palette_groups.get_group(group_name);
314 if (!group) {
315 return absl::NotFoundError(
316 absl::StrFormat("Unknown palette group: '%s'", group_name));
317 }
318 formatter.AddField("analysis_type", "single_group");
319 AnalyzeGroup(group_name, *group, formatter);
320 } else {
321 // Analyze all groups.
322 formatter.AddField("analysis_type", "all_groups");
323
324 int total_palettes = 0;
325 int total_colors = 0;
326 std::set<uint16_t> global_unique;
327
328 formatter.BeginArray("groups");
329 for (int i = 0; i < kNumGroups; ++i) {
330 const std::string name = kAllGroupNames[i];
331 const auto* group = game_data.palette_groups.get_group(name);
332 if (!group)
333 continue;
334
335 int group_colors = 0;
336 for (size_t p = 0; p < group->size(); ++p) {
337 const auto& pal = group->palette_ref(p);
338 group_colors += static_cast<int>(pal.size());
339 for (size_t c = 0; c < pal.size(); ++c) {
340 global_unique.insert(pal[c].snes());
341 }
342 }
343 total_palettes += static_cast<int>(group->size());
344 total_colors += group_colors;
345
346 std::string summary = absl::StrFormat("%s: %zu palettes, %d colors", name,
347 group->size(), group_colors);
348 formatter.AddArrayItem(summary);
349 }
350 formatter.EndArray();
351
352 formatter.AddField("total_groups", kNumGroups);
353 formatter.AddField("total_palettes", total_palettes);
354 formatter.AddField("total_colors", total_colors);
355 formatter.AddField("unique_colors_global",
356 static_cast<int>(global_unique.size()));
357
358 // Brightness histogram (buckets of 32).
359 formatter.BeginObject("brightness_distribution");
360 int buckets[8] = {};
361 for (int i = 0; i < kNumGroups; ++i) {
362 const auto* group = game_data.palette_groups.get_group(kAllGroupNames[i]);
363 if (!group)
364 continue;
365 for (size_t p = 0; p < group->size(); ++p) {
366 const auto& pal = group->palette_ref(p);
367 for (size_t c = 0; c < pal.size(); ++c) {
368 int br = ColorBrightness(pal[c]);
369 int bucket = std::min(br / 32, 7);
370 buckets[bucket]++;
371 }
372 }
373 }
374 formatter.AddField("0-31_dark", buckets[0]);
375 formatter.AddField("32-63", buckets[1]);
376 formatter.AddField("64-95", buckets[2]);
377 formatter.AddField("96-127", buckets[3]);
378 formatter.AddField("128-159", buckets[4]);
379 formatter.AddField("160-191", buckets[5]);
380 formatter.AddField("192-223", buckets[6]);
381 formatter.AddField("224-255_bright", buckets[7]);
382 formatter.EndObject();
383 }
384
385 formatter.EndObject();
386 return absl::OkStatus();
387}
388
389} // namespace handlers
390} // namespace cli
391} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
SNES Color container.
Definition snes_color.h:110
constexpr ImVec4 rgb() const
Get RGB values (WARNING: stored as 0-255 in ImVec4)
Definition snes_color.h:183
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
std::string FormatColorHex(const gfx::SnesColor &color)
void FormatPaletteColors(const gfx::SnesPalette &palette, resources::OutputFormatter &formatter)
void AnalyzeGroup(const std::string &group_name, const gfx::PaletteGroup &group, resources::OutputFormatter &formatter)
uint32_t GetPaletteAddress(const std::string &group_name, size_t palette_index, size_t color_index)
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
absl::Status LoadPalettes(const Rom &rom, GameData &data)
Definition game_data.cc:208
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
PaletteGroup * get_group(const std::string &group_name)
Represents a group of palettes.
const SnesPalette & palette_ref(int i) const
void set_rom(Rom *rom)
Definition game_data.h:77
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92