9#include "absl/strings/str_format.h"
22 std::vector<std::pair<uint32_t, uint32_t>> ranges) {
23 std::sort(ranges.begin(), ranges.end());
24 std::vector<std::pair<uint32_t, uint32_t>> coalesced;
25 coalesced.reserve(ranges.size());
26 for (
const auto& range : ranges) {
27 if (coalesced.empty() || coalesced.back().second < range.first) {
28 coalesced.push_back(range);
31 coalesced.back().second = std::max(coalesced.back().second, range.second);
64 return rom_ !=
nullptr;
73 return state ? state->color_revision : 0;
78 rom_ = game_data ? game_data->
rom() :
nullptr;
91 if (state->initialized) {
101 const char* group_names[] = {
"ow_main",
"ow_aux",
"ow_animated",
102 "hud",
"global_sprites",
"armors",
103 "swords",
"shields",
"sprites_aux1",
104 "sprites_aux2",
"sprites_aux3",
"dungeon_main",
105 "grass",
"3d_object",
"ow_mini_map"};
107 for (
const auto& group_name : group_names) {
109 auto* group = palette_groups->
get_group(group_name);
111 std::vector<SnesPalette> originals;
112 for (
size_t i = 0; i < group->size(); i++) {
113 originals.push_back(group->palette(i));
115 state->original_palettes[group_name] = originals;
117 }
catch (
const std::exception& e) {
123 state->initialized =
true;
127 return game_data !=
nullptr &&
game_data_ == game_data &&
169 int palette_index,
int color_index)
const {
170 const auto* group =
GetGroup(group_name);
171 if (!group || palette_index < 0 || palette_index >= group->size()) {
175 const auto& palette = group->palette_ref(palette_index);
176 if (color_index < 0 || color_index >= palette.size()) {
180 return palette[color_index];
184 int palette_index,
int color_index,
187 return absl::FailedPreconditionError(
"PaletteManager not initialized");
192 return absl::NotFoundError(
193 absl::StrFormat(
"Palette group '%s' not found", group_name));
196 if (palette_index < 0 || palette_index >= group->size()) {
197 return absl::InvalidArgumentError(absl::StrFormat(
198 "Palette index %d out of range [0, %d)", palette_index, group->size()));
201 auto* palette = group->mutable_palette(palette_index);
202 if (color_index < 0 || color_index >= palette->size()) {
203 return absl::InvalidArgumentError(absl::StrFormat(
204 "Color index %d out of range [0, %d)", color_index, palette->size()));
208 if (state->color_edit && (state->color_edit->group_name != group_name ||
209 state->color_edit->palette_index != palette_index ||
210 state->color_edit->color_index != color_index)) {
213 const SnesColor original_color = (*palette)[color_index];
214 if (original_color.
snes() == new_color.
snes()) {
215 return absl::OkStatus();
217 (*palette)[color_index] = new_color;
220 const auto timestamp_ms =
221 std::chrono::duration_cast<std::chrono::milliseconds>(
222 std::chrono::system_clock::now().time_since_epoch())
225 color_index, original_color,
226 new_color,
static_cast<uint64_t
>(timestamp_ms)};
227 if (state->color_edit) {
228 state->color_edit->new_color = new_color;
229 state->color_edit->timestamp_ms = change.timestamp_ms;
231 state->batch_changes.push_back(change);
232 return absl::OkStatus();
237 palette_index, color_index});
239 return absl::OkStatus();
244 const std::vector<PaletteColorChange>& changes,
245 const std::function<absl::Status()>& participant) {
247 return absl::FailedPreconditionError(
248 "Palette command session is unavailable");
251 if (state->color_edit || state->batch_depth ||
252 state->save_transaction_snapshot) {
253 return absl::FailedPreconditionError(
254 "Finish the active palette operation first");
256 std::set<std::tuple<std::string, int, int>> addresses;
257 std::vector<SnesColor*> slots;
258 for (
const auto& change : changes) {
260 if (!group || change.palette_index < 0 ||
261 change.palette_index >= group->size()) {
262 return absl::InvalidArgumentError(
"Invalid command palette");
264 auto* palette = group->mutable_palette(change.palette_index);
265 if (change.color_index < 0 || change.color_index >= palette->size() ||
267 .emplace(change.group_name, change.palette_index,
270 return absl::InvalidArgumentError(
"Invalid or duplicate command color");
272 auto* slot = &(*palette)[change.color_index];
273 if (slot->snes() != change.original_color.snes()) {
274 return absl::FailedPreconditionError(
275 "Palette changed since command planning");
277 slots.push_back(slot);
279 const auto before = *state;
280 for (
size_t i = 0; i < changes.size(); ++i) {
281 *slots[i] = changes[i].new_color;
282 MarkModified(changes[i].group_name, changes[i].palette_index,
283 changes[i].color_index);
285 const auto status = participant();
287 for (
size_t i = 0; i < changes.size(); ++i)
288 *slots[i] = changes[i].original_color;
292 for (
const auto& change : changes) {
296 change.palette_index, change.color_index});
298 return absl::OkStatus();
302 int palette_index,
int color_index) {
304 return SetColor(group_name, palette_index, color_index, original);
311 return absl::FailedPreconditionError(
"PaletteManager not initialized");
316 auto it = state->original_palettes.find(group_name);
317 if (it == state->original_palettes.end() || palette_index < 0 ||
318 palette_index >= it->second.size()) {
319 return absl::NotFoundError(
"Original palette not found");
323 if (!group || palette_index >= group->size()) {
324 return absl::NotFoundError(
"Palette group or index not found");
328 *group->mutable_palette(palette_index) = it->second[palette_index];
331 if (
auto modified_it = state->modified_palettes.find(group_name);
332 modified_it != state->modified_palettes.end()) {
333 modified_it->second.erase(palette_index);
334 if (modified_it->second.empty()) {
335 state->modified_palettes.erase(modified_it);
338 if (
auto colors_it = state->modified_colors.find(group_name);
339 colors_it != state->modified_colors.end()) {
340 colors_it->second.erase(palette_index);
341 if (colors_it->second.empty()) {
342 state->modified_colors.erase(colors_it);
351 return absl::OkStatus();
355 int palette_index,
int color_index,
356 uint64_t caller_id) {
358 return absl::FailedPreconditionError(
359 "Color interaction requires an initialized session outside a batch");
361 const auto* group =
GetGroup(group_name);
362 if (!group || palette_index < 0 || palette_index >= group->size() ||
364 color_index >= group->palette_ref(palette_index).size()) {
365 return absl::InvalidArgumentError(
"Invalid color interaction target");
368 const auto color = group->palette_ref(palette_index)[color_index];
371 group_name, palette_index, color_index, color, color, 0};
372 return absl::OkStatus();
380 auto change = std::move(*state->color_edit);
381 state->color_edit.reset();
382 if (change.original_color.snes() != change.new_color.snes()) {
392 const auto change = std::move(*state->color_edit);
393 state->color_edit.reset();
395 if (group && change.palette_index < group->size()) {
396 auto* palette = group->mutable_palette(change.palette_index);
397 if (change.color_index < palette->size() &&
398 (*palette)[change.color_index].snes() != change.original_color.snes()) {
399 (*palette)[change.color_index] = change.original_color;
400 MarkModified(change.group_name, change.palette_index, change.color_index);
402 change.group_name, change.palette_index,
403 change.color_index});
410 return state->color_edit.has_value() &&
411 (caller_id == 0 || state->color_edit_caller == caller_id);
422 const auto* state =
FindState(game_data);
423 return state !=
nullptr && !state->modified_palettes.empty();
427 std::vector<std::string> groups;
429 groups.push_back(group_name);
440 int palette_index)
const {
445 return it->second.contains(palette_index);
449 int palette_index,
int color_index)
const {
451 if (group_it ==
CurrentState()->modified_colors.end()) {
455 auto pal_it = group_it->second.find(palette_index);
456 if (pal_it == group_it->second.end()) {
460 return pal_it->second.contains(color_index);
469 const auto* state =
FindState(game_data);
470 if (state ==
nullptr) {
475 for (
const auto& [_, palette_map] : state->modified_colors) {
476 for (
const auto& [__, color_set] : palette_map) {
477 count += color_set.size();
483std::vector<std::pair<uint32_t, uint32_t>>
488std::vector<std::pair<uint32_t, uint32_t>>
491 std::vector<std::pair<uint32_t, uint32_t>> ranges;
492 const auto* state =
FindState(game_data);
493 if (state ==
nullptr) {
498 for (
const auto& [group_name, palette_map] : state->modified_colors) {
499 for (
const auto& [palette_index, color_indices] : palette_map) {
500 for (
int color_index : color_indices) {
501 const uint32_t begin =
503 ranges.emplace_back(begin, begin + 2u);
508 return CoalescePaletteWriteRanges(std::move(ranges));
511std::vector<std::pair<uint32_t, uint32_t>>
513 const std::string& group_name)
const {
514 std::vector<std::pair<uint32_t, uint32_t>> ranges;
516 const auto group_it = state->modified_colors.find(group_name);
517 if (group_it == state->modified_colors.end()) {
521 for (
const auto& [palette_index, color_indices] : group_it->second) {
522 for (
int color_index : color_indices) {
523 const uint32_t begin =
525 ranges.emplace_back(begin, begin + 2u);
528 return CoalescePaletteWriteRanges(std::move(ranges));
536 return absl::FailedPreconditionError(
"PaletteManager not initialized");
544 return absl::FailedPreconditionError(
"No ROM available for palette save");
549 return absl::NotFoundError(
550 absl::StrFormat(
"Palette group '%s' not found", group_name));
555 if (pal_it ==
CurrentState()->modified_palettes.end() ||
556 pal_it->second.empty()) {
558 return absl::OkStatus();
562 for (
int palette_idx : pal_it->second) {
563 auto* palette = group->mutable_palette(palette_idx);
568 if (color_it !=
CurrentState()->modified_colors[group_name].end()) {
569 for (
int color_idx : color_it->second) {
582 for (
size_t i = 0; i < group->size() && i < originals.size(); i++) {
583 originals[i] = group->palette(i);
600 return absl::OkStatus();
605 return absl::FailedPreconditionError(
"PaletteManager not initialized");
617 return absl::OkStatus();
623 return absl::FailedPreconditionError(
"PaletteManager not initialized");
627 if (state->save_transaction_snapshot.has_value()) {
628 return absl::FailedPreconditionError(
629 "Palette save transaction is already active");
631 state->save_transaction_snapshot =
633 state->modified_palettes, state->modified_colors};
634 return absl::OkStatus();
639 if (!state->save_transaction_snapshot.has_value()) {
642 state->original_palettes =
643 std::move(state->save_transaction_snapshot->original_palettes);
644 state->modified_palettes =
645 std::move(state->save_transaction_snapshot->modified_palettes);
646 state->modified_colors =
647 std::move(state->save_transaction_snapshot->modified_colors);
648 state->save_transaction_snapshot.reset();
657 return absl::FailedPreconditionError(
"PaletteManager not initialized");
664 if (modified_groups.empty()) {
665 return absl::OkStatus();
669 for (
const auto& group_name : modified_groups) {
677 return absl::OkStatus();
695 if (pal_it ==
CurrentState()->modified_palettes.end()) {
701 if (orig_it !=
CurrentState()->original_palettes.end()) {
702 for (
int palette_idx : pal_it->second) {
703 if (palette_idx < orig_it->second.size()) {
704 *group->mutable_palette(palette_idx) = orig_it->second[palette_idx];
745 auto changes = std::move(state->undo_stack.back());
746 state->undo_stack.pop_back();
750 for (
auto it = changes.rbegin(); it != changes.rend(); ++it) {
751 const auto& change = *it;
753 if (group && change.palette_index >= 0 &&
754 change.palette_index < group->size()) {
755 auto* palette = group->mutable_palette(change.palette_index);
756 if (change.color_index >= 0 && change.color_index < palette->size()) {
757 (*palette)[change.color_index] = change.original_color;
764 state->redo_stack.push_back(changes);
766 const auto* owner_rom =
rom_;
767 for (
const auto& change : changes) {
772 change.palette_index, change.color_index});
783 auto changes = std::move(state->redo_stack.back());
784 state->redo_stack.pop_back();
786 for (
const auto& change : changes) {
788 if (group && change.palette_index >= 0 &&
789 change.palette_index < group->size()) {
790 auto* palette = group->mutable_palette(change.palette_index);
791 if (change.color_index >= 0 && change.color_index < palette->size()) {
792 (*palette)[change.color_index] = change.new_color;
799 state->undo_stack.push_back(changes);
801 const auto* owner_rom =
rom_;
802 for (
const auto& change : changes) {
807 change.palette_index, change.color_index});
819 if (state->batch_depth || state->save_transaction_snapshot)
821 if (state->color_edit && state->color_edit->original_color.snes() !=
822 state->color_edit->new_color.snes()) {
825 return !state->undo_stack.empty() &&
831 return !state->redo_stack.empty() && !state->batch_depth &&
832 !state->save_transaction_snapshot &&
833 (!state->color_edit || state->color_edit->original_color.snes() ==
834 state->color_edit->new_color.snes()) &&
839 const std::vector<PaletteColorChange>& changes,
bool redo)
const {
842 std::map<std::tuple<std::string, int, int>, uint16_t> staged;
843 for (
size_t step = 0; step < changes.size(); ++step) {
844 const auto& change = changes[redo ? step : changes.size() - 1 - step];
845 const auto* group =
GetGroup(change.group_name);
846 if (!group || change.palette_index < 0 ||
847 change.palette_index >= group->size())
849 const auto& palette = group->palette_ref(change.palette_index);
850 if (change.color_index < 0 || change.color_index >= palette.size())
852 const auto [at, inserted] = staged.try_emplace(
853 std::make_tuple(change.group_name, change.palette_index,
855 palette[change.color_index].snes());
856 const auto expected = redo ? change.original_color : change.new_color;
857 if (at->second != expected.snes())
859 at->second = (redo ? change.new_color : change.original_color).snes();
902 if (state->batch_depth == 0 && !state->batch_changes.empty()) {
905 std::vector<PaletteColorChange> changes;
906 for (
const auto& change : state->batch_changes) {
907 auto existing = std::find_if(
909 return entry.group_name == change.group_name &&
910 entry.palette_index == change.palette_index &&
911 entry.color_index == change.color_index;
913 if (existing == changes.end()) {
914 changes.push_back(change);
916 existing->new_color = change.new_color;
917 existing->timestamp_ms = change.timestamp_ms;
920 changes.erase(std::remove_if(changes.begin(), changes.end(),
922 return change.original_color.snes() ==
923 change.new_color.snes();
926 state->batch_changes.clear();
927 if (!changes.empty()) {
930 const auto* owner_rom =
rom_;
931 for (
const auto& change : changes) {
958 }
catch (
const std::exception&) {
964 const std::string& group_name)
const {
971 ->get_group(group_name);
974 }
catch (
const std::exception&) {
981 int color_index)
const {
983 auto it = state->original_palettes.find(group_name);
984 if (it == state->original_palettes.end() || palette_index < 0 ||
985 palette_index >= it->second.size()) {
989 const auto& palette = it->second[palette_index];
990 if (color_index < 0 || color_index >= palette.size()) {
994 return palette[color_index];
1002 if (changes.empty()) {
1026 const auto* owner_rom =
rom_;
1036 int palette_index,
int color_index) {
1038 const auto* group =
GetGroup(group_name);
1039 const auto original_it = state->original_palettes.find(group_name);
1040 const bool matches_original =
1041 group !=
nullptr && palette_index >= 0 && palette_index < group->size() &&
1043 color_index < group->palette_ref(palette_index).size() &&
1044 original_it != state->original_palettes.end() &&
1045 palette_index < original_it->second.size() &&
1046 color_index < original_it->second[palette_index].size() &&
1047 group->palette_ref(palette_index)[color_index].snes() ==
1048 original_it->second[palette_index][color_index].snes();
1050 if (!matches_original) {
1051 state->modified_palettes[group_name].insert(palette_index);
1052 state->modified_colors[group_name][palette_index].insert(color_index);
1056 if (
auto group_it = state->modified_colors.find(group_name);
1057 group_it != state->modified_colors.end()) {
1058 if (
auto palette_it = group_it->second.find(palette_index);
1059 palette_it != group_it->second.end()) {
1060 palette_it->second.erase(color_index);
1061 if (palette_it->second.empty()) {
1062 group_it->second.erase(palette_it);
1065 if (group_it->second.empty()) {
1066 state->modified_colors.erase(group_it);
1070 const auto colors_group_it = state->modified_colors.find(group_name);
1071 const bool palette_still_modified =
1072 colors_group_it != state->modified_colors.end() &&
1073 colors_group_it->second.contains(palette_index);
1074 if (!palette_still_modified) {
1075 if (
auto group_it = state->modified_palettes.find(group_name);
1076 group_it != state->modified_palettes.end()) {
1077 group_it->second.erase(palette_index);
1078 if (group_it->second.empty()) {
1079 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 single-color undo step.
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)
bool CanReplayHistory(const std::vector< PaletteColorChange > &changes, bool redo) const
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.
uint64_t ColorRevision(const zelda3::GameData *game_data) const
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.
absl::Status BeginColorEdit(const std::string &group_name, int palette_index, int color_index, uint64_t caller_id=0)
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.
void CancelColorEdit(uint64_t caller_id=0)
bool CanRedo() const
Check if the complete redo entry matches current colors. Conflicting history is retained,...
bool InBatch() const
Check if currently in a batch operation.
bool CanUndo() const
Check if the complete undo entry matches current colors. Conflicting history is retained,...
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.
absl::Status ApplyExternalChanges(const zelda3::GameData *owner, const std::vector< PaletteColorChange > &changes, const std::function< absl::Status()> &participant)
void EndBatch()
End a batch operation.
int RegisterChangeListener(ChangeCallback callback)
Register a callback for palette change events.
void RecordChanges(std::vector< PaletteColorChange > changes)
Helper: Record one undo step (a gesture or an entire batch).
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
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.
void CommitColorEdit(uint64_t caller_id=0)
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.
bool HasColorEdit(uint64_t caller_id=0) const
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.
constexpr uint16_t snes() const
Get SNES 15-bit color.
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.
int color_index
Index of color within palette.
std::string group_name
Palette group name (e.g., "ow_main")
int palette_index
Index of palette within group.
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::deque< std::vector< PaletteColorChange > > redo_stack
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::optional< PaletteColorChange > color_edit
std::deque< std::vector< PaletteColorChange > > undo_stack
std::unordered_map< std::string, std::unordered_set< int > > modified_palettes
uint64_t color_edit_caller
gfx::PaletteGroupMap palette_groups