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 <stdexcept>
8#include <tuple>
9
10#include "absl/strings/str_format.h"
14#include "rom/rom.h"
15#include "util/macro.h"
16#include "zelda3/game_data.h"
17
18namespace yaze {
19namespace gfx {
20
21namespace {
22
23std::vector<std::pair<uint32_t, uint32_t>> CoalescePaletteWriteRanges(
24 std::vector<std::pair<uint32_t, uint32_t>> ranges) {
25 std::sort(ranges.begin(), ranges.end());
26 std::vector<std::pair<uint32_t, uint32_t>> coalesced;
27 coalesced.reserve(ranges.size());
28 for (const auto& range : ranges) {
29 if (coalesced.empty() || coalesced.back().second < range.first) {
30 coalesced.push_back(range);
31 continue;
32 }
33 coalesced.back().second = std::max(coalesced.back().second, range.second);
34 }
35 return coalesced;
36}
37
38} // namespace
39
41 if (!game_data_) {
42 return &unbound_state_;
43 }
44 return &session_states_.try_emplace(game_data_).first->second;
45}
46
48 if (!game_data_) {
49 return &unbound_state_;
50 }
51 auto it = session_states_.find(game_data_);
52 return it == session_states_.end() ? &unbound_state_ : &it->second;
53}
54
56 const zelda3::GameData* game_data) const {
57 if (!game_data) {
58 return nullptr;
59 }
60 auto it = session_states_.find(game_data);
61 return it == session_states_.end() ? nullptr : &it->second;
62}
63
65 if (!game_data_) {
66 return rom_ != nullptr;
67 }
68 const auto* state = FindState(game_data_);
69 return state != nullptr && state->initialized && rom_ == game_data_->rom();
70}
71
73 const zelda3::GameData* game_data) const {
74 const auto* state = FindState(game_data);
75 return state ? state->color_revision : 0;
76}
77
79 game_data_ = game_data;
80 rom_ = game_data ? game_data->rom() : nullptr;
81 if (game_data) {
82 session_states_.try_emplace(game_data);
83 }
84}
85
87 if (!game_data) {
88 return;
89 }
90
91 ActivateSession(game_data);
92 auto* state = CurrentState();
93 if (state->initialized) {
94 return;
95 }
96
97 *state = SessionState{};
98
99 // Load original palette snapshots for all groups
100 auto* palette_groups = &game_data_->palette_groups;
101
102 // Snapshot all palette groups
103 const char* group_names[] = {"ow_main", "ow_aux", "ow_animated",
104 "hud", "global_sprites", "armors",
105 "swords", "shields", "sprites_aux1",
106 "sprites_aux2", "sprites_aux3", "dungeon_main",
107 "grass", "3d_object", "ow_mini_map"};
108
109 for (const auto& group_name : group_names) {
110 try {
111 auto* group = palette_groups->get_group(group_name);
112 if (group) {
113 std::vector<SnesPalette> originals;
114 for (size_t i = 0; i < group->size(); i++) {
115 originals.push_back(group->palette(i));
116 }
117 state->original_palettes[group_name] = originals;
118 }
119 } catch (const std::exception& e) {
120 // Group doesn't exist, skip
121 continue;
122 }
123 }
124
125 state->initialized = true;
126}
127
129 return game_data != nullptr && game_data_ == game_data &&
130 rom_ == game_data->rom();
131}
132
133bool PaletteManager::IsManaging(const zelda3::GameData* game_data) const {
134 return IsSessionActive(game_data) && IsInitialized();
135}
136
138 if (!game_data) {
139 return;
140 }
141 if (game_data_ == game_data) {
142 game_data_ = nullptr;
143 rom_ = nullptr;
144 }
145 session_states_.erase(game_data);
146}
147
149 // Legacy initialization - not supported in new architecture
150 // Keep ROM pointer for backwards compatibility but log warning
151 if (!rom) {
152 return;
153 }
154 game_data_ = nullptr;
155 rom_ = rom;
157}
158
160 game_data_ = nullptr;
161 rom_ = nullptr;
162 session_states_.clear();
164 change_listeners_.clear();
166}
167
168// ========== Color Operations ==========
169
170SnesColor PaletteManager::GetColor(const std::string& group_name,
171 int palette_index, int color_index) const {
172 const auto* group = GetGroup(group_name);
173 if (!group || palette_index < 0 || palette_index >= group->size()) {
174 return SnesColor();
175 }
176
177 const auto& palette = group->palette_ref(palette_index);
178 if (color_index < 0 || color_index >= palette.size()) {
179 return SnesColor();
180 }
181
182 return palette[color_index];
183}
184
185absl::Status PaletteManager::SetColor(const std::string& group_name,
186 int palette_index, int color_index,
187 const SnesColor& new_color) {
188 if (!IsInitialized()) {
189 return absl::FailedPreconditionError("PaletteManager not initialized");
190 }
191
192 auto* group = GetMutableGroup(group_name);
193 if (!group) {
194 return absl::NotFoundError(
195 absl::StrFormat("Palette group '%s' not found", group_name));
196 }
197
198 if (palette_index < 0 || palette_index >= group->size()) {
199 return absl::InvalidArgumentError(absl::StrFormat(
200 "Palette index %d out of range [0, %d)", palette_index, group->size()));
201 }
202
203 auto* palette = group->mutable_palette(palette_index);
204 if (color_index < 0 || color_index >= palette->size()) {
205 return absl::InvalidArgumentError(absl::StrFormat(
206 "Color index %d out of range [0, %d)", color_index, palette->size()));
207 }
208
209 auto* state = CurrentState();
210 if (state->color_edit && (state->color_edit->group_name != group_name ||
211 state->color_edit->palette_index != palette_index ||
212 state->color_edit->color_index != color_index)) {
214 }
215 const SnesColor original_color = (*palette)[color_index];
216 if (original_color.snes() == new_color.snes()) {
217 return absl::OkStatus();
218 }
219 (*palette)[color_index] = new_color;
220 MarkModified(group_name, palette_index, color_index);
221
222 const auto timestamp_ms =
223 std::chrono::duration_cast<std::chrono::milliseconds>(
224 std::chrono::system_clock::now().time_since_epoch())
225 .count();
226 PaletteColorChange change{group_name, palette_index,
227 color_index, original_color,
228 new_color, static_cast<uint64_t>(timestamp_ms)};
229 if (state->color_edit) {
230 state->color_edit->new_color = new_color;
231 state->color_edit->timestamp_ms = change.timestamp_ms;
232 } else if (InBatch()) {
233 state->batch_changes.push_back(change);
234 return absl::OkStatus();
235 } else {
236 RecordChange(change);
237 }
239 palette_index, color_index});
240
241 return absl::OkStatus();
242}
243
245 const zelda3::GameData* owner,
246 const std::vector<PaletteColorChange>& changes,
247 const std::function<absl::Status()>& participant) {
248 if (!owner || !IsManaging(owner) || !IsInitialized() || !participant) {
249 return absl::FailedPreconditionError(
250 "Palette command session is unavailable");
251 }
252 auto* state = CurrentState();
253 if (state->color_edit || state->batch_depth ||
254 state->save_transaction_snapshot) {
255 return absl::FailedPreconditionError(
256 "Finish the active palette operation first");
257 }
258 std::set<std::tuple<std::string, int, int>> addresses;
259 std::vector<SnesColor*> slots;
260 for (const auto& change : changes) {
261 auto* group = GetMutableGroup(change.group_name);
262 if (!group || change.palette_index < 0 ||
263 change.palette_index >= group->size()) {
264 return absl::InvalidArgumentError("Invalid command palette");
265 }
266 auto* palette = group->mutable_palette(change.palette_index);
267 if (change.color_index < 0 || change.color_index >= palette->size() ||
268 !addresses
269 .emplace(change.group_name, change.palette_index,
270 change.color_index)
271 .second) {
272 return absl::InvalidArgumentError("Invalid or duplicate command color");
273 }
274 auto* slot = &(*palette)[change.color_index];
275 if (slot->snes() != change.original_color.snes()) {
276 return absl::FailedPreconditionError(
277 "Palette changed since command planning");
278 }
279 slots.push_back(slot);
280 }
281 const auto before = *state;
282 for (size_t i = 0; i < changes.size(); ++i) {
283 *slots[i] = changes[i].new_color;
284 MarkModified(changes[i].group_name, changes[i].palette_index,
285 changes[i].color_index);
286 }
287 const auto status = participant();
288 if (!status.ok()) {
289 for (size_t i = 0; i < changes.size(); ++i)
290 *slots[i] = changes[i].original_color;
291 *state = before;
292 return status;
293 }
294 for (const auto& change : changes) {
295 if (!IsManaging(owner))
296 break;
298 change.palette_index, change.color_index});
299 }
300 return absl::OkStatus();
301}
302
303absl::Status PaletteManager::ResetColor(const std::string& group_name,
304 int palette_index, int color_index) {
305 SnesColor original = GetOriginalColor(group_name, palette_index, color_index);
306 return SetColor(group_name, palette_index, color_index, original);
307}
308
309absl::Status PaletteManager::ResetPalette(const std::string& group_name,
310 int palette_index) {
312 if (!IsInitialized()) {
313 return absl::FailedPreconditionError("PaletteManager not initialized");
314 }
315
316 // Check if original snapshot exists
317 auto* state = CurrentState();
318 auto it = state->original_palettes.find(group_name);
319 if (it == state->original_palettes.end() || palette_index < 0 ||
320 palette_index >= it->second.size()) {
321 return absl::NotFoundError("Original palette not found");
322 }
323
324 auto* group = GetMutableGroup(group_name);
325 if (!group || palette_index >= group->size()) {
326 return absl::NotFoundError("Palette group or index not found");
327 }
328
329 // Restore from original
330 *group->mutable_palette(palette_index) = it->second[palette_index];
331
332 // Clear modified flags for this palette
333 if (auto modified_it = state->modified_palettes.find(group_name);
334 modified_it != state->modified_palettes.end()) {
335 modified_it->second.erase(palette_index);
336 if (modified_it->second.empty()) {
337 state->modified_palettes.erase(modified_it);
338 }
339 }
340 if (auto colors_it = state->modified_colors.find(group_name);
341 colors_it != state->modified_colors.end()) {
342 colors_it->second.erase(palette_index);
343 if (colors_it->second.empty()) {
344 state->modified_colors.erase(colors_it);
345 }
346 }
347
348 // Notify listeners
350 palette_index, -1};
351 NotifyListeners(event);
352
353 return absl::OkStatus();
354}
355
356absl::Status PaletteManager::BeginColorEdit(const std::string& group_name,
357 int palette_index, int color_index,
358 uint64_t caller_id) {
359 if (!IsInitialized() || InBatch()) {
360 return absl::FailedPreconditionError(
361 "Color interaction requires an initialized session outside a batch");
362 }
363 const auto* group = GetGroup(group_name);
364 if (!group || palette_index < 0 || palette_index >= group->size() ||
365 color_index < 0 ||
366 color_index >= group->palette_ref(palette_index).size()) {
367 return absl::InvalidArgumentError("Invalid color interaction target");
368 }
370 const auto color = group->palette_ref(palette_index)[color_index];
371 CurrentState()->color_edit_caller = caller_id;
373 group_name, palette_index, color_index, color, color, 0};
374 return absl::OkStatus();
375}
376
377void PaletteManager::CommitColorEdit(uint64_t caller_id) {
378 auto* state = CurrentState();
379 if (!HasColorEdit(caller_id)) {
380 return;
381 }
382 auto change = std::move(*state->color_edit);
383 state->color_edit.reset();
384 if (change.original_color.snes() != change.new_color.snes()) {
385 RecordChange(change);
386 }
387}
388
389void PaletteManager::CancelColorEdit(uint64_t caller_id) {
390 auto* state = CurrentState();
391 if (!HasColorEdit(caller_id)) {
392 return;
393 }
394 const auto change = std::move(*state->color_edit);
395 state->color_edit.reset();
396 auto* group = GetMutableGroup(change.group_name);
397 if (group && change.palette_index < group->size()) {
398 auto* palette = group->mutable_palette(change.palette_index);
399 if (change.color_index < palette->size() &&
400 (*palette)[change.color_index].snes() != change.original_color.snes()) {
401 (*palette)[change.color_index] = change.original_color;
402 MarkModified(change.group_name, change.palette_index, change.color_index);
404 change.group_name, change.palette_index,
405 change.color_index});
406 }
407 }
408}
409
410bool PaletteManager::HasColorEdit(uint64_t caller_id) const {
411 const auto* state = CurrentState();
412 return state->color_edit.has_value() &&
413 (caller_id == 0 || state->color_edit_caller == caller_id);
414}
415
416// ========== Dirty Tracking ==========
417
419 return !CurrentState()->modified_palettes.empty();
420}
421
423 const zelda3::GameData* game_data) const {
424 const auto* state = FindState(game_data);
425 return state != nullptr && !state->modified_palettes.empty();
426}
427
428std::vector<std::string> PaletteManager::GetModifiedGroups() const {
429 std::vector<std::string> groups;
430 for (const auto& [group_name, _] : CurrentState()->modified_palettes) {
431 groups.push_back(group_name);
432 }
433 return groups;
434}
435
436bool PaletteManager::IsGroupModified(const std::string& group_name) const {
437 auto it = CurrentState()->modified_palettes.find(group_name);
438 return it != CurrentState()->modified_palettes.end() && !it->second.empty();
439}
440
441bool PaletteManager::IsPaletteModified(const std::string& group_name,
442 int palette_index) const {
443 auto it = CurrentState()->modified_palettes.find(group_name);
444 if (it == CurrentState()->modified_palettes.end()) {
445 return false;
446 }
447 return it->second.contains(palette_index);
448}
449
450bool PaletteManager::IsColorModified(const std::string& group_name,
451 int palette_index, int color_index) const {
452 auto group_it = CurrentState()->modified_colors.find(group_name);
453 if (group_it == CurrentState()->modified_colors.end()) {
454 return false;
455 }
456
457 auto pal_it = group_it->second.find(palette_index);
458 if (pal_it == group_it->second.end()) {
459 return false;
460 }
461
462 return pal_it->second.contains(color_index);
463}
464
468
470 const zelda3::GameData* game_data) const {
471 const auto* state = FindState(game_data);
472 if (state == nullptr) {
473 return 0;
474 }
475
476 size_t count = 0;
477 for (const auto& [_, palette_map] : state->modified_colors) {
478 for (const auto& [__, color_set] : palette_map) {
479 count += color_set.size();
480 }
481 }
482 return count;
483}
484
485absl::StatusOr<uint32_t> PaletteManager::ResolveColorAddress(
486 const zelda3::GameData* game_data, const std::string& group_name,
487 int palette_index, int color_index) const {
488 if (!game_data || !game_data->rom()) {
489 return absl::FailedPreconditionError("No ROM session for palette address");
490 }
491 const PaletteGroup* group = nullptr;
492 try {
493 group = game_data->palette_groups.get_group(group_name);
494 } catch (const std::out_of_range& e) {
495 return absl::InvalidArgumentError(e.what());
496 }
497 if (!group || palette_index < 0 || palette_index >= group->size()) {
498 return absl::OutOfRangeError("Modified palette no longer exists");
499 }
500 if (color_index < 0 ||
501 color_index >= group->palette_ref(palette_index).size()) {
502 return absl::OutOfRangeError("Modified palette color no longer exists");
503 }
504
505 const size_t rom_size = game_data->rom()->vector().size();
506 if (group_name == "ow_main" || group_name == "ow_aux") {
508 const auto kind = group_name == "ow_main"
509 ? OverworldPaletteKind::kMain
510 : OverworldPaletteKind::kAuxiliary;
512 const auto record,
514 kind, palette_index, rom_size));
515 return record.ColorAddress(color_index);
516 }
517
518 // Other groups retain their existing address layout.
519 uint32_t address;
520 try {
521 address = GetPaletteAddress(group_name, palette_index, color_index);
522 } catch (const std::out_of_range& e) {
523 return absl::InvalidArgumentError(e.what());
524 }
525 if (address > rom_size || rom_size - address < 2) {
526 return absl::OutOfRangeError("Modified palette color exceeds ROM bounds");
527 }
528 return address;
529}
530
531absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
535
536absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
538 const zelda3::GameData* game_data) const {
539 std::vector<std::pair<uint32_t, uint32_t>> ranges;
540 const auto* state = FindState(game_data);
541 if (state == nullptr) {
542 return ranges;
543 }
544 ranges.reserve(GetModifiedColorCount(game_data));
545
546 for (const auto& [group_name, palette_map] : state->modified_colors) {
547 for (const auto& [palette_index, color_indices] : palette_map) {
548 for (int color_index : color_indices) {
549 ASSIGN_OR_RETURN(const auto begin,
550 ResolveColorAddress(game_data, group_name,
551 palette_index, color_index));
552 ranges.emplace_back(begin, begin + 2u);
553 }
554 }
555 }
556
557 return CoalescePaletteWriteRanges(std::move(ranges));
558}
559
560absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
562 const std::string& group_name) const {
563 std::vector<std::pair<uint32_t, uint32_t>> ranges;
564 const auto* state = CurrentState();
565 const auto group_it = state->modified_colors.find(group_name);
566 if (group_it == state->modified_colors.end()) {
567 return ranges;
568 }
569
570 for (const auto& [palette_index, color_indices] : group_it->second) {
571 for (int color_index : color_indices) {
572 ASSIGN_OR_RETURN(const auto begin,
574 palette_index, color_index));
575 ranges.emplace_back(begin, begin + 2u);
576 }
577 }
578 return CoalescePaletteWriteRanges(std::move(ranges));
579}
580
581// ========== Persistence ==========
582
583absl::Status PaletteManager::SaveGroup(const std::string& group_name) {
584 if (!IsInitialized()) {
585 return absl::FailedPreconditionError("PaletteManager not initialized");
586 }
587
588 Rom* rom = rom_;
589 if (!rom && game_data_) {
590 rom = game_data_->rom();
591 }
592 if (!rom) {
593 return absl::FailedPreconditionError("No ROM available for palette save");
594 }
595
596 auto* group = GetMutableGroup(group_name);
597 if (!group) {
598 return absl::NotFoundError(
599 absl::StrFormat("Palette group '%s' not found", group_name));
600 }
601
602 // Get modified palettes for this group
603 auto pal_it = CurrentState()->modified_palettes.find(group_name);
604 if (pal_it == CurrentState()->modified_palettes.end() ||
605 pal_it->second.empty()) {
606 // No changes to save
608 return absl::OkStatus();
609 }
610
611 // Resolve every modified color before writing or committing edit metadata.
612 std::vector<std::pair<uint32_t, uint16_t>> writes;
613 for (int palette_idx : pal_it->second) {
614 const auto group_it = CurrentState()->modified_colors.find(group_name);
615 if (group_it == CurrentState()->modified_colors.end()) {
616 continue;
617 }
618 const auto color_it = group_it->second.find(palette_idx);
619 if (color_it != group_it->second.end()) {
620 for (int color_idx : color_it->second) {
621 ASSIGN_OR_RETURN(const auto address,
623 palette_idx, color_idx));
624 writes.emplace_back(address,
625 group->palette_ref(palette_idx)[color_idx].snes());
626 }
627 }
628 }
630 for (const auto& [address, color] : writes) {
631 RETURN_IF_ERROR(rom->WriteShort(address, color));
632 }
633
634 // Update original snapshots
635 auto& originals = CurrentState()->original_palettes[group_name];
636 for (size_t i = 0; i < group->size() && i < originals.size(); i++) {
637 originals[i] = group->palette(i);
638 }
639
640 // Clear modified flags for this group
641 ClearModifiedFlags(group_name);
642
643 // Mark ROM as dirty
644 rom->set_dirty(true);
645
646 // Notify listeners
648 -1, -1};
649 NotifyListeners(event);
650
651 // Notify Arena for bitmap propagation to other editors
652 Arena::Get().NotifyPaletteModified(group_name, -1);
653
654 return absl::OkStatus();
655}
656
658 if (!IsInitialized()) {
659 return absl::FailedPreconditionError("PaletteManager not initialized");
660 }
661
662 // A bad later group must not leave earlier groups saved or marked clean.
663 const auto ranges = GetModifiedColorWriteRanges();
664 RETURN_IF_ERROR(ranges.status());
665
666 // Save all modified groups
667 for (const auto& group_name : GetModifiedGroups()) {
668 RETURN_IF_ERROR(SaveGroup(group_name));
669 }
670
671 // Notify listeners
673 NotifyListeners(event);
674
675 return absl::OkStatus();
676}
677
680 if (!IsInitialized()) {
681 return absl::FailedPreconditionError("PaletteManager not initialized");
682 }
683
684 auto* state = CurrentState();
685 if (state->save_transaction_snapshot.has_value()) {
686 return absl::FailedPreconditionError(
687 "Palette save transaction is already active");
688 }
689 state->save_transaction_snapshot =
691 state->modified_palettes, state->modified_colors};
692 return absl::OkStatus();
693}
694
696 auto* state = CurrentState();
697 if (!state->save_transaction_snapshot.has_value()) {
698 return;
699 }
700 state->original_palettes =
701 std::move(state->save_transaction_snapshot->original_palettes);
702 state->modified_palettes =
703 std::move(state->save_transaction_snapshot->modified_palettes);
704 state->modified_colors =
705 std::move(state->save_transaction_snapshot->modified_colors);
706 state->save_transaction_snapshot.reset();
707}
708
712
714 if (!IsInitialized()) {
715 return absl::FailedPreconditionError("PaletteManager not initialized");
716 }
717
718 // Get all modified groups and notify Arena for each
719 // This triggers bitmap refresh in other editors WITHOUT saving to ROM
720 auto modified_groups = GetModifiedGroups();
721
722 if (modified_groups.empty()) {
723 return absl::OkStatus(); // Nothing to preview
724 }
725
727 for (const auto& group_name : modified_groups) {
728 Arena::Get().NotifyPaletteModified(group_name, -1);
729 }
730
731 // Notify listeners that preview was applied
733 NotifyListeners(event);
734
735 return absl::OkStatus();
736}
737
738void PaletteManager::DiscardGroup(const std::string& group_name) {
739 if (HasColorEdit() && CurrentState()->color_edit->group_name == group_name) {
741 }
742 if (!IsInitialized()) {
743 return;
744 }
745
746 auto* group = GetMutableGroup(group_name);
747 if (!group) {
748 return;
749 }
750
751 // Get modified palettes
752 auto pal_it = CurrentState()->modified_palettes.find(group_name);
753 if (pal_it == CurrentState()->modified_palettes.end()) {
754 return;
755 }
756
757 // Restore from original snapshots
758 auto orig_it = CurrentState()->original_palettes.find(group_name);
759 if (orig_it != CurrentState()->original_palettes.end()) {
760 for (int palette_idx : pal_it->second) {
761 if (palette_idx < orig_it->second.size()) {
762 *group->mutable_palette(palette_idx) = orig_it->second[palette_idx];
763 }
764 }
765 }
766
767 // Clear modified flags
768 ClearModifiedFlags(group_name);
769
770 // Notify listeners
772 group_name, -1, -1};
773 NotifyListeners(event);
774}
775
777 if (!IsInitialized()) {
778 return;
779 }
780
781 // Discard all modified groups
782 for (const auto& group_name : GetModifiedGroups()) {
783 DiscardGroup(group_name);
784 }
785
786 // Clear undo/redo
787 ClearHistory();
788
789 // Notify listeners
791 NotifyListeners(event);
792}
793
794// ========== Undo/Redo ==========
795
797 if (!CanUndo()) {
798 return;
799 }
801
802 auto* state = CurrentState();
803 auto changes = std::move(state->undo_stack.back());
804 state->undo_stack.pop_back();
805
806 // Apply the whole step before publishing any color notifications. Reverse
807 // order preserves the first value when a batch edits one slot repeatedly.
808 for (auto it = changes.rbegin(); it != changes.rend(); ++it) {
809 const auto& change = *it;
810 auto* group = GetMutableGroup(change.group_name);
811 if (group && change.palette_index >= 0 &&
812 change.palette_index < group->size()) {
813 auto* palette = group->mutable_palette(change.palette_index);
814 if (change.color_index >= 0 && change.color_index < palette->size()) {
815 (*palette)[change.color_index] = change.original_color;
816 MarkModified(change.group_name, change.palette_index,
817 change.color_index);
818 }
819 }
820 }
821
822 state->redo_stack.push_back(changes);
823 const auto* owner = game_data_;
824 const auto* owner_rom = rom_;
825 for (const auto& change : changes) {
826 if (game_data_ != owner || rom_ != owner_rom) {
827 break; // A listener activated another ROM session.
828 }
830 change.palette_index, change.color_index});
831 }
832}
833
835 if (!CanRedo()) {
836 return;
837 }
839
840 auto* state = CurrentState();
841 auto changes = std::move(state->redo_stack.back());
842 state->redo_stack.pop_back();
843
844 for (const auto& change : changes) {
845 auto* group = GetMutableGroup(change.group_name);
846 if (group && change.palette_index >= 0 &&
847 change.palette_index < group->size()) {
848 auto* palette = group->mutable_palette(change.palette_index);
849 if (change.color_index >= 0 && change.color_index < palette->size()) {
850 (*palette)[change.color_index] = change.new_color;
851 MarkModified(change.group_name, change.palette_index,
852 change.color_index);
853 }
854 }
855 }
856
857 state->undo_stack.push_back(changes);
858 const auto* owner = game_data_;
859 const auto* owner_rom = rom_;
860 for (const auto& change : changes) {
861 if (game_data_ != owner || rom_ != owner_rom) {
862 break; // A listener activated another ROM session.
863 }
865 change.palette_index, change.color_index});
866 }
867}
868
874
876 const auto* state = CurrentState();
877 if (state->batch_depth || state->save_transaction_snapshot)
878 return false;
879 if (state->color_edit && state->color_edit->original_color.snes() !=
880 state->color_edit->new_color.snes()) {
881 return CanReplayHistory({*state->color_edit}, /*redo=*/false);
882 }
883 return !state->undo_stack.empty() &&
884 CanReplayHistory(state->undo_stack.back(), /*redo=*/false);
885}
886
888 const auto* state = CurrentState();
889 return !state->redo_stack.empty() && !state->batch_depth &&
890 !state->save_transaction_snapshot &&
891 (!state->color_edit || state->color_edit->original_color.snes() ==
892 state->color_edit->new_color.snes()) &&
893 CanReplayHistory(state->redo_stack.back(), /*redo=*/true);
894}
895
897 const std::vector<PaletteColorChange>& changes, bool redo) const {
898 if (changes.empty())
899 return false;
900 std::map<std::tuple<std::string, int, int>, uint16_t> staged;
901 for (size_t step = 0; step < changes.size(); ++step) {
902 const auto& change = changes[redo ? step : changes.size() - 1 - step];
903 const auto* group = GetGroup(change.group_name);
904 if (!group || change.palette_index < 0 ||
905 change.palette_index >= group->size())
906 return false;
907 const auto& palette = group->palette_ref(change.palette_index);
908 if (change.color_index < 0 || change.color_index >= palette.size())
909 return false;
910 const auto [at, inserted] = staged.try_emplace(
911 std::make_tuple(change.group_name, change.palette_index,
912 change.color_index),
913 palette[change.color_index].snes());
914 const auto expected = redo ? change.original_color : change.new_color;
915 if (at->second != expected.snes())
916 return false;
917 at->second = (redo ? change.new_color : change.original_color).snes();
918 }
919 return true;
920}
921
923 return CurrentState()->undo_stack.size();
924}
925
927 return CurrentState()->redo_stack.size();
928}
929
930// ========== Change Notifications ==========
931
933 int id = next_callback_id_++;
934 change_listeners_[id] = callback;
935 return id;
936}
937
939 change_listeners_.erase(callback_id);
940}
941
942// ========== Batch Operations ==========
943
947 if (CurrentState()->batch_depth == 1) {
948 CurrentState()->batch_changes.clear();
949 }
950}
951
953 if (CurrentState()->batch_depth == 0) {
954 return;
955 }
956
958
959 auto* state = CurrentState();
960 if (state->batch_depth == 0 && !state->batch_changes.empty()) {
961 // Coalesce repeated writes to one slot. A batch that returns every slot
962 // to its starting value has no history entry and preserves Redo.
963 std::vector<PaletteColorChange> changes;
964 for (const auto& change : state->batch_changes) {
965 auto existing = std::find_if(
966 changes.begin(), changes.end(), [&](const PaletteColorChange& entry) {
967 return entry.group_name == change.group_name &&
968 entry.palette_index == change.palette_index &&
969 entry.color_index == change.color_index;
970 });
971 if (existing == changes.end()) {
972 changes.push_back(change);
973 } else {
974 existing->new_color = change.new_color;
975 existing->timestamp_ms = change.timestamp_ms;
976 }
977 }
978 changes.erase(std::remove_if(changes.begin(), changes.end(),
979 [](const PaletteColorChange& change) {
980 return change.original_color.snes() ==
981 change.new_color.snes();
982 }),
983 changes.end());
984 state->batch_changes.clear();
985 if (!changes.empty()) {
986 RecordChanges(changes);
987 const auto* owner = game_data_;
988 const auto* owner_rom = rom_;
989 for (const auto& change : changes) {
990 if (game_data_ != owner || rom_ != owner_rom) {
991 break; // Do not publish this batch in another ROM session.
992 }
994 change.group_name, change.palette_index,
995 change.color_index});
996 }
997 }
998 }
999}
1000
1002 return CurrentState()->batch_depth > 0;
1003}
1004
1005// ========== Private Helpers ==========
1006
1007PaletteGroup* PaletteManager::GetMutableGroup(const std::string& group_name) {
1008 if (!IsInitialized()) {
1009 return nullptr;
1010 }
1011 try {
1012 if (game_data_) {
1013 return game_data_->palette_groups.get_group(group_name);
1014 }
1015 return nullptr; // Legacy ROM-only mode not supported
1016 } catch (const std::exception&) {
1017 return nullptr;
1018 }
1019}
1020
1022 const std::string& group_name) const {
1023 if (!IsInitialized()) {
1024 return nullptr;
1025 }
1026 try {
1027 if (game_data_) {
1028 return const_cast<PaletteGroupMap*>(&game_data_->palette_groups)
1029 ->get_group(group_name);
1030 }
1031 return nullptr; // Legacy ROM-only mode not supported
1032 } catch (const std::exception&) {
1033 return nullptr;
1034 }
1035}
1036
1037SnesColor PaletteManager::GetOriginalColor(const std::string& group_name,
1038 int palette_index,
1039 int color_index) const {
1040 const auto* state = CurrentState();
1041 auto it = state->original_palettes.find(group_name);
1042 if (it == state->original_palettes.end() || palette_index < 0 ||
1043 palette_index >= it->second.size()) {
1044 return SnesColor();
1045 }
1046
1047 const auto& palette = it->second[palette_index];
1048 if (color_index < 0 || color_index >= palette.size()) {
1049 return SnesColor();
1050 }
1051
1052 return palette[color_index];
1053}
1054
1056 RecordChanges({change});
1057}
1058
1059void PaletteManager::RecordChanges(std::vector<PaletteColorChange> changes) {
1060 if (changes.empty()) {
1061 return;
1062 }
1063 CurrentState()->undo_stack.push_back(std::move(changes));
1064
1065 // Limit history size
1066 if (CurrentState()->undo_stack.size() > kMaxUndoHistory) {
1067 CurrentState()->undo_stack.pop_front();
1068 }
1069
1070 // Clear redo stack (can't redo after a new change)
1071 CurrentState()->redo_stack.clear();
1072}
1073
1075 // Publish changes after the model and history agree. Saved notifications do
1076 // not change colors; discard-all already publishes each affected group.
1082 }
1083 const auto* owner = game_data_;
1084 const auto* owner_rom = rom_;
1085 for (const auto& [_, callback] : change_listeners_) {
1086 if (game_data_ != owner || rom_ != owner_rom) {
1087 break;
1088 }
1089 callback(event);
1090 }
1091}
1092
1093void PaletteManager::MarkModified(const std::string& group_name,
1094 int palette_index, int color_index) {
1095 auto* state = CurrentState();
1096 const auto* group = GetGroup(group_name);
1097 const auto original_it = state->original_palettes.find(group_name);
1098 const bool matches_original =
1099 group != nullptr && palette_index >= 0 && palette_index < group->size() &&
1100 color_index >= 0 &&
1101 color_index < group->palette_ref(palette_index).size() &&
1102 original_it != state->original_palettes.end() &&
1103 palette_index < original_it->second.size() &&
1104 color_index < original_it->second[palette_index].size() &&
1105 group->palette_ref(palette_index)[color_index].snes() ==
1106 original_it->second[palette_index][color_index].snes();
1107
1108 if (!matches_original) {
1109 state->modified_palettes[group_name].insert(palette_index);
1110 state->modified_colors[group_name][palette_index].insert(color_index);
1111 return;
1112 }
1113
1114 if (auto group_it = state->modified_colors.find(group_name);
1115 group_it != state->modified_colors.end()) {
1116 if (auto palette_it = group_it->second.find(palette_index);
1117 palette_it != group_it->second.end()) {
1118 palette_it->second.erase(color_index);
1119 if (palette_it->second.empty()) {
1120 group_it->second.erase(palette_it);
1121 }
1122 }
1123 if (group_it->second.empty()) {
1124 state->modified_colors.erase(group_it);
1125 }
1126 }
1127
1128 const auto colors_group_it = state->modified_colors.find(group_name);
1129 const bool palette_still_modified =
1130 colors_group_it != state->modified_colors.end() &&
1131 colors_group_it->second.contains(palette_index);
1132 if (!palette_still_modified) {
1133 if (auto group_it = state->modified_palettes.find(group_name);
1134 group_it != state->modified_palettes.end()) {
1135 group_it->second.erase(palette_index);
1136 if (group_it->second.empty()) {
1137 state->modified_palettes.erase(group_it);
1138 }
1139 }
1140 }
1141}
1142
1143void PaletteManager::ClearModifiedFlags(const std::string& group_name) {
1144 CurrentState()->modified_palettes.erase(group_name);
1145 CurrentState()->modified_colors.erase(group_name);
1146}
1147
1148} // namespace gfx
1149} // 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:52
void set_dirty(bool dirty)
Definition rom.h:201
const auto & vector() const
Definition rom.h:217
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:1153
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.
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.
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,...
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetModifiedColorWriteRanges() const
Get exact, coalesced half-open ROM ranges for modified colors. Invalid or unbound addresses return an...
void Initialize(zelda3::GameData *game_data)
Initialize the palette manager with GameData.
void ResetForTesting()
Reset all state for test isolation.
absl::StatusOr< 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. Resolution errors leave the pending...
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.
absl::StatusOr< uint32_t > ResolveColorAddress(const zelda3::GameData *game_data, const std::string &group_name, int palette_index, int color_index) const
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
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
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.
const SnesPalette & palette_ref(int i) const
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