yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_exit.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <cstdlib>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/status/statusor.h"
11#include "rom/rom.h"
12#include "util/macro.h"
13#include "zelda3/common.h"
16
17namespace yaze::zelda3 {
18
19absl::StatusOr<std::vector<OverworldExit>> LoadExits(Rom* rom) {
20 if (rom == nullptr) {
21 return absl::InvalidArgumentError("Cannot load exits without a ROM");
22 }
23 const int NumberOfOverworldExits = 0x4F;
24 std::vector<OverworldExit> exits;
25 for (int i = 0; i < NumberOfOverworldExits; i++) {
26 uint16_t exit_room_id;
27 uint8_t exit_map_id;
28 uint16_t exit_vram;
29 uint16_t exit_y_scroll;
30 uint16_t exit_x_scroll;
31 uint16_t exit_y_player;
32 uint16_t exit_x_player;
33 uint16_t exit_y_camera;
34 uint16_t exit_x_camera;
35 uint8_t exit_scroll_mod_y;
36 uint8_t exit_scroll_mod_x;
37 uint16_t exit_door_type_1;
38 uint16_t exit_door_type_2;
40 exit_room_id, (OWExitRoomId + (i * 2)), exit_map_id, OWExitMapId + i,
41 exit_vram, OWExitVram + (i * 2), exit_y_scroll, OWExitYScroll + (i * 2),
42 exit_x_scroll, OWExitXScroll + (i * 2), exit_y_player,
43 OWExitYPlayer + (i * 2), exit_x_player, OWExitXPlayer + (i * 2),
44 exit_y_camera, OWExitYCamera + (i * 2), exit_x_camera,
45 OWExitXCamera + (i * 2), exit_scroll_mod_y, OWExitUnk1 + i,
46 exit_scroll_mod_x, OWExitUnk2 + i, exit_door_type_1,
47 OWExitDoorType1 + (i * 2), exit_door_type_2,
48 OWExitDoorType2 + (i * 2)));
49
50 exits.emplace_back(exit_room_id, exit_map_id, exit_vram, exit_y_scroll,
51 exit_x_scroll, exit_y_player, exit_x_player,
52 exit_y_camera, exit_x_camera, exit_scroll_mod_y,
53 exit_scroll_mod_x, exit_door_type_1, exit_door_type_2,
54 exit_x_player == 0xFFFF && exit_y_player == 0xFFFF);
55 }
56 return exits;
57}
58
59void OverworldExit::UpdateMapProperties(uint16_t map_id, const void* context) {
60 // Sync player position from drag system
61 // ZScream: ExitMode.cs:229-244 updates PlayerX/PlayerY, then calls
62 // UpdateMapStuff
63 x_player_ = static_cast<uint16_t>(x_);
64 y_player_ = static_cast<uint16_t>(y_);
65 map_id_ = map_id;
66
67 // FIX Bug 3: Query actual area size from overworld
68 // ZScream: ExitOW.cs:210-212
69 int area_size_x = 256;
70 int area_size_y = 256;
71
72 // Missing context maps use the same small-area geometry as nullptr.
73 // Checked editor operations validate destination ownership separately.
74 if (context != nullptr) {
75 const auto* overworld = static_cast<const Overworld*>(context);
76 if (const auto* map = overworld->overworld_map(map_id)) {
77 const auto area_size = map->area_size();
78 if (area_size == AreaSizeEnum::LargeArea) {
79 area_size_x = area_size_y = 768;
80 } else if (area_size == AreaSizeEnum::WideArea) {
81 area_size_x = 768;
82 } else if (area_size == AreaSizeEnum::TallArea) {
83 area_size_y = 768;
84 }
85 }
86 }
87
88 // FIX Bug 5: Normalize map_id FIRST before using for calculations
89 // ZScream: ExitOW.cs:214
90 uint8_t normalized_map_id = map_id % 0x40;
91
92 // Calculate map grid position
93 // ZScream: ExitOW.cs:216-217
94 int mapX = normalized_map_id % 8;
95 int mapY = normalized_map_id / 8;
96
97 // Calculate game coordinates (map-local tile position)
98 // ZScream: ExitOW.cs:219-220
99 game_x_ = static_cast<int>((std::abs(x_ - (mapX * 512)) / 16));
100 game_y_ = static_cast<int>((std::abs(y_ - (mapY * 512)) / 16));
101
102 // Clamp to valid range based on area size
103 // ZScream: ExitOW.cs:222-234
104 int max_game_x = (area_size_x == 256) ? 31 : 63;
105 int max_game_y = (area_size_y == 256) ? 31 : 63;
106 game_x_ = std::clamp(game_x_, 0, max_game_x);
107 game_y_ = std::clamp(game_y_, 0, max_game_y);
108
109 // Map base coordinates in world space
110 // ZScream: ExitOW.cs:237-238 (mapx, mapy)
111 int mapx = (normalized_map_id & 7) << 9; // * 512
112 int mapy = (normalized_map_id & 56) << 6; // (map_id / 8) * 512
113
114 if (is_automatic_) {
115 // Auto-calculate scroll and camera from player position
116 // ZScream: ExitOW.cs:256-309
117
118 // Clamp signed values before storing them. Narrowing a negative scroll
119 // first wraps it to $FFxx and incorrectly selects the far map edge.
120 x_scroll_ = static_cast<uint16_t>(std::clamp(
121 static_cast<int>(x_player_) - 120, mapx, mapx + area_size_x));
122 y_scroll_ = static_cast<uint16_t>(std::clamp(
123 static_cast<int>(y_player_) - 80, mapy, mapy + area_size_y + 32));
124 x_camera_ = static_cast<uint16_t>(std::clamp(
125 static_cast<int>(x_player_) + 7, mapx + 127, mapx + 127 + area_size_x));
126 y_camera_ =
127 static_cast<uint16_t>(std::clamp(static_cast<int>(y_player_) + 31,
128 mapy + 111, mapy + 143 + area_size_y));
129 }
130
131 // Calculate VRAM location from scroll values
132 // ZScream: ExitOW.cs:312-315
133 int16_t vram_x_scroll = static_cast<int16_t>(x_scroll_ - mapx);
134 int16_t vram_y_scroll = static_cast<int16_t>(y_scroll_ - mapy);
135
136 map_pos_ = static_cast<uint16_t>(((vram_y_scroll & 0xFFF0) << 3) |
137 ((vram_x_scroll & 0xFFF0) >> 3));
138}
139
140absl::Status SaveExits(Rom* rom, const std::vector<OverworldExit>& exits) {
141 if (rom == nullptr) {
142 return absl::InvalidArgumentError("Cannot save exits without a ROM");
143 }
144 if (exits.size() < kNumOverworldExits) {
145 return absl::InvalidArgumentError("Saving exits requires all 79 records");
146 }
147 // Validate every destination before the first mutation, including the
148 // auxiliary map bytes used only when a special room destination changes.
149 struct TableRange {
150 size_t address;
151 size_t bytes;
152 };
153 constexpr TableRange kTables[] = {
168 };
169 const size_t rom_size = rom->vector().size();
170 for (const auto& table : kTables) {
171 if (table.address > rom_size || table.bytes > rom_size - table.address) {
172 return absl::OutOfRangeError("ROM does not contain complete exit tables");
173 }
174 }
175
176 for (int i = 0; i < kNumOverworldExits; i++) {
177 ASSIGN_OR_RETURN(auto stored_room, rom->ReadWord(OWExitRoomId + (i * 2)));
178 ASSIGN_OR_RETURN(auto stored_map, rom->ReadByte(OWExitMapId + i));
179 const bool destination_changed =
180 stored_room != exits[i].room_id_ || stored_map != exits[i].map_id_;
181 RETURN_IF_ERROR(rom->WriteShort(OWExitRoomId + (i * 2), exits[i].room_id_));
182 RETURN_IF_ERROR(rom->WriteByte(OWExitMapId + i, exits[i].map_id_));
183 RETURN_IF_ERROR(rom->WriteShort(OWExitVram + (i * 2), exits[i].map_pos_));
185 rom->WriteShort(OWExitYScroll + (i * 2), exits[i].y_scroll_));
187 rom->WriteShort(OWExitXScroll + (i * 2), exits[i].x_scroll_));
188 // $FFFF/$FFFF is the existing on-disk unused-record sentinel. Preserve
189 // the remaining record fields so deletion can be undone without invention.
191 rom->WriteShort(OWExitYPlayer + (i * 2),
192 exits[i].deleted_ ? 0xFFFF : exits[i].y_player_));
194 rom->WriteShort(OWExitXPlayer + (i * 2),
195 exits[i].deleted_ ? 0xFFFF : exits[i].x_player_));
197 rom->WriteShort(OWExitYCamera + (i * 2), exits[i].y_camera_));
199 rom->WriteShort(OWExitXCamera + (i * 2), exits[i].x_camera_));
200 RETURN_IF_ERROR(rom->WriteByte(OWExitUnk1 + i, exits[i].scroll_mod_y_));
201 RETURN_IF_ERROR(rom->WriteByte(OWExitUnk2 + i, exits[i].scroll_mod_x_));
203 rom->WriteShort(OWExitDoorType1 + (i * 2), exits[i].door_type_1_));
205 rom->WriteShort(OWExitDoorType2 + (i * 2), exits[i].door_type_2_));
206
207 // These auxiliary bytes may intentionally differ from the exit table in a
208 // hack. Only an authored destination change may update them.
209 if (!destination_changed) {
210 continue;
211 }
212 if (exits[i].room_id_ == 0x0180) {
214 rom->WriteByte(OWExitDoorPosition + 0, exits[i].map_id_ & 0xFF));
215 } else if (exits[i].room_id_ == 0x0181) {
217 rom->WriteByte(OWExitDoorPosition + 2, exits[i].map_id_ & 0xFF));
218 } else if (exits[i].room_id_ == 0x0182) {
220 rom->WriteByte(OWExitDoorPosition + 4, exits[i].map_id_ & 0xFF));
221 }
222 }
223
224 return absl::OkStatus();
225}
226
227} // namespace yaze::zelda3
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
absl::Status ReadTransaction(T &var, int address, Args &&... args)
Definition rom.h:151
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:1104
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:1036
const auto & vector() const
Definition rom.h:217
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:1044
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:1153
void UpdateMapProperties(uint16_t map_id, const void *context) override
Update exit properties when moved or map changes.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
Zelda 3 specific classes and functions.
constexpr int OWExitYScroll
constexpr int OWExitDoorType2
constexpr int OWExitYCamera
constexpr int OWExitDoorPosition
constexpr int OWExitXScroll
constexpr int OWExitRoomId
constexpr int OWExitXCamera
constexpr int OWExitUnk1
constexpr int OWExitYPlayer
absl::StatusOr< std::vector< OverworldExit > > LoadExits(Rom *rom)
constexpr int OWExitMapId
constexpr int OWExitUnk2
constexpr int OWExitDoorType1
constexpr int kNumOverworldExits
absl::Status SaveExits(Rom *rom, const std::vector< OverworldExit > &exits)
constexpr int OWExitXPlayer
constexpr int OWExitVram
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22