yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
command_palette_goto.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <vector>
6
7#include "absl/strings/str_format.h"
10#include "zelda3/common.h"
12
13namespace yaze {
14namespace editor {
15namespace {
16
17std::string Lower(std::string_view text) {
18 std::string out(text);
19 std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {
20 return static_cast<char>(std::tolower(c));
21 });
22 return out;
23}
24
25bool IsSpace(char c) {
26 return std::isspace(static_cast<unsigned char>(c)) != 0;
27}
28
29std::string_view Trim(std::string_view text) {
30 while (!text.empty() && IsSpace(text.front()))
31 text.remove_prefix(1);
32 while (!text.empty() && IsSpace(text.back()))
33 text.remove_suffix(1);
34 return text;
35}
36
37std::optional<int> ParseDigits(std::string_view digits, int base) {
38 // Six digits covers every supported range and cannot overflow int.
39 if (digits.empty() || digits.size() > 6)
40 return std::nullopt;
41 int value = 0;
42 for (char c : digits) {
43 int digit = -1;
44 if (c >= '0' && c <= '9') {
45 digit = c - '0';
46 } else if (base == 16 && c >= 'a' && c <= 'f') {
47 digit = 10 + (c - 'a');
48 }
49 if (digit < 0 || digit >= base)
50 return std::nullopt;
51 value = value * base + digit;
52 }
53 return value;
54}
55
57 const char* word;
59};
60
61constexpr KeywordInfo kKeywords[] = {
62 {"room", GotoKind::kRoom}, {"rm", GotoKind::kRoom},
64 {"ow", GotoKind::kOverworldMap}, {"message", GotoKind::kMessage},
65 {"msg", GotoKind::kMessage}, {"sprite", GotoKind::kSprite},
66 {"spr", GotoKind::kSprite},
67};
68
69} // namespace
70
71std::optional<int> ParseGotoId(std::string_view token) {
72 const std::string lower = Lower(Trim(token));
73 std::string_view text(lower);
74 if (text.empty())
75 return std::nullopt;
76 if (text.front() == '#')
77 return ParseDigits(text.substr(1), 10);
78 if (text.front() == '$')
79 return ParseDigits(text.substr(1), 16);
80 if (text.size() > 2 && text.substr(0, 2) == "0x")
81 return ParseDigits(text.substr(2), 16);
82 if (text.size() > 1 && text.back() == 'h')
83 return ParseDigits(text.substr(0, text.size() - 1), 16);
84 return ParseDigits(text, 16);
85}
86
88 switch (kind) {
89 case GotoKind::kRoom:
90 return 0x127;
94 return 0xFFFF;
96 return 0xFF;
97 }
98 return 0;
99}
100
101const char* GotoKindName(GotoKind kind) {
102 switch (kind) {
103 case GotoKind::kRoom:
104 return "room";
106 return "map";
108 return "message";
110 return "sprite";
111 }
112 return "";
113}
114
116 return kind != GotoKind::kSprite;
117}
118
119std::optional<GotoQuery> ParseGotoQuery(std::string_view query) {
120 std::string lower = Lower(Trim(query));
121 std::string_view text(lower);
122 for (const char* prefix : {"go to ", "goto ", "go "}) {
123 const std::string_view p(prefix);
124 if (text.substr(0, p.size()) == p) {
125 text = Trim(text.substr(p.size()));
126 break;
127 }
128 }
129
130 // Keyword: leading letters.
131 size_t word_end = 0;
132 while (word_end < text.size() &&
133 std::isalpha(static_cast<unsigned char>(text[word_end]))) {
134 ++word_end;
135 }
136 const std::string_view word = text.substr(0, word_end);
137 const KeywordInfo* keyword = nullptr;
138 for (const auto& info : kKeywords) {
139 if (word == info.word) {
140 keyword = &info;
141 break;
142 }
143 }
144 if (!keyword)
145 return std::nullopt;
146
147 // Separator: whitespace and/or ':' is required ("r4a" is not a go-to).
148 // `query` is used untrimmed so "room " (trailing space) counts.
149 std::string_view rest = text.substr(word_end);
150 const bool query_has_trailing_space =
151 !query.empty() && IsSpace(query.back()) && rest.empty();
152 if (rest.empty() && !query_has_trailing_space)
153 return std::nullopt;
154 if (!rest.empty() && rest.front() != ':' && !IsSpace(rest.front()))
155 return std::nullopt;
156 if (!rest.empty() && rest.front() == ':')
157 rest.remove_prefix(1);
158 rest = Trim(rest);
159
160 GotoQuery result;
161 result.kind = keyword->kind;
162 if (rest.empty()) {
164 return result;
165 }
166 for (char c : rest) {
167 if (IsSpace(c))
168 return std::nullopt; // "map editor stuff": not a go-to
169 }
170 const auto id = ParseGotoId(rest);
171 if (!id)
172 return std::nullopt;
173 result.id = *id;
174 result.status = (*id >= 0 && *id <= GotoMaxId(result.kind))
177 return result;
178}
179
180CommandEntry BuildGotoEntry(const GotoQuery& query, size_t session_id) {
181 CommandEntry entry;
183 const char* kind_name = GotoKindName(query.kind);
184 const int max_id = GotoMaxId(query.kind);
185 const int width = max_id > 0xFF ? 3 : 2;
186
188 entry.name = absl::StrFormat("Go to %s …", kind_name);
189 entry.enabled = false;
190 entry.note = absl::StrFormat(
191 "Type an id: 4A, 0x4A, $4A or 4Ah (hex), #74 (decimal). Range "
192 "0x%0*X-0x%0*X",
193 width, 0, width, max_id);
194 return entry;
195 }
197 entry.name = absl::StrFormat("Go to %s 0x%0*X", kind_name, width, query.id);
198 entry.enabled = false;
199 entry.note = absl::StrFormat("Out of range: %s ids are 0x%0*X-0x%0*X",
200 kind_name, width, 0, width, max_id);
201 return entry;
202 }
203
204 std::string label;
205 switch (query.kind) {
206 case GotoKind::kRoom:
207 label = zelda3::GetRoomLabel(query.id);
208 break;
210 label = zelda3::GetOverworldMapLabel(query.id);
211 break;
213 label = zelda3::GetSpriteLabel(query.id);
214 break;
216 break; // Messages have no resource label; the editor shows the text.
217 }
218 entry.name =
219 label.empty()
220 ? absl::StrFormat("Go to %s 0x%0*X", kind_name, width, query.id)
221 : absl::StrFormat("Go to %s 0x%0*X — %s", kind_name, width, query.id,
222 label);
223 entry.description = absl::StrFormat("Jump to %s 0x%0*X (%d)", kind_name,
224 width, query.id, query.id);
225
226 if (!GotoKindHasJumpPath(query.kind)) {
227 entry.enabled = false;
228 entry.note = "No sprite jump path yet; open the Sprite editor manually";
229 return entry;
230 }
231
232 const int id = query.id;
233 switch (query.kind) {
234 case GotoKind::kRoom:
235 entry.callback = [id, session_id]() {
236 if (auto* bus = ContentRegistry::Context::event_bus())
237 bus->Publish(JumpToRoomRequestEvent::Create(id, session_id));
238 };
239 break;
241 entry.callback = [id, session_id]() {
242 if (auto* bus = ContentRegistry::Context::event_bus())
243 bus->Publish(JumpToMapRequestEvent::Create(id, session_id));
244 };
245 break;
247 entry.callback = [id, session_id]() {
248 if (auto* bus = ContentRegistry::Context::event_bus())
249 bus->Publish(JumpToMessageRequestEvent::Create(id, session_id));
250 };
251 break;
253 break;
254 }
255 return entry;
256}
257
258} // namespace editor
259} // namespace yaze
::yaze::EventBus * event_bus()
Get the current EventBus instance.
std::optional< int > ParseDigits(std::string_view digits, int base)
CommandEntry BuildGotoEntry(const GotoQuery &query, size_t session_id)
const char * GotoKindName(GotoKind kind)
bool GotoKindHasJumpPath(GotoKind kind)
True when a jump path exists for kind (sprites have none yet).
std::optional< int > ParseGotoId(std::string_view token)
Parse a single id token using the formats above. nullopt if malformed.
int GotoMaxId(GotoKind kind)
Inclusive upper bound for kind.
std::optional< GotoQuery > ParseGotoQuery(std::string_view query)
std::string GetSpriteLabel(int id)
Convenience function to get a sprite label.
std::string GetRoomLabel(int id)
Convenience function to get a room label.
constexpr int kNumOverworldMaps
Definition common.h:85
std::string GetOverworldMapLabel(int id)
Convenience function to get an overworld map label.
static constexpr const char * kNavigation
std::function< void()> callback
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToMessageRequestEvent Create(int message, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)