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
23struct SourceLine {
24 size_t offset;
25 absl::string_view text;
26};
27
32
34 std::array<int, kMinecartTrackSlotCount> values{};
36 bool guarded = false;
37};
38
39bool IsSpace(char value) {
40 return std::isspace(static_cast<unsigned char>(value)) != 0;
41}
42
43bool IsHexDigit(char value) {
44 return std::isxdigit(static_cast<unsigned char>(value)) != 0;
45}
46
47absl::string_view Trim(absl::string_view value) {
48 while (!value.empty() && IsSpace(value.front())) {
49 value.remove_prefix(1);
50 }
51 while (!value.empty() && IsSpace(value.back())) {
52 value.remove_suffix(1);
53 }
54 return value;
55}
56
57absl::string_view CodeForLine(absl::string_view line) {
58 const size_t comment = line.find(';');
59 return Trim(line.substr(0, comment));
60}
61
62std::vector<SourceLine> SplitLines(absl::string_view source) {
63 std::vector<SourceLine> lines;
64 size_t offset = 0;
65 while (offset < source.size()) {
66 const size_t newline = source.find('\n', offset);
67 const size_t end =
68 newline == absl::string_view::npos ? source.size() : newline;
69 lines.push_back({offset, source.substr(offset, end - offset)});
70 if (newline == absl::string_view::npos) {
71 break;
72 }
73 offset = newline + 1;
74 }
75 return lines;
76}
77
78absl::StatusOr<std::vector<ParsedToken>> ParseDwLine(const SourceLine& line,
79 absl::string_view label) {
80 const size_t comment = line.text.find(';');
81 const size_t code_end =
82 comment == absl::string_view::npos ? line.text.size() : comment;
83 size_t cursor = 0;
84 while (cursor < code_end && IsSpace(line.text[cursor])) {
85 ++cursor;
86 }
87 if (cursor + 2 > code_end ||
88 std::tolower(static_cast<unsigned char>(line.text[cursor])) != 'd' ||
89 std::tolower(static_cast<unsigned char>(line.text[cursor + 1])) != 'w' ||
90 cursor + 2 == code_end || !IsSpace(line.text[cursor + 2])) {
91 return absl::InvalidArgumentError(absl::StrFormat(
92 "Unexpected statement in %s: %s", label, CodeForLine(line.text)));
93 }
94 cursor += 2;
95
96 std::vector<ParsedToken> tokens;
97 bool requires_token = true;
98 while (true) {
99 while (cursor < code_end && IsSpace(line.text[cursor])) {
100 ++cursor;
101 }
102 if (cursor == code_end) {
103 if (requires_token) {
104 return absl::InvalidArgumentError(
105 absl::StrFormat("Empty operand in %s", label));
106 }
107 break;
108 }
109
110 const size_t token_start = cursor;
111 if (line.text[cursor] != '$' || cursor + 5 > code_end ||
112 !std::all_of(
113 line.text.begin() + static_cast<std::ptrdiff_t>(cursor + 1),
114 line.text.begin() + static_cast<std::ptrdiff_t>(cursor + 5),
115 IsHexDigit)) {
116 return absl::InvalidArgumentError(
117 absl::StrFormat("%s values must use exact 16-bit $hhhh operands: %s",
118 label, CodeForLine(line.text)));
119 }
120 cursor += 5;
121 if (cursor < code_end && IsHexDigit(line.text[cursor])) {
122 return absl::InvalidArgumentError(
123 absl::StrFormat("%s contains an over-wide 16-bit operand: %s", label,
124 CodeForLine(line.text)));
125 }
126
127 int value = 0;
128 for (size_t digit = token_start + 1; digit < token_start + 5; ++digit) {
129 const char hex = line.text[digit];
130 value <<= 4;
131 if (hex >= '0' && hex <= '9') {
132 value += hex - '0';
133 } else {
134 value += 10 + std::tolower(static_cast<unsigned char>(hex)) - 'a';
135 }
136 }
137 tokens.push_back({value, {line.offset + token_start, 5}});
138 requires_token = false;
139
140 while (cursor < code_end && IsSpace(line.text[cursor])) {
141 ++cursor;
142 }
143 if (cursor == code_end) {
144 break;
145 }
146 if (line.text[cursor] != ',') {
147 return absl::InvalidArgumentError(
148 absl::StrFormat("Unexpected operand separator in %s: %s", label,
149 CodeForLine(line.text)));
150 }
151 ++cursor;
152 requires_token = true;
153 }
154 if (tokens.empty()) {
155 return absl::InvalidArgumentError(
156 absl::StrFormat("Empty dw statement in %s", label));
157 }
158 return tokens;
159}
160
161absl::StatusOr<ParsedSection> ParseSection(const std::vector<SourceLine>& lines,
162 size_t first_line, size_t end_line,
163 absl::string_view label) {
164 enum class GuardState { kPrefix, kEnabled, kDisabled, kComplete };
165 GuardState state = GuardState::kPrefix;
166 bool saw_guard = false;
167 bool saw_else = false;
168 bool saw_endif = false;
169 std::vector<ParsedToken> prefix;
170 std::vector<ParsedToken> enabled;
171 std::vector<ParsedToken> disabled;
172
173 for (size_t line_index = first_line + 1; line_index < end_line;
174 ++line_index) {
175 const absl::string_view code = CodeForLine(lines[line_index].text);
176 if (code.empty()) {
177 continue;
178 }
179 if (code == kPlannedTrackGuard) {
180 if (state != GuardState::kPrefix || saw_guard) {
181 return absl::InvalidArgumentError(
182 absl::StrFormat("Nested or repeated guard in %s", label));
183 }
184 saw_guard = true;
185 state = GuardState::kEnabled;
186 continue;
187 }
188 if (code == "else") {
189 if (!saw_guard || state != GuardState::kEnabled || saw_else) {
190 return absl::InvalidArgumentError(
191 absl::StrFormat("Unexpected else in %s", label));
192 }
193 saw_else = true;
194 state = GuardState::kDisabled;
195 continue;
196 }
197 if (code == "endif") {
198 if (!saw_else || state != GuardState::kDisabled || saw_endif) {
199 return absl::InvalidArgumentError(
200 absl::StrFormat("Unexpected endif in %s", label));
201 }
202 saw_endif = true;
203 state = GuardState::kComplete;
204 continue;
205 }
206 if (state == GuardState::kComplete) {
207 return absl::InvalidArgumentError(absl::StrFormat(
208 "Unexpected content after guarded table in %s: %s", label, code));
209 }
210
211 auto tokens_or = ParseDwLine(lines[line_index], label);
212 if (!tokens_or.ok()) {
213 return tokens_or.status();
214 }
215 std::vector<ParsedToken>* destination = &prefix;
216 if (state == GuardState::kEnabled) {
217 destination = &enabled;
218 } else if (state == GuardState::kDisabled) {
219 destination = &disabled;
220 }
221 destination->insert(destination->end(), tokens_or->begin(),
222 tokens_or->end());
223 }
224
225 ParsedSection result;
226 std::vector<ParsedToken> active;
227 if (!saw_guard) {
228 if (prefix.size() != kMinecartTrackSlotCount) {
229 return absl::InvalidArgumentError(
230 absl::StrFormat("%s must contain exactly %zu values; found %zu",
231 label, kMinecartTrackSlotCount, prefix.size()));
232 }
233 active = std::move(prefix);
234 } else {
235 if (!saw_else || !saw_endif) {
236 return absl::InvalidArgumentError(
237 absl::StrFormat("Incomplete planned-track guard in %s", label));
238 }
239 if (prefix.size() != 4 || enabled.size() != 28 || disabled.size() != 28) {
240 return absl::InvalidArgumentError(absl::StrFormat(
241 "%s guarded layout requires exactly 4 prefix, 28 enabled, and 28 "
242 "disabled values; found %zu, %zu, and %zu",
243 label, prefix.size(), enabled.size(), disabled.size()));
244 }
245 active = std::move(prefix);
246 active.insert(active.end(), enabled.begin(), enabled.end());
247 result.guarded = true;
248 }
249
250 for (size_t index = 0; index < kMinecartTrackSlotCount; ++index) {
251 result.values[index] = active[index].value;
252 result.editable_spans[index] = active[index].span;
253 }
254 return result;
255}
256
257absl::Status ValidateTrack(const MinecartTrack& track, size_t index) {
258 if (track.id != static_cast<int>(index)) {
259 return absl::InvalidArgumentError(
260 absl::StrFormat("Minecart track ID at slot %zu must be %zu; found %d",
261 index, index, track.id));
262 }
263 for (const auto& [field_name, value] :
264 std::array<std::pair<absl::string_view, int>, 3>{
265 {{"room_id", track.room_id},
266 {"start_x", track.start_x},
267 {"start_y", track.start_y}}}) {
268 if (value < 0 || value > 0xFFFF) {
269 return absl::InvalidArgumentError(absl::StrFormat(
270 "Minecart track %zu %s must be a 16-bit value; found %d", index,
271 field_name, value));
272 }
273 }
274 return absl::OkStatus();
275}
276
277std::string FormatHexWord(int value) {
278 return absl::StrFormat("$%04X", value);
279}
280
281} // namespace
282
283absl::StatusOr<MinecartTrackSourceDocument> MinecartTrackSourceDocument::Parse(
284 std::string source_bytes) {
285 const std::vector<SourceLine> lines = SplitLines(source_bytes);
286 std::array<size_t, kSectionLabels.size()> section_lines{};
287 for (size_t section = 0; section < kSectionLabels.size(); ++section) {
288 int matches = 0;
289 for (size_t line = 0; line < lines.size(); ++line) {
290 if (CodeForLine(lines[line].text) == kSectionLabels[section]) {
291 section_lines[section] = line;
292 ++matches;
293 }
294 }
295 if (matches != 1) {
296 return absl::InvalidArgumentError(
297 absl::StrFormat("Expected exactly one %s label, found %d",
298 kSectionLabels[section], matches));
299 }
300 if (section > 0 && section_lines[section] <= section_lines[section - 1]) {
301 return absl::InvalidArgumentError(
302 "Minecart table sections must be ordered Rooms, X, then Y");
303 }
304 }
305
306 std::array<ParsedSection, kSectionLabels.size()> parsed_sections;
307 for (size_t section = 0; section < kSectionLabels.size(); ++section) {
308 const size_t end_line = section + 1 < kSectionLabels.size()
309 ? section_lines[section + 1]
310 : lines.size();
311 auto parsed_or = ParseSection(lines, section_lines[section], end_line,
312 kSectionLabels[section]);
313 if (!parsed_or.ok()) {
314 return parsed_or.status();
315 }
316 parsed_sections[section] = std::move(*parsed_or);
317 }
318 if (parsed_sections[0].guarded != parsed_sections[1].guarded ||
319 parsed_sections[0].guarded != parsed_sections[2].guarded) {
320 return absl::InvalidArgumentError(
321 "Minecart sections must use the same flat or guarded layout");
322 }
323
325 document.source_bytes_ = std::move(source_bytes);
326 document.guarded_ = parsed_sections[0].guarded;
327 document.tracks_.reserve(kMinecartTrackSlotCount);
328 for (size_t index = 0; index < kMinecartTrackSlotCount; ++index) {
329 document.tracks_.push_back(
330 {static_cast<int>(index), parsed_sections[0].values[index],
331 parsed_sections[1].values[index], parsed_sections[2].values[index]});
332 }
333 for (size_t field = 0; field < parsed_sections.size(); ++field) {
334 document.editable_token_spans_[field] =
335 parsed_sections[field].editable_spans;
336 }
337 return document;
338}
339
340absl::StatusOr<std::string> MinecartTrackSourceDocument::Render(
341 const std::vector<MinecartTrack>& tracks) const {
342 if (tracks.size() != kMinecartTrackSlotCount) {
343 return absl::InvalidArgumentError(absl::StrFormat(
344 "Minecart source requires exactly %zu tracks; found %zu",
346 }
347 for (size_t index = 0; index < tracks.size(); ++index) {
348 const absl::Status status = ValidateTrack(tracks[index], index);
349 if (!status.ok()) {
350 return status;
351 }
352 }
353
354 std::string rendered = source_bytes_;
355 for (size_t index = 0; index < tracks.size(); ++index) {
356 const std::array<int, 3> original = {
357 tracks_[index].room_id, tracks_[index].start_x, tracks_[index].start_y};
358 const std::array<int, 3> updated = {
359 tracks[index].room_id, tracks[index].start_x, tracks[index].start_y};
360 for (size_t field = 0; field < updated.size(); ++field) {
361 if (updated[field] == original[field]) {
362 continue;
363 }
365 editable_token_spans_[field][index];
366 if (span.length != 5 || span.offset + span.length > rendered.size() ||
367 rendered[span.offset] != '$') {
368 return absl::DataLossError(
369 "Minecart source token span no longer matches the parsed bytes");
370 }
371 rendered.replace(span.offset, span.length, FormatHexWord(updated[field]));
372 }
373 }
374 return rendered;
375}
376
377} // 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
absl::StatusOr< std::vector< ParsedToken > > ParseDwLine(const SourceLine &line, absl::string_view label)
absl::StatusOr< ParsedSection > ParseSection(const std::vector< SourceLine > &lines, size_t first_line, size_t end_line, absl::string_view label)
std::vector< SourceLine > SplitLines(absl::string_view source)
constexpr std::array< absl::string_view, 3 > kSectionLabels
absl::Status ValidateTrack(const MinecartTrack &track, size_t index)
Editors are the view controllers for the application.
constexpr size_t kMinecartTrackSlotCount
Room transition destination.
Definition zelda.h:448