yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
catalog_object_transfer.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <map>
8#include <set>
9#include <utility>
10#include <vector>
11
12#include "absl/status/status.h"
16#include "zelda3/game_data.h"
18
19namespace yaze::editor {
20namespace {
21
22using TileKey = std::pair<uint16_t, uint16_t>;
23using TilePixels = std::array<uint8_t, 64>;
24constexpr int kTileSize = 8;
26constexpr int kTilesPerRow = kSheetWidth / kTileSize;
28constexpr int kMaxDimension = 2048;
29constexpr size_t kMaxPixels = 1024 * 1024;
30
31bool ValidDimensions(int width, int height) {
32 return width > 0 && height > 0 && width <= kMaxDimension &&
33 height <= kMaxDimension && width % kTileSize == 0 &&
34 height % kTileSize == 0 &&
35 static_cast<size_t>(width) * height <= kMaxPixels;
36}
37
38size_t SheetOffset(uint16_t tile, int x, int y) {
39 return (tile / kTilesPerRow * kTileSize + y) * kSheetWidth +
40 tile % kTilesPerRow * kTileSize + x;
41}
42
43} // namespace
44
45absl::StatusOr<PixelIndexPlan> PlanCatalogObjectTransfer(
46 const GraphicsEditorState& state, const CatalogAssetPreview& asset,
47 const CatalogArtRect& crop) {
48 const auto* store = ActiveSheetStore(state);
49 if (!store || !state.source_context ||
50 state.source_context->owner != state.game_data ||
51 !state.source_context->resolve_live_source) {
52 return absl::FailedPreconditionError(
53 "Open Graphics from a supported object in this session");
54 }
55 const auto& source = *state.source_context;
56 if (!ValidDimensions(source.pattern_width, source.pattern_height) ||
57 source.pattern_cells.empty()) {
58 return absl::FailedPreconditionError(
59 "The object's source pattern geometry is unavailable");
60 }
61 if (asset.bpp != 3 || !ValidDimensions(asset.width, asset.height) ||
62 asset.indexed_pixels.size() !=
63 static_cast<size_t>(asset.width) * asset.height ||
64 std::any_of(asset.indexed_pixels.begin(), asset.indexed_pixels.end(),
65 [](uint8_t pixel) { return pixel > 7; })) {
66 return absl::InvalidArgumentError(
67 "Object artwork requires a complete 3bpp image with indices 0..7");
68 }
69 if (crop.x < 0 || crop.y < 0 || !ValidDimensions(crop.width, crop.height) ||
70 crop.x % kTileSize || crop.y % kTileSize || crop.width > asset.width ||
71 crop.height > asset.height || crop.x > asset.width - crop.width ||
72 crop.y > asset.height - crop.height) {
73 return absl::InvalidArgumentError(
74 "Select a tile-aligned crop inside the source artwork");
75 }
76 if (crop.width > source.pattern_width ||
77 crop.height > source.pattern_height) {
78 return absl::InvalidArgumentError(
79 "Selected artwork exceeds the object's source pattern");
80 }
81
82 std::map<std::pair<int, int>, const GraphicsSourceCell*> cells;
83 for (const auto& cell : source.pattern_cells) {
84 if (cell.x < 0 || cell.y < 0 || cell.x >= source.pattern_width / 8 ||
85 cell.y >= source.pattern_height / 8 ||
86 cell.target >= source.targets.size() ||
87 !cells.emplace(std::make_pair(cell.x, cell.y), &cell).second) {
88 return absl::FailedPreconditionError(
89 "The object's source pattern has invalid or ambiguous cells");
90 }
91 const auto& target = source.targets[cell.target];
92 if (target.tile_index >= kTilesPerSheet ||
93 !store->HasSheet(target.sheet_id)) {
94 return absl::FailedPreconditionError(
95 "An object source tile is unavailable in this session");
96 }
97 }
98
99 for (int y = 0; y < crop.height; ++y) {
100 for (int x = 0; x < crop.width; ++x) {
101 if (asset.indexed_pixels[(crop.y + y) * asset.width + crop.x + x] != 0 &&
102 !cells.contains({x / 8, y / 8})) {
103 return absl::FailedPreconditionError(
104 "Selected artwork needs a tile in a hole of the object pattern");
105 }
106 }
107 }
108
109 // Constraints include unselected pattern cells. Their original physical
110 // glyph must survive when it aliases a tile selected by a smaller crop.
111 std::map<TileKey, TilePixels> demands;
112 std::set<TileKey> selected;
113 for (const auto& [position, cell] : cells) {
114 const auto& target = source.targets[cell->target];
115 const TileKey key{target.sheet_id, target.tile_index};
116 const bool inside = cell->x < crop.width / 8 && cell->y < crop.height / 8;
117 TilePixels desired;
118 const auto before = store->Sheet(target.sheet_id);
119 for (int y = 0; y < kTileSize; ++y) {
120 for (int x = 0; x < kTileSize; ++x) {
121 if (inside) {
122 const int raw_x = cell->hflip ? 7 - x : x;
123 const int raw_y = cell->vflip ? 7 - y : y;
124 desired[raw_y * 8 + raw_x] =
125 asset.indexed_pixels[(crop.y + cell->y * 8 + y) * asset.width +
126 crop.x + cell->x * 8 + x];
127 } else {
128 desired[y * 8 + x] = before[SheetOffset(target.tile_index, x, y)];
129 }
130 }
131 }
132 const auto [prior, inserted] = demands.emplace(key, desired);
133 if (!inserted && prior->second != desired) {
134 return absl::FailedPreconditionError(
135 "Selected artwork conflicts with another use of a shared source "
136 "tile in this object");
137 }
138 if (inside) {
139 selected.insert(key);
140 }
141 }
142 if (selected.empty()) {
143 return absl::FailedPreconditionError(
144 "Selected artwork does not address an object source tile");
145 }
146
147 std::map<uint16_t, std::vector<uint8_t>> after_sheets;
148 std::vector<PixelIndexTarget> targets;
149 for (const auto& [sheet, tile] : selected) {
150 if (!after_sheets.contains(sheet)) {
151 const auto before = store->Sheet(sheet);
152 after_sheets.emplace(sheet,
153 std::vector<uint8_t>(before.begin(), before.end()));
154 }
155 const auto& desired = demands.at({sheet, tile});
156 auto& after = after_sheets.at(sheet);
157 for (int y = 0; y < kTileSize; ++y) {
158 for (int x = 0; x < kTileSize; ++x) {
159 after[SheetOffset(tile, x, y)] = desired[y * 8 + x];
160 }
161 }
162 targets.push_back({sheet, tile});
163 }
164 return PlanPixelIndexReplacement(state, targets, after_sheets,
165 "Apply catalog art to object source");
166}
167
168} // namespace yaze::editor
Shared state between GraphicsEditor panel components.
std::optional< GraphicsEditContext > source_context
One ROM session's graphics sheet pixels, with a revision per sheet.
The Graphics editor's path for sheet pixel writes.
Editors are the view controllers for the application.
zelda3::GraphicsSheetStore * ActiveSheetStore(const GraphicsEditorState &state)
absl::StatusOr< PixelIndexPlan > PlanPixelIndexReplacement(const GraphicsEditorState &state, const std::vector< PixelIndexTarget > &targets, const std::map< uint16_t, std::vector< uint8_t > > &after_sheets, std::string description)
absl::StatusOr< PixelIndexPlan > PlanCatalogObjectTransfer(const GraphicsEditorState &state, const CatalogAssetPreview &asset, const CatalogArtRect &crop)