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
208 int palette_idx;
209 if (!absl::SimpleAtoi(palette_str, &palette_idx)) {
210 return absl::InvalidArgumentError(
211 absl::StrFormat("Invalid palette index: '%s'", palette_str));
212 }
213
214 int color_idx;
215 if (!absl::SimpleAtoi(index_str, &color_idx)) {
216 return absl::InvalidArgumentError(
217 absl::StrFormat("Invalid color index: '%s'", index_str));
218 }
219
220 // Parse RGB hex color (RRGGBB, with or without leading # or 0x).
221 std::string hex = color_str;
222 if (!hex.empty() && hex[0] == '#')
223 hex = hex.substr(1);
224 if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
225 hex = hex.substr(2);
226
227 if (hex.size() != 6) {
228 return absl::InvalidArgumentError(absl::StrFormat(
229 "Color must be a 6-digit hex RGB value (e.g., FF0000). Got: '%s'",
230 color_str));
231 }
232
233 unsigned int r_val, g_val, b_val;
234 if (sscanf(hex.c_str(), "%02x%02x%02x", &r_val, &g_val, &b_val) != 3) {
235 return absl::InvalidArgumentError(
236 absl::StrFormat("Failed to parse color hex: '%s'", color_str));
237 }
238
239 uint8_t r = static_cast<uint8_t>(r_val);
240 uint8_t g = static_cast<uint8_t>(g_val);
241 uint8_t b = static_cast<uint8_t>(b_val);
242
243 // Load palette data from ROM.
244 zelda3::GameData game_data;
245 RETURN_IF_ERROR(LoadPalettes(rom, game_data));
246
247 auto* group = game_data.palette_groups.get_group(group_name);
248 if (!group) {
249 return absl::NotFoundError(
250 absl::StrFormat("Unknown palette group: '%s'", group_name));
251 }
252
253 if (palette_idx < 0 || palette_idx >= static_cast<int>(group->size())) {
254 return absl::InvalidArgumentError(absl::StrFormat(
255 "Palette index %d out of range [0, %d)", palette_idx, group->size()));
256 }
257
258 const auto& pal = group->palette_ref(palette_idx);
259 if (color_idx < 0 || color_idx >= static_cast<int>(pal.size())) {
260 return absl::InvalidArgumentError(absl::StrFormat(
261 "Color index %d out of range [0, %d)", color_idx, pal.size()));
262 }
263
264 // Build the new SNES color.
265 gfx::SnesColor new_color(r, g, b);
266
267 // Read the old color for reporting.
268 auto old_color = group->GetColor(palette_idx, color_idx);
269 std::string old_hex = FormatColorHex(old_color);
270
271 formatter.BeginObject("Palette Color Set");
272 formatter.AddField("group", group_name);
273 formatter.AddField("palette_index", palette_idx);
274 formatter.AddField("color_index", color_idx);
275 formatter.AddField("old_color", old_hex);
276 formatter.AddField("new_color", FormatColorHex(new_color));
277 formatter.AddHexField("old_snes", old_color.snes(), 4);
278 formatter.AddHexField("new_snes", new_color.snes(), 4);
279
280 if (write) {
281 // Compute the ROM address for this color entry and write 2 bytes.
282 uint32_t addr = gfx::GetPaletteAddress(group_name, palette_idx, color_idx);
283 uint16_t snes_val = new_color.snes();
284 RETURN_IF_ERROR(rom->WriteShort(addr, snes_val));
285
286 formatter.AddField("status", "written");
287 formatter.AddHexField("rom_address", addr, 6);
288 } else {
289 formatter.AddField("status", "dry_run");
290 formatter.AddField("message", "Use --write to apply the change to the ROM");
291 }
292
293 formatter.EndObject();
294 return absl::OkStatus();
295}
296
297// ---------------------------------------------------------------------------
298// palette-analyze
299// ---------------------------------------------------------------------------
301 Rom* rom, const resources::ArgumentParser& parser,
302 resources::OutputFormatter& formatter) {
303 auto group_name_opt = parser.GetString("group");
304
305 // Load palette data from ROM.
306 zelda3::GameData game_data;
307 RETURN_IF_ERROR(LoadPalettes(rom, game_data));
308
309 formatter.BeginObject("Palette Analysis");
310
311 if (group_name_opt.has_value()) {
312 // Analyze a single group.
313 const std::string& group_name = group_name_opt.value();
314 const auto* group = game_data.palette_groups.get_group(group_name);
315 if (!group) {
316 return absl::NotFoundError(
317 absl::StrFormat("Unknown palette group: '%s'", group_name));
318 }
319 formatter.AddField("analysis_type", "single_group");
320 AnalyzeGroup(group_name, *group, formatter);
321 } else {
322 // Analyze all groups.
323 formatter.AddField("analysis_type", "all_groups");
324
325 int total_palettes = 0;
326 int total_colors = 0;
327 std::set<uint16_t> global_unique;
328
329 formatter.BeginArray("groups");
330 for (int i = 0; i < kNumGroups; ++i) {
331 const std::string name = kAllGroupNames[i];
332 const auto* group = game_data.palette_groups.get_group(name);
333 if (!group)
334 continue;
335
336 int group_colors = 0;
337 for (size_t p = 0; p < group->size(); ++p) {
338 const auto& pal = group->palette_ref(p);
339 group_colors += static_cast<int>(pal.size());
340 for (size_t c = 0; c < pal.size(); ++c) {
341 global_unique.insert(pal[c].snes());
342 }
343 }
344 total_palettes += static_cast<int>(group->size());
345 total_colors += group_colors;
346
347 std::string summary = absl::StrFormat("%s: %zu palettes, %d colors", name,
348 group->size(), group_colors);
349 formatter.AddArrayItem(summary);
350 }
351 formatter.EndArray();
352
353 formatter.AddField("total_groups", kNumGroups);
354 formatter.AddField("total_palettes", total_palettes);
355 formatter.AddField("total_colors", total_colors);
356 formatter.AddField("unique_colors_global",
357 static_cast<int>(global_unique.size()));
358
359 // Brightness histogram (buckets of 32).
360 formatter.BeginObject("brightness_distribution");
361 int buckets[8] = {};
362 for (int i = 0; i < kNumGroups; ++i) {
363 const auto* group = game_data.palette_groups.get_group(kAllGroupNames[i]);
364 if (!group)
365 continue;
366 for (size_t p = 0; p < group->size(); ++p) {
367 const auto& pal = group->palette_ref(p);
368 for (size_t c = 0; c < pal.size(); ++c) {
369 int br = ColorBrightness(pal[c]);
370 int bucket = std::min(br / 32, 7);
371 buckets[bucket]++;
372 }
373 }
374 }
375 formatter.AddField("0-31_dark", buckets[0]);
376 formatter.AddField("32-63", buckets[1]);
377 formatter.AddField("64-95", buckets[2]);
378 formatter.AddField("96-127", buckets[3]);
379 formatter.AddField("128-159", buckets[4]);
380 formatter.AddField("160-191", buckets[5]);
381 formatter.AddField("192-223", buckets[6]);
382 formatter.AddField("224-255_bright", buckets[7]);
383 formatter.EndObject();
384 }
385
386 formatter.EndObject();
387 return absl::OkStatus();
388}
389
390} // namespace handlers
391} // namespace cli
392} // 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 WriteShort(int addr, uint16_t value)
Definition rom.cc:518
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:206
#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:76
gfx::PaletteGroupMap palette_groups
Definition game_data.h:91