yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
minecart_track_source.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <cstddef>
7#include <string>
8#include <utility>
9#include <vector>
10
11#include "absl/status/status.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/string_view.h"
14
15namespace yaze::editor {
16namespace {
17
18constexpr absl::string_view kPlannedTrackGuard =
19 "if !ENABLE_MINECART_PLANNED_TRACK_TABLE == 1";
20constexpr std::array<absl::string_view, 3> kSectionLabels = {
21 ".TrackStartingRooms", ".TrackStartingX", ".TrackStartingY"};
22// Oracle's table-size checks emit no data. Keep this allowlist exact: other
23// statements after a table could change its layout and invalidate token slots.
24constexpr std::array<absl::string_view, 3> kSectionEndLabels = {
25 ".TrackStartingRoomsEnd", ".TrackStartingXEnd", ".TrackStartingYEnd"};
26
27struct SourceLine {
28 size_t offset;
29 absl::string_view text;
30};
31
36
38 std::array<int, kMinecartTrackSlotCount> values{};
40 bool guarded = false;
41};
42
43bool IsSpace(char value) {
44 return std::isspace(static_cast<unsigned char>(value)) != 0;
45}
46
47bool IsHexDigit(char value) {
48 return std::isxdigit(static_cast<unsigned char>(value)) != 0;
49}
50
51absl::string_view Trim(absl::string_view value) {
52 while (!value.empty() && IsSpace(value.front())) {
53 value.remove_prefix(1);
54 }
55 while (!value.empty() && IsSpace(value.back())) {
56 value.remove_suffix(1);
57 }
58 return value;
59}
60
61absl::string_view CodeForLine(absl::string_view line) {
62 const size_t comment = line.find(';');
63 return Trim(line.substr(0, comment));
64}
65
66void SkipSpace(absl::string_view* code) {
67 while (!code->empty() && IsSpace(code->front()))
68 code->remove_prefix(1);
69}
70
71bool Consume(absl::string_view* code, absl::string_view token) {
72 if (code->substr(0, token.size()) != token)
73 return false;
74 code->remove_prefix(token.size());
75 return true;
76}
77
78bool IsExpectedSizeAssertion(absl::string_view code, size_t section) {
79 if (!Consume(&code, "assert") || code.empty() || !IsSpace(code.front()))
80 return false;
81 SkipSpace(&code);
82 if (!Consume(&code, kSectionEndLabels[section]))
83 return false;
84 SkipSpace(&code);
85 if (!Consume(&code, "-"))
86 return false;
87 SkipSpace(&code);
88 if (!Consume(&code, kSectionLabels[section]))
89 return false;
90 SkipSpace(&code);
91 if (!Consume(&code, "=="))
92 return false;
93 SkipSpace(&code);
94 if (!Consume(&code, "$20"))
95 return false;
96 SkipSpace(&code);
97 if (!Consume(&code, "*"))
98 return false;
99 SkipSpace(&code);
100 if (!Consume(&code, "2"))
101 return false;
102 SkipSpace(&code);
103 if (code.empty())
104 return true;
105 if (!Consume(&code, ","))
106 return false;
107 SkipSpace(&code);
108 if (!Consume(&code, "\""))
109 return false;
110 const size_t close = code.find('"');
111 if (close == absl::string_view::npos || close == 0 ||
112 code.substr(0, close).find('\\') != absl::string_view::npos)
113 return false;
114 code.remove_prefix(close + 1);
115 SkipSpace(&code);
116 return code.empty();
117}
118
119std::vector<SourceLine> SplitLines(absl::string_view source) {
120 std::vector<SourceLine> lines;
121 size_t offset = 0;
122 while (offset < source.size()) {
123 const size_t newline = source.find('\n', offset);
124 const size_t end =
125 newline == absl::string_view::npos ? source.size() : newline;
126 lines.push_back({offset, source.substr(offset, end - offset)});
127 if (newline == absl::string_view::npos) {
128 break;
129 }
130 offset = newline + 1;
131 }
132 return lines;
133}
134
135absl::StatusOr<std::vector<ParsedToken>> ParseDwLine(const SourceLine& line,
136 absl::string_view label) {
137 const size_t comment = line.text.find(';');
138 const size_t code_end =
139 comment == absl::string_view::npos ? line.text.size() : comment;
140 size_t cursor = 0;
141 while (cursor < code_end && IsSpace(line.text[cursor])) {
142 ++cursor;
143 }
144 if (cursor + 2 > code_end ||
145 std::tolower(static_cast<unsigned char>(line.text[cursor])) != 'd' ||
146 std::tolower(static_cast<unsigned char>(line.text[cursor + 1])) != 'w' ||
147 cursor + 2 == code_end || !IsSpace(line.text[cursor + 2])) {
148 return absl::InvalidArgumentError(absl::StrFormat(
149 "Unexpected statement in %s: %s", label, CodeForLine(line.text)));
150 }
151 cursor += 2;
152
153 std::vector<ParsedToken> tokens;
154 bool requires_token = true;
155 while (true) {
156 while (cursor < code_end && IsSpace(line.text[cursor])) {
157 ++cursor;
158 }
159 if (cursor == code_end) {
160 if (requires_token) {
161 return absl::InvalidArgumentError(
162 absl::StrFormat("Empty operand in %s", label));
163 }
164 break;
165 }
166
167 const size_t token_start = cursor;
168 if (line.text[cursor] != '$' || cursor + 5 > code_end ||
169 !std::all_of(
170 line.text.begin() + static_cast<std::ptrdiff_t>(cursor + 1),
171 line.text.begin() + static_cast<std::ptrdiff_t>(cursor + 5),
172 IsHexDigit)) {
173 return absl::InvalidArgumentError(
174 absl::StrFormat("%s values must use exact 16-bit $hhhh operands: %s",
175 label, CodeForLine(line.text)));
176 }
177 cursor += 5;
178 if (cursor < code_end && IsHexDigit(line.text[cursor])) {
179 return absl::InvalidArgumentError(
180 absl::StrFormat("%s contains an over-wide 16-bit operand: %s", label,
181 CodeForLine(line.text)));
182 }
183
184 int value = 0;
185 for (size_t digit = token_start + 1; digit < token_start + 5; ++digit) {
186 const char hex = line.text[digit];
187 value <<= 4;
188 if (hex >= '0' && hex <= '9') {
189 value += hex - '0';
190 } else {
191 value += 10 + std::tolower(static_cast<unsigned char>(hex)) - 'a';
192 }
193 }
194 tokens.push_back({value, {line.offset + token_start, 5}});
195 requires_token = false;
196
197 while (cursor < code_end && IsSpace(line.text[cursor])) {
198 ++cursor;
199 }
200 if (cursor == code_end) {
201 break;
202 }
203 if (line.text[cursor] != ',') {
204 return absl::InvalidArgumentError(
205 absl::StrFormat("Unexpected operand separator in %s: %s", label,
206 CodeForLine(line.text)));
207 }
208 ++cursor;
209 requires_token = true;
210 }
211 if (tokens.empty()) {
212 return absl::InvalidArgumentError(
213 absl::StrFormat("Empty dw statement in %s", label));
214 }
215 return tokens;
216}
217
218absl::StatusOr<ParsedSection> ParseSection(const std::vector<SourceLine>& lines,
219 size_t first_line, size_t end_line,
220 size_t section) {
221 const absl::string_view label = kSectionLabels[section];
222 enum class GuardState { kPrefix, kEnabled, kDisabled, kComplete };
223 enum class TrailerState { kNone, kEndLabel, kAssertion };
224 GuardState state = GuardState::kPrefix;
225 TrailerState trailer = TrailerState::kNone;
226 bool saw_guard = false;
227 bool saw_else = false;
228 bool saw_endif = false;
229 std::vector<ParsedToken> prefix;
230 std::vector<ParsedToken> enabled;
231 std::vector<ParsedToken> disabled;
232
233 for (size_t line_index = first_line + 1; line_index < end_line;
234 ++line_index) {
235 const absl::string_view code = CodeForLine(lines[line_index].text);
236 if (code.empty()) {
237 continue;
238 }
239 if (trailer != TrailerState::kNone) {
240 if (trailer == TrailerState::kEndLabel &&
241 IsExpectedSizeAssertion(code, section)) {
242 trailer = TrailerState::kAssertion;
243 continue;
244 }
245 return absl::InvalidArgumentError(absl::StrFormat(
246 "Unexpected content after %s table end: %s", label, code));
247 }
248 if (code == kSectionEndLabels[section]) {
249 if ((saw_guard && state != GuardState::kComplete) ||
250 (!saw_guard && prefix.size() != kMinecartTrackSlotCount)) {
251 return absl::InvalidArgumentError(absl::StrFormat(
252 "Size assertion follows an incomplete %s table", label));
253 }
254 trailer = TrailerState::kEndLabel;
255 continue;
256 }
257 if (code == kPlannedTrackGuard) {
258 if (state != GuardState::kPrefix || saw_guard) {
259 return absl::InvalidArgumentError(
260 absl::StrFormat("Nested or repeated guard in %s", label));
261 }
262 saw_guard = true;
263 state = GuardState::kEnabled;
264 continue;
265 }
266 if (code == "else") {
267 if (!saw_guard || state != GuardState::kEnabled || saw_else) {
268 return absl::InvalidArgumentError(
269 absl::StrFormat("Unexpected else in %s", label));
270 }
271 saw_else = true;
272 state = GuardState::kDisabled;
273 continue;
274 }
275 if (code == "endif") {
276 if (!saw_else || state != GuardState::kDisabled || saw_endif) {
277 return absl::InvalidArgumentError(
278 absl::StrFormat("Unexpected endif in %s", label));
279 }
280 saw_endif = true;
281 state = GuardState::kComplete;
282 continue;
283 }
284 if (state == GuardState::kComplete) {
285 return absl::InvalidArgumentError(absl::StrFormat(
286 "Unexpected content after guarded table in %s: %s", label, code));
287 }
288
289 auto tokens_or = ParseDwLine(lines[line_index], label);
290 if (!tokens_or.ok()) {
291 return tokens_or.status();
292 }
293 std::vector<ParsedToken>* destination = &prefix;
294 if (state == GuardState::kEnabled) {
295 destination = &enabled;
296 } else if (state == GuardState::kDisabled) {
297 destination = &disabled;
298 }
299 destination->insert(destination->end(), tokens_or->begin(),
300 tokens_or->end());
301 }
302
303 if (trailer == TrailerState::kEndLabel) {
304 return absl::InvalidArgumentError(
305 absl::StrFormat("Missing size assertion after %s table end", label));
306 }
307
308 ParsedSection result;
309 std::vector<ParsedToken> active;
310 if (!saw_guard) {
311 if (prefix.size() != kMinecartTrackSlotCount) {
312 return absl::InvalidArgumentError(
313 absl::StrFormat("%s must contain exactly %zu values; found %zu",
314 label, kMinecartTrackSlotCount, prefix.size()));
315 }
316 active = std::move(prefix);
317 } else {
318 if (!saw_else || !saw_endif) {
319 return absl::InvalidArgumentError(
320 absl::StrFormat("Incomplete planned-track guard in %s", label));
321 }
322 if (prefix.size() != 4 || enabled.size() != 28 || disabled.size() != 28) {
323 return absl::InvalidArgumentError(absl::StrFormat(
324 "%s guarded layout requires exactly 4 prefix, 28 enabled, and 28 "
325 "disabled values; found %zu, %zu, and %zu",
326 label, prefix.size(), enabled.size(), disabled.size()));
327 }
328 active = std::move(prefix);
329 active.insert(active.end(), enabled.begin(), enabled.end());
330 result.guarded = true;
331 }
332
333 for (size_t index = 0; index < kMinecartTrackSlotCount; ++index) {
334 result.values[index] = active[index].value;
335 result.editable_spans[index] = active[index].span;
336 }
337 return result;
338}
339
340absl::Status ValidateTrack(const MinecartTrack& track, size_t index) {
341 if (track.id != static_cast<int>(index)) {
342 return absl::InvalidArgumentError(
343 absl::StrFormat("Minecart track ID at slot %zu must be %zu; found %d",
344 index, index, track.id));
345 }
346 for (const auto& [field_name, value] :
347 std::array<std::pair<absl::string_view, int>, 3>{
348 {{"room_id", track.room_id},
349 {"start_x", track.start_x},
350 {"start_y", track.start_y}}}) {
351 if (value < 0 || value > 0xFFFF) {
352 return absl::InvalidArgumentError(absl::StrFormat(
353 "Minecart track %zu %s must be a 16-bit value; found %d", index,
354 field_name, value));
355 }
356 }
357 return absl::OkStatus();
358}
359
360std::string FormatHexWord(int value) {
361 return absl::StrFormat("$%04X", value);
362}
363
364} // namespace
365
366absl::StatusOr<MinecartTrackSourceDocument> MinecartTrackSourceDocument::Parse(
367 std::string source_bytes) {
368 const std::vector<SourceLine> lines = SplitLines(source_bytes);
369 std::array<size_t, kSectionLabels.size()> section_lines{};
370 for (size_t section = 0; section < kSectionLabels.size(); ++section) {
371 int matches = 0;
372 for (size_t line = 0; line < lines.size(); ++line) {
373 if (CodeForLine(lines[line].text) == kSectionLabels[section]) {
374 section_lines[section] = line;
375 ++matches;
376 }
377 }
378 if (matches != 1) {
379 return absl::InvalidArgumentError(
380 absl::StrFormat("Expected exactly one %s label, found %d",
381 kSectionLabels[section], matches));
382 }
383 if (section > 0 && section_lines[section] <= section_lines[section - 1]) {
384 return absl::InvalidArgumentError(
385 "Minecart table sections must be ordered Rooms, X, then Y");
386 }
387 }
388
389 std::array<ParsedSection, kSectionLabels.size()> parsed_sections;
390 for (size_t section = 0; section < kSectionLabels.size(); ++section) {
391 const size_t end_line = section + 1 < kSectionLabels.size()
392 ? section_lines[section + 1]
393 : lines.size();
394 auto parsed_or =
395 ParseSection(lines, section_lines[section], end_line, section);
396 if (!parsed_or.ok()) {
397 return parsed_or.status();
398 }
399 parsed_sections[section] = std::move(*parsed_or);
400 }
401 if (parsed_sections[0].guarded != parsed_sections[1].guarded ||
402 parsed_sections[0].guarded != parsed_sections[2].guarded) {
403 return absl::InvalidArgumentError(
404 "Minecart sections must use the same flat or guarded layout");
405 }
406
408 document.source_bytes_ = std::move(source_bytes);
409 document.guarded_ = parsed_sections[0].guarded;
410 document.tracks_.reserve(kMinecartTrackSlotCount);
411 for (size_t index = 0; index < kMinecartTrackSlotCount; ++index) {
412 document.tracks_.push_back(
413 {static_cast<int>(index), parsed_sections[0].values[index],
414 parsed_sections[1].values[index], parsed_sections[2].values[index]});
415 }
416 for (size_t field = 0; field < parsed_sections.size(); ++field) {
417 document.editable_token_spans_[field] =
418 parsed_sections[field].editable_spans;
419 }
420 return document;
421}
422
423absl::StatusOr<std::string> MinecartTrackSourceDocument::Render(
424 const std::vector<MinecartTrack>& tracks) const {
425 if (tracks.size() != kMinecartTrackSlotCount) {
426 return absl::InvalidArgumentError(absl::StrFormat(
427 "Minecart source requires exactly %zu tracks; found %zu",
429 }
430 for (size_t index = 0; index < tracks.size(); ++index) {
431 const absl::Status status = ValidateTrack(tracks[index], index);
432 if (!status.ok()) {
433 return status;
434 }
435 }
436
437 std::string rendered = source_bytes_;
438 for (size_t index = 0; index < tracks.size(); ++index) {
439 const std::array<int, 3> original = {
440 tracks_[index].room_id, tracks_[index].start_x, tracks_[index].start_y};
441 const std::array<int, 3> updated = {
442 tracks[index].room_id, tracks[index].start_x, tracks[index].start_y};
443 for (size_t field = 0; field < updated.size(); ++field) {
444 if (updated[field] == original[field]) {
445 continue;
446 }
448 editable_token_spans_[field][index];
449 if (span.length != 5 || span.offset + span.length > rendered.size() ||
450 rendered[span.offset] != '$') {
451 return absl::DataLossError(
452 "Minecart source token span no longer matches the parsed bytes");
453 }
454 rendered.replace(span.offset, span.length, FormatHexWord(updated[field]));
455 }
456 }
457 return rendered;
458}
459
460} // namespace yaze::editor
static absl::StatusOr< MinecartTrackSourceDocument > Parse(std::string source_bytes)
const std::vector< MinecartTrack > & tracks() const
std::array< MinecartTrackSourceTokenSpan, kMinecartTrackSlotCount > FieldTokenSpans
absl::StatusOr< std::string > Render(const std::vector< MinecartTrack > &tracks) const
bool Consume(absl::string_view *code, absl::string_view token)
absl::StatusOr< std::vector< ParsedToken > > ParseDwLine(const SourceLine &line, absl::string_view label)
std::vector< SourceLine > SplitLines(absl::string_view source)
absl::StatusOr< ParsedSection > ParseSection(const std::vector< SourceLine > &lines, size_t first_line, size_t end_line, size_t section)
constexpr std::array< absl::string_view, 3 > kSectionLabels
absl::Status ValidateTrack(const MinecartTrack &track, size_t index)
constexpr std::array< absl::string_view, 3 > kSectionEndLabels
bool IsExpectedSizeAssertion(absl::string_view code, size_t section)
Editors are the view controllers for the application.
constexpr size_t kMinecartTrackSlotCount
Room transition destination.
Definition zelda.h:448