Porytiles
Loading...
Searching...
No Matches
project_tileset_artifact_writer.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <iostream>
7#include <random>
8#include <ranges>
9#include <sstream>
10#include <string>
11
12#include "fmt/format.h"
13
19
20namespace {
21
22using namespace porytiles2;
23
24std::filesystem::path create_tmpdir()
25{
26 int maxTries = 1000;
27 auto tmpDir = std::filesystem::temp_directory_path();
28 int i = 0;
29 std::random_device randomDevice;
30 std::mt19937 mersennePrng(randomDevice());
31 std::uniform_int_distribution<uint64_t> uniformIntDistribution(0);
32 std::filesystem::path path;
33 while (true) {
34 std::stringstream stringStream;
35 stringStream << std::hex << uniformIntDistribution(mersennePrng);
36 path = tmpDir / ("porytiles_" + stringStream.str());
37 if (std::filesystem::create_directory(path)) {
38 break;
39 }
40 if (i == maxTries) {
41 panic("tmpfiles::createTmpdir getTmpdirPath took too many tries");
42 }
43 i++;
44 }
45 return path;
46}
47
49save_layer_png(const PngRgbaImageSaver &saver, const Image<Rgba32> &layer_png, const std::filesystem::path &path)
50{
51 auto result = saver.save_to_file(layer_png, path);
52 if (!result.has_value()) {
53 return result;
54 }
55 return {};
56}
57
58ChainableResult<void> save_tiles_png(
59 const PngIndexedImageSaver &saver,
60 const Image<IndexPixel> &tiles_png,
61 const std::filesystem::path &path,
62 TilesPalMode tiles_pal_mode)
63{
64 auto result = saver.save_to_file(tiles_png, path, tiles_pal_mode);
65 if (!result.has_value()) {
66 return result;
67 }
68 return {};
69}
70
71ChainableResult<void> save_metatiles_bin(const std::vector<TilemapEntry> &entries, const std::filesystem::path &path)
72{
73 std::ofstream out{path};
74 for (const auto &entry : entries) {
75 // TODO: does this code work as expected on a big-endian machine?
76 const auto tile_value = static_cast<uint16_t>(
77 (entry.tile_index() & 0x3ff) | ((entry.hflip() & 1) << 10) | ((entry.vflip() & 1) << 11) |
78 ((entry.pal_index() & 0xf) << 12));
79 out << static_cast<std::uint8_t>(tile_value);
80 out << static_cast<std::uint8_t>(tile_value >> 8);
81 }
82 out.flush();
83 return {};
84}
85
87save_metatile_attributes_bin(const std::vector<MetatileAttribute> &attributes, const std::filesystem::path &path)
88{
89 // TODO: will need different handling for firered attrs
90 std::ofstream out{path};
91 for (const auto &attribute : attributes) {
92 const std::uint16_t behavior = attribute.behavior();
93 const auto layer_type = static_cast<std::uint8_t>(attribute.layer_type());
94 // TODO: does this code work as expected on a big-endian machine?
95 const auto attribute_value = static_cast<std::uint16_t>((behavior & 0xff) | ((layer_type & 0xf) << 12));
96 out << static_cast<std::uint8_t>(attribute_value);
97 out << static_cast<std::uint8_t>(attribute_value >> 8);
98 }
99 out.flush();
100 return {};
101}
102
104save_palette(const Palette<Rgba32> &pal, const std::filesystem::path &path, const FilePalSaver &saver)
105{
106 const auto save_result = saver.save(pal, path);
107 if (!save_result.has_value()) {
108 return ChainableResult<void>{FormattableError{fmt::format("{}: failed to save", path.c_str())}, save_result};
109 }
110 return {};
111}
112
113} // namespace
114
115namespace porytiles2 {
116
118{
119 if (!transaction_root_.empty()) {
120 return std::unexpected{"transaction already in progress"};
121 }
122 transaction_root_ = create_tmpdir();
123
124 return {};
125}
126
128{
129 if (transaction_root_.empty()) {
130 return FormattableError{"no transaction in progress"};
131 }
132
133 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> backed_up_files;
134 std::vector<std::filesystem::path> new_files;
135
136 const auto backup_root = create_tmpdir();
137 try {
138 // Phase 1: Collect all source files and their destinations
139 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> files_to_copy;
140 for (const auto &entry : std::filesystem::recursive_directory_iterator(transaction_root_)) {
141 if (entry.is_regular_file()) {
142 auto relative_path = std::filesystem::relative(entry.path(), transaction_root_);
143 auto dest_path = project_root_ / relative_path;
144 files_to_copy.emplace_back(entry.path(), dest_path);
145 }
146 }
147
148 // Phase 2: Backup existing files that will be overwritten
149 for (const auto &dest : files_to_copy | std::views::values) {
150 if (std::filesystem::exists(dest)) {
151 auto backup_relative = std::filesystem::relative(dest, project_root_);
152 auto backup_path = backup_root / backup_relative;
153
154 // Create backup directory structure
155 std::filesystem::create_directories(backup_path.parent_path());
156
157 // Copy existing file to backup
158 std::filesystem::copy_file(dest, backup_path);
159 backed_up_files.emplace_back(dest, backup_path);
160 }
161 }
162
163 // Phase 3: Copy all new files to their destinations
164 for (const auto &[src, dest] : files_to_copy) {
165 // Create parent directories if needed
166 std::filesystem::create_directories(dest.parent_path());
167
168 // Track whether this is a new file (not an overwrite)
169 const bool is_new_file = !std::filesystem::exists(dest);
170
171 // Copy the file (overwrite if exists)
172 std::filesystem::copy_file(src, dest, std::filesystem::copy_options::overwrite_existing);
173
174 if (is_new_file) {
175 new_files.push_back(dest);
176 }
177 }
178
179 // Phase 4: Success - clean up transaction and backup directories
180 std::filesystem::remove_all(transaction_root_);
181 std::filesystem::remove_all(backup_root);
182 transaction_root_.clear();
183
184 return {};
185 }
186 catch (const std::filesystem::filesystem_error &e) {
187 // Phase 5: Error occurred - restore backups and clean up new files
188 try {
189 // Restore backed up files
190 for (const auto &[original_path, backup_path] : backed_up_files) {
191 if (std::filesystem::exists(backup_path)) {
192 std::filesystem::copy_file(
193 backup_path, original_path, std::filesystem::copy_options::overwrite_existing);
194 }
195 }
196
197 // Remove any new files that were created
198 for (const auto &new_file : new_files) {
199 if (std::filesystem::exists(new_file)) {
200 std::filesystem::remove(new_file);
201 }
202 }
203 }
204 catch (const std::filesystem::filesystem_error &restore_error) {
205 // Critical error during restore - log but continue cleanup
206 // TODO: emit a diagnostic here?
207 }
208
209 // Clean up temporary directories
210 if (std::filesystem::exists(backup_root)) {
211 std::filesystem::remove_all(backup_root);
212 }
213 if (std::filesystem::exists(transaction_root_)) {
214 std::filesystem::remove_all(transaction_root_);
215 }
216 transaction_root_.clear();
217
218 return FormattableError{"failed to commit transaction: {}", FormatParam{e.what()}};
219 }
220}
221
223{
224 if (transaction_root_.empty()) {
225 return std::unexpected{"no transaction in progress"};
226 }
227
228 try {
229 if (std::filesystem::exists(transaction_root_)) {
230 std::filesystem::remove_all(transaction_root_);
231 }
232 transaction_root_.clear();
233 return {};
234 }
235 catch (const std::filesystem::filesystem_error &e) {
236 transaction_root_.clear();
237 return std::unexpected{fmt::format("failed to rollback transaction: {}", e.what())};
238 }
239}
240
242ProjectTilesetArtifactWriter::write(const ArtifactKey &dest_key, const TilesetArtifact &artifact, const Tileset &src)
243{
244 if (transaction_root_.empty()) {
245 return FormattableError{"no transaction in progress"};
246 }
247
248 // Compute the destination path within the transaction directory
249 const auto relative_path = std::filesystem::path{dest_key.key()}.lexically_relative(project_root_);
250 const auto transaction_dest_path = transaction_root_ / relative_path;
251
252 // Create parent directories if needed
253 std::filesystem::create_directories(transaction_dest_path.parent_path());
254
255 // Handle different artifact types
256 switch (artifact.type()) {
257 // Porytiles artifacts
259 return save_layer_png(*png_rgba_saver_, src.porytiles_component().bottom(), transaction_dest_path);
261 return save_layer_png(*png_rgba_saver_, src.porytiles_component().middle(), transaction_dest_path);
263 return save_layer_png(*png_rgba_saver_, src.porytiles_component().top(), transaction_dest_path);
265 // TODO: implement
266 return {};
268 // TODO: implement
269 return {};
271 // TODO: implement
272 return {};
273
274 // Porymap artifacts
276 return save_metatiles_bin(src.porymap_component().metatiles_bin(), transaction_dest_path);
278 return save_metatile_attributes_bin(src.porymap_component().metatile_attributes_bin(), transaction_dest_path);
281 tiles_pal_mode_config, config_->tiles_pal_mode(src.name()), "failed to get tiles_pal_mode config", void);
282 return save_tiles_png(
283 *png_indexed_saver_,
285 transaction_dest_path,
286 tiles_pal_mode_config.value());
287 }
289 // TODO: implement
290 return {};
292 if (!artifact.index().has_value()) {
293 panic("took TilesetArtifact::Type::pal_n branch but missing pal index");
294 }
295 const auto index = artifact.index().value();
296 return save_palette(src.porymap_component().pal_at(index), transaction_dest_path, *pal_saver_);
297 }
298
299 // Default case
300 default:
301 panic("unhandled TilesetArtifact::Type");
302 }
303}
304
305} // namespace porytiles2
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, msg, return_type)
Unwraps a ChainableResult, chaining a new error message on failure.
A type-safe wrapper for artifact keys.
const std::string & key() const
Gets the underlying string value.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A service interface that saves a Palette to a given file.
virtual ChainableResult< void > save(const Palette< Rgba32 > &pal, 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:117
A template for two-dimensional images with arbitrarily typed pixel values.
Definition image.hpp:24
ChainableResult< ConfigValue< TilesPalMode > > tiles_pal_mode(const std::string &tileset) const
A palette container for colors that support transparency checking.
Definition palette.hpp:34
An image saver that saves PNG files from an Image with an index pixel type.
virtual ChainableResult< void > save_to_file(const Image< IndexPixel > &image, const std::filesystem::path &path, TilesPalMode mode) const
An image saver that saves PNG files from an Image with an Rgba32 pixel type.
virtual ChainableResult< void > save_to_file(const Image< Rgba32 > &image, const std::filesystem::path &path) const
const std::vector< TilemapEntry > & metatiles_bin() const
const Image< IndexPixel > & tiles_png() const
const Palette< Rgba32 > & pal_at(unsigned int pal_index) const
const std::vector< MetatileAttribute > & metatile_attributes_bin() const
Result< void > rollback() override
Rolls back all buffered write operations in the current transaction.
ChainableResult< void > write(const ArtifactKey &dest_key, const TilesetArtifact &artifact, const Tileset &src) override
Writes an artifact from a Tileset to the backing store.
ChainableResult< void > commit() override
Commits all buffered write operations in the current transaction.
Result< void > begin_transaction() override
Begins a new transaction for atomic write operations.
Represents a Pokémon Generation III decomp tileset artifact with type and optional metadata.
Type type() const
Gets the artifact type.
@ porytiles_anim_frame
Animation frame PNG for Porytiles-format animation.
@ attributes_csv
CSV file containing metatile attribute overrides.
@ middle_png
Middle layer PNG input image.
@ metatile_attributes_bin
Metatile attributes output for Porymap.
@ bottom_png
Bottom layer PNG input image.
@ top_png
Top layer PNG input image.
@ pal_n
JASC palette data file.
@ metatiles_bin
Metatile data output for Porymap.
@ pal_override_n
JASC palette override file.
@ tiles_png
Combined tile sheet PNG output for Porymap.
@ porymap_anim_frame
Animation frame PNG for Porymap-format animation.
std::optional< unsigned int > index() const
Gets the artifact index if present.
A complete tileset containing both Porytiles and Porymap components.
Definition tileset.hpp:14
const PorytilesTilesetComponent & porytiles_component() const
Definition tileset.hpp:37
const PorymapTilesetComponent & porymap_component() const
Definition tileset.hpp:47
const std::string & name() const
Definition tileset.hpp:32
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.hpp:53
std::expected< T, E > Result
A result with some type T on success, otherwise an error of type E.
Definition result.hpp:25