feat(overworld): integrate overworld_item in map handling
- Added inclusion of `overworld_item.h` in both `overworld_map.h` and `overworld.cc` to facilitate item management within the overworld. - Enhanced the `SaveItems` function to reset bomb door lookup tables and update item pointers, ensuring proper handling of overworld items. - Improved data writing logic for overworld items, including adjustments for pointer reuse and metadata updates. Benefits: - Streamlines item management in the overworld, enhancing functionality and maintainability. - Ensures compatibility with expanded ROM structures, improving overall game experience.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
#include "overworld.h"
|
#include "overworld.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
#include "util/hex.h"
|
#include "util/hex.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/macro.h"
|
#include "util/macro.h"
|
||||||
|
#include "zelda3/overworld/overworld_item.h"
|
||||||
|
|
||||||
namespace yaze {
|
namespace yaze {
|
||||||
namespace zelda3 {
|
namespace zelda3 {
|
||||||
@@ -2572,34 +2574,54 @@ absl::Status Overworld::SaveMap16Tiles() {
|
|||||||
absl::Status Overworld::SaveEntrances() {
|
absl::Status Overworld::SaveEntrances() {
|
||||||
util::logf("Saving Entrances");
|
util::logf("Saving Entrances");
|
||||||
|
|
||||||
// Use expanded entrance tables if available
|
auto write_entrance = [&](int index, uint32_t map_addr, uint32_t pos_addr,
|
||||||
|
uint32_t id_addr) -> absl::Status {
|
||||||
|
// Mirrors ZeldaFullEditor/Save.cs::SaveOWEntrances (see lines ~1081-1085)
|
||||||
|
// where MapID and MapPos are written as 16-bit words and EntranceID as a byte.
|
||||||
|
RETURN_IF_ERROR(
|
||||||
|
rom()->WriteShort(map_addr, all_entrances_[index].map_id_));
|
||||||
|
RETURN_IF_ERROR(
|
||||||
|
rom()->WriteShort(pos_addr, all_entrances_[index].map_pos_));
|
||||||
|
RETURN_IF_ERROR(
|
||||||
|
rom()->WriteByte(id_addr, all_entrances_[index].entrance_id_));
|
||||||
|
return absl::OkStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always keep the legacy tables in sync for pure vanilla ROMs so e.g. Hyrule
|
||||||
|
// Magic expects them. ZScream does the same in SaveOWEntrances.
|
||||||
|
for (int i = 0; i < kNumOverworldEntrances; ++i) {
|
||||||
|
RETURN_IF_ERROR(write_entrance(i, kOverworldEntranceMap + (i * 2),
|
||||||
|
kOverworldEntrancePos + (i * 2),
|
||||||
|
kOverworldEntranceEntranceId + i));
|
||||||
|
}
|
||||||
|
|
||||||
if (expanded_entrances_) {
|
if (expanded_entrances_) {
|
||||||
for (int i = 0; i < kNumOverworldEntrances; i++) {
|
// For ZS v3+ ROMs, mirror writes into the expanded tables the way
|
||||||
RETURN_IF_ERROR(rom()->WriteShort(kOverworldEntranceMapExpanded + (i * 2),
|
// ZeldaFullEditor does when the ASM patch is active.
|
||||||
all_entrances_[i].map_id_))
|
for (int i = 0; i < kNumOverworldEntrances; ++i) {
|
||||||
RETURN_IF_ERROR(rom()->WriteShort(kOverworldEntrancePosExpanded + (i * 2),
|
RETURN_IF_ERROR(write_entrance(i,
|
||||||
all_entrances_[i].map_pos_))
|
kOverworldEntranceMapExpanded + (i * 2),
|
||||||
RETURN_IF_ERROR(rom()->WriteByte(kOverworldEntranceEntranceIdExpanded + i,
|
kOverworldEntrancePosExpanded + (i * 2),
|
||||||
all_entrances_[i].entrance_id_))
|
kOverworldEntranceEntranceIdExpanded + i));
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < kNumOverworldEntrances; i++) {
|
|
||||||
RETURN_IF_ERROR(rom()->WriteShort(kOverworldEntranceMap + (i * 2),
|
|
||||||
all_entrances_[i].map_id_))
|
|
||||||
RETURN_IF_ERROR(rom()->WriteShort(kOverworldEntrancePos + (i * 2),
|
|
||||||
all_entrances_[i].map_pos_))
|
|
||||||
RETURN_IF_ERROR(rom()->WriteByte(kOverworldEntranceEntranceId + i,
|
|
||||||
all_entrances_[i].entrance_id_))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < kNumOverworldHoles; i++) {
|
for (int i = 0; i < kNumOverworldHoles; ++i) {
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteShort(kOverworldHoleArea + (i * 2), all_holes_[i].map_id_))
|
rom()->WriteShort(kOverworldHoleArea + (i * 2), all_holes_[i].map_id_));
|
||||||
|
|
||||||
|
// ZeldaFullEditor/Data/Overworld.cs::LoadHoles() adds 0x400 when loading
|
||||||
|
// (see lines ~1006-1014). SaveOWEntrances subtracts it before writing
|
||||||
|
// (Save.cs lines ~1088-1092). We replicate that here so vanilla ROMs
|
||||||
|
// receive the expected values.
|
||||||
|
uint16_t rom_map_pos =
|
||||||
|
static_cast<uint16_t>(all_holes_[i].map_pos_ >= 0x400
|
||||||
|
? all_holes_[i].map_pos_ - 0x400
|
||||||
|
: all_holes_[i].map_pos_);
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteShort(kOverworldHolePos + (i * 2), all_holes_[i].map_pos_))
|
rom()->WriteShort(kOverworldHolePos + (i * 2), rom_map_pos));
|
||||||
RETURN_IF_ERROR(rom()->WriteByte(kOverworldHoleEntrance + i,
|
RETURN_IF_ERROR(rom()->WriteByte(kOverworldHoleEntrance + i,
|
||||||
all_holes_[i].entrance_id_))
|
all_holes_[i].entrance_id_));
|
||||||
}
|
}
|
||||||
|
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
@@ -2632,13 +2654,13 @@ absl::Status Overworld::SaveExits() {
|
|||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteShort(OWExitXScroll + (i * 2), all_exits_[i].x_scroll_));
|
rom()->WriteShort(OWExitXScroll + (i * 2), all_exits_[i].x_scroll_));
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteByte(OWExitYPlayer + (i * 2), all_exits_[i].y_player_));
|
rom()->WriteShort(OWExitYPlayer + (i * 2), all_exits_[i].y_player_));
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteByte(OWExitXPlayer + (i * 2), all_exits_[i].x_player_));
|
rom()->WriteShort(OWExitXPlayer + (i * 2), all_exits_[i].x_player_));
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteByte(OWExitYCamera + (i * 2), all_exits_[i].y_camera_));
|
rom()->WriteShort(OWExitYCamera + (i * 2), all_exits_[i].y_camera_));
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteByte(OWExitXCamera + (i * 2), all_exits_[i].x_camera_));
|
rom()->WriteShort(OWExitXCamera + (i * 2), all_exits_[i].x_camera_));
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
rom()->WriteByte(OWExitUnk1 + i, all_exits_[i].scroll_mod_y_));
|
rom()->WriteByte(OWExitUnk1 + i, all_exits_[i].scroll_mod_y_));
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(
|
||||||
@@ -2647,6 +2669,17 @@ absl::Status Overworld::SaveExits() {
|
|||||||
all_exits_[i].door_type_1_));
|
all_exits_[i].door_type_1_));
|
||||||
RETURN_IF_ERROR(rom()->WriteShort(OWExitDoorType2 + (i * 2),
|
RETURN_IF_ERROR(rom()->WriteShort(OWExitDoorType2 + (i * 2),
|
||||||
all_exits_[i].door_type_2_));
|
all_exits_[i].door_type_2_));
|
||||||
|
|
||||||
|
if (all_exits_[i].room_id_ == 0x0180) {
|
||||||
|
RETURN_IF_ERROR(rom()->WriteByte(OWExitDoorPosition + 0,
|
||||||
|
all_exits_[i].map_id_ & 0xFF));
|
||||||
|
} else if (all_exits_[i].room_id_ == 0x0181) {
|
||||||
|
RETURN_IF_ERROR(rom()->WriteByte(OWExitDoorPosition + 2,
|
||||||
|
all_exits_[i].map_id_ & 0xFF));
|
||||||
|
} else if (all_exits_[i].room_id_ == 0x0182) {
|
||||||
|
RETURN_IF_ERROR(rom()->WriteByte(OWExitDoorPosition + 4,
|
||||||
|
all_exits_[i].map_id_ & 0xFF));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
@@ -2682,78 +2715,117 @@ bool CompareItemsArrays(std::vector<OverworldItem> item_array1,
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
absl::Status Overworld::SaveItems() {
|
absl::Status Overworld::SaveItems() {
|
||||||
std::vector<std::vector<OverworldItem>> room_items(
|
const int pointer_count = zelda3::kNumOverworldMaps;
|
||||||
kNumOverworldMapItemPointers);
|
|
||||||
|
|
||||||
for (int i = 0; i < kNumOverworldMapItemPointers; i++) {
|
std::vector<std::vector<OverworldItem>> room_items(pointer_count);
|
||||||
room_items[i] = std::vector<OverworldItem>();
|
|
||||||
for (const OverworldItem& item : all_items_) {
|
// Reset bomb door lookup table used by special item (0x86)
|
||||||
if (item.room_map_id_ == i) {
|
for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
|
||||||
room_items[i].emplace_back(item);
|
RETURN_IF_ERROR(rom()->WriteShort(zelda3::kOverworldBombDoorItemLocationsNew +
|
||||||
if (item.id_ == 0x86) {
|
(i * 2),
|
||||||
RETURN_IF_ERROR(rom()->WriteWord(
|
0x0000));
|
||||||
0x16DC5 + (i * 2), (item.game_x_ + (item.game_y_ * 64)) * 2));
|
}
|
||||||
}
|
|
||||||
}
|
for (const OverworldItem& item : all_items_) {
|
||||||
|
if (item.deleted) continue;
|
||||||
|
|
||||||
|
const int map_index = static_cast<int>(item.room_map_id_);
|
||||||
|
if (map_index < 0 || map_index >= pointer_count) {
|
||||||
|
LOG_WARN("Overworld::SaveItems",
|
||||||
|
"Skipping item with map index %d outside pointer table (size=%d)",
|
||||||
|
map_index, pointer_count);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
room_items[map_index].push_back(item);
|
||||||
|
|
||||||
|
if (item.id_ == 0x86) {
|
||||||
|
const int lookup_index = std::min(map_index, zelda3::kNumOverworldMaps - 1);
|
||||||
|
RETURN_IF_ERROR(rom()->WriteShort(
|
||||||
|
zelda3::kOverworldBombDoorItemLocationsNew + (lookup_index * 2),
|
||||||
|
static_cast<uint16_t>((item.game_x_ + (item.game_y_ * 64)) * 2)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int data_pos = kOverworldItemsPointers + 0x100;
|
// Prepare pointer reuse cache
|
||||||
int item_pointers[kNumOverworldMapItemPointers];
|
std::vector<int> item_pointers(pointer_count, -1);
|
||||||
int item_pointers_reuse[kNumOverworldMapItemPointers];
|
std::vector<int> item_pointers_reuse(pointer_count, -1);
|
||||||
int empty_pointer = 0;
|
|
||||||
for (int i = 0; i < kNumOverworldMapItemPointers; i++) {
|
for (int i = 0; i < pointer_count; ++i) {
|
||||||
item_pointers_reuse[i] = -1;
|
item_pointers_reuse[i] = -1;
|
||||||
for (int ci = 0; ci < i; ci++) {
|
for (int ci = 0; ci < i; ++ci) {
|
||||||
if (room_items[i].empty()) {
|
if (room_items[i].empty()) {
|
||||||
item_pointers_reuse[i] = -2;
|
item_pointers_reuse[i] = -2; // reuse empty terminator
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy into separator vectors from i to ci, then ci to end
|
if (CompareItemsArrays(room_items[i], room_items[ci])) {
|
||||||
if (CompareItemsArrays(
|
|
||||||
std::vector<OverworldItem>(room_items[i].begin(),
|
|
||||||
room_items[i].end()),
|
|
||||||
std::vector<OverworldItem>(room_items[ci].begin(),
|
|
||||||
room_items[ci].end()))) {
|
|
||||||
item_pointers_reuse[i] = ci;
|
item_pointers_reuse[i] = ci;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < kNumOverworldMapItemPointers; i++) {
|
// Item data always lives in the vanilla data block
|
||||||
|
int data_pos = zelda3::kOverworldItemsStartDataNew;
|
||||||
|
int empty_pointer = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < pointer_count; ++i) {
|
||||||
if (item_pointers_reuse[i] == -1) {
|
if (item_pointers_reuse[i] == -1) {
|
||||||
item_pointers[i] = data_pos;
|
item_pointers[i] = data_pos;
|
||||||
for (const OverworldItem& item : room_items[i]) {
|
for (const OverworldItem& item : room_items[i]) {
|
||||||
short map_pos =
|
const uint16_t map_pos =
|
||||||
static_cast<short>(((item.game_y_ << 6) + item.game_x_) << 1);
|
static_cast<uint16_t>(((item.game_y_ << 6) + item.game_x_) << 1);
|
||||||
|
const uint32_t data =
|
||||||
|
static_cast<uint32_t>(map_pos & 0xFF) |
|
||||||
|
(static_cast<uint32_t>((map_pos >> 8) & 0xFF) << 8) |
|
||||||
|
(static_cast<uint32_t>(item.id_) << 16);
|
||||||
|
|
||||||
uint32_t data = static_cast<uint8_t>(map_pos & 0xFF) |
|
|
||||||
static_cast<uint8_t>(map_pos >> 8) |
|
|
||||||
static_cast<uint8_t>(item.id_);
|
|
||||||
RETURN_IF_ERROR(rom()->WriteLong(data_pos, data));
|
RETURN_IF_ERROR(rom()->WriteLong(data_pos, data));
|
||||||
data_pos += 3;
|
data_pos += 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
empty_pointer = data_pos;
|
empty_pointer = data_pos;
|
||||||
RETURN_IF_ERROR(rom()->WriteWord(data_pos, 0xFFFF));
|
RETURN_IF_ERROR(rom()->WriteShort(data_pos, 0xFFFF));
|
||||||
data_pos += 2;
|
data_pos += 2;
|
||||||
} else if (item_pointers_reuse[i] == -2) {
|
} else if (item_pointers_reuse[i] == -2) {
|
||||||
item_pointers[i] = empty_pointer;
|
if (empty_pointer < 0) {
|
||||||
|
item_pointers[i] = data_pos;
|
||||||
|
empty_pointer = data_pos;
|
||||||
|
RETURN_IF_ERROR(rom()->WriteShort(data_pos, 0xFFFF));
|
||||||
|
data_pos += 2;
|
||||||
|
} else {
|
||||||
|
item_pointers[i] = empty_pointer;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
item_pointers[i] = item_pointers[item_pointers_reuse[i]];
|
item_pointers[i] = item_pointers[item_pointers_reuse[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
int snesaddr = PcToSnes(item_pointers[i]);
|
|
||||||
RETURN_IF_ERROR(
|
|
||||||
rom()->WriteWord(kOverworldItemsPointers + (i * 2), snesaddr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data_pos > kOverworldItemsEndData) {
|
if (data_pos > kOverworldItemsEndData) {
|
||||||
return absl::AbortedError("Too many items");
|
return absl::AbortedError("Too many items");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update pointer table metadata to the expanded location used by ZScream
|
||||||
|
RETURN_IF_ERROR(rom()->WriteLong(
|
||||||
|
zelda3::overworldItemsAddress, PcToSnes(zelda3::kOverworldItemsPointersNew)));
|
||||||
|
RETURN_IF_ERROR(rom()->WriteByte(
|
||||||
|
zelda3::overworldItemsAddressBank,
|
||||||
|
static_cast<uint8_t>((PcToSnes(zelda3::kOverworldItemsStartDataNew) >> 16) &
|
||||||
|
0xFF)));
|
||||||
|
|
||||||
|
// Clear pointer table (write zero) to avoid stale values when pointer count shrinks
|
||||||
|
for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
|
||||||
|
RETURN_IF_ERROR(rom()->WriteShort(zelda3::kOverworldItemsPointersNew + (i * 2),
|
||||||
|
0x0000));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < pointer_count; ++i) {
|
||||||
|
const uint32_t snes_addr = PcToSnes(item_pointers[i]);
|
||||||
|
RETURN_IF_ERROR(rom()->WriteShort(zelda3::kOverworldItemsPointersNew + (i * 2),
|
||||||
|
static_cast<uint16_t>(snes_addr & 0xFFFF)));
|
||||||
|
}
|
||||||
|
|
||||||
util::logf("End of Items : %d", data_pos);
|
util::logf("End of Items : %d", data_pos);
|
||||||
|
|
||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "app/gfx/types/snes_palette.h"
|
#include "app/gfx/types/snes_palette.h"
|
||||||
#include "app/gfx/types/snes_tile.h"
|
#include "app/gfx/types/snes_tile.h"
|
||||||
#include "app/rom.h"
|
#include "app/rom.h"
|
||||||
|
#include "zelda3/overworld/overworld_item.h"
|
||||||
|
|
||||||
namespace yaze {
|
namespace yaze {
|
||||||
namespace zelda3 {
|
namespace zelda3 {
|
||||||
|
|||||||
Reference in New Issue
Block a user