yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
cutscene_shot.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <cstdint>
7#include <set>
8#include <string>
9
10#include "absl/strings/str_format.h"
11#include "nlohmann/json.hpp"
12
13namespace yaze::zelda3 {
14namespace {
15
16using ordered_json = nlohmann::ordered_json;
17
18// OverworldTransitionPositionY/X ($02A8C4/$02A944): each area's parent origin.
19constexpr std::array<uint16_t, 64> kVanillaOriginY = {
20 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
21 0x0000, 0x0000, 0x0200, 0x0000, 0x0000, 0x0000, 0x0000, 0x0200,
22 0x0400, 0x0400, 0x0400, 0x0400, 0x0400, 0x0400, 0x0400, 0x0400,
23 0x0600, 0x0600, 0x0600, 0x0600, 0x0600, 0x0600, 0x0600, 0x0600,
24 0x0600, 0x0600, 0x0800, 0x0600, 0x0600, 0x0800, 0x0600, 0x0600,
25 0x0A00, 0x0A00, 0x0A00, 0x0A00, 0x0A00, 0x0A00, 0x0A00, 0x0A00,
26 0x0C00, 0x0C00, 0x0C00, 0x0C00, 0x0C00, 0x0C00, 0x0C00, 0x0C00,
27 0x0C00, 0x0C00, 0x0E00, 0x0E00, 0x0E00, 0x0C00, 0x0C00, 0x0E00};
28constexpr std::array<uint16_t, 64> kVanillaOriginX = {
29 0x0000, 0x0000, 0x0400, 0x0600, 0x0600, 0x0A00, 0x0A00, 0x0E00,
30 0x0000, 0x0000, 0x0400, 0x0600, 0x0600, 0x0A00, 0x0A00, 0x0E00,
31 0x0000, 0x0200, 0x0400, 0x0600, 0x0800, 0x0A00, 0x0C00, 0x0E00,
32 0x0000, 0x0000, 0x0400, 0x0600, 0x0600, 0x0A00, 0x0C00, 0x0C00,
33 0x0000, 0x0000, 0x0400, 0x0600, 0x0600, 0x0A00, 0x0C00, 0x0C00,
34 0x0000, 0x0200, 0x0400, 0x0600, 0x0800, 0x0A00, 0x0C00, 0x0E00,
35 0x0000, 0x0000, 0x0400, 0x0600, 0x0800, 0x0A00, 0x0A00, 0x0E00,
36 0x0000, 0x0000, 0x0400, 0x0600, 0x0800, 0x0A00, 0x0A00, 0x0E00};
37
38constexpr int kMaxWorldPixel = 0xFFFF;
39
40std::string Hex2(int value) {
41 return absl::StrFormat("0x%02X", value);
42}
43
44absl::Status FieldError(const std::string& where, const std::string& what) {
45 return absl::InvalidArgumentError(
46 absl::StrFormat("cutscene shots: %s %s", where, what));
47}
48
49// Accepts an integer or a string ("0x2D" or "45").
50absl::StatusOr<int> ReadInt(const ordered_json& object, const char* key,
51 const std::string& where, int min, int max) {
52 if (!object.contains(key)) {
53 return FieldError(where, absl::StrFormat("is missing \"%s\"", key));
54 }
55 const auto& value = object.at(key);
56 long long number = 0;
57 if (value.is_number_integer()) {
58 number = value.get<long long>();
59 } else if (value.is_string()) {
60 const std::string text = value.get<std::string>();
61 try {
62 size_t used = 0;
63 number = std::stoll(text, &used, 0);
64 if (used != text.size()) {
65 return FieldError(
66 where, absl::StrFormat("\"%s\" is not a number: %s", key, text));
67 }
68 } catch (...) {
69 return FieldError(
70 where, absl::StrFormat("\"%s\" is not a number: %s", key, text));
71 }
72 } else {
73 return FieldError(where, absl::StrFormat("\"%s\" must be a number", key));
74 }
75 if (number < min || number > max) {
76 return FieldError(where, absl::StrFormat("\"%s\" %lld is outside %d-%d",
77 key, number, min, max));
78 }
79 return static_cast<int>(number);
80}
81
82bool ValidName(const std::string& name) {
83 return !name.empty() &&
84 std::all_of(name.begin(), name.end(), [](unsigned char c) {
85 return std::isalnum(c) || c == '_';
86 });
87}
88
89} // namespace
90
91absl::StatusOr<CutsceneShotSet> ParseCutsceneShots(std::string_view json) {
92 const ordered_json root = ordered_json::parse(json, nullptr, false);
93 if (root.is_discarded() || !root.is_object()) {
94 return absl::InvalidArgumentError("cutscene shots: not a JSON object");
95 }
96 if (!root.contains("version") || !root.at("version").is_number_integer() ||
97 root.at("version").get<int>() != 1) {
98 return absl::InvalidArgumentError(
99 "cutscene shots: \"version\" must be the integer 1");
100 }
101 if (!root.contains("shots") || !root.at("shots").is_array()) {
102 return absl::InvalidArgumentError(
103 "cutscene shots: \"shots\" must be an array");
104 }
105
106 CutsceneShotSet set;
107 std::set<std::string> names;
108 for (size_t i = 0; i < root.at("shots").size(); ++i) {
109 const ordered_json& entry = root.at("shots")[i];
110 std::string where = absl::StrFormat("shot %zu", i);
111 if (!entry.is_object()) {
112 return FieldError(where, "must be an object");
113 }
114 CutsceneShot shot;
115 if (!entry.contains("name") || !entry.at("name").is_string()) {
116 return FieldError(where, "is missing a string \"name\"");
117 }
118 shot.name = entry.at("name").get<std::string>();
119 if (!ValidName(shot.name)) {
120 return FieldError(
121 where, absl::StrFormat("name \"%s\" must use only A-Z, a-z, 0-9 "
122 "and _",
123 shot.name));
124 }
125 if (!names.insert(shot.name).second) {
126 return FieldError(
127 where, absl::StrFormat("name \"%s\" is used twice", shot.name));
128 }
129 where = absl::StrFormat("shot \"%s\"", shot.name);
130 auto area = ReadInt(entry, "area", where, 0, 0x7F);
131 if (!area.ok())
132 return area.status();
133 shot.area = *area;
134
135 if (!entry.contains("camera") || !entry.at("camera").is_object()) {
136 return FieldError(where, "is missing a \"camera\" object");
137 }
138 auto cx =
139 ReadInt(entry.at("camera"), "x", where + " camera", 0, kMaxWorldPixel);
140 if (!cx.ok())
141 return cx.status();
142 auto cy =
143 ReadInt(entry.at("camera"), "y", where + " camera", 0, kMaxWorldPixel);
144 if (!cy.ok())
145 return cy.status();
146 shot.camera_x = *cx;
147 shot.camera_y = *cy;
148
149 if (!entry.contains("link") || !entry.at("link").is_object()) {
150 return FieldError(where, "is missing a \"link\" object");
151 }
152 const ordered_json& link = entry.at("link");
153 auto lx = ReadInt(link, "x", where + " link", 0, kMaxWorldPixel);
154 if (!lx.ok())
155 return lx.status();
156 auto ly = ReadInt(link, "y", where + " link", 0, kMaxWorldPixel);
157 if (!ly.ok())
158 return ly.status();
159 auto lf = ReadInt(link, "facing", where + " link", 0, 3);
160 if (!lf.ok())
161 return lf.status();
162 shot.link = CutsceneLink{*lx, *ly, *lf};
163
164 if (entry.contains("actors")) {
165 if (!entry.at("actors").is_array()) {
166 return FieldError(where, "\"actors\" must be an array");
167 }
168 for (size_t a = 0; a < entry.at("actors").size(); ++a) {
169 const ordered_json& actor_json = entry.at("actors")[a];
170 const std::string actor_where =
171 absl::StrFormat("%s actor %zu", where, a);
172 if (!actor_json.is_object()) {
173 return FieldError(actor_where, "must be an object");
174 }
175 CutsceneActor actor;
176 auto sprite = ReadInt(actor_json, "sprite", actor_where, 0, 0xFF);
177 if (!sprite.ok())
178 return sprite.status();
179 auto ax = ReadInt(actor_json, "x", actor_where, 0, kMaxWorldPixel);
180 if (!ax.ok())
181 return ax.status();
182 auto ay = ReadInt(actor_json, "y", actor_where, 0, kMaxWorldPixel);
183 if (!ay.ok())
184 return ay.status();
185 auto af = ReadInt(actor_json, "facing", actor_where, 0, 3);
186 if (!af.ok())
187 return af.status();
188 actor.sprite = *sprite;
189 actor.x = *ax;
190 actor.y = *ay;
191 actor.facing = *af;
192 if (actor_json.contains("note")) {
193 if (!actor_json.at("note").is_string()) {
194 return FieldError(actor_where, "\"note\" must be a string");
195 }
196 actor.note = actor_json.at("note").get<std::string>();
197 }
198 shot.actors.push_back(std::move(actor));
199 }
200 }
201 set.shots.push_back(std::move(shot));
202 }
203 return set;
204}
205
206absl::StatusOr<std::string> SerializeCutsceneShots(const CutsceneShotSet& set) {
207 // Round-trip through the parser's rules so an invalid set is never written.
208 ordered_json root;
209 root["version"] = 1;
210 root["shots"] = ordered_json::array();
211 for (const CutsceneShot& shot : set.shots) {
212 ordered_json entry;
213 entry["name"] = shot.name;
214 entry["area"] = Hex2(shot.area);
215 entry["camera"] = {{"x", shot.camera_x}, {"y", shot.camera_y}};
216 entry["link"] = {
217 {"x", shot.link.x}, {"y", shot.link.y}, {"facing", shot.link.facing}};
218 entry["actors"] = ordered_json::array();
219 for (const CutsceneActor& actor : shot.actors) {
220 ordered_json actor_json;
221 actor_json["sprite"] = Hex2(actor.sprite);
222 actor_json["x"] = actor.x;
223 actor_json["y"] = actor.y;
224 actor_json["facing"] = actor.facing;
225 if (!actor.note.empty()) {
226 actor_json["note"] = actor.note;
227 }
228 entry["actors"].push_back(std::move(actor_json));
229 }
230 root["shots"].push_back(std::move(entry));
231 }
232 std::string text = root.dump(2) + "\n";
233 auto check = ParseCutsceneShots(text);
234 if (!check.ok()) {
235 return check.status();
236 }
237 return text;
238}
239
241 CameraBounds bounds;
242 bounds.min_x = area.origin_x;
243 bounds.min_y = area.origin_y;
244 bounds.max_x = area.origin_x + area.width - kCutsceneViewportWidth;
245 bounds.max_y = area.origin_y + area.height - (kCutsceneViewportHeight + 2);
246 return bounds;
247}
248
249ClampedCamera ClampCamera(const AreaExtent& area, int x, int y) {
250 const CameraBounds bounds = CameraBoundsForArea(area);
251 ClampedCamera camera;
252 camera.requested_x = x;
253 camera.requested_y = y;
254 camera.x = std::clamp(x, bounds.min_x, bounds.max_x);
255 camera.y = std::clamp(y, bounds.min_y, bounds.max_y);
256 return camera;
257}
258
259absl::StatusOr<AreaExtent> VanillaAreaExtent(int area) {
260 if (area < 0 || area > 0x7F) {
261 return absl::InvalidArgumentError(absl::StrFormat(
262 "Area 0x%02X has no vanilla camera layout (special areas are not "
263 "supported)",
264 area));
265 }
266 const int index = area & 0x3F;
267 AreaExtent extent;
268 extent.origin_x = kVanillaOriginX[index];
269 extent.origin_y = kVanillaOriginY[index];
270 // A large area's four screens all name the parent's origin, so the parent
271 // (top-left) screen and the screen to its right share an X origin.
272 const int parent = (extent.origin_y / 512) * 8 + extent.origin_x / 512;
273 const int right = parent + 1;
274 const bool large = (parent % 8) != 7 &&
275 kVanillaOriginX[right] == kVanillaOriginX[parent] &&
276 kVanillaOriginY[right] == kVanillaOriginY[parent];
277 extent.width = large ? 1024 : 512;
278 extent.height = large ? 1024 : 512;
279 return extent;
280}
281
283 const int screen = parent & 0x3F;
284 AreaExtent extent;
285 extent.origin_x = (screen % 8) * 512;
286 extent.origin_y = (screen / 8) * 512;
287 extent.width =
288 size == AreaSizeEnum::LargeArea || size == AreaSizeEnum::WideArea ? 1024
289 : 512;
290 extent.height =
291 size == AreaSizeEnum::LargeArea || size == AreaSizeEnum::TallArea ? 1024
292 : 512;
293 return extent;
294}
295
296MapRect ViewportOnMap(const AreaExtent& area, int camera_x, int camera_y,
297 float scale) {
298 return MapRect{
299 (camera_x - area.origin_x) * scale, (camera_y - area.origin_y) * scale,
301}
302
303MapRect PointOnMap(const AreaExtent& area, int world_x, int world_y,
304 float scale) {
305 return MapRect{(world_x - area.origin_x) * scale,
306 (world_y - area.origin_y) * scale, 0, 0};
307}
308
309bool PointInViewport(int camera_x, int camera_y, int world_x, int world_y) {
310 return world_x >= camera_x && world_x < camera_x + kCutsceneViewportWidth &&
311 world_y >= camera_y && world_y < camera_y + kCutsceneViewportHeight;
312}
313
314} // namespace yaze::zelda3
constexpr std::array< uint16_t, 64 > kVanillaOriginX
absl::Status FieldError(const std::string &where, const std::string &what)
constexpr std::array< uint16_t, 64 > kVanillaOriginY
absl::StatusOr< int > ReadInt(const ordered_json &object, const char *key, const std::string &where, int min, int max)
Zelda 3 specific classes and functions.
absl::StatusOr< std::string > SerializeCutsceneShots(const CutsceneShotSet &set)
CameraBounds CameraBoundsForArea(const AreaExtent &area)
nlohmann::json json
AreaSizeEnum
Area size enumeration for v3+ ROMs.
absl::StatusOr< AreaExtent > VanillaAreaExtent(int area)
constexpr int kCutsceneViewportWidth
MapRect PointOnMap(const AreaExtent &area, int world_x, int world_y, float scale)
MapRect ViewportOnMap(const AreaExtent &area, int camera_x, int camera_y, float scale)
absl::StatusOr< CutsceneShotSet > ParseCutsceneShots(std::string_view json)
bool PointInViewport(int camera_x, int camera_y, int world_x, int world_y)
constexpr int kCutsceneViewportHeight
AreaExtent AreaExtentFromParent(int parent, AreaSizeEnum size)
ClampedCamera ClampCamera(const AreaExtent &area, int x, int y)
std::vector< CutsceneShot > shots
std::vector< CutsceneActor > actors