yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_tile_patterns.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <optional>
6
7#include "absl/strings/ascii.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_split.h"
10#include "absl/strings/strip.h"
12
13namespace yaze::zelda3 {
14namespace {
15
16constexpr size_t kMaxSourceBytes = 65536;
17constexpr uint32_t kFirstPatternPc = 0x1B52;
18constexpr uint32_t kPatternBankEnd = 0x8000;
19
20std::optional<uint32_t> HexLiteral(absl::string_view text, size_t digits) {
21 text = absl::StripAsciiWhitespace(text);
22 if (text.size() < 2 || text.size() > digits + 1 || text.front() != '$') {
23 return std::nullopt;
24 }
25 uint32_t value = 0;
26 for (char ch : text.substr(1)) {
27 if (!absl::ascii_isxdigit(ch)) {
28 return std::nullopt;
29 }
30 value = value * 16 +
31 (ch <= '9' ? ch - '0' : absl::ascii_tolower(ch) - 'a' + 10);
32 }
33 return value;
34}
35
36// Semicolons inside an assertion message are text, not assembler comments.
37absl::string_view StripComment(absl::string_view line) {
38 bool quoted = false;
39 for (size_t i = 0; i < line.size(); ++i) {
40 if (line[i] == '"')
41 quoted = !quoted;
42 if (line[i] == ';' && !quoted)
43 return line.substr(0, i);
44 }
45 return line;
46}
47
48std::optional<uint32_t> ProgramCounterAssertion(absl::string_view operand) {
49 if (!absl::ConsumePrefix(&operand, "pc()"))
50 return std::nullopt;
51 operand = absl::StripAsciiWhitespace(operand);
52 if (!absl::ConsumePrefix(&operand, "=="))
53 return std::nullopt;
54 const auto comma = operand.find(',');
55 const auto address = HexLiteral(operand.substr(0, comma), 6);
56 if (!address || *address < 0x009B52 || *address > 0x00FFFF)
57 return std::nullopt;
58 if (comma != absl::string_view::npos) {
59 auto message = absl::StripAsciiWhitespace(operand.substr(comma + 1));
60 if (message.size() < 2 || message.front() != '"' || message.back() != '"')
61 return std::nullopt;
62 message.remove_prefix(1);
63 message.remove_suffix(1);
64 if (message.find_first_of("\\\"") != absl::string_view::npos)
65 return std::nullopt;
66 }
67 return address;
68}
69
70bool IsLabel(absl::string_view text) {
71 if (text.empty() || text.back() != ':')
72 return false;
73 text.remove_suffix(1);
74 if (!text.empty() && text.front() == '#')
75 text.remove_prefix(1);
76 if (text.empty() ||
77 !(absl::ascii_isalpha(text.front()) || text.front() == '_')) {
78 return false;
79 }
80 for (char ch : text) {
81 if (!(absl::ascii_isalnum(ch) || ch == '_'))
82 return false;
83 }
84 return true;
85}
86
87} // namespace
88
89absl::StatusOr<DungeonTilePatterns> ParseDungeonTilePatterns(
90 absl::string_view source) {
91 if (source.size() > kMaxSourceBytes) {
92 return absl::ResourceExhaustedError(
93 "Dungeon pattern source exceeds 64 KiB");
94 }
96 result.source_text = std::string(source);
97 std::optional<uint32_t> cursor;
98 int line_number = 0;
99 for (absl::string_view line : absl::StrSplit(source, '\n')) {
100 ++line_number;
101 line = absl::StripAsciiWhitespace(StripComment(line));
102 if (line.empty() || line == "lorom" || IsLabel(line))
103 continue;
104 const auto fail = [&](absl::string_view reason) {
105 return absl::InvalidArgumentError(
106 absl::StrFormat("Dungeon pattern line %d: %s", line_number, reason));
107 };
108 const size_t split = line.find_first_of(" \t");
109 const auto command = line.substr(0, split);
110 const auto operand = split == absl::string_view::npos
111 ? absl::string_view{}
112 : absl::StripAsciiWhitespace(line.substr(split));
113 if (command == "org") {
114 const auto address = HexLiteral(operand, 6);
115 if (!address || *address < 0x009B52 || *address > 0x00FFFE ||
116 (*address & 1) != 0) {
117 return fail(
118 "org must name an aligned bank-00 object-data address "
119 "($009B52-$00FFFE)");
120 }
121 cursor = *address - 0x8000;
122 } else if (command == "dw") {
123 if (!cursor)
124 return fail("dw requires an org first");
125 for (absl::string_view token : absl::StrSplit(operand, ',')) {
126 const auto word = HexLiteral(token, 4);
127 if (!word)
128 return fail("dw requires literal $0000-$FFFF words");
129 if (*cursor < kFirstPatternPc || *cursor + 2 > kPatternBankEnd) {
130 return fail("tile words cross the bank-00 object-data boundary");
131 }
132 if (!result.words.emplace(*cursor, static_cast<uint16_t>(*word))
133 .second) {
134 return fail("tile words overlap an earlier declaration");
135 }
136 token = absl::StripAsciiWhitespace(token);
137 result.tokens.emplace(
139 static_cast<size_t>(token.data() - source.data()),
140 token.size()});
141 *cursor += 2;
142 }
143 } else if (command == "assert") {
144 if (!cursor)
145 return fail("assert pc() requires an org first");
146 const auto address = ProgramCounterAssertion(operand);
147 if (!address) {
148 return fail(
149 "assert requires pc() == $00xxxx with an optional quoted "
150 "message; expressions are not supported");
151 }
152 const uint32_t actual = *cursor + 0x8000;
153 if (*address != actual) {
154 return fail(absl::StrFormat(
155 "assert pc() failed: expected $%06X, current address $%06X",
156 *address, actual));
157 }
158 } else {
159 return fail(
160 "unsupported statement; only labels, lorom, org, literal dw "
161 "and assert pc() == $00xxxx are supported");
162 }
163 }
164 if (result.words.empty()) {
165 return absl::InvalidArgumentError(
166 "Dungeon pattern source contains no tile words");
167 }
168 return result;
169}
170
171absl::StatusOr<DungeonTilePatterns> LoadDungeonTilePatterns(
172 const std::string& path) {
173 std::error_code error;
174 const auto size = std::filesystem::file_size(path, error);
175 if (error)
176 return absl::NotFoundError("Cannot read dungeon pattern source: " + path);
177 if (size > kMaxSourceBytes) {
178 return absl::ResourceExhaustedError(
179 "Dungeon pattern source exceeds 64 KiB: " + path);
180 }
181 std::ifstream file(path, std::ios::binary);
182 if (!file)
183 return absl::NotFoundError("Cannot open dungeon pattern source: " + path);
184 // Bound the read even if the file grows after file_size().
185 std::string text(kMaxSourceBytes + 1, '\0');
186 file.read(text.data(), static_cast<std::streamsize>(text.size()));
187 text.resize(static_cast<size_t>(file.gcount()));
188 if (file.bad())
189 return absl::DataLossError("Cannot read dungeon pattern source: " + path);
190 auto result = ParseDungeonTilePatterns(text);
191 if (!result.ok()) {
192 return absl::Status(result.status().code(),
193 path + ": " + std::string(result.status().message()));
194 }
195 const auto canonical_path = std::filesystem::canonical(path, error);
196 if (error)
197 return absl::NotFoundError("Cannot resolve dungeon pattern source: " +
198 path);
199 result->source_path = canonical_path.string();
200 return result;
201}
202
204 const DungeonTilePatterns& expected,
205 const std::map<uint32_t, uint16_t>& replacements) {
206 if (expected.source_path.empty()) {
207 return absl::FailedPreconditionError("Pattern source has no file path");
208 }
210 {expected.source_path},
211 {"dungeon tile patterns", "dungeon pattern source"});
212 if (!lock.ok())
213 return lock.status();
214 auto current = LoadDungeonTilePatterns(expected.source_path);
215 if (!current.ok())
216 return current.status();
217 if (current->source_text != expected.source_text) {
218 return absl::AbortedError(
219 "Wall pattern source changed after opening; reopen before applying");
220 }
221 std::map<size_t, std::pair<size_t, uint16_t>, std::greater<size_t>> edits;
222 for (const auto& [pc, word] : replacements) {
223 const auto token = current->tokens.find(pc);
224 if (token == current->tokens.end()) {
225 return absl::FailedPreconditionError(
226 "Tile is not owned by this pattern source");
227 }
228 edits.emplace(token->second.offset,
229 std::make_pair(token->second.length, word));
230 }
231 std::string after = current->source_text;
232 for (const auto& [offset, edit] : edits) {
233 after.replace(offset, edit.first, absl::StrFormat("$%04X", edit.second));
234 }
235 auto parsed = ParseDungeonTilePatterns(after);
236 if (!parsed.ok())
237 return parsed.status();
238 auto expected_words = current->words;
239 for (const auto& [pc, word] : replacements)
240 expected_words.at(pc) = word;
241 if (parsed->words != expected_words) {
242 return absl::DataLossError("Pattern edit changed unrelated tile words");
243 }
245 **lock,
246 {{.target = expected.source_path,
247 .before = expected.source_text,
248 .after = after}},
250}
251
252} // namespace yaze::zelda3
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
std::optional< uint32_t > HexLiteral(absl::string_view text, size_t digits)
std::optional< uint32_t > ProgramCounterAssertion(absl::string_view operand)
Zelda 3 specific classes and functions.
absl::StatusOr< DungeonTilePatterns > LoadDungeonTilePatterns(const std::string &path)
absl::Status PublishDungeonTilePatterns(const DungeonTilePatterns &expected, const std::map< uint32_t, uint16_t > &replacements)
absl::StatusOr< DungeonTilePatterns > ParseDungeonTilePatterns(absl::string_view source)
std::map< uint32_t, Token > tokens
std::map< uint32_t, uint16_t > words