6#include "absl/strings/str_format.h"
19 std::vector<std::pair<uint32_t, uint32_t>> ranges) {
20 std::sort(ranges.begin(), ranges.end());
21 std::vector<std::pair<uint32_t, uint32_t>> coalesced;
22 coalesced.reserve(ranges.size());
23 for (
const auto& range : ranges) {
24 if (coalesced.empty() || coalesced.back().second < range.first) {
25 coalesced.push_back(range);
28 coalesced.back().second = std::max(coalesced.back().second, range.second);
61 return rom_ !=
nullptr;
69 rom_ = game_data ? game_data->
rom() :
nullptr;
82 if (state->initialized) {
92 const char* group_names[] = {
"ow_main",
"ow_aux",
"ow_animated",
93 "hud",
"global_sprites",
"armors",
94 "swords",
"shields",
"sprites_aux1",
95 "sprites_aux2",
"sprites_aux3",
"dungeon_main",
96 "grass",
"3d_object",
"ow_mini_map"};
98 for (
const auto& group_name : group_names) {
100 auto* group = palette_groups->
get_group(group_name);
102 std::vector<SnesPalette> originals;
103 for (
size_t i = 0; i < group->size(); i++) {
104 originals.push_back(group->palette(i));
106 state->original_palettes[group_name] = originals;
108 }
catch (
const std::exception& e) {
114 state->initialized =
true;
118 return game_data !=
nullptr &&
game_data_ == game_data &&
160 int palette_index,
int color_index)
const {
161 const auto* group =
GetGroup(group_name);
162 if (!group || palette_index < 0 || palette_index >= group->size()) {
166 const auto& palette = group->palette_ref(palette_index);
167 if (color_index < 0 || color_index >= palette.size()) {
171 return palette[color_index];
175 int palette_index,
int color_index,
178 return absl::FailedPreconditionError(
"PaletteManager not initialized");
183 return absl::NotFoundError(
184 absl::StrFormat(
"Palette group '%s' not found", group_name));
187 if (palette_index < 0 || palette_index >= group->size()) {
188 return absl::InvalidArgumentError(absl::StrFormat(
189 "Palette index %d out of range [0, %d)", palette_index, group->size()));
192 auto* palette = group->mutable_palette(palette_index);
193 if (color_index < 0 || color_index >= palette->size()) {
194 return absl::InvalidArgumentError(absl::StrFormat(
195 "Color index %d out of range [0, %d)", color_index, palette->size()));
199 SnesColor original_color = (*palette)[color_index];
202 (*palette)[color_index] = new_color;
209 auto now = std::chrono::system_clock::now();
210 auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
211 now.time_since_epoch())
215 color_index, original_color,
216 new_color,
static_cast<uint64_t
>(timestamp_ms)};
221 group_name, palette_index, color_index};
225 auto now = std::chrono::system_clock::now();
226 auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
227 now.time_since_epoch())
230 {group_name, palette_index, color_index, original_color, new_color,
231 static_cast<uint64_t
>(timestamp_ms)});
234 return absl::OkStatus();
238 int palette_index,
int color_index) {
240 return SetColor(group_name, palette_index, color_index, original);
246 return absl::FailedPreconditionError(
"PaletteManager not initialized");
251 auto it = state->original_palettes.find(group_name);
252 if (it == state->original_palettes.end() || palette_index < 0 ||
253 palette_index >= it->second.size()) {
254 return absl::NotFoundError(
"Original palette not found");
258 if (!group || palette_index >= group->size()) {
259 return absl::NotFoundError(
"Palette group or index not found");
263 *group->mutable_palette(palette_index) = it->second[palette_index];
266 if (
auto modified_it = state->modified_palettes.find(group_name);
267 modified_it != state->modified_palettes.end()) {
268 modified_it->second.erase(palette_index);
269 if (modified_it->second.empty()) {
270 state->modified_palettes.erase(modified_it);
273 if (
auto colors_it = state->modified_colors.find(group_name);
274 colors_it != state->modified_colors.end()) {
275 colors_it->second.erase(palette_index);
276 if (colors_it->second.empty()) {
277 state->modified_colors.erase(colors_it);
286 return absl::OkStatus();
297 const auto* state =
FindState(game_data);
298 return state !=
nullptr && !state->modified_palettes.empty();
302 std::vector<std::string> groups;
304 groups.push_back(group_name);
315 int palette_index)
const {
320 return it->second.contains(palette_index);
324 int palette_index,
int color_index)
const {
326 if (group_it ==
CurrentState()->modified_colors.end()) {
330 auto pal_it = group_it->second.find(palette_index);
331 if (pal_it == group_it->second.end()) {
335 return pal_it->second.contains(color_index);
344 const auto* state =
FindState(game_data);
345 if (state ==
nullptr) {
350 for (
const auto& [_, palette_map] : state->modified_colors) {
351 for (
const auto& [__, color_set] : palette_map) {
352 count += color_set.size();
358std::vector<std::pair<uint32_t, uint32_t>>
363std::vector<std::pair<uint32_t, uint32_t>>
366 std::vector<std::pair<uint32_t, uint32_t>> ranges;
367 const auto* state =
FindState(game_data);
368 if (state ==
nullptr) {
373 for (
const auto& [group_name, palette_map] : state->modified_colors) {
374 for (
const auto& [palette_index, color_indices] : palette_map) {
375 for (
int color_index : color_indices) {
376 const uint32_t begin =
378 ranges.emplace_back(begin, begin + 2u);
383 return CoalescePaletteWriteRanges(std::move(ranges));
386std::vector<std::pair<uint32_t, uint32_t>>
388 const std::string& group_name)
const {
389 std::vector<std::pair<uint32_t, uint32_t>> ranges;
391 const auto group_it = state->modified_colors.find(group_name);
392 if (group_it == state->modified_colors.end()) {
396 for (
const auto& [palette_index, color_indices] : group_it->second) {
397 for (
int color_index : color_indices) {
398 const uint32_t begin =
400 ranges.emplace_back(begin, begin + 2u);
403 return CoalescePaletteWriteRanges(std::move(ranges));
410 return absl::FailedPreconditionError(
"PaletteManager not initialized");
418 return absl::FailedPreconditionError(
"No ROM available for palette save");
423 return absl::NotFoundError(
424 absl::StrFormat(
"Palette group '%s' not found", group_name));
429 if (pal_it ==
CurrentState()->modified_palettes.end() ||
430 pal_it->second.empty()) {
432 return absl::OkStatus();
436 for (
int palette_idx : pal_it->second) {
437 auto* palette = group->mutable_palette(palette_idx);
442 if (color_it !=
CurrentState()->modified_colors[group_name].end()) {
443 for (
int color_idx : color_it->second) {
456 for (
size_t i = 0; i < group->size() && i < originals.size(); i++) {
457 originals[i] = group->palette(i);
474 return absl::OkStatus();
479 return absl::FailedPreconditionError(
"PaletteManager not initialized");
491 return absl::OkStatus();
496 return absl::FailedPreconditionError(
"PaletteManager not initialized");
500 if (state->save_transaction_snapshot.has_value()) {
501 return absl::FailedPreconditionError(
502 "Palette save transaction is already active");
504 state->save_transaction_snapshot =
506 state->modified_palettes, state->modified_colors};
507 return absl::OkStatus();
512 if (!state->save_transaction_snapshot.has_value()) {
515 state->original_palettes =
516 std::move(state->save_transaction_snapshot->original_palettes);
517 state->modified_palettes =
518 std::move(state->save_transaction_snapshot->modified_palettes);
519 state->modified_colors =
520 std::move(state->save_transaction_snapshot->modified_colors);
521 state->save_transaction_snapshot.reset();
530 return absl::FailedPreconditionError(
"PaletteManager not initialized");
537 if (modified_groups.empty()) {
538 return absl::OkStatus();
541 for (
const auto& group_name : modified_groups) {
549 return absl::OkStatus();
564 if (pal_it ==
CurrentState()->modified_palettes.end()) {
570 if (orig_it !=
CurrentState()->original_palettes.end()) {
571 for (
int palette_idx : pal_it->second) {
572 if (palette_idx < orig_it->second.size()) {
573 *group->mutable_palette(palette_idx) = orig_it->second[palette_idx];
617 if (group && change.palette_index < group->size()) {
618 auto* palette = group->mutable_palette(change.palette_index);
619 if (change.color_index < palette->size()) {
620 (*palette)[change.color_index] = change.original_color;
621 MarkModified(change.group_name, change.palette_index, change.color_index);
645 if (group && change.palette_index < group->size()) {
646 auto* palette = group->mutable_palette(change.palette_index);
647 if (change.color_index < palette->size()) {
648 (*palette)[change.color_index] = change.new_color;
649 MarkModified(change.group_name, change.palette_index, change.color_index);
744 }
catch (
const std::exception&) {
750 const std::string& group_name)
const {
757 ->get_group(group_name);
760 }
catch (
const std::exception&) {
767 int color_index)
const {
769 auto it = state->original_palettes.find(group_name);
770 if (it == state->original_palettes.end() || palette_index < 0 ||
771 palette_index >= it->second.size()) {
775 const auto& palette = it->second[palette_index];
776 if (color_index < 0 || color_index >= palette.size()) {
780 return palette[color_index];
802 int palette_index,
int color_index) {
804 const auto* group =
GetGroup(group_name);
805 const auto original_it = state->original_palettes.find(group_name);
806 const bool matches_original =
807 group !=
nullptr && palette_index >= 0 && palette_index < group->size() &&
809 color_index < group->palette_ref(palette_index).size() &&
810 original_it != state->original_palettes.end() &&
811 palette_index < original_it->second.size() &&
812 color_index < original_it->second[palette_index].size() &&
813 group->palette_ref(palette_index)[color_index].snes() ==
814 original_it->second[palette_index][color_index].snes();
816 if (!matches_original) {
817 state->modified_palettes[group_name].insert(palette_index);
818 state->modified_colors[group_name][palette_index].insert(color_index);
822 if (
auto group_it = state->modified_colors.find(group_name);
823 group_it != state->modified_colors.end()) {
824 if (
auto palette_it = group_it->second.find(palette_index);
825 palette_it != group_it->second.end()) {
826 palette_it->second.erase(color_index);
827 if (palette_it->second.empty()) {
828 group_it->second.erase(palette_it);
831 if (group_it->second.empty()) {
832 state->modified_colors.erase(group_it);
836 const auto colors_group_it = state->modified_colors.find(group_name);
837 const bool palette_still_modified =
838 colors_group_it != state->modified_colors.end() &&
839 colors_group_it->second.contains(palette_index);
840 if (!palette_still_modified) {
841 if (
auto group_it = state->modified_palettes.find(group_name);
842 group_it != state->modified_palettes.end()) {
843 group_it->second.erase(palette_index);
844 if (group_it->second.empty()) {
845 state->modified_palettes.erase(group_it);
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
void set_dirty(bool dirty)
absl::Status WriteShort(int addr, uint16_t value)
void NotifyPaletteModified(const std::string &group_name, int palette_index=-1)
Notify all listeners that a palette has been modified.
void RecordChange(const PaletteColorChange &change)
Helper: Record a change for undo.
absl::Status SetColor(const std::string &group_name, int palette_index, int color_index, const SnesColor &new_color)
Set a color in a palette (records change for undo)
std::vector< std::string > GetModifiedGroups() const
Get list of modified palette group names.
static constexpr size_t kMaxUndoHistory
bool HasUnsavedChanges() const
Check if there are ANY unsaved changes.
bool IsGroupModified(const std::string &group_name) const
Check if a specific palette group has modifications.
absl::Status SaveGroup(const std::string &group_name)
Save a specific palette group to ROM.
std::vector< std::pair< uint32_t, uint32_t > > GetModifiedColorWriteRanges() const
Get exact, coalesced half-open ROM ranges for modified colors.
void BeginBatch()
Begin a batch operation (groups multiple changes into one undo step)
void RollbackSaveTransaction()
PaletteGroup * GetMutableGroup(const std::string &group_name)
Helper: Get mutable palette group.
void ActivateSession(zelda3::GameData *game_data)
Select the palette state for a ROM session without resnapshotting it.
size_t GetUndoStackSize() const
Get size of undo stack.
void Undo()
Undo the most recent change.
size_t GetRedoStackSize() const
Get size of redo stack.
void ClearHistory()
Clear undo/redo history.
std::unordered_map< int, ChangeCallback > change_listeners_
Change listeners.
std::vector< std::pair< uint32_t, uint32_t > > GetModifiedGroupColorWriteRanges(const std::string &group_name) const
Get exact, coalesced half-open ROM ranges for one modified group.
void UnregisterChangeListener(int callback_id)
Unregister a change listener.
Rom * rom_
ROM instance (not owned) - legacy, used when game_data_ is null.
bool IsManaging(const zelda3::GameData *game_data) const
Check whether the manager is bound to this exact GameData and ROM.
bool CanRedo() const
Check if redo is available.
bool InBatch() const
Check if currently in a batch operation.
bool CanUndo() const
Check if undo is available.
void CommitSaveTransaction()
void Initialize(zelda3::GameData *game_data)
Initialize the palette manager with GameData.
void ResetForTesting()
Reset all state for test isolation.
absl::Status ResetColor(const std::string &group_name, int palette_index, int color_index)
Reset a single color to its original ROM value.
const SessionState * FindState(const zelda3::GameData *game_data) const
void NotifyListeners(const PaletteChangeEvent &event)
Helper: Notify all listeners of an event.
void ClearModifiedFlags(const std::string &group_name)
Helper: Clear modified flags for a group.
void EndBatch()
End a batch operation.
int RegisterChangeListener(ChangeCallback callback)
Register a callback for palette change events.
std::unordered_map< const zelda3::GameData *, SessionState > session_states_
Per-GameData state. RomSession owns the keys and releases them on teardown.
SessionState unbound_state_
State used only before a GameData session is selected (legacy/tests).
absl::Status ResetPalette(const std::string &group_name, int palette_index)
Reset an entire palette to original ROM values.
SnesColor GetOriginalColor(const std::string &group_name, int palette_index, int color_index) const
Helper: Get original color from snapshot.
bool IsColorModified(const std::string &group_name, int palette_index, int color_index) const
Check if a specific color is modified.
void MarkModified(const std::string &group_name, int palette_index, int color_index)
Helper: Mark a color as modified.
void DiscardGroup(const std::string &group_name)
Discard changes for a specific group.
bool IsInitialized() const
Check if manager is initialized.
zelda3::GameData * game_data_
GameData instance (not owned) - preferred.
void DiscardAllChanges()
Discard ALL unsaved changes.
size_t GetModifiedColorCount() const
Get count of modified colors across all groups.
const PaletteGroup * GetGroup(const std::string &group_name) const
Helper: Get const palette group.
SnesColor GetColor(const std::string &group_name, int palette_index, int color_index) const
Get a color from a palette.
std::function< void(const PaletteChangeEvent &)> ChangeCallback
SessionState * CurrentState()
absl::Status SaveAllToRom()
Save ALL modified palettes to ROM.
absl::Status BeginSaveTransaction()
bool IsPaletteModified(const std::string &group_name, int palette_index) const
Check if a specific palette is modified.
absl::Status ApplyPreviewChanges()
Apply preview changes to other editors without saving to ROM.
void Redo()
Redo the most recently undone change.
bool IsSessionActive(const zelda3::GameData *game_data) const
Check whether this exact GameData is the active palette session.
void ReleaseSession(const zelda3::GameData *game_data)
Forget all palette tracking for a closing or reloaded ROM session.
std::vector< std::pair< uint32_t, uint32_t > > CoalescePaletteWriteRanges(std::vector< std::pair< uint32_t, uint32_t > > ranges)
uint32_t GetPaletteAddress(const std::string &group_name, size_t palette_index, size_t color_index)
#define RETURN_IF_ERROR(expr)
Event notification for palette changes.
@ kColorChanged
Single color was modified.
@ kPaletteReset
Entire palette was reset.
@ kAllDiscarded
All changes discarded.
@ kGroupDiscarded
Palette group changes were discarded.
@ kAllSaved
All changes saved to ROM.
@ kGroupSaved
Palette group was saved to ROM.
Represents a single color change operation.
Represents a mapping of palette groups.
PaletteGroup * get_group(const std::string &group_name)
Represents a group of palettes.
std::unordered_map< std::string, std::vector< SnesPalette > > original_palettes
std::vector< PaletteColorChange > batch_changes
std::unordered_map< std::string, std::unordered_map< int, std::unordered_set< int > > > modified_colors
std::unordered_map< std::string, std::vector< SnesPalette > > original_palettes
std::optional< SaveTransactionSnapshot > save_transaction_snapshot
std::deque< PaletteColorChange > redo_stack
std::unordered_map< std::string, std::unordered_set< int > > modified_palettes
std::deque< PaletteColorChange > undo_stack
gfx::PaletteGroupMap palette_groups