yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_chest_edits.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <optional>
5
7#include "util/macro.h"
11
12namespace yaze::editor {
13namespace {
14// Existing-record edits never reorder the chest table or change its capacity.
15// Keep only the affected record so undo cannot rewrite another chest's contents.
16class ChestContentsAction final : public UndoAction {
17 public:
18 using Restore = std::function<absl::Status(int, size_t, chest_data)>;
19 ChestContentsAction(int room_id, size_t index, chest_data before,
20 chest_data after, Restore restore)
21 : room_id_(room_id),
22 index_(index),
23 before_(before),
24 after_(after),
25 restore_(std::move(restore)) {}
26 absl::Status Undo() override { return restore_(room_id_, index_, before_); }
27 absl::Status Redo() override { return restore_(room_id_, index_, after_); }
28 std::string Description() const override {
29 return absl::StrFormat("Edit room %03X chest %d contents", room_id_,
30 index_ + 1);
31 }
32 size_t MemoryUsage() const override { return 2 * sizeof(chest_data); }
33
34 private:
36 size_t index_;
39};
40
41std::optional<size_t> ObjectForChest(const zelda3::Room& room, size_t chest) {
42 for (size_t i = 0; i < room.GetTileObjects().size(); ++i) {
44 return i;
45 }
46 }
47 return std::nullopt;
48}
49
50// Compare only the table-backed records, in authored order. Ordinary object
51// changes must not re-plan unrelated global tables or load unopened rooms.
52bool SameSpecialObjects(const std::vector<zelda3::RoomObject>& before,
53 const std::vector<zelda3::RoomObject>& after,
54 zelda3::ObjectOption option) {
55 const auto matches = [option](const auto& object) {
56 return (object.options() & option) != zelda3::ObjectOption::Nothing;
57 };
58 auto a = before.begin();
59 auto b = after.begin();
60 while (true) {
61 a = std::find_if(a, before.end(), matches);
62 b = std::find_if(b, after.end(), matches);
63 if (a == before.end() || b == after.end()) {
64 return a == before.end() && b == after.end();
65 }
66 if (a->x() != b->x() || a->y() != b->y() ||
67 a->GetLayerValue() != b->GetLayerValue()) {
68 return false;
69 }
70 if (option == zelda3::ObjectOption::Torch) {
71 if (a->lit_ != b->lit_ ||
72 a->torch_reserved_bit() != b->torch_reserved_bit()) {
73 return false;
74 }
75 } else if (a->block_behavior_layer() != b->block_behavior_layer() ||
76 a->block_load_order() != b->block_load_order()) {
77 return false;
78 }
79 ++a;
80 ++b;
81 }
82}
83
84std::vector<std::optional<size_t>> ObjectOrigins(size_t count) {
85 std::vector<std::optional<size_t>> origins(count);
86 for (size_t i = 0; i < count; ++i)
87 origins[i] = i;
88 return origins;
89}
90} // namespace
91
92absl::Status DungeonEditorV2::EditChest(int room_id, size_t index,
93 uint8_t item_id, bool big_chest) {
94 auto* room = rooms_.GetIfLoaded(room_id);
95 if (!room || room->rom() != rom_ || !room->AreChestsLoaded()) {
96 return absl::FailedPreconditionError("Chest contents are not loaded");
97 }
98 if (index >= room->GetChests().size()) {
99 return absl::InvalidArgumentError(
100 "Select an existing chest contents record");
101 }
102 const auto before = room->GetChests()[index];
103 const chest_data after{item_id, big_chest};
104 if (before.id == after.id && before.size == after.size) {
105 return absl::OkStatus();
106 }
107 if (before.size != big_chest) {
108 const auto object_index = ObjectForChest(*room, index);
109 if (!object_index) {
110 return absl::FailedPreconditionError(
111 "Chest object and contents do not match; review the room before "
112 "changing chest type");
113 }
114 auto objects = room->GetTileObjects();
115 auto& object = objects[*object_index];
116 object.set_id(big_chest ? 0xFB1 : 0xF99);
117 object.size_ = zelda3::CanonicalRoomObjectSize(object.id_, 0);
118 object.set_options(zelda3::ObjectOption::Chest);
119 std::vector<std::optional<chest_data>> overrides(objects.size());
120 overrides[*object_index] = after;
121 ASSIGN_OR_RETURN(auto chests,
123 room->GetTileObjects(), room->GetChests(), objects,
124 ObjectOrigins(objects.size()), overrides));
125 return ApplyChestObjectEdit(room_id, objects, chests, {*object_index});
126 }
127 auto chests = room->GetChests();
128 chests[index] = after;
130 PreflightObjectMutation(room_id, room->GetTileObjects(), chests));
132 RETURN_IF_ERROR(RestoreChest(room_id, index, after));
133 undo_manager_.Push(std::make_unique<ChestContentsAction>(
134 room_id, index, before, after,
135 [this](int id, size_t restored_index, chest_data chest) {
136 return RestoreChest(id, restored_index, chest);
137 }));
138 return absl::OkStatus();
139}
140
141absl::Status DungeonEditorV2::DeleteChest(int room_id, size_t index) {
142 auto* room = rooms_.GetIfLoaded(room_id);
143 if (!room || room->rom() != rom_ || !room->AreChestsLoaded()) {
144 return absl::FailedPreconditionError("Chest contents are not loaded");
145 }
146 if (index >= room->GetChests().size()) {
147 return absl::InvalidArgumentError("Select an existing chest");
148 }
149 const auto object_index = ObjectForChest(*room, index);
150 if (!object_index) {
151 return absl::FailedPreconditionError(
152 "Chest object and contents do not match; review the room before "
153 "deleting a chest");
154 }
155 auto objects = room->GetTileObjects();
156 auto origins = ObjectOrigins(objects.size());
157 objects.erase(objects.begin() + *object_index);
158 origins.erase(origins.begin() + *object_index);
160 room->GetTileObjects(), room->GetChests(),
161 objects, origins));
162 return ApplyChestObjectEdit(room_id, objects, chests, {});
163}
164
166 int room_id, const std::vector<zelda3::RoomObject>& objects,
167 const std::vector<chest_data>& chests, bool check_streams) {
168 const auto* room = rooms_.GetIfLoaded(room_id);
169 if (!room || room->rom() != rom_) {
170 return absl::FailedPreconditionError("Room objects are not loaded");
171 }
172 if (objects.size() > zelda3::kMaxTileObjects &&
173 objects.size() > room->GetTileObjects().size()) {
174 return absl::ResourceExhaustedError("Room object limit reached");
175 }
176 const bool torches_changed = !SameSpecialObjects(
177 room->GetTileObjects(), objects, zelda3::ObjectOption::Torch);
178 const bool blocks_changed = !SameSpecialObjects(
179 room->GetTileObjects(), objects, zelda3::ObjectOption::Block);
180 if (torches_changed && !room->AreTorchesLoaded()) {
181 return absl::FailedPreconditionError(
182 "Load the room's torch table before editing torches");
183 }
184 if (torches_changed || blocks_changed) {
185 // Rooms own graphics resources and cannot be copied. Materialize only the
186 // small block table to establish ownership, then replace it with authored
187 // candidates (including pending block-slot identities). No graphics load.
188 zelda3::Room staged(room_id, rom_);
189 if (blocks_changed) {
190 if (!room->AreBlocksLoaded()) {
191 return absl::FailedPreconditionError(
192 "Load the room's block table before editing pushable blocks");
193 }
194 staged.LoadBlocks();
195 }
196 staged.SetTileObjects(objects);
197 if (torches_changed)
198 staged.MarkTorchesDirty();
199 if (blocks_changed)
200 staged.MarkBlocksDirty();
201 const auto lookup = [&](int id) -> const zelda3::Room* {
202 return id == room_id ? &staged : rooms_.GetIfMaterialized(id);
203 };
204 std::vector<std::pair<uint32_t, uint32_t>> ranges;
205 if (torches_changed) {
206 ASSIGN_OR_RETURN(auto torch_ranges,
208 rom_, static_cast<int>(rooms_.size()), lookup));
209 ranges.insert(ranges.end(), torch_ranges.begin(), torch_ranges.end());
210 }
211 if (blocks_changed) {
212 ASSIGN_OR_RETURN(auto block_ranges,
214 rom_, static_cast<int>(rooms_.size()), lookup));
215 ranges.insert(ranges.end(), block_ranges.begin(), block_ranges.end());
216 }
222 "special object authoring", "DungeonEditorV2", nullptr));
223 }
224 }
225 if (check_streams) {
226 auto candidate = CaptureDungeonSelectionEditState(*room);
227 candidate.objects = objects;
229 PreflightStreamMutation(room_id, candidate, kSelectionObjects));
230 }
231 const auto& original = room->GetChests();
232 if (original.size() == chests.size() &&
233 std::equal(
234 original.begin(), original.end(), chests.begin(),
235 [](auto a, auto b) { return a.id == b.id && a.size == b.size; })) {
236 return absl::OkStatus();
237 }
238 if (!room->AreChestsLoaded() || !rom_ || !rom_->is_loaded()) {
239 return absl::FailedPreconditionError("Chest contents are not loaded");
240 }
241 // The shared save planner accounts for physical records in unopened rooms
242 // and every other room's pending edits, without writing any ROM bytes.
243 zelda3::Room staged(room_id, rom_);
244 staged.GetChests() = chests;
245 staged.MarkChestsDirty();
247 rom_, static_cast<int>(rooms_.size()),
248 [&](int id) -> const zelda3::Room* {
249 return id == room_id
250 ? &staged
251 : rooms_.GetIfMaterialized(id);
252 }));
256 dependencies_.project->rom_metadata.write_policy, plan.write_ranges(),
257 "chest authoring", "DungeonEditorV2", nullptr));
258 }
259 return absl::OkStatus();
260}
261
263 int room_id, const std::vector<zelda3::RoomObject>& objects,
264 const std::vector<chest_data>& chests,
265 const std::vector<size_t>& selection) {
266 RETURN_IF_ERROR(PreflightObjectMutation(room_id, objects, chests));
268 BeginUndoSnapshot(room_id);
269 RETURN_IF_ERROR(RestoreRoomObjects(room_id, objects, selection, chests));
270 FinalizeUndoAction(room_id);
271 return absl::OkStatus();
272}
273
274absl::Status DungeonEditorV2::RestoreChest(int room_id, size_t index,
276 auto* room = rooms_.GetIfLoaded(room_id);
277 if (!room || room->rom() != rom_ || !room->AreChestsLoaded() ||
278 index >= room->GetChests().size()) {
279 return absl::FailedPreconditionError(
280 "Chest contents record is unavailable");
281 }
282 room->GetChests()[index] = chest;
283 room->MarkChestsDirty();
284 // Contents do not alter the visual chest object. Do not flash a different room
285 // or dirty its tile-object stream when this record is restored offscreen.
287 return absl::OkStatus();
288}
289
290} // namespace yaze::editor
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
bool loaded() const
Check if the manifest has been loaded.
absl::Status PreflightObjectMutation(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< chest_data > &chests, bool check_streams=true)
absl::Status RestoreRoomObjects(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< size_t > &selected_indices, const std::vector< chest_data > &chests)
absl::Status DeleteChest(int room_id, size_t index)
absl::Status PreflightStreamMutation(int room_id, const DungeonSelectionEditState &candidate, uint8_t domains)
absl::Status RestoreChest(int room_id, size_t index, chest_data chest)
absl::Status EditChest(int room_id, size_t index, uint8_t item_id, bool big_chest)
absl::Status ApplyChestObjectEdit(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< chest_data > &chests, const std::vector< size_t > &selection)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
UndoManager undo_manager_
Definition editor.h:371
EditorDependencies dependencies_
Definition editor.h:370
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
void Push(std::unique_ptr< UndoAction > action)
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
ChestContentsAction(int room_id, size_t index, chest_data before, chest_data after, Restore restore)
void MarkChestsDirty()
Definition room.h:326
const std::vector< chest_data > & GetChests() const
Definition room.h:323
void LoadBlocks()
Definition room.cc:4515
void MarkBlocksDirty()
Definition room.h:439
void MarkTorchesDirty()
Definition room.h:436
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void SetTileObjects(const std::vector< RoomObject > &objects)
Definition room.h:721
struct chest chest
Treasure chest.
struct chest_data chest_data
Legacy chest data structure.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::optional< size_t > ObjectForChest(const zelda3::Room &room, size_t chest)
bool SameSpecialObjects(const std::vector< zelda3::RoomObject > &before, const std::vector< zelda3::RoomObject > &after, zelda3::ObjectOption option)
Editors are the view controllers for the application.
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
DungeonSelectionEditState CaptureDungeonSelectionEditState(const zelda3::Room &room)
constexpr uint8_t kSelectionObjects
std::optional< size_t > ChestIndexForObject(const std::vector< RoomObject > &objects, size_t object_index)
Definition chest_edit.cc:63
absl::StatusOr< ChestSavePlan > BuildChestSavePlan(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:4269
constexpr size_t kMaxTileObjects
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetTorchSaveWriteRanges(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3343
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetBlockSaveWriteRanges(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3830
absl::StatusOr< std::vector< chest_data > > PlanChestObjectEdit(const std::vector< RoomObject > &before_objects, const std::vector< chest_data > &before_chests, const std::vector< RoomObject > &after_objects, const std::vector< std::optional< size_t > > &source_indices, const std::vector< std::optional< chest_data > > &chest_overrides)
Definition chest_edit.cc:94
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Legacy chest data structure.
Definition zelda.h:438
Treasure chest.
Definition zelda.h:425
project::YazeProject * project
Definition editor.h:174
RomWritePolicy write_policy
Definition project.h:225
core::HackManifest hack_manifest
Definition project.h:360