6#include "absl/strings/str_format.h"
42 return rom_ !=
nullptr;
50 rom_ = game_data ? game_data->
rom() :
nullptr;
63 if (state->initialized) {
73 const char* group_names[] = {
"ow_main",
"ow_aux",
"ow_animated",
74 "hud",
"global_sprites",
"armors",
75 "swords",
"shields",
"sprites_aux1",
76 "sprites_aux2",
"sprites_aux3",
"dungeon_main",
77 "grass",
"3d_object",
"ow_mini_map"};
79 for (
const auto& group_name : group_names) {
81 auto* group = palette_groups->
get_group(group_name);
83 std::vector<SnesPalette> originals;
84 for (
size_t i = 0; i < group->size(); i++) {
85 originals.push_back(group->palette(i));
87 state->original_palettes[group_name] = originals;
89 }
catch (
const std::exception& e) {
95 state->initialized =
true;
99 return game_data !=
nullptr &&
game_data_ == game_data &&
141 int palette_index,
int color_index)
const {
142 const auto* group =
GetGroup(group_name);
143 if (!group || palette_index < 0 || palette_index >= group->size()) {
147 const auto& palette = group->palette_ref(palette_index);
148 if (color_index < 0 || color_index >= palette.size()) {
152 return palette[color_index];
156 int palette_index,
int color_index,
159 return absl::FailedPreconditionError(
"PaletteManager not initialized");
164 return absl::NotFoundError(
165 absl::StrFormat(
"Palette group '%s' not found", group_name));
168 if (palette_index < 0 || palette_index >= group->size()) {
169 return absl::InvalidArgumentError(absl::StrFormat(
170 "Palette index %d out of range [0, %d)", palette_index, group->size()));
173 auto* palette = group->mutable_palette(palette_index);
174 if (color_index < 0 || color_index >= palette->size()) {
175 return absl::InvalidArgumentError(absl::StrFormat(
176 "Color index %d out of range [0, %d)", color_index, palette->size()));
180 SnesColor original_color = (*palette)[color_index];
183 (*palette)[color_index] = new_color;
190 auto now = std::chrono::system_clock::now();
191 auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
192 now.time_since_epoch())
196 color_index, original_color,
197 new_color,
static_cast<uint64_t
>(timestamp_ms)};
202 group_name, palette_index, color_index};
206 auto now = std::chrono::system_clock::now();
207 auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
208 now.time_since_epoch())
211 {group_name, palette_index, color_index, original_color, new_color,
212 static_cast<uint64_t
>(timestamp_ms)});
215 return absl::OkStatus();
219 int palette_index,
int color_index) {
221 return SetColor(group_name, palette_index, color_index, original);
227 return absl::FailedPreconditionError(
"PaletteManager not initialized");
232 auto it = state->original_palettes.find(group_name);
233 if (it == state->original_palettes.end() || palette_index < 0 ||
234 palette_index >= it->second.size()) {
235 return absl::NotFoundError(
"Original palette not found");
239 if (!group || palette_index >= group->size()) {
240 return absl::NotFoundError(
"Palette group or index not found");
244 *group->mutable_palette(palette_index) = it->second[palette_index];
247 if (
auto modified_it = state->modified_palettes.find(group_name);
248 modified_it != state->modified_palettes.end()) {
249 modified_it->second.erase(palette_index);
250 if (modified_it->second.empty()) {
251 state->modified_palettes.erase(modified_it);
254 if (
auto colors_it = state->modified_colors.find(group_name);
255 colors_it != state->modified_colors.end()) {
256 colors_it->second.erase(palette_index);
257 if (colors_it->second.empty()) {
258 state->modified_colors.erase(colors_it);
267 return absl::OkStatus();
278 const auto* state =
FindState(game_data);
279 return state !=
nullptr && !state->modified_palettes.empty();
283 std::vector<std::string> groups;
285 groups.push_back(group_name);
296 int palette_index)
const {
301 return it->second.contains(palette_index);
305 int palette_index,
int color_index)
const {
307 if (group_it ==
CurrentState()->modified_colors.end()) {
311 auto pal_it = group_it->second.find(palette_index);
312 if (pal_it == group_it->second.end()) {
316 return pal_it->second.contains(color_index);
325 const auto* state =
FindState(game_data);
326 if (state ==
nullptr) {
331 for (
const auto& [_, palette_map] : state->modified_colors) {
332 for (
const auto& [__, color_set] : palette_map) {
333 count += color_set.size();
339std::vector<std::pair<uint32_t, uint32_t>>
344std::vector<std::pair<uint32_t, uint32_t>>
347 std::vector<std::pair<uint32_t, uint32_t>> ranges;
348 const auto* state =
FindState(game_data);
349 if (state ==
nullptr) {
354 for (
const auto& [group_name, palette_map] : state->modified_colors) {
355 for (
const auto& [palette_index, color_indices] : palette_map) {
356 for (
int color_index : color_indices) {
357 const uint32_t begin =
359 ranges.emplace_back(begin, begin + 2u);
364 std::sort(ranges.begin(), ranges.end());
365 std::vector<std::pair<uint32_t, uint32_t>> coalesced;
366 coalesced.reserve(ranges.size());
367 for (
const auto& range : ranges) {
368 if (coalesced.empty() || coalesced.back().second < range.first) {
369 coalesced.push_back(range);
372 coalesced.back().second = std::max(coalesced.back().second, range.second);
381 return absl::FailedPreconditionError(
"PaletteManager not initialized");
389 return absl::FailedPreconditionError(
"No ROM available for palette save");
394 return absl::NotFoundError(
395 absl::StrFormat(
"Palette group '%s' not found", group_name));
400 if (pal_it ==
CurrentState()->modified_palettes.end() ||
401 pal_it->second.empty()) {
403 return absl::OkStatus();
407 for (
int palette_idx : pal_it->second) {
408 auto* palette = group->mutable_palette(palette_idx);
413 if (color_it !=
CurrentState()->modified_colors[group_name].end()) {
414 for (
int color_idx : color_it->second) {
427 for (
size_t i = 0; i < group->size() && i < originals.size(); i++) {
428 originals[i] = group->palette(i);
445 return absl::OkStatus();
450 return absl::FailedPreconditionError(
"PaletteManager not initialized");
462 return absl::OkStatus();
467 return absl::FailedPreconditionError(
"PaletteManager not initialized");
471 if (state->save_transaction_snapshot.has_value()) {
472 return absl::FailedPreconditionError(
473 "Palette save transaction is already active");
475 state->save_transaction_snapshot =
477 state->modified_palettes, state->modified_colors};
478 return absl::OkStatus();
483 if (!state->save_transaction_snapshot.has_value()) {
486 state->original_palettes =
487 std::move(state->save_transaction_snapshot->original_palettes);
488 state->modified_palettes =
489 std::move(state->save_transaction_snapshot->modified_palettes);
490 state->modified_colors =
491 std::move(state->save_transaction_snapshot->modified_colors);
492 state->save_transaction_snapshot.reset();
501 return absl::FailedPreconditionError(
"PaletteManager not initialized");
508 if (modified_groups.empty()) {
509 return absl::OkStatus();
512 for (
const auto& group_name : modified_groups) {
520 return absl::OkStatus();
535 if (pal_it ==
CurrentState()->modified_palettes.end()) {
541 if (orig_it !=
CurrentState()->original_palettes.end()) {
542 for (
int palette_idx : pal_it->second) {
543 if (palette_idx < orig_it->second.size()) {
544 *group->mutable_palette(palette_idx) = orig_it->second[palette_idx];
588 if (group && change.palette_index < group->size()) {
589 auto* palette = group->mutable_palette(change.palette_index);
590 if (change.color_index < palette->size()) {
591 (*palette)[change.color_index] = change.original_color;
592 MarkModified(change.group_name, change.palette_index, change.color_index);
616 if (group && change.palette_index < group->size()) {
617 auto* palette = group->mutable_palette(change.palette_index);
618 if (change.color_index < palette->size()) {
619 (*palette)[change.color_index] = change.new_color;
620 MarkModified(change.group_name, change.palette_index, change.color_index);
715 }
catch (
const std::exception&) {
721 const std::string& group_name)
const {
728 ->get_group(group_name);
731 }
catch (
const std::exception&) {
738 int color_index)
const {
740 auto it = state->original_palettes.find(group_name);
741 if (it == state->original_palettes.end() || palette_index < 0 ||
742 palette_index >= it->second.size()) {
746 const auto& palette = it->second[palette_index];
747 if (color_index < 0 || color_index >= palette.size()) {
751 return palette[color_index];
773 int palette_index,
int color_index) {
775 const auto* group =
GetGroup(group_name);
776 const auto original_it = state->original_palettes.find(group_name);
777 const bool matches_original =
778 group !=
nullptr && palette_index >= 0 && palette_index < group->size() &&
780 color_index < group->palette_ref(palette_index).size() &&
781 original_it != state->original_palettes.end() &&
782 palette_index < original_it->second.size() &&
783 color_index < original_it->second[palette_index].size() &&
784 group->palette_ref(palette_index)[color_index].snes() ==
785 original_it->second[palette_index][color_index].snes();
787 if (!matches_original) {
788 state->modified_palettes[group_name].insert(palette_index);
789 state->modified_colors[group_name][palette_index].insert(color_index);
793 if (
auto group_it = state->modified_colors.find(group_name);
794 group_it != state->modified_colors.end()) {
795 if (
auto palette_it = group_it->second.find(palette_index);
796 palette_it != group_it->second.end()) {
797 palette_it->second.erase(color_index);
798 if (palette_it->second.empty()) {
799 group_it->second.erase(palette_it);
802 if (group_it->second.empty()) {
803 state->modified_colors.erase(group_it);
807 const auto colors_group_it = state->modified_colors.find(group_name);
808 const bool palette_still_modified =
809 colors_group_it != state->modified_colors.end() &&
810 colors_group_it->second.contains(palette_index);
811 if (!palette_still_modified) {
812 if (
auto group_it = state->modified_palettes.find(group_name);
813 group_it != state->modified_palettes.end()) {
814 group_it->second.erase(palette_index);
815 if (group_it->second.empty()) {
816 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.
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.
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