yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
menu_tilemap.h
Go to the documentation of this file.
1#ifndef YAZE_APP_ZELDA3_SCREEN_MENU_TILEMAP_H
2#define YAZE_APP_ZELDA3_SCREEN_MENU_TILEMAP_H
3
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <functional>
8#include <string>
9#include <vector>
10
11#include "absl/status/status.h"
12#include "absl/status/statusor.h"
15
16namespace yaze::zelda3 {
17
18// Core (no-ImGui) model for a 2bpp SNES BG tilemap used by Oracle of
19// Secrets' custom menu system (BG3). Files are little-endian arrays of
20// 16-bit "vhopppcc cccccccc" words -- see gfx::TileInfo / WordToTileInfo /
21// TileInfoToWord in app/gfx/types/snes_tile.h, which this class reuses
22// directly for the per-cell codec.
23//
24// Full maps are 2048 bytes (32x32 words). Some Oracle files only cover the
25// rows they actually use: quest_icons.tilemap is 128 bytes (32x2 words),
26// hud.tilemap is 384 bytes (32x6 words). The map is always 32 words wide;
27// row count = size / 64 bytes. Any other size is rejected by
28// ValidateSize()/LoadFromBytes() with a descriptive error, and Save()
29// never changes the file's byte size.
31 public:
32 static constexpr int kCols = 32;
33 static constexpr int kBytesPerRow = kCols * 2; // 64
34 static constexpr int kMaxRows = 32;
35 static constexpr int kMaxBytes = kBytesPerRow * kMaxRows; // 2048
36
37 // A rectangular selection expressed in cell (word) coordinates.
38 struct Rect {
39 int row = 0;
40 int col = 0;
41 int rows = 0;
42 int cols = 0;
43 };
44
45 // A rectangular block of cells captured for copy/paste, independent of
46 // where it came from.
47 struct Clipboard {
48 int rows = 0;
49 int cols = 0;
50 std::vector<gfx::TileInfo> cells; // row-major, rows*cols entries
51 };
52
53 // Renders one 2bpp pixel (0-3) of `tile_id` at local (local_x, local_y)
54 // in [0,8). Implementations should return 0 for tiles outside whatever
55 // CHR source they're backed by rather than throwing/crashing.
56 using ChrPixelFn =
57 std::function<uint8_t(int tile_id, int local_x, int local_y)>;
58
60
61 // Returns OkStatus() iff a buffer of this size would be an accepted
62 // menu tilemap (32-word-wide, whole number of rows, 64 <= size <= 2048).
63 static absl::Status ValidateSize(size_t size);
64
65 // Loads from an in-memory buffer. `source_path` is remembered as the
66 // document's path (used by Save()/Revert()/ExternalChangeDetected());
67 // pass "" to load without an associated file (e.g. for tests).
68 absl::Status LoadFromBytes(std::vector<uint8_t> bytes,
69 std::string source_path = "");
70 absl::Status LoadFromFile(const std::string& path);
71
72 // Writes the current bytes to disk via a temp-file + rename so a reader
73 // never observes a partially-written file. Before the *first* successful
74 // save performed by this document instance, the pristine (pre-edit)
75 // bytes captured at load time are written to "<dest>.bak" (only once;
76 // later saves do not touch the backup). Fails (no file is touched) if
77 // the encoded size would differ from `bytes_.size()` at load time, or if
78 // no path is set (use SaveAs instead).
79 absl::Status Save();
80 absl::Status SaveAs(const std::string& path);
81
82 // Reloads bytes_ from path_, discarding in-memory edits. Clears the
83 // dirty flag; does not touch the backup-written flag.
84 absl::Status Revert();
85
86 // Compares the file at path_ on disk against the content this document
87 // last loaded from or saved to it (never the unsaved in-memory edits).
88 // Returns true iff the bytes differ. Content-based on purpose: size and
89 // mtime cannot see a same-size rewrite inside one timestamp tick, and a
90 // touch that leaves the bytes alone is not a change. Returns an error if
91 // path_ is empty or the file is missing/unreadable.
92 absl::StatusOr<bool> ExternalChangeDetected() const;
93
94 // "Keep my edits" after an external change: adopts the file's current
95 // on-disk content as the baseline (so ExternalChangeDetected() stops
96 // reporting it and the next save's .bak captures what it overwrites)
97 // while keeping the in-memory bytes. dirty() becomes "differs from the
98 // file on disk".
99 absl::Status AcknowledgeExternalChange();
100
101 int rows() const { return rows_; }
102 int cols() const { return kCols; }
103 const std::string& path() const { return path_; }
104 bool dirty() const { return dirty_; }
105 bool loaded() const { return rows_ > 0; }
106 bool has_backup() const { return backup_written_; }
107
108 // Replaces the in-memory bytes (same size only) for undo/redo. Unlike
109 // LoadFromBytes() it keeps the file baseline, backup bookkeeping and path,
110 // and recomputes dirty() as "differs from the file as last loaded/saved".
111 absl::Status RestoreBytes(std::vector<uint8_t> bytes);
112 const std::string& backup_path() const { return backup_path_; }
113 const std::vector<uint8_t>& raw_bytes() const { return bytes_; }
114
115 bool InBounds(int row, int col) const {
116 return row >= 0 && row < rows_ && col >= 0 && col < kCols;
117 }
118
119 // Out-of-bounds reads return a zeroed TileInfo (tile 0, palette 0, no
120 // flags); out-of-bounds writes are a no-op (Set* return false).
121 gfx::TileInfo GetCell(int row, int col) const;
122 uint16_t GetCellWord(int row, int col) const;
123 bool SetCell(int row, int col, gfx::TileInfo info);
124 bool SetCellWord(int row, int col, uint16_t word);
125
126 // All rect operations clip `rect` to the document bounds first, so
127 // callers may pass selections that hang off the edge. Returns the
128 // number of cells actually touched.
129 Clipboard CopyRect(Rect rect) const;
130 int PasteRect(int row, int col, const Clipboard& clip);
131 int FillRect(Rect rect, gfx::TileInfo info);
132 int EraseRect(Rect rect, uint16_t erase_word = 0x0000);
133
134 // Renders to an "indexed" image: width_px = kCols*8 (256), height_px =
135 // rows()*8. Each pixel value is `subpalette*4 + color` in [0,31], where
136 // `color` (0-3) comes from `chr` and `subpalette` (0-7) plus the cell's
137 // H/V flip are applied from the encoded TileInfo. Color 0 within a
138 // subpalette is left as index `subpalette*4` -- see
139 // ComposeMenuTilemapRgba() for how that becomes transparent.
140 std::vector<uint8_t> RenderIndexed(const ChrPixelFn& chr) const;
141 int render_width() const { return kCols * 8; }
142 int render_height() const { return rows_ * 8; }
143
144 private:
145 void ClipRect(Rect& rect) const;
146
147 std::vector<uint8_t> bytes_;
148 // The file's content as last read by Load*() or written by Save*(): the
149 // baseline for dirty() after RestoreBytes(), the source of the one-time
150 // ".bak", and what ExternalChangeDetected() compares the disk against.
151 std::vector<uint8_t> pristine_bytes_;
152 int rows_ = 0;
153 std::string path_;
154 bool dirty_ = false;
155 bool backup_written_ = false;
156 std::string backup_path_;
157};
158
159// Composites an indexed image (as produced by RenderIndexed) into RGBA8.
160// `subpalette_colors[p*4 + c]` is the color for subpalette p, color c;
161// c == 0 in every subpalette is always transparent (alpha 0), matching
162// the SNES BG convention the menu tilemap uses (see
163// docs/public/usage/screen-editor.md for the Oracle-specific +1 CGRAM
164// mapping that produces this table). Output is width*height*4 bytes.
165std::vector<uint8_t> ComposeMenuTilemapRgba(
166 const std::vector<uint8_t>& indexed, int width, int height,
167 const std::array<gfx::SnesColor, 32>& subpalette_colors);
168
169// Builds a ChrPixelFn over an 8bpp-indexed tilesheet buffer laid out the
170// way gfx::SnesTo8bppSheet() produces it: chunks of 128 tiles (16 cols x
171// 8 rows of 8x8 tiles, 8192 bytes/chunk), tile_id / 128 selects the
172// chunk. This matches both zelda3::Load2BppGraphics()'s concatenated
173// 7-sheet buffer and a raw .bin chr file decoded the same way (see
174// ResolveMenuChrFromFile() in menu_tilemap_sources.h). The returned
175// function borrows `sheet_8bpp_indexed` by reference and is only valid
176// while that vector is alive.
178 const std::vector<uint8_t>& sheet_8bpp_indexed);
179
180} // namespace yaze::zelda3
181
182#endif // YAZE_APP_ZELDA3_SCREEN_MENU_TILEMAP_H
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
std::vector< uint8_t > RenderIndexed(const ChrPixelFn &chr) const
absl::Status LoadFromBytes(std::vector< uint8_t > bytes, std::string source_path="")
const std::vector< uint8_t > & raw_bytes() const
absl::StatusOr< bool > ExternalChangeDetected() const
bool SetCellWord(int row, int col, uint16_t word)
static constexpr int kBytesPerRow
bool SetCell(int row, int col, gfx::TileInfo info)
std::function< uint8_t(int tile_id, int local_x, int local_y)> ChrPixelFn
static constexpr int kMaxRows
Clipboard CopyRect(Rect rect) const
std::vector< uint8_t > pristine_bytes_
static constexpr int kMaxBytes
absl::Status LoadFromFile(const std::string &path)
std::vector< uint8_t > bytes_
int FillRect(Rect rect, gfx::TileInfo info)
static absl::Status ValidateSize(size_t size)
const std::string & backup_path() const
absl::Status RestoreBytes(std::vector< uint8_t > bytes)
gfx::TileInfo GetCell(int row, int col) const
uint16_t GetCellWord(int row, int col) const
int EraseRect(Rect rect, uint16_t erase_word=0x0000)
bool InBounds(int row, int col) const
int PasteRect(int row, int col, const Clipboard &clip)
absl::Status SaveAs(const std::string &path)
void ClipRect(Rect &rect) const
const std::string & path() const
Zelda 3 specific classes and functions.
MenuTilemapDocument::ChrPixelFn MakeChrPixelFn(const std::vector< uint8_t > &sheet_8bpp_indexed)
std::vector< uint8_t > ComposeMenuTilemapRgba(const std::vector< uint8_t > &indexed, int width, int height, const std::array< gfx::SnesColor, 32 > &subpalette_colors)