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 }
173 if (objects.size() > zelda3::kMaxTileObjects &&
174 objects.size() > room->GetTileObjects().size()) {
175 return absl::ResourceExhaustedError("Room object limit reached");
176 }
177 const bool torches_changed = !SameSpecialObjects(
178 room->GetTileObjects(), objects, zelda3::ObjectOption::Torch);
179 const bool blocks_changed = !SameSpecialObjects(
180 room->GetTileObjects(), objects, zelda3::ObjectOption::Block);
181 if (torches_changed && !room->AreTorchesLoaded()) {
182 return absl::FailedPreconditionError(
183 "Load the room's torch table before editing torches");
184 }
185 if (torches_changed || blocks_changed) {
186 // Rooms own graphics resources and cannot be copied. Materialize only the
187 // small block table to establish ownership, then replace it with authored
188 // candidates (including pending block-slot identities). No graphics load.
189 zelda3::Room staged(room_id, rom_);
190 if (blocks_changed) {
191 if (!room->AreBlocksLoaded()) {
192 return absl::FailedPreconditionError(
193 "Load the room's block table before editing pushable blocks");
194 }
195 staged.LoadBlocks();
196 }
197 staged.SetTileObjects(objects);
198 if (torches_changed)
199 staged.MarkTorchesDirty();
200 if (blocks_changed)
201 staged.MarkBlocksDirty();
202 const auto lookup = [&](int id) -> const zelda3::Room* {
203 return id == room_id ? &staged : rooms_.GetIfMaterialized(id);
204 };
205 std::vector<std::pair<uint32_t, uint32_t>> ranges;
206 if (torches_changed) {
207 ASSIGN_OR_RETURN(auto torch_ranges,
209 rom_, static_cast<int>(rooms_.size()), lookup));
210 ranges.insert(ranges.end(), torch_ranges.begin(), torch_ranges.end());
211 }
212 if (blocks_changed) {
213 ASSIGN_OR_RETURN(auto block_ranges,
215 rom_, static_cast<int>(rooms_.size()), lookup));
216 ranges.insert(ranges.end(), block_ranges.begin(), block_ranges.end());
217 }
223 "special object authoring", "DungeonEditorV2", nullptr));
224 }
225 }
226 if (check_streams) {
227 auto candidate = CaptureDungeonSelectionEditState(*room);
228 candidate.objects = objects;
230 PreflightStreamMutation(room_id, candidate, kSelectionObjects));
231 }
232 const auto& original = room->GetChests();
233 if (original.size() == chests.size() &&
234 std::equal(
235 original.begin(), original.end(), chests.begin(),
236 [](auto a, auto b) { return a.id == b.id && a.size == b.size; })) {
237 return absl::OkStatus();
238 }
239 if (!room->AreChestsLoaded() || !rom_ || !rom_->is_loaded()) {
240 return absl::FailedPreconditionError("Chest contents are not loaded");
241 }
242 // The shared save planner accounts for physical records in unopened rooms
243 // and every other room's pending edits, without writing any ROM bytes.
244 zelda3::Room staged(room_id, rom_);
245 staged.GetChests() = chests;
246 staged.MarkChestsDirty();
248 rom_, static_cast<int>(rooms_.size()),
249 [&](int id) -> const zelda3::Room* {
250 return id == room_id
251 ? &staged
252 : rooms_.GetIfMaterialized(id);
253 }));
257 dependencies_.project->rom_metadata.write_policy, plan.write_ranges(),
258 "chest authoring", "DungeonEditorV2", nullptr));
259 }
260 return absl::OkStatus();
261}
262
264 int room_id, const std::vector<zelda3::RoomObject>& objects,
265 const std::vector<chest_data>& chests,
266 const std::vector<size_t>& selection) {
267 RETURN_IF_ERROR(PreflightObjectMutation(room_id, objects, chests));
269 BeginUndoSnapshot(room_id);
270 RETURN_IF_ERROR(RestoreRoomObjects(room_id, objects, selection, chests));
271 FinalizeUndoAction(room_id);
272 return absl::OkStatus();
273}
274
275absl::Status DungeonEditorV2::RestoreChest(int room_id, size_t index,
277 auto* room = rooms_.GetIfLoaded(room_id);
278 if (!room || room->rom() != rom_ || !room->AreChestsLoaded() ||
279 index >= room->GetChests().size()) {
280 return absl::FailedPreconditionError(
281 "Chest contents record is unavailable");
282 }
283 room->GetChests()[index] = chest;
284 room->MarkChestsDirty();
285 // Contents do not alter the visual chest object. Do not flash a different room
286 // or dirty its tile-object stream when this record is restored offscreen.
288 return absl::OkStatus();
289}
290
291} // namespace yaze::editor
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
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 PreflightMinecartObjectCollision(int room_id, const std::vector< zelda3::RoomObject > &objects) const
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:383
EditorDependencies dependencies_
Definition editor.h:382
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:4516
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:4270
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:3344
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:3831
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:229
core::HackManifest hack_manifest
Definition project.h:376