10#include "fmt/format.h"
11#include "fmt/ranges.h"
12#include "nlohmann/json.hpp"
25[[nodiscard]] std::optional<metatile::Layer> layer_from_string(
const std::string &str)
27 if (str ==
"bottom") {
28 return metatile::Layer::bottom;
30 if (str ==
"middle") {
31 return metatile::Layer::middle;
34 return metatile::Layer::top;
39[[nodiscard]] std::optional<metatile::Subtile> subtile_from_string(
const std::string &str)
41 if (str ==
"northwest") {
42 return metatile::Subtile::northwest;
44 if (str ==
"northeast") {
45 return metatile::Subtile::northeast;
47 if (str ==
"southwest") {
48 return metatile::Subtile::southwest;
50 if (str ==
"southeast") {
51 return metatile::Subtile::southeast;
59 case metatile::Subtile::northwest:
61 case metatile::Subtile::northeast:
63 case metatile::Subtile::southwest:
65 case metatile::Subtile::southeast:
68 panic(
"unhandled Subtile value");
82[[nodiscard]] std::size_t byte_offset_to_line_index(
const std::filesystem::path &json_path, std::size_t byte_offset)
84 std::ifstream in{json_path};
89 std::size_t line_index = 0;
90 std::size_t current_byte = 0;
92 while (in.get(ch) && current_byte < byte_offset) {
112[[nodiscard]] std::size_t find_key_line_index(
const std::filesystem::path &json_path,
const std::string &key_name)
114 std::ifstream in{json_path};
119 const std::string pattern =
"\"" + key_name +
"\"";
121 std::size_t line_index = 0;
122 while (std::getline(in, line)) {
123 if (line.find(pattern) != std::string::npos) {
131[[nodiscard]] std::vector<AnimOverrideEntry>
132parse_override_entries(
const std::string &context_name,
const nlohmann::json &overrides_node)
134 std::vector<AnimOverrideEntry> overrides;
135 for (
const auto &entry_node : overrides_node) {
138 entry.
metatile_id = entry_node.at(
"id").get<std::size_t>();
140 const auto layer_str = entry_node.at(
"layer").get<std::string>();
141 const auto layer_opt = layer_from_string(layer_str);
142 if (!layer_opt.has_value()) {
143 panic(
"anim.json: '" + context_name +
"' override has invalid layer '" + layer_str +
"'");
145 entry.layer = *layer_opt;
147 const auto subtile_str = entry_node.at(
"subtile").get<std::string>();
148 const auto subtile_opt = subtile_from_string(subtile_str);
149 if (!subtile_opt.has_value()) {
150 panic(
"anim.json: '" + context_name +
"' override has invalid subtile '" + subtile_str +
"'");
152 entry.subtile = *subtile_opt;
154 entry.frame_subtile = entry_node.at(
"frame_subtile").get<std::size_t>();
155 entry.pal_index = entry_node.at(
"pal_index").get<std::size_t>();
156 entry.h_flip = entry_node.at(
"hflip").get<
bool>();
157 entry.v_flip = entry_node.at(
"vflip").get<
bool>();
159 overrides.push_back(entry);
164[[nodiscard]] nlohmann::ordered_json serialize_override_entries(
const std::vector<AnimOverrideEntry> &entries)
166 nlohmann::ordered_json overrides_array = nlohmann::ordered_json::array();
167 for (
const auto &entry : entries) {
168 nlohmann::ordered_json obj;
169 obj[
"id"] = entry.metatile_id;
171 obj[
"subtile"] = subtile_to_json_string(entry.subtile);
172 obj[
"frame_subtile"] = entry.frame_subtile;
173 obj[
"pal_index"] = entry.pal_index;
174 obj[
"hflip"] = entry.h_flip;
175 obj[
"vflip"] = entry.v_flip;
176 overrides_array.push_back(std::move(obj));
178 return overrides_array;
181AnimParams parse_animation_params(
const std::string &anim_name,
const nlohmann::json &node)
185 if (node.contains(
"frame_factor")) {
186 params.
frame_factor(node[
"frame_factor"].get<std::size_t>());
189 if (node.contains(
"frame_offset")) {
190 params.
frame_offset(node[
"frame_offset"].get<std::size_t>());
194 if (node.contains(
"frames")) {
195 std::vector<DynamicCasedName> frame_names;
196 for (
const auto &frame : node[
"frames"]) {
204 if (node.contains(
"frame_order")) {
205 std::vector<DynamicCasedName> frame_order;
206 for (
const auto &frame : node[
"frame_order"]) {
216 if (node.contains(
"counter_max")) {
217 params.
counter_max(node[
"counter_max"].get<std::size_t>());
220 if (node.contains(
"overrides")) {
221 if (!node[
"overrides"].is_array()) {
222 panic(
"anim.json: animation '" + anim_name +
"' overrides must be an array");
224 params.
overrides(parse_override_entries(anim_name, node[
"overrides"]));
227 if (node.contains(
"tile_offset")) {
228 params.
tile_offset(node[
"tile_offset"].get<std::size_t>());
235nlohmann::ordered_json serialize_animation_params(
const AnimParams ¶ms)
237 nlohmann::ordered_json node;
249 nlohmann::ordered_json frames_array = nlohmann::ordered_json::array();
250 for (
const auto &frame : params.frame_names()) {
251 frames_array.push_back(frame.to_snake_case());
253 node[
"frames"] = frames_array;
256 nlohmann::ordered_json frame_order_array = nlohmann::ordered_json::array();
257 for (
const auto &frame : params.frame_order()) {
258 frame_order_array.push_back(frame.to_snake_case());
260 node[
"frame_order"] = frame_order_array;
271 node[
"overrides"] = serialize_override_entries(params.
overrides());
286 if (!std::filesystem::exists(json_path)) {
288 std::vector<std::string>{
"Parameters file not found.",
"Expected file: {}"},
293 std::ifstream in{json_path};
296 std::vector<std::string>{
"Failed to open anim.json for reading.",
"path: {}"},
300 nlohmann::json root = nlohmann::json::parse(in);
302 std::map<DynamicCasedName, AnimParams> result;
304 if (!root.is_object()) {
306 std::vector<std::string> err_lines;
307 err_lines.push_back(format_->
format(
308 "{}:1: Invalid anim.json format, expected a JSON object at the root level.",
310 err_lines.emplace_back();
311 auto context = printer.print(json_path, std::vector<std::size_t>{0});
312 err_lines.insert(err_lines.end(), context.begin(), context.end());
316 for (
const auto &[anim_name, anim_node] : root.items()) {
317 if (anim_name ==
"primary_references") {
321 const std::size_t line_idx = find_key_line_index(json_path, anim_name);
325 if (expected_snake != anim_name) {
327 std::vector<std::string> err_lines;
328 err_lines.push_back(format_->
format(
329 "{}:{}: Animation name '{}' must be snake_case (expected '{}').",
334 err_lines.emplace_back();
335 auto context = printer.print(json_path, std::vector{line_idx});
336 err_lines.insert(err_lines.end(), context.begin(), context.end());
340 if (!anim_node.is_object()) {
342 std::vector<std::string> err_lines;
343 err_lines.push_back(format_->
format(
344 "{}:{}: Invalid animation entry, '{}' should be a JSON object with frame_factor, frame_offset, "
349 err_lines.emplace_back();
350 auto context = printer.print(json_path, std::vector{line_idx});
351 err_lines.insert(err_lines.end(), context.begin(), context.end());
355 auto parsed = parse_animation_params(anim_name, anim_node);
360 std::set<DynamicCasedName> valid_frames(
361 parsed_params.frame_names().begin(), parsed_params.frame_names().end());
363 for (
const auto &frame : parsed_params.frame_order()) {
364 if (!valid_frames.contains(frame)) {
366 std::vector<std::string> err_lines;
367 err_lines.push_back(format_->
format(
368 "{}:{}: frame_order entry '{}' does not exist in frames list.",
372 err_lines.emplace_back();
375 std::vector<std::string> frame_strs;
376 frame_strs.reserve(parsed_params.frame_names().size());
377 for (
const auto &f : parsed_params.frame_names()) {
378 frame_strs.push_back(f.to_snake_case());
380 err_lines.push_back(format_->
format(
381 "Valid frames are: {}.",
382 FormatParam{fmt::format(
"[{}]", fmt::join(frame_strs,
", ")), Style::bold}));
383 err_lines.emplace_back();
384 auto context = printer.print(json_path, std::vector<std::size_t>{line_idx});
385 err_lines.insert(err_lines.end(), context.begin(), context.end());
393 catch (
const nlohmann::json::parse_error &e) {
395 std::vector<std::string> err_lines;
397 const auto byte_offset =
static_cast<std::size_t
>(e.byte);
398 const auto line_idx = byte_offset_to_line_index(json_path, byte_offset);
399 err_lines.push_back(format_->
format(
400 "{}:{}: Failed to parse anim.json: {}.",
404 err_lines.emplace_back();
405 auto context = printer.print(json_path, std::vector<std::size_t>{line_idx});
406 err_lines.insert(err_lines.end(), context.begin(), context.end());
410 catch (
const nlohmann::json::exception &e) {
412 std::vector<std::string>{
"{}: Failed to parse anim.json: {}."},
413 std::vector<std::vector<FormatParam>>{
421 if (!std::filesystem::exists(json_path)) {
423 std::vector<std::string>{
"Parameters file not found.",
"Expected file: {}"},
428 std::ifstream in{json_path};
431 std::vector<std::string>{
"Failed to open anim.json for reading.",
"path: {}"},
435 nlohmann::json root = nlohmann::json::parse(in);
437 std::map<DynamicCasedName, std::vector<AnimOverrideEntry>> result;
439 if (!root.is_object() || !root.contains(
"primary_references")) {
443 const auto &refs_node = root[
"primary_references"];
444 if (!refs_node.is_object()) {
446 const auto line_idx = find_key_line_index(json_path,
"primary_references");
447 std::vector<std::string> err_lines;
448 err_lines.push_back(format_->
format(
449 "{}:{}: 'primary_references' must be a JSON object.",
452 err_lines.emplace_back();
453 auto context = printer.print(json_path, std::vector{line_idx});
454 err_lines.insert(err_lines.end(), context.begin(), context.end());
458 for (
const auto &[prim_anim_name, prim_anim_node] : refs_node.items()) {
459 const auto line_idx = find_key_line_index(json_path, prim_anim_name);
461 if (!prim_anim_node.is_object()) {
463 std::vector<std::string> err_lines;
464 err_lines.push_back(format_->
format(
465 "{}:{}: Primary reference '{}' must be a JSON object.",
469 err_lines.emplace_back();
470 auto context = printer.print(json_path, std::vector{line_idx});
471 err_lines.insert(err_lines.end(), context.begin(), context.end());
475 if (!prim_anim_node.contains(
"overrides") || !prim_anim_node[
"overrides"].is_array()) {
477 std::vector<std::string> err_lines;
478 err_lines.push_back(format_->
format(
479 "{}:{}: Primary reference '{}' must contain an 'overrides' array.",
483 err_lines.emplace_back();
484 auto context = printer.print(json_path, std::vector{line_idx});
485 err_lines.insert(err_lines.end(), context.begin(), context.end());
489 auto entries = parse_override_entries(prim_anim_name, prim_anim_node[
"overrides"]);
495 catch (
const nlohmann::json::parse_error &e) {
497 std::vector<std::string> err_lines;
499 const auto byte_offset =
static_cast<std::size_t
>(e.byte);
500 const auto line_idx = byte_offset_to_line_index(json_path, byte_offset);
501 err_lines.push_back(format_->
format(
502 "{}:{}: Failed to parse anim.json: {}.",
506 err_lines.emplace_back();
507 auto context = printer.print(json_path, std::vector<std::size_t>{line_idx});
508 err_lines.insert(err_lines.end(), context.begin(), context.end());
512 catch (
const nlohmann::json::exception &e) {
514 std::vector<std::string>{
"{}: Failed to parse anim.json: {}."},
515 std::vector<std::vector<FormatParam>>{
521 const std::filesystem::path &json_path,
522 const std::map<DynamicCasedName, AnimParams> ¶ms,
523 const std::map<
DynamicCasedName, std::vector<AnimOverrideEntry>> &primary_references)
const
527 if (json_path.has_parent_path()) {
528 std::filesystem::create_directories(json_path.parent_path());
531 nlohmann::ordered_json root;
532 for (
const auto &[name, anim_params] : params) {
533 root[name.to_snake_case()] = serialize_animation_params(anim_params);
536 if (!primary_references.empty()) {
537 nlohmann::ordered_json refs_node;
538 for (
const auto &[prim_anim_name, entries] : primary_references) {
539 nlohmann::ordered_json anim_ref_node;
540 anim_ref_node[
"overrides"] = serialize_override_entries(entries);
541 refs_node[prim_anim_name.to_snake_case()] = std::move(anim_ref_node);
543 root[
"primary_references"] = std::move(refs_node);
546 std::ofstream out(json_path);
549 std::vector<std::string>{
"Failed to open anim.json for writing.",
"path: {}"},
559 std::vector<std::string>{
"Failed to write anim.json.",
"Error occurred while writing to: {}"},
565 catch (
const nlohmann::json::exception &e) {
567 std::vector<std::string>{
"Failed to serialize anim.json.",
"JSON error: {}"},
568 std::vector<std::vector<FormatParam>>{{}, {
FormatParam{e.what()}}}};
AnimJsonParser(gsl::not_null< const TextFormatter * > format)
ChainableResult< void > write(const std::filesystem::path &json_path, const std::map< DynamicCasedName, AnimParams > ¶ms, const std::map< DynamicCasedName, std::vector< AnimOverrideEntry > > &primary_references={}) const
Writes animation parameters to an anim.json file.
ChainableResult< std::map< DynamicCasedName, std::vector< AnimOverrideEntry > > > parse_primary_references(const std::filesystem::path &json_path) const
Parses the primary_references section from an anim.json file.
ChainableResult< std::map< DynamicCasedName, AnimParams > > parse(const std::filesystem::path &json_path) const
Parses an anim.json file into a map of animation parameters.
Configuration parameters for a single tileset animation.
const DynamicCasedName & cased_name() const
Returns the structured name for this animation, preserving case format information.
const std::vector< AnimOverrideEntry > & overrides() const
Returns the manual override entries for this animation.
const std::vector< DynamicCasedName > & frame_order() const
Returns the playback sequence.
std::size_t frame_offset() const
Returns the frame offset (remainder value for timer modulo check).
std::size_t tile_offset() const
Returns the VRAM tile offset for this animation.
const std::vector< DynamicCasedName > & frame_names() const
Returns the unique frame definitions.
std::size_t frame_factor() const
Returns the frame factor (modulus divisor for timer).
std::size_t counter_max() const
Returns the animation counter maximum value.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A smart string wrapper that preserves word structure for lossless case format conversion.
std::string to_snake_case() const
Outputs all words flattened and joined with underscores.
static DynamicCasedName from_snake_case(const std::string &input)
Constructs from a snake_case input string.
A service for printing file lines with highlighted lines and line numbers.
static const Style bold
Bold text formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > ¶ms) const
Formats a string with styled parameters using fmtlib syntax.
constexpr std::size_t default_counter_max
constexpr std::size_t default_frame_offset
constexpr std::size_t default_frame_factor
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
A manual override that maps a specific metatile entry to an animation subtile.
std::size_t metatile_id
The metatile ID this override applies to (corresponds to JSON "id" field).