Porytiles
Loading...
Searching...
No Matches
project_tileset_artifact_reader.cpp
Go to the documentation of this file.
2
3#include <expected>
4#include <filesystem>
5#include <format>
6#include <fstream>
7#include <functional>
8#include <iterator>
9#include <optional>
10
22
23#include <iostream>
24
25namespace {
26
27using namespace porytiles;
28
29ChainableResult<void> import_layer_png(
30 Tileset &dest,
31 const ArtifactKey &src_key,
32 const std::filesystem::path &project_root,
33 const PngRgbaImageLoader &loader,
34 const std::function<void(PorytilesTilesetComponent &, const Image<Rgba32> &)> &layer_img_setter)
35{
36 // Keys are relative to project_root, so prepend for file I/O
37 auto image_result = loader.load_from_file((project_root / src_key.key()).string());
38 if (!image_result.has_value()) {
39 switch (image_result.error().type()) {
40 case ImageLoadError::Type::file_not_found:
41 case ImageLoadError::Type::unsupported_channel_count:
42 case ImageLoadError::Type::other_load_error:
44 FormattableError{"Failed to load layer image '{}'.", FormatParam{src_key.key(), Style::bold}},
45 image_result};
46 default:
47 panic("unhandled ImageLoadError type");
48 }
49 }
50 layer_img_setter(dest.porytiles_component(), *image_result.value());
51 return {};
52}
53
54ChainableResult<void> import_porytiles_palette(
55 Tileset &dest,
56 const ArtifactKey &src_key,
57 std::size_t index,
58 const std::filesystem::path &project_root,
59 const FilePaletteLoader &loader)
60{
61 if (index >= palette::num_palettes) {
62 panic(std::format("invalid palette index {}: out of range", index));
63 }
64
65 // Keys are relative to project_root, so prepend for file I/O
67 palette,
68 loader.load_with_wildcards((project_root / src_key.key()).string()),
69 void,
70 "Failed to load palette file.");
71 dest.porytiles_component().set_palette(index, palette);
72
73 return {};
74}
75
95template <SupportsTransparency PixelType, typename LoaderType, typename ComponentGetter>
96ChainableResult<void> import_anim_frame_impl(
97 Tileset &dest,
98 const ArtifactKey &src_key,
99 const std::filesystem::path &project_root,
100 const std::string &anim_name,
101 const std::string &frame_name,
102 const LoaderType &loader,
103 ComponentGetter component_getter,
104 std::string_view error_context)
105{
106 // Use shared helper to load PNG and extract tiles
107 const auto png_path = project_root / src_key.key();
109 load_result,
110 load_animation_frame_from_png<PixelType>(png_path, frame_name, loader),
111 void,
112 "{}: failed to load {}",
113 FormatParam(src_key.key(), Style::bold),
114 FormatParam(std::string(error_context)));
115
116 // Get or create the animation in the component
117 auto &component = component_getter(dest);
118 if (!component.has_anim(anim_name)) {
119 Animation<PixelType> anim{anim_name};
120 component.add_anim(std::move(anim));
121 }
122
123 auto &anim = component.anims().at(anim_name);
124
125 if (frame_name == "key") {
126 // Key frame
127 anim.key_frame(std::move(load_result.frame));
128 }
129 else {
130 // Regular frame - use string-based map storage
131 anim.put_frame(frame_name, std::move(load_result.frame));
132 }
133
134 // Update dimensions in params if not already set
135 auto params = anim.params();
136 if (params.width_tiles() == 0 && params.height_tiles() == 0) {
137 params.width_tiles(load_result.width_tiles);
138 params.height_tiles(load_result.height_tiles);
139 anim.params(std::move(params));
140 }
141
142 return {};
143}
144
145} // namespace
146
147namespace porytiles {
148
149// Porymap artifacts
151{
152 // Keys are relative to project_root_, so prepend for file I/O
153 PT_TRY_ASSIGN_PASS_ERR(entries, parse_metatiles_bin(project_root_ / src_key.key()), void);
154 for (auto &entry : entries) {
155 dest.porymap_component().push_back_tilemap_entry(std::move(entry));
156 }
157 return {};
158}
159
162{
163 // Keys are relative to project_root_, so prepend for file I/O
164 const auto path = project_root_ / src_key.key();
166 attributes, parse_metatile_attributes(path, *schema_), void, "Failed to read metatile_attributes.bin.");
167 for (auto &attribute : attributes) {
168 dest.porymap_component().push_back_attribute(std::move(attribute));
169 }
170 return {};
171}
172
174{
175 // Keys are relative to project_root_, so prepend for file I/O
176 PT_TRY_ASSIGN_PASS_ERR(image, load_indexed_png(project_root_ / src_key.key(), *png_indexed_loader_), void);
177 dest.porymap_component().tiles_png(*image);
178 return {};
179}
180
182ProjectTilesetArtifactReader::read_porymap_palette_n(Tileset &dest, const ArtifactKey &src_key, std::size_t index) const
183{
184 if (index >= palette::num_palettes) {
185 panic(std::format("invalid palette index {}: out of range", index));
186 }
187 // Keys are relative to project_root_, so prepend for file I/O
188 PT_TRY_ASSIGN_PASS_ERR(palette, load_porymap_palette(project_root_ / src_key.key(), *palette_loader_), void);
189 dest.porymap_component().set_palette(index, std::move(palette));
190 return {};
191}
192
194 Tileset &dest,
195 const std::string &anim_name,
196 const ArtifactKey &params_key,
197 const std::vector<std::pair<std::string, ArtifactKey>> &frame_keys) const
198{
199 // Load each frame using the unified template helper
200 // The first call creates the animation in the component, subsequent calls add frames to it
201 for (const auto &[frame_name, frame_key] : frame_keys) {
203 (import_anim_frame_impl<IndexPixel>(
204 dest,
205 frame_key,
206 project_root_,
207 anim_name,
208 frame_name,
209 *png_indexed_loader_,
210 [](Tileset &t) -> PorymapTilesetComponent & { return t.porymap_component(); },
211 "Porymap animation frame")),
212 void);
213 }
214
215 // Skip param loading if there is no generated header file present
216 std::filesystem::path params_path = project_root_ / params_key.key();
217 if (!std::filesystem::exists(params_path)) {
218 return {};
219 }
220
221 // Setup callback info, use Porytiles-managed callback regardless of metadata
222 const auto tileset_cased = extract_tileset_cased_name(dest.name());
223 const std::string callback_func = anim::managed_callback_name(dest.name());
224
225 // Parse C code for animation params
227 parsed_params,
228 anim_code_parser_->parse_from_callback(params_path, callback_func, tileset_cased, true),
229 void,
230 "{}: Failed to parse animation parameters.",
231 FormatParam(params_path, Style::bold));
232
233 // Apply params to the specific animation if found
234 if (parsed_params.contains(DynamicCasedName{anim_name})) {
235 auto &anim = dest.porymap_component().anims().at(anim_name);
236 auto existing_params = anim.params();
237 auto new_params = parsed_params.at(DynamicCasedName{anim_name});
238
239 // Preserve dimensions from frame import (C code doesn't have this info)
240 new_params.width_tiles(existing_params.width_tiles());
241 new_params.height_tiles(existing_params.height_tiles());
242
243 anim.params(std::move(new_params));
244 }
245
246 return {};
247}
248
249// Porytiles artifacts
251{
253 import_layer_png(
254 dest,
255 src_key,
256 project_root_,
257 *png_rgba_loader_,
258 [](PorytilesTilesetComponent &comp, const Image<Rgba32> &img) { comp.bottom(img); }),
259 void);
260 return {};
261}
262
264{
266 import_layer_png(
267 dest,
268 src_key,
269 project_root_,
270 *png_rgba_loader_,
271 [](PorytilesTilesetComponent &comp, const Image<Rgba32> &img) { comp.middle(img); }),
272 void);
273 return {};
274}
275
277{
279 import_layer_png(
280 dest,
281 src_key,
282 project_root_,
283 *png_rgba_loader_,
284 [](PorytilesTilesetComponent &comp, const Image<Rgba32> &img) { comp.top(img); }),
285 void);
286 return {};
287}
288
290{
291 // The CSV still loads using the owning tileset's name. The schema is supposed to be project-global, but per-tileset
292 // formatting config like role_pins resolves at that tileset's config scope.
294 load_result,
295 attributes_csv_loader_->load((project_root_ / src_key.key()).string(), *schema_, *provider_map_, dest.name()),
296 void);
297 for (const auto &[metatile_id, attribute] : load_result.attributes) {
298 dest.porytiles_component().insert_attribute(metatile_id, attribute);
299 }
300 // Record, per role, whether the active pin column was present. The decompiler's round-trip merge reads this to
301 // decide between preserving prior pin state (column present) and pinning every row (column absent).
302 for (const auto &[role, present] : load_result.active_pin_column_present) {
305 }
306 return {};
307}
308
310 Tileset &dest, const ArtifactKey &src_key, std::size_t index) const
311{
312 PT_TRY_CALL_PASS_ERR(import_porytiles_palette(dest, src_key, index, project_root_, *palette_loader_), void);
313 return {};
314}
315
317 Tileset &dest,
318 const std::string &anim_name,
319 const ArtifactKey &params_key,
320 const std::optional<ArtifactKey> &key_frame_key,
321 const std::vector<std::pair<std::string, ArtifactKey>> &frame_keys) const
322{
323 // Parse anim.json to get params for this animation
324 // Keys are relative to project_root_, so prepend for file I/O
326 parsed_anim_params,
327 anim_json_parser_->parse((project_root_ / params_key.key()).string()),
328 void,
329 "{}: Failed to parse animation parameters.",
330 FormatParam(project_root_ / params_key.key(), Style::bold));
331
332 // Find params for this specific animation
333 auto it = parsed_anim_params.find(DynamicCasedName{anim_name});
334 if (it == parsed_anim_params.end()) {
336 "{}: Animation '{}' not found in animation parameters file.",
337 FormatParam{project_root_ / params_key.key(), Style::bold},
338 FormatParam{anim_name, Style::bold}}};
339 }
340
341 const auto &params = it->second;
342
343 // Create the animation with params FIRST, before loading frames
344 // This ensures YAML-specified width_tiles/height_tiles take precedence over auto-detected dimensions
345 Animation<Rgba32> anim{anim_name, params};
346 dest.porytiles_component().add_anim(std::move(anim));
347
348 // Load key frame using the unified template helper (only present for automatic/hybrid frame linking)
349 if (key_frame_key.has_value()) {
351 (import_anim_frame_impl<Rgba32>(
352 dest,
353 key_frame_key.value(),
354 project_root_,
355 anim_name,
356 "key",
357 *png_rgba_loader_,
358 [](Tileset &t) -> PorytilesTilesetComponent & { return t.porytiles_component(); },
359 "Porytiles animation frame")),
360 void);
361 }
362
363 // Load remaining frames using the unified template helper
364 for (const auto &[frame_name, frame_key] : frame_keys) {
366 (import_anim_frame_impl<Rgba32>(
367 dest,
368 frame_key,
369 project_root_,
370 anim_name,
371 frame_name,
372 *png_rgba_loader_,
373 [](Tileset &t) -> PorytilesTilesetComponent & { return t.porytiles_component(); },
374 "Porytiles animation frame")),
375 void);
376 }
377
378 return {};
379}
380
381[[nodiscard]] ChainableResult<void>
383{
384 const auto json_path = project_root_ / params_key.key();
385
387 parsed_refs,
388 anim_json_parser_->parse_primary_references(json_path),
389 void,
390 "{}: Failed to parse primary animation references.",
391 FormatParam(json_path, Style::bold));
392
393 if (parsed_refs.empty()) {
394 return {};
395 }
396
397 std::map<std::string, std::vector<AnimOverrideEntry>> converted;
398 for (auto &[cased_name, entries] : parsed_refs) {
399 converted[cased_name.to_snake_case()] = std::move(entries);
400 }
401
402 dest.porytiles_component().primary_anim_overrides(std::move(converted));
403 return {};
404}
405
406} // namespace porytiles
Shared helper for loading animation frames from PNG files.
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
#define PT_TRY_CALL_PASS_ERR(expr, return_type)
Unwraps a void ChainableResult, passing through the error chain with an empty FormattableError when t...
#define PT_TRY_ASSIGN_PASS_ERR(var, expr, return_type)
Unwraps a ChainableResult, passing through the error chain with an empty FormattableError when types ...
ChainableResult< std::map< DynamicCasedName, AnimParams > > parse_from_callback(const std::filesystem::path &c_file_path, const std::string &callback_func_name, const DynamicCasedName &tileset_cased_name, bool porytiles_managed) const
Parses animation parameters by following the callback chain.
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.
A complete tileset animation with name, configuration, and frame data.
Definition animation.hpp:89
const AnimFrame< PixelType > & key_frame() const
Returns the key frame of this animation.
A type-safe wrapper for artifact keys.
const std::string & key() const
ChainableResult< AttributesCsvLoadResult > load(const std::filesystem::path &path, const Schema &schema, const ProviderMap &providers, const std::string &tileset_name) const
Loads metatile attributes from a CSV file.
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.
A service interface that loads a fixed-length Palette from a given file.
virtual ChainableResult< Palette< Rgba32, palette::max_size > > load_with_wildcards(const std::filesystem::path &path) const =0
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
A template for two-dimensional images with arbitrarily typed pixel values.
Definition image.hpp:21
An image loader that reads PNG files to create an Image with an Rgba32 pixel type.
ChainableResult< std::unique_ptr< Image< Rgba32 > >, ImageLoadError > load_from_file(const std::filesystem::path &path) const
const std::map< std::string, Animation< IndexPixel > > & anims() const
void set_palette(std::size_t palette_index, Palette< Rgba32, palette::max_size > palette)
void push_back_tilemap_entry(TilemapEntry entry)
Add a TilemapEntry to the end of the entry vector.
void push_back_attribute(MetatileAttribute attribute)
Add a MetatileAttribute to the end of the attribute vector.
const Image< IndexPixel > & tiles_png() const
void set_palette(std::size_t palette_index, Palette< Rgba32, palette::max_size > palette)
PriorPinColumnState prior_pin_column_state(FieldRole role) const
Returns the recorded prior pin-column state for a role, or no_csv when none was recorded.
void insert_attribute(std::size_t metatile_id, MetatileAttribute attribute)
Insert a MetatileAttribute to the end of the attribute vector.
const std::map< std::string, std::vector< AnimOverrideEntry > > & primary_anim_overrides() const
ChainableResult< void > read_metatile_attributes_bin(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_top_png(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_porymap_palette_n(Tileset &dest, const ArtifactKey &src_key, std::size_t index) const override
ChainableResult< void > read_attributes_csv(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_middle_png(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_porytiles_anim(Tileset &dest, const std::string &anim_name, const ArtifactKey &params_key, const std::optional< ArtifactKey > &key_frame_key, const std::vector< std::pair< std::string, ArtifactKey > > &frame_keys) const override
ChainableResult< void > read_bottom_png(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_porymap_anim(Tileset &dest, const std::string &anim_name, const ArtifactKey &params_key, const std::vector< std::pair< std::string, ArtifactKey > > &frame_keys) const override
ChainableResult< void > read_porytiles_primary_anim_references(Tileset &dest, const ArtifactKey &params_key) const override
ChainableResult< void > read_tiles_png(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_metatiles_bin(Tileset &dest, const ArtifactKey &src_key) const override
ChainableResult< void > read_porytiles_palette_n(Tileset &dest, const ArtifactKey &src_key, std::size_t index) const override
static const Style bold
Bold text formatting.
A complete tileset containing both Porytiles and Porymap components.
Definition tileset.hpp:12
const PorytilesTilesetComponent & porytiles_component() const
Definition tileset.hpp:35
const PorymapTilesetComponent & porymap_component() const
Definition tileset.hpp:45
const std::string & name() const
Definition tileset.hpp:30
std::string managed_callback_name(const std::string &tileset_name)
Definition animation.hpp:32
constexpr std::size_t num_palettes
Definition palette.hpp:21
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
ChainableResult< Palette< Rgba32, palette::max_size > > load_porymap_palette(const std::filesystem::path &path, const FilePaletteLoader &loader)
Loads a Porymap palette file (e.g., 00.pal).
DynamicCasedName extract_tileset_cased_name(const std::string &tileset_name)
Extracts the tileset short name and wraps it in a DynamicCasedName.
ChainableResult< std::vector< TilemapEntry > > parse_metatiles_bin(const std::filesystem::path &path)
Parses a metatiles.bin file into TilemapEntry objects.
ChainableResult< std::vector< MetatileAttribute > > parse_metatile_attributes(const std::filesystem::path &path, const Schema &schema)
Parses a metatile_attributes.bin file according to a metatile attribute schema.
ChainableResult< std::unique_ptr< Image< IndexPixel > > > load_indexed_png(const std::filesystem::path &path, const PngIndexedImageLoader &loader)
Loads an indexed PNG file (e.g., tiles.png).
Utility functions for string manipulation and formatting.