yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_manager.cc
Go to the documentation of this file.
1#include "palette_manager.h"
2
3#include <algorithm>
4#include <chrono>
5#include <map>
6#include <set>
7#include <tuple>
8
9#include "absl/strings/str_format.h"
12#include "rom/rom.h"
13#include "util/macro.h"
14#include "zelda3/game_data.h"
15
16namespace yaze {
17namespace gfx {
18
19namespace {
20
21std::vector<std::pair<uint32_t, uint32_t>> CoalescePaletteWriteRanges(
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);
29 continue;
30 }
31 coalesced.back().second = std::max(coalesced.back().second, range.second);
32 }
33 return coalesced;
34}
35
36} // namespace
37
39 if (!game_data_) {
40 return &unbound_state_;
41 }
42 return &session_states_.try_emplace(game_data_).first->second;
43}
44
46 if (!game_data_) {
47 return &unbound_state_;
48 }
49 auto it = session_states_.find(game_data_);
50 return it == session_states_.end() ? &unbound_state_ : &it->second;
51}
52
54 const zelda3::GameData* game_data) const {
55 if (!game_data) {
56 return nullptr;
57 }
58 auto it = session_states_.find(game_data);
59 return it == session_states_.end() ? nullptr : &it->second;
60}
61
63 if (!game_data_) {
64 return rom_ != nullptr;
65 }
66 const auto* state = FindState(game_data_);
67 return state != nullptr && state->initialized && rom_ == game_data_->rom();
68}
69
71 const zelda3::GameData* game_data) const {
72 const auto* state = FindState(game_data);
73 return state ? state->color_revision : 0;
74}
75
77 game_data_ = game_data;
78 rom_ = game_data ? game_data->rom() : nullptr;
79 if (game_data) {
80 session_states_.try_emplace(game_data);
81 }
82}
83
85 if (!game_data) {
86 return;
87 }
88
89 ActivateSession(game_data);
90 auto* state = CurrentState();
91 if (state->initialized) {
92 return;
93 }
94
95 *state = SessionState{};
96
97 // Load original palette snapshots for all groups
98 auto* palette_groups = &game_data_->palette_groups;
99
100 // Snapshot all palette groups
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"};
106
107 for (const auto& group_name : group_names) {
108 try {
109 auto* group = palette_groups->get_group(group_name);
110 if (group) {
111 std::vector<SnesPalette> originals;
112 for (size_t i = 0; i < group->size(); i++) {
113 originals.push_back(group->palette(i));
114 }
115 state->original_palettes[group_name] = originals;
116 }
117 } catch (const std::exception& e) {
118 // Group doesn't exist, skip
119 continue;
120 }
121 }
122
123 state->initialized = true;
124}
125
127 return game_data != nullptr && game_data_ == game_data &&
128 rom_ == game_data->rom();
129}
130
131bool PaletteManager::IsManaging(const zelda3::GameData* game_data) const {
132 return IsSessionActive(game_data) && IsInitialized();
133}
134
136 if (!game_data) {
137 return;
138 }
139 if (game_data_ == game_data) {
140 game_data_ = nullptr;
141 rom_ = nullptr;
142 }
143 session_states_.erase(game_data);
144}
145
147 // Legacy initialization - not supported in new architecture
148 // Keep ROM pointer for backwards compatibility but log warning
149 if (!rom) {
150 return;
151 }
152 game_data_ = nullptr;
153 rom_ = rom;
155}
156
158 game_data_ = nullptr;
159 rom_ = nullptr;
160 session_states_.clear();
162 change_listeners_.clear();
164}
165
166// ========== Color Operations ==========
167
168SnesColor PaletteManager::GetColor(const std::string& group_name,
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()) {
172 return SnesColor();
173 }
174
175 const auto& palette = group->palette_ref(palette_index);
176 if (color_index < 0 || color_index >= palette.size()) {
177 return SnesColor();
178 }
179
180 return palette[color_index];
181}
182
183absl::Status PaletteManager::SetColor(const std::string& group_name,
184 int palette_index, int color_index,
185 const SnesColor& new_color) {
186 if (!IsInitialized()) {
187 return absl::FailedPreconditionError("PaletteManager not initialized");
188 }
189
190 auto* group = GetMutableGroup(group_name);
191 if (!group) {
192 return absl::NotFoundError(
193 absl::StrFormat("Palette group '%s' not found", group_name));
194 }
195
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()));
199 }
200
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()));
205 }
206
207 auto* state = CurrentState();
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)) {
212 }
213 const SnesColor original_color = (*palette)[color_index];
214 if (original_color.snes() == new_color.snes()) {
215 return absl::OkStatus();
216 }
217 (*palette)[color_index] = new_color;
218 MarkModified(group_name, palette_index, color_index);
219
220 const auto timestamp_ms =
221 std::chrono::duration_cast<std::chrono::milliseconds>(
222 std::chrono::system_clock::now().time_since_epoch())
223 .count();
224 PaletteColorChange change{group_name, palette_index,
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;
230 } else if (InBatch()) {
231 state->batch_changes.push_back(change);
232 return absl::OkStatus();
233 } else {
234 RecordChange(change);
235 }
237 palette_index, color_index});
238
239 return absl::OkStatus();
240}
241
243 const zelda3::GameData* owner,
244 const std::vector<PaletteColorChange>& changes,
245 const std::function<absl::Status()>& participant) {
246 if (!owner || !IsManaging(owner) || !IsInitialized() || !participant) {
247 return absl::FailedPreconditionError(
248 "Palette command session is unavailable");
249 }
250 auto* state = CurrentState();
251 if (state->color_edit || state->batch_depth ||
252 state->save_transaction_snapshot) {
253 return absl::FailedPreconditionError(
254 "Finish the active palette operation first");
255 }
256 std::set<std::tuple<std::string, int, int>> addresses;
257 std::vector<SnesColor*> slots;
258 for (const auto& change : changes) {
259 auto* group = GetMutableGroup(change.group_name);
260 if (!group || change.palette_index < 0 ||
261 change.palette_index >= group->size()) {
262 return absl::InvalidArgumentError("Invalid command palette");
263 }
264 auto* palette = group->mutable_palette(change.palette_index);
265 if (change.color_index < 0 || change.color_index >= palette->size() ||
266 !addresses
267 .emplace(change.group_name, change.palette_index,
268 change.color_index)
269 .second) {
270 return absl::InvalidArgumentError("Invalid or duplicate command color");
271 }
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");
276 }
277 slots.push_back(slot);
278 }
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);
284 }
285 const auto status = participant();
286 if (!status.ok()) {
287 for (size_t i = 0; i < changes.size(); ++i)
288 *slots[i] = changes[i].original_color;
289 *state = before;
290 return status;
291 }
292 for (const auto& change : changes) {
293 if (!IsManaging(owner))
294 break;
296 change.palette_index, change.color_index});
297 }
298 return absl::OkStatus();
299}
300
301absl::Status PaletteManager::ResetColor(const std::string& group_name,
302 int palette_index, int color_index) {
303 SnesColor original = GetOriginalColor(group_name, palette_index, color_index);
304 return SetColor(group_name, palette_index, color_index, original);
305}
306
307absl::Status PaletteManager::ResetPalette(const std::string& group_name,
308 int palette_index) {
310 if (!IsInitialized()) {
311 return absl::FailedPreconditionError("PaletteManager not initialized");
312 }
313
314 // Check if original snapshot exists
315 auto* state = CurrentState();
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");
320 }
321
322 auto* group = GetMutableGroup(group_name);
323 if (!group || palette_index >= group->size()) {
324 return absl::NotFoundError("Palette group or index not found");
325 }
326
327 // Restore from original
328 *group->mutable_palette(palette_index) = it->second[palette_index];
329
330 // Clear modified flags for this palette
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);
336 }
337 }
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);
343 }
344 }
345
346 // Notify listeners
348 palette_index, -1};
349 NotifyListeners(event);
350
351 return absl::OkStatus();
352}
353
354absl::Status PaletteManager::BeginColorEdit(const std::string& group_name,
355 int palette_index, int color_index,
356 uint64_t caller_id) {
357 if (!IsInitialized() || InBatch()) {
358 return absl::FailedPreconditionError(
359 "Color interaction requires an initialized session outside a batch");
360 }
361 const auto* group = GetGroup(group_name);
362 if (!group || palette_index < 0 || palette_index >= group->size() ||
363 color_index < 0 ||
364 color_index >= group->palette_ref(palette_index).size()) {
365 return absl::InvalidArgumentError("Invalid color interaction target");
366 }
368 const auto color = group->palette_ref(palette_index)[color_index];
369 CurrentState()->color_edit_caller = caller_id;
371 group_name, palette_index, color_index, color, color, 0};
372 return absl::OkStatus();
373}
374
375void PaletteManager::CommitColorEdit(uint64_t caller_id) {
376 auto* state = CurrentState();
377 if (!HasColorEdit(caller_id)) {
378 return;
379 }
380 auto change = std::move(*state->color_edit);
381 state->color_edit.reset();
382 if (change.original_color.snes() != change.new_color.snes()) {
383 RecordChange(change);
384 }
385}
386
387void PaletteManager::CancelColorEdit(uint64_t caller_id) {
388 auto* state = CurrentState();
389 if (!HasColorEdit(caller_id)) {
390 return;
391 }
392 const auto change = std::move(*state->color_edit);
393 state->color_edit.reset();
394 auto* group = GetMutableGroup(change.group_name);
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});
404 }
405 }
406}
407
408bool PaletteManager::HasColorEdit(uint64_t caller_id) const {
409 const auto* state = CurrentState();
410 return state->color_edit.has_value() &&
411 (caller_id == 0 || state->color_edit_caller == caller_id);
412}
413
414// ========== Dirty Tracking ==========
415
417 return !CurrentState()->modified_palettes.empty();
418}
419
421 const zelda3::GameData* game_data) const {
422 const auto* state = FindState(game_data);
423 return state != nullptr && !state->modified_palettes.empty();
424}
425
426std::vector<std::string> PaletteManager::GetModifiedGroups() const {
427 std::vector<std::string> groups;
428 for (const auto& [group_name, _] : CurrentState()->modified_palettes) {
429 groups.push_back(group_name);
430 }
431 return groups;
432}
433
434bool PaletteManager::IsGroupModified(const std::string& group_name) const {
435 auto it = CurrentState()->modified_palettes.find(group_name);
436 return it != CurrentState()->modified_palettes.end() && !it->second.empty();
437}
438
439bool PaletteManager::IsPaletteModified(const std::string& group_name,
440 int palette_index) const {
441 auto it = CurrentState()->modified_palettes.find(group_name);
442 if (it == CurrentState()->modified_palettes.end()) {
443 return false;
444 }
445 return it->second.contains(palette_index);
446}
447
448bool PaletteManager::IsColorModified(const std::string& group_name,
449 int palette_index, int color_index) const {
450 auto group_it = CurrentState()->modified_colors.find(group_name);
451 if (group_it == CurrentState()->modified_colors.end()) {
452 return false;
453 }
454
455 auto pal_it = group_it->second.find(palette_index);
456 if (pal_it == group_it->second.end()) {
457 return false;
458 }
459
460 return pal_it->second.contains(color_index);
461}
462
466
468 const zelda3::GameData* game_data) const {
469 const auto* state = FindState(game_data);
470 if (state == nullptr) {
471 return 0;
472 }
473
474 size_t count = 0;
475 for (const auto& [_, palette_map] : state->modified_colors) {
476 for (const auto& [__, color_set] : palette_map) {
477 count += color_set.size();
478 }
479 }
480 return count;
481}
482
483std::vector<std::pair<uint32_t, uint32_t>>
487
488std::vector<std::pair<uint32_t, uint32_t>>
490 const zelda3::GameData* game_data) const {
491 std::vector<std::pair<uint32_t, uint32_t>> ranges;
492 const auto* state = FindState(game_data);
493 if (state == nullptr) {
494 return ranges;
495 }
496 ranges.reserve(GetModifiedColorCount(game_data));
497
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 =
502 GetPaletteAddress(group_name, palette_index, color_index);
503 ranges.emplace_back(begin, begin + 2u);
504 }
505 }
506 }
507
508 return CoalescePaletteWriteRanges(std::move(ranges));
509}
510
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;
515 const auto* state = CurrentState();
516 const auto group_it = state->modified_colors.find(group_name);
517 if (group_it == state->modified_colors.end()) {
518 return ranges;
519 }
520
521 for (const auto& [palette_index, color_indices] : group_it->second) {
522 for (int color_index : color_indices) {
523 const uint32_t begin =
524 GetPaletteAddress(group_name, palette_index, color_index);
525 ranges.emplace_back(begin, begin + 2u);
526 }
527 }
528 return CoalescePaletteWriteRanges(std::move(ranges));
529}
530
531// ========== Persistence ==========
532
533absl::Status PaletteManager::SaveGroup(const std::string& group_name) {
535 if (!IsInitialized()) {
536 return absl::FailedPreconditionError("PaletteManager not initialized");
537 }
538
539 Rom* rom = rom_;
540 if (!rom && game_data_) {
541 rom = game_data_->rom();
542 }
543 if (!rom) {
544 return absl::FailedPreconditionError("No ROM available for palette save");
545 }
546
547 auto* group = GetMutableGroup(group_name);
548 if (!group) {
549 return absl::NotFoundError(
550 absl::StrFormat("Palette group '%s' not found", group_name));
551 }
552
553 // Get modified palettes for this group
554 auto pal_it = CurrentState()->modified_palettes.find(group_name);
555 if (pal_it == CurrentState()->modified_palettes.end() ||
556 pal_it->second.empty()) {
557 // No changes to save
558 return absl::OkStatus();
559 }
560
561 // Write each modified palette
562 for (int palette_idx : pal_it->second) {
563 auto* palette = group->mutable_palette(palette_idx);
564
565 // Get modified colors for this palette
566 auto color_it =
567 CurrentState()->modified_colors[group_name].find(palette_idx);
568 if (color_it != CurrentState()->modified_colors[group_name].end()) {
569 for (int color_idx : color_it->second) {
570 // Calculate ROM address using the helper function
571 uint32_t address =
572 GetPaletteAddress(group_name, palette_idx, color_idx);
573
574 // Write color to ROM - write the 16-bit SNES color value
575 RETURN_IF_ERROR(rom->WriteShort(address, (*palette)[color_idx].snes()));
576 }
577 }
578 }
579
580 // Update original snapshots
581 auto& originals = CurrentState()->original_palettes[group_name];
582 for (size_t i = 0; i < group->size() && i < originals.size(); i++) {
583 originals[i] = group->palette(i);
584 }
585
586 // Clear modified flags for this group
587 ClearModifiedFlags(group_name);
588
589 // Mark ROM as dirty
590 rom->set_dirty(true);
591
592 // Notify listeners
594 -1, -1};
595 NotifyListeners(event);
596
597 // Notify Arena for bitmap propagation to other editors
598 Arena::Get().NotifyPaletteModified(group_name, -1);
599
600 return absl::OkStatus();
601}
602
604 if (!IsInitialized()) {
605 return absl::FailedPreconditionError("PaletteManager not initialized");
606 }
607
608 // Save all modified groups
609 for (const auto& group_name : GetModifiedGroups()) {
610 RETURN_IF_ERROR(SaveGroup(group_name));
611 }
612
613 // Notify listeners
615 NotifyListeners(event);
616
617 return absl::OkStatus();
618}
619
622 if (!IsInitialized()) {
623 return absl::FailedPreconditionError("PaletteManager not initialized");
624 }
625
626 auto* state = CurrentState();
627 if (state->save_transaction_snapshot.has_value()) {
628 return absl::FailedPreconditionError(
629 "Palette save transaction is already active");
630 }
631 state->save_transaction_snapshot =
633 state->modified_palettes, state->modified_colors};
634 return absl::OkStatus();
635}
636
638 auto* state = CurrentState();
639 if (!state->save_transaction_snapshot.has_value()) {
640 return;
641 }
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();
649}
650
654
656 if (!IsInitialized()) {
657 return absl::FailedPreconditionError("PaletteManager not initialized");
658 }
659
660 // Get all modified groups and notify Arena for each
661 // This triggers bitmap refresh in other editors WITHOUT saving to ROM
662 auto modified_groups = GetModifiedGroups();
663
664 if (modified_groups.empty()) {
665 return absl::OkStatus(); // Nothing to preview
666 }
667
669 for (const auto& group_name : modified_groups) {
670 Arena::Get().NotifyPaletteModified(group_name, -1);
671 }
672
673 // Notify listeners that preview was applied
675 NotifyListeners(event);
676
677 return absl::OkStatus();
678}
679
680void PaletteManager::DiscardGroup(const std::string& group_name) {
681 if (HasColorEdit() && CurrentState()->color_edit->group_name == group_name) {
683 }
684 if (!IsInitialized()) {
685 return;
686 }
687
688 auto* group = GetMutableGroup(group_name);
689 if (!group) {
690 return;
691 }
692
693 // Get modified palettes
694 auto pal_it = CurrentState()->modified_palettes.find(group_name);
695 if (pal_it == CurrentState()->modified_palettes.end()) {
696 return;
697 }
698
699 // Restore from original snapshots
700 auto orig_it = CurrentState()->original_palettes.find(group_name);
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];
705 }
706 }
707 }
708
709 // Clear modified flags
710 ClearModifiedFlags(group_name);
711
712 // Notify listeners
714 group_name, -1, -1};
715 NotifyListeners(event);
716}
717
719 if (!IsInitialized()) {
720 return;
721 }
722
723 // Discard all modified groups
724 for (const auto& group_name : GetModifiedGroups()) {
725 DiscardGroup(group_name);
726 }
727
728 // Clear undo/redo
729 ClearHistory();
730
731 // Notify listeners
733 NotifyListeners(event);
734}
735
736// ========== Undo/Redo ==========
737
739 if (!CanUndo()) {
740 return;
741 }
743
744 auto* state = CurrentState();
745 auto changes = std::move(state->undo_stack.back());
746 state->undo_stack.pop_back();
747
748 // Apply the whole step before publishing any color notifications. Reverse
749 // order preserves the first value when a batch edits one slot repeatedly.
750 for (auto it = changes.rbegin(); it != changes.rend(); ++it) {
751 const auto& change = *it;
752 auto* group = GetMutableGroup(change.group_name);
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;
758 MarkModified(change.group_name, change.palette_index,
759 change.color_index);
760 }
761 }
762 }
763
764 state->redo_stack.push_back(changes);
765 const auto* owner = game_data_;
766 const auto* owner_rom = rom_;
767 for (const auto& change : changes) {
768 if (game_data_ != owner || rom_ != owner_rom) {
769 break; // A listener activated another ROM session.
770 }
772 change.palette_index, change.color_index});
773 }
774}
775
777 if (!CanRedo()) {
778 return;
779 }
781
782 auto* state = CurrentState();
783 auto changes = std::move(state->redo_stack.back());
784 state->redo_stack.pop_back();
785
786 for (const auto& change : changes) {
787 auto* group = GetMutableGroup(change.group_name);
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;
793 MarkModified(change.group_name, change.palette_index,
794 change.color_index);
795 }
796 }
797 }
798
799 state->undo_stack.push_back(changes);
800 const auto* owner = game_data_;
801 const auto* owner_rom = rom_;
802 for (const auto& change : changes) {
803 if (game_data_ != owner || rom_ != owner_rom) {
804 break; // A listener activated another ROM session.
805 }
807 change.palette_index, change.color_index});
808 }
809}
810
816
818 const auto* state = CurrentState();
819 if (state->batch_depth || state->save_transaction_snapshot)
820 return false;
821 if (state->color_edit && state->color_edit->original_color.snes() !=
822 state->color_edit->new_color.snes()) {
823 return CanReplayHistory({*state->color_edit}, /*redo=*/false);
824 }
825 return !state->undo_stack.empty() &&
826 CanReplayHistory(state->undo_stack.back(), /*redo=*/false);
827}
828
830 const auto* state = CurrentState();
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()) &&
835 CanReplayHistory(state->redo_stack.back(), /*redo=*/true);
836}
837
839 const std::vector<PaletteColorChange>& changes, bool redo) const {
840 if (changes.empty())
841 return false;
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())
848 return false;
849 const auto& palette = group->palette_ref(change.palette_index);
850 if (change.color_index < 0 || change.color_index >= palette.size())
851 return false;
852 const auto [at, inserted] = staged.try_emplace(
853 std::make_tuple(change.group_name, change.palette_index,
854 change.color_index),
855 palette[change.color_index].snes());
856 const auto expected = redo ? change.original_color : change.new_color;
857 if (at->second != expected.snes())
858 return false;
859 at->second = (redo ? change.new_color : change.original_color).snes();
860 }
861 return true;
862}
863
865 return CurrentState()->undo_stack.size();
866}
867
869 return CurrentState()->redo_stack.size();
870}
871
872// ========== Change Notifications ==========
873
875 int id = next_callback_id_++;
876 change_listeners_[id] = callback;
877 return id;
878}
879
881 change_listeners_.erase(callback_id);
882}
883
884// ========== Batch Operations ==========
885
889 if (CurrentState()->batch_depth == 1) {
890 CurrentState()->batch_changes.clear();
891 }
892}
893
895 if (CurrentState()->batch_depth == 0) {
896 return;
897 }
898
900
901 auto* state = CurrentState();
902 if (state->batch_depth == 0 && !state->batch_changes.empty()) {
903 // Coalesce repeated writes to one slot. A batch that returns every slot
904 // to its starting value has no history entry and preserves Redo.
905 std::vector<PaletteColorChange> changes;
906 for (const auto& change : state->batch_changes) {
907 auto existing = std::find_if(
908 changes.begin(), changes.end(), [&](const PaletteColorChange& entry) {
909 return entry.group_name == change.group_name &&
910 entry.palette_index == change.palette_index &&
911 entry.color_index == change.color_index;
912 });
913 if (existing == changes.end()) {
914 changes.push_back(change);
915 } else {
916 existing->new_color = change.new_color;
917 existing->timestamp_ms = change.timestamp_ms;
918 }
919 }
920 changes.erase(std::remove_if(changes.begin(), changes.end(),
921 [](const PaletteColorChange& change) {
922 return change.original_color.snes() ==
923 change.new_color.snes();
924 }),
925 changes.end());
926 state->batch_changes.clear();
927 if (!changes.empty()) {
928 RecordChanges(changes);
929 const auto* owner = game_data_;
930 const auto* owner_rom = rom_;
931 for (const auto& change : changes) {
932 if (game_data_ != owner || rom_ != owner_rom) {
933 break; // Do not publish this batch in another ROM session.
934 }
936 change.group_name, change.palette_index,
937 change.color_index});
938 }
939 }
940 }
941}
942
944 return CurrentState()->batch_depth > 0;
945}
946
947// ========== Private Helpers ==========
948
949PaletteGroup* PaletteManager::GetMutableGroup(const std::string& group_name) {
950 if (!IsInitialized()) {
951 return nullptr;
952 }
953 try {
954 if (game_data_) {
955 return game_data_->palette_groups.get_group(group_name);
956 }
957 return nullptr; // Legacy ROM-only mode not supported
958 } catch (const std::exception&) {
959 return nullptr;
960 }
961}
962
964 const std::string& group_name) const {
965 if (!IsInitialized()) {
966 return nullptr;
967 }
968 try {
969 if (game_data_) {
970 return const_cast<PaletteGroupMap*>(&game_data_->palette_groups)
971 ->get_group(group_name);
972 }
973 return nullptr; // Legacy ROM-only mode not supported
974 } catch (const std::exception&) {
975 return nullptr;
976 }
977}
978
979SnesColor PaletteManager::GetOriginalColor(const std::string& group_name,
980 int palette_index,
981 int color_index) const {
982 const auto* state = CurrentState();
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()) {
986 return SnesColor();
987 }
988
989 const auto& palette = it->second[palette_index];
990 if (color_index < 0 || color_index >= palette.size()) {
991 return SnesColor();
992 }
993
994 return palette[color_index];
995}
996
998 RecordChanges({change});
999}
1000
1001void PaletteManager::RecordChanges(std::vector<PaletteColorChange> changes) {
1002 if (changes.empty()) {
1003 return;
1004 }
1005 CurrentState()->undo_stack.push_back(std::move(changes));
1006
1007 // Limit history size
1008 if (CurrentState()->undo_stack.size() > kMaxUndoHistory) {
1009 CurrentState()->undo_stack.pop_front();
1010 }
1011
1012 // Clear redo stack (can't redo after a new change)
1013 CurrentState()->redo_stack.clear();
1014}
1015
1017 // Publish changes after the model and history agree. Saved notifications do
1018 // not change colors; discard-all already publishes each affected group.
1024 }
1025 const auto* owner = game_data_;
1026 const auto* owner_rom = rom_;
1027 for (const auto& [_, callback] : change_listeners_) {
1028 if (game_data_ != owner || rom_ != owner_rom) {
1029 break;
1030 }
1031 callback(event);
1032 }
1033}
1034
1035void PaletteManager::MarkModified(const std::string& group_name,
1036 int palette_index, int color_index) {
1037 auto* state = CurrentState();
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() &&
1042 color_index >= 0 &&
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();
1049
1050 if (!matches_original) {
1051 state->modified_palettes[group_name].insert(palette_index);
1052 state->modified_colors[group_name][palette_index].insert(color_index);
1053 return;
1054 }
1055
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);
1063 }
1064 }
1065 if (group_it->second.empty()) {
1066 state->modified_colors.erase(group_it);
1067 }
1068 }
1069
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);
1080 }
1081 }
1082 }
1083}
1084
1085void PaletteManager::ClearModifiedFlags(const std::string& group_name) {
1086 CurrentState()->modified_palettes.erase(group_name);
1087 CurrentState()->modified_colors.erase(group_name);
1088}
1089
1090} // namespace gfx
1091} // 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:49
void set_dirty(bool dirty)
Definition rom.h:189
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:897
static Arena & Get()
Definition arena.cc:24
void NotifyPaletteModified(const std::string &group_name, int palette_index=-1)
Notify all listeners that a palette has been modified.
Definition arena.cc:562
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)
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 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.
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
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)
Definition snes.cc:22
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
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
Rom * rom() const
Definition game_data.h:80