7#include "absl/strings/cord.h"
23 : plan_(std::move(plan)), restore_(std::move(restore)) {}
24 absl::Status
Undo()
override {
return restore_(plan_.before); }
25 absl::Status
Redo()
override {
return restore_(plan_.after); }
27 return absl::StrFormat(
"Replace room %03X contents", plan_.target_room_id);
30 size_t bytes =
sizeof(plan_);
31 for (
const auto* document : {&plan_.before, &plan_.after}) {
32 const auto& c = document->contents;
59 return absl::FailedPreconditionError(
60 "A changed room domain has saving disabled; enable it before "
63 return absl::OkStatus();
71 constexpr uint16_t kMaxTorchTableBytes = 0x120;
72 if (length > kMaxTorchTableBytes || (length & 1) != 0 ||
74 return absl::FailedPreconditionError(
75 "Torch table has an invalid length or lies outside the ROM");
78 bool found_room =
false;
79 while (offset < length) {
84 if (segment_room == 0xFFFF)
87 return absl::FailedPreconditionError(
88 "Torch table has an invalid room ID");
90 if (segment_room == room_id) {
92 return absl::FailedPreconditionError(absl::StrFormat(
93 "Torch table contains duplicate segments for room %03X", room_id));
97 bool terminated =
false;
98 while (offset < length) {
102 if (entry == 0xFFFF) {
108 return absl::FailedPreconditionError(absl::StrFormat(
109 "Torch segment for room %03X has no end marker", segment_room));
112 return absl::OkStatus();
122 const auto table_bank = (table_snes >> 16) & 0xFF;
123 if ((table_snes & 0xFFFF) < 0x8000 || table_bank == 0x7E ||
124 table_bank == 0x7F || table_bank == 0xFE || table_bank == 0xFF ||
125 bank == 0x7E || bank == 0x7F || bank == 0xFE || bank == 0xFF) {
126 return absl::FailedPreconditionError(
"Invalid room header pointer table");
128 const auto table_pc =
SnesToPc(table_snes);
131 return absl::FailedPreconditionError(
132 "Room header pointer table is truncated");
135 const auto header_pc =
SnesToPc((bank << 16) | header);
136 if (header < 0x8000 ||
static_cast<uint64_t
>(header_pc) + 14 > rom.
size()) {
137 return absl::FailedPreconditionError(
"Replacement room header is invalid");
140 if (other_id == room_id)
143 rom.
ReadWord(table_pc + other_id * 2));
144 const auto other_pc =
SnesToPc((bank << 16) | other_header);
145 if (other_header < 0x8000 ||
146 static_cast<uint64_t
>(other_pc) + 14 > rom.
size())
148 if (header_pc < other_pc + 14 && other_pc < header_pc + 14) {
149 auto status = absl::FailedPreconditionError(absl::StrFormat(
150 "Room %03X header overlaps room %03X; exclude room properties or "
151 "give the destination independent header storage before replacing it",
157 return absl::OkStatus();
163 return absl::OkStatus();
165 std::map<int, uint8_t> masks;
166 for (
const auto& zone : saved)
167 masks[zone.room_id] = zone.sram_bit_mask;
175 for (
const auto& [
id, mask] : masks) {
180 if (prior && !(prior & (prior - 1)) && !(used & prior)) {
182 return absl::OkStatus();
184 for (
unsigned bit = 1; bit <= 128; bit <<= 1) {
187 return absl::OkStatus();
190 return absl::ResourceExhaustedError(
191 "No free water-fill save-state bit remains");
198 return absl::InvalidArgumentError(
"Choose a room in the current ROM");
201 if (existing && existing->rom() !=
rom_) {
202 return absl::FailedPreconditionError(
"Room belongs to another ROM");
205 ((!existing->AreTorchesLoaded() && existing->torches_dirty()) ||
206 (!existing->AreBlocksLoaded() && existing->blocks_dirty()))) {
207 return absl::FailedPreconditionError(
208 "Finish loading the room's torch/block edits before copying it");
212 if (!existing || !existing->AreTorchesLoaded()) {
215 if (!existing || !existing->AreSpritesLoaded()) {
216 if (existing && existing->sprites_dirty()) {
217 return absl::FailedPreconditionError(
218 "Finish loading the room's sprite edits first");
222 if (!existing || !existing->ArePotItemsLoaded()) {
223 if (existing && existing->pot_items_dirty()) {
224 return absl::FailedPreconditionError(
225 "Finish loading the room's pot-item edits first");
229 if ((!existing || !existing->AreObjectsLoaded()) &&
237 if (!room.AreSpritesLoaded()) {
238 return absl::FailedPreconditionError(
"Room sprites did not load");
240 if (!room.AreChestsLoaded()) {
241 if (room.chests_dirty()) {
242 return absl::FailedPreconditionError(
243 "Finish loading the room's chest edits first");
247 room.EnsurePotItemsLoaded();
248 if (!room.water_fill_dirty() && !room.has_water_fill_zone()) {
250 for (
const auto& zone : zones) {
251 if (zone.room_id != room_id)
253 room.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
254 for (
const auto offset : zone.fill_offsets)
255 room.SetWaterFillTile(offset % 64, offset / 64,
true);
256 room.ClearWaterFillDirty();
260 if (!room.AreTorchesLoaded())
262 if (!room.AreBlocksLoaded())
264 if (!room.AreChestsLoaded() || !room.ArePotItemsLoaded() ||
265 !room.AreTorchesLoaded() || !room.AreBlocksLoaded()) {
266 return absl::FailedPreconditionError(
267 "Room data did not fully load; repair the source before copying it");
269 return absl::OkStatus();
279 int target_room_id,
int source_room_id,
const std::string& json,
281 if (source_room_id < -1) {
282 return absl::InvalidArgumentError(
"Invalid source room");
284 if (source_room_id == target_room_id) {
285 return absl::InvalidArgumentError(
"Choose a different source room");
288 if (source_room_id >= 0) {
298 if (source_room_id >= 0) {
299 plan.clone_source_room_id = source_room_id;
300 plan.clone_source_before = std::move(source);
303 if (!viewer || viewer->current_room_id() != target_room_id)
305 plan.before.contents.selected_objects =
306 viewer->object_interaction().GetSelectedObjectIndices();
307 plan.before.contents.selected_entities = viewer->object_interaction()
308 .entity_coordinator()
309 .SelectedEntitiesForEdit();
311 room_viewers_.ForEach([&](
int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
312 capture_selection(viewer.get());
329 auto scratch_game = std::make_unique<zelda3::GameData>();
330 auto shadow = std::make_unique<DungeonEditorV2>(&scratch);
333 shadow->SetGameData(scratch_game.get());
336 auto& copy = shadow->rooms_[id];
343 for (
auto&
object : copy.GetTileObjects())
345 copy.SetLoaded(original.
IsLoaded());
352 for (
const auto& zone : saved_zones) {
353 if (shadow->rooms_.GetIfMaterialized(zone.room_id))
355 auto& copy = shadow->rooms_[zone.room_id];
356 copy.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
357 for (
const auto offset : zone.fill_offsets)
358 copy.SetWaterFillTile(offset % 64, offset / 64,
true);
359 copy.ClearWaterFillDirty();
364 const auto prior_dirty = target.CaptureSaveDirtySnapshot();
365 target.ClearSaveDirtyState();
366 target.ClearCustomCollisionDirty();
367 target.ClearWaterFillDirty();
370 const auto changed_dirty = target.CaptureSaveDirtySnapshot();
371 target.RestoreSaveDirtySnapshot(prior_dirty);
372 auto combined = prior_dirty;
373 combined.header |= changed_dirty.header;
374 combined.object_stream |= changed_dirty.object_stream;
375 combined.object_stream_header |= changed_dirty.object_stream_header;
376 combined.sprites |= changed_dirty.sprites;
377 combined.chests |= changed_dirty.chests;
378 combined.pot_items |= changed_dirty.pot_items;
379 combined.torches |= changed_dirty.torches;
380 combined.blocks |= changed_dirty.blocks;
381 combined.custom_collision |= changed_dirty.custom_collision;
382 combined.water_fill |= changed_dirty.water_fill;
384 combined.block_load_orders = changed_dirty.block_load_orders;
385 target.RestoreSaveDirtySnapshot(combined);
387 target.header_dirty()) {
392 if (changed_dirty.water_fill &&
394 return absl::FailedPreconditionError(
395 "Water-fill state bits changed; preview again");
397 bool changed_other_mask =
false;
399 const auto* copy = shadow->rooms_.GetIfMaterialized(
id);
402 changed_other_mask =
true;
404 if (changed_other_mask) {
405 return absl::FailedPreconditionError(
406 "Replacement would reassign another room's water-fill state bit");
411 std::vector<std::pair<uint32_t, uint32_t>> ranges;
413 const auto& after = scratch.
vector();
414 if (before.size() != after.size()) {
415 return absl::FailedPreconditionError(
416 "Room replacement cannot resize the ROM");
418 for (
size_t i = 0; i < before.size();) {
419 if (before[i] == after[i]) {
423 const size_t start = i++;
424 while (i < before.size() && before[i] != after[i])
426 ranges.emplace_back(start, i);
431 "room clone/import",
"DungeonEditorV2",
nullptr));
433 return absl::OkStatus();
438 auto validate = [&]() -> absl::Status {
440 if (plan.
rom !=
rom_ || !target || target->rom() !=
rom_ ||
444 return absl::FailedPreconditionError(
445 "Room changed since preview; preview again");
452 return absl::FailedPreconditionError(
453 "Source room changed; preview again");
460 return absl::InvalidArgumentError(
461 "Room preview does not match its copy options");
463 return absl::OkStatus();
467 return absl::OkStatus();
480 return absl::FailedPreconditionError(
481 "Room history belongs to another ROM");
482 return RestoreRoomTransfer(id, state);
484 return absl::OkStatus();
490 if (!room || room->rom() !=
rom_ || room->id() != room_id) {
491 return absl::FailedPreconditionError(
492 "Room replacement target is not loaded");
495 for (
auto&
object : room->GetTileObjects())
500 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::StatusOr< uint8_t > ReadByte(int offset) const
const auto & vector() const
absl::StatusOr< uint16_t > ReadWord(int offset) const
absl::StatusOr< uint32_t > ReadLong(int offset) const
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)
void FinalizePendingUndoActions()
absl::Status EnsureRoomTransferLoaded(int room_id)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
static bool IsValidRoomId(int room_id)
void RefreshRoomMetadataViews(int room_id)
absl::StatusOr< std::string > ExportRoomDocument(int room_id)
absl::Status EnsureConnectionRoomLoaded(int room_id)
absl::StatusOr< DungeonRoomTransferPlan > PreviewRoomTransfer(int target_room_id, int source_room_id, const std::string &json, const DungeonRoomTransferOptions &options)
zelda3::GameData * game_data_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
bool undo_restore_triggered_ping_
absl::Status RestoreRoomTransfer(int room_id, const DungeonRoomDocument &document)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
void ForEachMaterialized(Fn &&fn)
UndoManager undo_manager_
EditorDependencies dependencies_
Abstract base for all undoable actions (Command pattern)
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo() override
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::function< absl::Status(const DungeonRoomDocument &)> Restore
absl::Status Undo() override
RoomTransferAction(DungeonRoomTransferPlan plan, Restore restore)
DungeonRoomTransferPlan plan_
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
void EnsureSpritesLoaded()
bool custom_collision_dirty() const
bool pot_items_dirty() const
bool object_stream_dirty() const
bool sprites_dirty() const
bool header_dirty() const
bool object_stream_header_dirty() const
bool has_water_fill_zone() const
bool chests_dirty() const
bool blocks_dirty() const
bool torches_dirty() const
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
bool water_fill_dirty() const
bool AreTorchesLoaded() const
bool AreBlocksLoaded() const
uint8_t water_fill_sram_bit_mask() const
struct chest_data chest_data
Legacy chest data structure.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::Status CheckTransferSaveFlags(const zelda3::Room &changed)
absl::Status ValidateTransferTorchSource(const Rom &rom, int room_id)
absl::Status AssignTransferredWaterMask(Rom &rom, const DungeonRoomStore &rooms, DungeonRoomTransferPlan &plan)
absl::Status ValidateTransferHeaderOwnership(const Rom &rom, int room_id)
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)
constexpr char kRoomTransferSharedHeaderPayload[]
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 kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
absl::StatusOr< DungeonStreamRecord > ReadDungeonSpriteStream(const Rom &rom, int room_id)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
DungeonSelectionEditState contents
zelda3::WaterFillZoneMap water
DungeonRoomTransferOptions options
std::optional< DungeonRoomDocument > clone_source_before
DungeonRoomDocument after
DungeonRoomDocument before
project::YazeProject * project
core::HackManifest hack_manifest
Represents a door in a dungeon room.