yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_room_transfer.cc
Go to the documentation of this file.
2
3#include <map>
4#include <memory>
5#include <utility>
6
8#include "core/features.h"
9#include "util/macro.h"
13
14namespace yaze::editor {
15namespace {
16
17class RoomTransferAction final : public UndoAction {
18 public:
19 using Restore = std::function<absl::Status(const DungeonRoomDocument&)>;
21 : plan_(std::move(plan)), restore_(std::move(restore)) {}
22 absl::Status Undo() override { return restore_(plan_.before); }
23 absl::Status Redo() override { return restore_(plan_.after); }
24 std::string Description() const override {
25 return absl::StrFormat("Replace room %03X contents", plan_.target_room_id);
26 }
27 size_t MemoryUsage() const override {
28 size_t bytes = sizeof(plan_);
29 for (const auto* document : {&plan_.before, &plan_.after}) {
30 const auto& c = document->contents;
31 bytes += c.objects.size() * sizeof(zelda3::RoomObject) +
32 c.chests.size() * sizeof(chest_data) +
33 c.doors.size() * sizeof(zelda3::Room::Door) +
34 c.sprites.size() * sizeof(DungeonSpriteSnapshot) +
35 c.items.size() * sizeof(zelda3::PotItem);
36 }
37 return bytes;
38 }
39
40 private:
43};
44
45absl::Status CheckTransferSaveFlags(const zelda3::Room& changed) {
46 const auto& f = core::FeatureFlags::get().dungeon;
47 if ((!f.kSaveObjects && (changed.object_stream_dirty() ||
48 changed.object_stream_header_dirty())) ||
49 (!f.kSaveRoomHeaders && changed.header_dirty()) ||
50 (!f.kSaveSprites && changed.sprites_dirty()) ||
51 (!f.kSaveChests && changed.chests_dirty()) ||
52 (!f.kSavePotItems && changed.pot_items_dirty()) ||
53 (!f.kSaveTorches && changed.torches_dirty()) ||
54 (!f.kSaveBlocks && changed.blocks_dirty()) ||
55 (!f.kSaveCollision && changed.custom_collision_dirty()) ||
56 (!f.kSaveWaterFillZones && changed.water_fill_dirty())) {
57 return absl::FailedPreconditionError(
58 "A changed room domain has saving disabled; enable it before "
59 "replacement");
60 }
61 return absl::OkStatus();
62}
63
64// The legacy torch loader has no status result and accepts a segment ending
65// at the declared table length. Verify its framing before trusting a new load.
66absl::Status ValidateTransferTorchSource(const Rom& rom, int room_id) {
67 ASSIGN_OR_RETURN(const auto length,
69 constexpr uint16_t kMaxTorchTableBytes = 0x120;
70 if (length > kMaxTorchTableBytes || (length & 1) != 0 ||
71 static_cast<uint64_t>(zelda3::kTorchData) + length > rom.size()) {
72 return absl::FailedPreconditionError(
73 "Torch table has an invalid length or lies outside the ROM");
74 }
75 size_t offset = 0;
76 bool found_room = false;
77 while (offset < length) {
78 ASSIGN_OR_RETURN(const auto segment_room,
79 rom.ReadWord(zelda3::kTorchData + offset));
80 offset += 2;
81 // Standalone end markers are tolerated by the existing table reader.
82 if (segment_room == 0xFFFF)
83 continue;
84 if (segment_room >= zelda3::kNumberOfRooms) {
85 return absl::FailedPreconditionError(
86 "Torch table has an invalid room ID");
87 }
88 if (segment_room == room_id) {
89 if (found_room) {
90 return absl::FailedPreconditionError(absl::StrFormat(
91 "Torch table contains duplicate segments for room %03X", room_id));
92 }
93 found_room = true;
94 }
95 bool terminated = false;
96 while (offset < length) {
97 ASSIGN_OR_RETURN(const auto entry,
98 rom.ReadWord(zelda3::kTorchData + offset));
99 offset += 2;
100 if (entry == 0xFFFF) {
101 terminated = true;
102 break;
103 }
104 }
105 if (!terminated) {
106 return absl::FailedPreconditionError(absl::StrFormat(
107 "Torch segment for room %03X has no end marker", segment_room));
108 }
109 }
110 return absl::OkStatus();
111}
112
113absl::Status AssignTransferredWaterMask(Rom& rom, const DungeonRoomStore& rooms,
115 if (!(plan.options.domains & kTransferWater) || !plan.after.water.has_data)
116 return absl::OkStatus();
117 ASSIGN_OR_RETURN(const auto saved, zelda3::LoadWaterFillTable(&rom));
118 std::map<int, uint8_t> masks;
119 for (const auto& zone : saved)
120 masks[zone.room_id] = zone.sram_bit_mask;
121 rooms.ForEachMaterialized([&](int id, const zelda3::Room& room) {
122 if (room.has_water_fill_zone())
123 masks[id] = room.water_fill_sram_bit_mask();
124 else if (room.water_fill_dirty())
125 masks.erase(id);
126 });
127 uint8_t used = 0;
128 for (const auto& [id, mask] : masks) {
129 if (id != plan.target_room_id)
130 used |= mask;
131 }
132 const auto prior = plan.before.water.sram_bit_mask;
133 if (prior && !(prior & (prior - 1)) && !(used & prior)) {
134 plan.after.water.sram_bit_mask = prior;
135 return absl::OkStatus();
136 }
137 for (unsigned bit = 1; bit <= 128; bit <<= 1) {
138 if (!(used & bit)) {
139 plan.after.water.sram_bit_mask = static_cast<uint8_t>(bit);
140 return absl::OkStatus();
141 }
142 }
143 return absl::ResourceExhaustedError(
144 "No free water-fill save-state bit remains");
145}
146
147} // namespace
148
150 if (!rom_ || !rom_->is_loaded() || !IsValidRoomId(room_id)) {
151 return absl::InvalidArgumentError("Choose a room in the current ROM");
152 }
153 const auto* existing = rooms_.GetIfMaterialized(room_id);
154 if (existing && existing->rom() != rom_) {
155 return absl::FailedPreconditionError("Room belongs to another ROM");
156 }
157 if (existing &&
158 ((!existing->AreTorchesLoaded() && existing->torches_dirty()) ||
159 (!existing->AreBlocksLoaded() && existing->blocks_dirty()))) {
160 return absl::FailedPreconditionError(
161 "Finish loading the room's torch/block edits before copying it");
162 }
163 // Validate before invoking the legacy void loaders. Empty/truncated streams
164 // must not become apparently valid empty source collections.
165 if (!existing || !existing->AreTorchesLoaded()) {
166 RETURN_IF_ERROR(ValidateTransferTorchSource(*rom_, room_id));
167 }
168 if (!existing || !existing->AreSpritesLoaded()) {
169 if (existing && existing->sprites_dirty()) {
170 return absl::FailedPreconditionError(
171 "Finish loading the room's sprite edits first");
172 }
174 }
175 if (!existing || !existing->ArePotItemsLoaded()) {
176 if (existing && existing->pot_items_dirty()) {
177 return absl::FailedPreconditionError(
178 "Finish loading the room's pot-item edits first");
179 }
181 }
182 if ((!existing || !existing->AreObjectsLoaded()) &&
183 rom_->size() >=
186 }
188 auto& room = *rooms_.GetIfLoaded(room_id);
189 room.EnsureSpritesLoaded();
190 if (!room.AreSpritesLoaded()) {
191 return absl::FailedPreconditionError("Room sprites did not load");
192 }
193 if (!room.AreChestsLoaded()) {
194 if (room.chests_dirty()) {
195 return absl::FailedPreconditionError(
196 "Finish loading the room's chest edits first");
197 }
198 room.LoadChests();
199 }
200 room.EnsurePotItemsLoaded();
201 if (!room.water_fill_dirty() && !room.has_water_fill_zone()) {
203 for (const auto& zone : zones) {
204 if (zone.room_id != room_id)
205 continue;
206 room.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
207 for (const auto offset : zone.fill_offsets)
208 room.SetWaterFillTile(offset % 64, offset / 64, true);
209 room.ClearWaterFillDirty();
210 break;
211 }
212 }
213 if (!room.AreTorchesLoaded())
214 room.LoadTorches();
215 if (!room.AreBlocksLoaded())
216 room.LoadBlocks();
217 if (!room.AreChestsLoaded() || !room.ArePotItemsLoaded() ||
218 !room.AreTorchesLoaded() || !room.AreBlocksLoaded()) {
219 return absl::FailedPreconditionError(
220 "Room data did not fully load; repair the source before copying it");
221 }
222 return absl::OkStatus();
223}
224
230
231absl::StatusOr<DungeonRoomTransferPlan> DungeonEditorV2::PreviewRoomTransfer(
232 int target_room_id, int source_room_id, const std::string& json,
233 const DungeonRoomTransferOptions& options) {
234 if (source_room_id < -1) {
235 return absl::InvalidArgumentError("Invalid source room");
236 }
237 if (source_room_id == target_room_id) {
238 return absl::InvalidArgumentError("Choose a different source room");
239 }
240 DungeonRoomDocument source;
241 if (source_room_id >= 0) {
243 source = CaptureDungeonRoomDocument(*rooms_.GetIfLoaded(source_room_id));
244 } else {
246 }
248 ASSIGN_OR_RETURN(auto plan,
250 source, options));
251 if (source_room_id >= 0) {
252 plan.clone_source_room_id = source_room_id;
253 plan.clone_source_before = std::move(source);
254 }
255 auto capture_selection = [&](DungeonCanvasViewer* viewer) {
256 if (!viewer || viewer->current_room_id() != target_room_id)
257 return;
258 plan.before.contents.selected_objects =
259 viewer->object_interaction().GetSelectedObjectIndices();
260 plan.before.contents.selected_entities = viewer->object_interaction()
261 .entity_coordinator()
262 .SelectedEntitiesForEdit();
263 };
264 room_viewers_.ForEach([&](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
265 capture_selection(viewer.get());
266 });
267 capture_selection(workbench_compare_viewer_.get());
268 capture_selection(workbench_viewer_.get());
269 RETURN_IF_ERROR(AssignTransferredWaterMask(*rom_, rooms_, plan));
270 if (plan.changed())
272 return plan;
273}
274
276 const DungeonRoomTransferPlan& plan) {
277 Rom scratch(*rom_);
278 // Only a detached model is visible to serializers: no live renderer, palette
279 // state, history, source files or borrowed room buffers can be mutated here.
280 // GameData is roughly 1.8 MB; keep it off small worker/WebAssembly stacks.
281 // Declare it before the editor so borrowed state outlives editor teardown.
282 auto scratch_game = std::make_unique<zelda3::GameData>();
283 auto shadow = std::make_unique<DungeonEditorV2>(&scratch);
284 if (game_data_)
285 scratch_game->version = game_data_->version;
286 shadow->SetGameData(scratch_game.get());
287 shadow->dependencies_.project = dependencies_.project;
288 rooms_.ForEachMaterialized([&](int id, const zelda3::Room& original) {
289 auto& copy = shadow->rooms_[id];
290 copy = zelda3::Room(id, &scratch, scratch_game.get());
291 if (original.AreTorchesLoaded())
292 copy.LoadTorches();
293 if (original.AreBlocksLoaded())
294 copy.LoadBlocks();
296 for (auto& object : copy.GetTileObjects())
297 object.SetRom(&scratch);
298 copy.SetLoaded(original.IsLoaded());
299 copy.RestoreSaveDirtySnapshot(original.CaptureSaveDirtySnapshot());
300 });
301 // The water table is project-owned and may precede room materialization.
302 // Include saved zones absent from the live room store in global preflight.
303 ASSIGN_OR_RETURN(const auto saved_zones,
305 for (const auto& zone : saved_zones) {
306 if (shadow->rooms_.GetIfMaterialized(zone.room_id))
307 continue;
308 auto& copy = shadow->rooms_[zone.room_id];
309 copy.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
310 for (const auto offset : zone.fill_offsets)
311 copy.SetWaterFillTile(offset % 64, offset / 64, true);
312 copy.ClearWaterFillDirty();
313 }
314 auto& target = *shadow->rooms_.GetIfLoaded(plan.target_room_id);
315 // Check only newly changed domains; older unrelated unsaved edits retain
316 // their normal Save behavior in the subsequent shared-table preflight.
317 const auto prior_dirty = target.CaptureSaveDirtySnapshot();
318 target.ClearSaveDirtyState();
319 target.ClearCustomCollisionDirty();
320 target.ClearWaterFillDirty();
321 ApplyDungeonRoomDocument(target, plan.after);
322 RETURN_IF_ERROR(CheckTransferSaveFlags(target));
323 const auto changed_dirty = target.CaptureSaveDirtySnapshot();
324 target.RestoreSaveDirtySnapshot(prior_dirty);
325 auto combined = prior_dirty;
326 combined.header |= changed_dirty.header;
327 combined.object_stream |= changed_dirty.object_stream;
328 combined.object_stream_header |= changed_dirty.object_stream_header;
329 combined.sprites |= changed_dirty.sprites;
330 combined.chests |= changed_dirty.chests;
331 combined.pot_items |= changed_dirty.pot_items;
332 combined.torches |= changed_dirty.torches;
333 combined.blocks |= changed_dirty.blocks;
334 combined.custom_collision |= changed_dirty.custom_collision;
335 combined.water_fill |= changed_dirty.water_fill;
336 combined.water_fill_sram_bit_mask = plan.after.water.sram_bit_mask;
337 combined.block_load_orders = changed_dirty.block_load_orders;
338 target.RestoreSaveDirtySnapshot(combined);
339 if (core::FeatureFlags::get().dungeon.kSaveRoomHeaders &&
340 target.header_dirty()) {
342 }
343 RETURN_IF_ERROR(shadow->SaveRoomImpl(plan.target_room_id, true));
344 if (changed_dirty.water_fill &&
345 target.water_fill_sram_bit_mask() != plan.after.water.sram_bit_mask) {
346 return absl::FailedPreconditionError(
347 "Water-fill state bits changed; preview again");
348 }
349 bool changed_other_mask = false;
350 rooms_.ForEachMaterialized([&](int id, const zelda3::Room& original) {
351 const auto* copy = shadow->rooms_.GetIfMaterialized(id);
352 if (id != plan.target_room_id && original.has_water_fill_zone() && copy &&
353 original.water_fill_sram_bit_mask() != copy->water_fill_sram_bit_mask())
354 changed_other_mask = true;
355 });
356 if (changed_other_mask) {
357 return absl::FailedPreconditionError(
358 "Replacement would reassign another room's water-fill state bit");
359 }
360 // Check the actual serialized changes as well as the save path's estimated
361 // ranges, covering shared torch/block tables and allocator metadata.
363 std::vector<std::pair<uint32_t, uint32_t>> ranges;
364 const auto& before = rom_->vector();
365 const auto& after = scratch.vector();
366 if (before.size() != after.size()) {
367 return absl::FailedPreconditionError(
368 "Room replacement cannot resize the ROM");
369 }
370 for (size_t i = 0; i < before.size();) {
371 if (before[i] == after[i]) {
372 ++i;
373 continue;
374 }
375 const size_t start = i++;
376 while (i < before.size() && before[i] != after[i])
377 ++i;
378 ranges.emplace_back(start, i);
379 }
383 "room clone/import", "DungeonEditorV2", nullptr));
384 }
385 return absl::OkStatus();
386}
387
389 const DungeonRoomTransferPlan& plan) {
390 auto validate = [&]() -> absl::Status {
391 const auto* target = rooms_.GetIfLoaded(plan.target_room_id);
392 if (plan.rom != rom_ || !target || target->rom() != rom_ ||
393 target->id() != plan.target_room_id ||
395 plan.before)) {
396 return absl::FailedPreconditionError(
397 "Room changed since preview; preview again");
398 }
399 if (plan.clone_source_room_id >= 0) {
400 const auto* source = rooms_.GetIfLoaded(plan.clone_source_room_id);
401 if (!source || source->rom() != rom_ || !plan.clone_source_before ||
403 *plan.clone_source_before)) {
404 return absl::FailedPreconditionError(
405 "Source room changed; preview again");
406 }
407 }
409 const auto checked,
410 PlanDungeonRoomTransfer(*target, plan.after, plan.options));
411 if (!SameDungeonRoomDocument(checked.after, plan.after)) {
412 return absl::InvalidArgumentError(
413 "Room preview does not match its copy options");
414 }
415 return absl::OkStatus();
416 };
417 RETURN_IF_ERROR(validate());
418 if (!plan.changed())
419 return absl::OkStatus();
422 RETURN_IF_ERROR(validate());
423 // Gesture finalization can alter shared-table occupancy in another room.
426 auto history = plan;
427 history.clone_source_before.reset();
428 undo_manager_.Push(std::make_unique<RoomTransferAction>(
429 std::move(history), [this, id = plan.target_room_id,
430 rom = rom_](const DungeonRoomDocument& state) {
431 if (rom_ != rom)
432 return absl::FailedPreconditionError(
433 "Room history belongs to another ROM");
434 return RestoreRoomTransfer(id, state);
435 }));
436 return absl::OkStatus();
437}
438
440 int room_id, const DungeonRoomDocument& document) {
441 auto* room = rooms_.GetIfLoaded(room_id);
442 if (!room || room->rom() != rom_ || room->id() != room_id) {
443 return absl::FailedPreconditionError(
444 "Room replacement target is not loaded");
445 }
446 ApplyDungeonRoomDocument(*room, document);
447 for (auto& object : room->GetTileObjects())
448 object.SetRom(rom_);
450 RefreshSelectionEditViews(room_id, &document.contents);
452 return absl::OkStatus();
453}
454
455} // namespace yaze::editor
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
const auto & vector() const
Definition rom.h:217
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:1044
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
bool loaded() const
Check if the manifest has been loaded.
absl::Status PreflightRoomTransfer(const DungeonRoomTransferPlan &plan)
absl::Status ApplyRoomTransfer(const DungeonRoomTransferPlan &plan)
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
void RefreshSelectionEditViews(int room_id, const DungeonSelectionEditState *selection)
static absl::Status ValidateRoomHeaderOwnership(const Rom &rom, int room_id)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
static bool IsValidRoomId(int room_id)
absl::StatusOr< std::string > ExportRoomDocument(int room_id)
absl::StatusOr< DungeonRoomTransferPlan > PreviewRoomTransfer(int target_room_id, int source_room_id, const std::string &json, const DungeonRoomTransferOptions &options)
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
absl::Status RestoreRoomTransfer(int room_id, const DungeonRoomDocument &document)
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")
void EnsureSpritesLoaded()
Definition room.cc:1033
bool custom_collision_dirty() const
Definition room.h:596
bool IsLoaded() const
Definition room.h:970
bool pot_items_dirty() const
Definition room.h:431
bool object_stream_dirty() const
Definition room.h:504
bool sprites_dirty() const
Definition room.h:318
bool header_dirty() const
Definition room.h:658
bool object_stream_header_dirty() const
Definition room.h:506
bool has_water_fill_zone() const
Definition room.h:603
bool chests_dirty() const
Definition room.h:325
bool blocks_dirty() const
Definition room.h:438
bool torches_dirty() const
Definition room.h:435
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
Definition room.h:673
bool water_fill_dirty() const
Definition room.h:654
bool AreTorchesLoaded() const
Definition room.h:976
bool AreBlocksLoaded() const
Definition room.h:982
uint8_t water_fill_sram_bit_mask() const
Definition room.h:644
struct chest_data chest_data
Legacy chest data structure.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status AssignTransferredWaterMask(Rom &rom, const DungeonRoomStore &rooms, DungeonRoomTransferPlan &plan)
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)
absl::StatusOr< DungeonRoomTransferPlan > PlanDungeonRoomTransfer(const zelda3::Room &target, const DungeonRoomDocument &source, DungeonRoomTransferOptions options)
bool SameDungeonRoomDocument(const DungeonRoomDocument &a, const DungeonRoomDocument &b)
absl::StatusOr< std::string > SerializeDungeonRoomDocument(const DungeonRoomDocument &document)
constexpr uint16_t kTransferWater
void ApplyDungeonRoomDocument(zelda3::Room &room, const DungeonRoomDocument &document)
absl::StatusOr< DungeonRoomDocument > ParseDungeonRoomDocument(const std::string &json)
DungeonRoomDocument CaptureDungeonRoomDocument(const zelda3::Room &room)
constexpr int kTorchesLengthPointer
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
absl::StatusOr< DungeonStreamRecord > ReadDungeonPotItemStream(const Rom &rom, int room_id)
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
constexpr int kTorchData
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
absl::StatusOr< DungeonStreamRecord > ReadDungeonSpriteStream(const Rom &rom, int room_id)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::optional< DungeonRoomDocument > clone_source_before
project::YazeProject * project
Definition editor.h:174
RomWritePolicy write_policy
Definition project.h:229
core::HackManifest hack_manifest
Definition project.h:376
zelda3_version version
Definition game_data.h:84
Represents a door in a dungeon room.
Definition room.h:339