9#include "absl/status/status.h"
10#include "absl/strings/numbers.h"
11#include "absl/strings/str_format.h"
25#ifdef YAZE_CLI_HAS_PNG
36 if (!std::isfinite(scale) || scale < 0.25f || scale > 8.0f) {
37 return absl::InvalidArgumentError(
38 "Render scale must be finite and between 0.25 and 8.0");
40 return absl::OkStatus();
43#ifdef YAZE_CLI_HAS_PNG
46 std::vector<uint8_t>* buf;
49void PngWrite(png_structp png, png_bytep data, png_size_t len) {
50 auto* ctx =
static_cast<PngCtx*
>(png_get_io_ptr(png));
51 ctx->buf->insert(ctx->buf->end(), data, data + len);
54void PngFlush(png_structp ) {}
66 if (tile == 0xB0 || tile == 0xB1)
67 return {0, 210, 170, 150};
68 if (tile >= 0xB2 && tile <= 0xB5)
69 return {0, 190, 255, 150};
71 return {255, 215, 0, 170};
72 if (tile >= 0xB7 && tile <= 0xBA)
73 return {255, 80, 0, 200};
74 if (tile >= 0xBB && tile <= 0xBE)
75 return {80, 150, 255, 150};
76 if (tile >= 0xD0 && tile <= 0xD3)
77 return {210, 0, 255, 170};
78 return {140, 140, 140, 120};
83 static const uint8_t kDigits[16][5] = {
84 {7, 5, 5, 5, 7}, {2, 6, 2, 2, 7}, {7, 1, 7, 4, 7}, {7, 1, 7, 1, 7},
85 {5, 5, 7, 1, 1}, {7, 4, 7, 1, 7}, {7, 4, 7, 5, 7}, {7, 1, 1, 2, 2},
86 {7, 5, 7, 5, 7}, {7, 5, 7, 1, 7}, {2, 5, 7, 5, 5}, {6, 5, 6, 5, 6},
87 {3, 4, 4, 4, 3}, {6, 5, 5, 5, 6}, {7, 4, 6, 4, 7}, {7, 4, 6, 4, 4}};
88 static const uint8_t kX[5] = {5, 5, 2, 5, 5};
89 static const uint8_t kH[5] = {5, 5, 7, 5, 5};
90 static const uint8_t kI[5] = {7, 2, 2, 2, 7};
91 static const uint8_t kColon[5] = {0, 2, 0, 2, 0};
92 if (c >=
'0' && c <=
'9')
93 return kDigits[c -
'0'];
94 if (c >=
'A' && c <=
'F')
95 return kDigits[10 + (c -
'A')];
109 const std::string& text, uint8_t r, uint8_t g, uint8_t b) {
110 constexpr float kPx = 2.0f;
111 const float w =
static_cast<float>(text.size()) * 4 * kPx + kPx;
112 const float h = 7 * kPx;
114 for (
size_t i = 0; i < text.size(); ++i) {
115 const uint8_t* glyph =
GlyphFor(text[i]);
118 const float gx = x + kPx +
static_cast<float>(i) * 4 * kPx;
119 for (
int row = 0; row < 5; ++row) {
120 for (
int col = 0; col < 3; ++col) {
121 if (glyph[row] & (4 >> col)) {
122 draw.
DrawFilledRect(gx + col * kPx, y + kPx + row * kPx, kPx, kPx, r,
137 return {80, 255, 120};
139 return {255, 80, 80};
141 return {90, 170, 255};
149 if (!absl::SimpleAtof(value, &scale)) {
150 return absl::InvalidArgumentError(
151 absl::StrFormat(
"Invalid scale value: %s", value));
153 if (
const auto status = ValidateRenderScale(scale); !status.ok()) {
160 : rom_(rom), game_data_(game_data) {}
164 if (
const auto status = ValidateRenderScale(req.
scale); !status.ok()) {
168 return absl::FailedPreconditionError(
"ROM not loaded");
171 return absl::FailedPreconditionError(
"GameData not available");
174 return absl::InvalidArgumentError(
175 absl::StrFormat(
"Invalid room_id 0x%02X", req.
room_id));
178#ifndef YAZE_CLI_HAS_PNG
179 return absl::UnimplementedError(
"PNG encoding unavailable (libpng missing)");
182 std::lock_guard<std::mutex> lock(
mu_);
205 if (!composite.is_active() || composite.width() <= 0 ||
206 composite.height() <= 0) {
207 return absl::InternalError(
"Composite bitmap is empty after render");
210 const int out_w =
static_cast<int>(composite.width() * req.
scale);
211 const int out_h =
static_cast<int>(composite.height() * req.
scale);
216 return rgba_or.status();
217 auto rgba = std::move(rgba_or).value();
225 auto png_or =
EncodePng(rgba, out_w, out_h);
227 return png_or.status();
230 result.
png_data = std::move(png_or).value();
231 result.
width = out_w;
238 if (
const auto status = ValidateRenderScale(req.
scale); !status.ok()) {
242 return absl::FailedPreconditionError(
"ROM not loaded");
245 return absl::FailedPreconditionError(
"GameData not available");
248 return absl::InvalidArgumentError(
249 absl::StrFormat(
"Invalid overworld screen 0x%02X", req.
screen_id));
252 return absl::InvalidArgumentError(
"Sprite phase must be 0, 1, or 2");
255#ifndef YAZE_CLI_HAS_PNG
256 return absl::UnimplementedError(
"PNG encoding unavailable (libpng missing)");
259 std::lock_guard<std::mutex> lock(
mu_);
268 if (requested ==
nullptr) {
269 return absl::InternalError(
"Overworld map missing after load");
271 const int parent = requested->parent();
273 if (parent_map ==
nullptr) {
274 return absl::InternalError(
"Overworld parent map missing after load");
279 std::string area_name =
"small";
280 switch (parent_map->area_size()) {
296 if (area_name ==
"small" && parent_map->is_large_map()) {
301 constexpr int kScreen = 512;
302 const int width = cols * kScreen;
303 const int height = rows * kScreen;
304 std::vector<uint8_t> native(
static_cast<size_t>(width) * height * 4, 0);
313 uint8_t& r, uint8_t& g, uint8_t& b) {
315 if (idx < palette.
size()) {
316 const auto rgb = palette[idx].rgb();
317 r =
static_cast<uint8_t
>(rgb.x);
318 g =
static_cast<uint8_t
>(rgb.y);
319 b =
static_cast<uint8_t
>(rgb.z);
323 for (
int dy = 0; dy < rows; ++dy) {
324 for (
int dx = 0; dx < cols; ++dx) {
325 const int screen = parent + dx + dy * 8;
328 std::vector<uint8_t> overlay_pixels;
329 bool overlay_is_background =
false;
333 if (layer.overlay_screen >= 0) {
334 overlay_pixels = std::move(layer.pixels);
335 overlay_is_background = layer.background;
341 if (map ==
nullptr) {
342 return absl::InternalError(
"Overworld child map missing");
344 const auto& pixels = map->bitmap_data();
345 const auto& palette = map->current_palette();
346 if (pixels.size() <
static_cast<size_t>(kScreen) * kScreen) {
347 return absl::InternalError(
348 absl::StrFormat(
"Map 0x%02X has no bitmap after build", screen));
350 result.
screens.push_back(screen);
351 for (
int y = 0; y < kScreen; ++y) {
352 for (
int x = 0; x < kScreen; ++x) {
353 const uint8_t idx = pixels[y * kScreen + x];
354 uint8_t r = 0, g = 0, b = 0;
355 palette_rgb(palette, idx, r, g, b);
356 if (!overlay_pixels.empty()) {
357 const uint8_t oidx = overlay_pixels[y * kScreen + x];
359 uint8_t orr = 0, og = 0, ob = 0;
360 palette_rgb(palette, oidx, orr, og, ob);
361 if (overlay_is_background) {
368 r =
static_cast<uint8_t
>((r + orr) / 2);
369 g =
static_cast<uint8_t
>((g + og) / 2);
370 b =
static_cast<uint8_t
>((b + ob) / 2);
374 const size_t base = (
static_cast<size_t>(dy * kScreen + y) * width +
377 native[base + 0] = r;
378 native[base + 1] = g;
379 native[base + 2] = b;
380 native[base + 3] = 255;
386 const int out_w =
static_cast<int>(width * req.
scale);
387 const int out_h =
static_cast<int>(height * req.
scale);
388 std::vector<uint8_t> rgba(
static_cast<size_t>(out_w) * out_h * 4, 255);
389 for (
int oy = 0; oy < out_h; ++oy) {
390 const int sy = oy * height / out_h;
391 for (
int ox = 0; ox < out_w; ++ox) {
392 const int sx = ox * width / out_w;
393 std::copy_n(native.begin() + (
static_cast<size_t>(sy) * width + sx) * 4,
394 4, rgba.begin() + (
static_cast<size_t>(oy) * out_w + ox) * 4);
400 const int origin_x = (parent % 8) * kScreen;
401 const int origin_y = ((parent % 64) / 8) * kScreen;
402 auto owned = [&](
int map_id) {
404 return map !=
nullptr && map->parent() == parent;
406 auto in_area = [&](
int lx,
int ly) {
407 return lx >= 0 && ly >= 0 && lx < width && ly < height;
411 for (
int x = 0; x <= width; x += 16) {
412 const bool edge = x % kScreen == 0;
413 draw.
DrawLine(
static_cast<float>(x), 0,
static_cast<float>(x),
414 static_cast<float>(height - 1), 255, 255, 255,
417 for (
int y = 0; y <= height; y += 16) {
418 const bool edge = y % kScreen == 0;
419 draw.
DrawLine(0,
static_cast<float>(y),
static_cast<float>(width - 1),
420 static_cast<float>(y), 255, 255, 255, edge ? 180 : 40);
424 auto mark = [&](
const std::string& kind,
int id,
int lx,
int ly,
425 const std::string& label, uint8_t r, uint8_t g, uint8_t b,
426 int phase = -1,
int list_index = -1) {
427 draw.
DrawFilledRect(
static_cast<float>(lx),
static_cast<float>(ly), 16, 16,
429 draw.
DrawRect(
static_cast<float>(lx),
static_cast<float>(ly), 16, 16, r, g,
431 DrawLabel(draw,
static_cast<float>(lx),
static_cast<float>(ly + 17), label,
433 result.
markers.push_back({kind, id, phase, list_index, lx, ly});
437 for (
const auto& item : overworld.
all_items()) {
438 if (item.deleted || !owned(item.room_map_id_))
440 const int lx = item.x_ - origin_x;
441 const int ly = item.y_ - origin_y;
442 if (!in_area(lx, ly))
444 mark(
"item", item.id_, lx, ly, absl::StrFormat(
"I%02X", item.id_), 255,
449 for (
const auto& entrance : overworld.
entrances()) {
450 if (entrance.deleted || !owned(entrance.map_id_))
452 const int lx = entrance.x_ - origin_x;
453 const int ly = entrance.y_ - origin_y;
454 if (!in_area(lx, ly))
456 mark(
"entrance", entrance.entrance_id_, lx, ly,
457 absl::StrFormat(
"E%02X", entrance.entrance_id_), 255, 255, 255);
461 for (
const auto& hole : overworld.
holes()) {
462 if (hole.deleted || !owned(hole.map_id_))
464 const int lx = hole.x_ - origin_x;
465 const int ly = hole.y_ - origin_y;
466 if (!in_area(lx, ly))
468 mark(
"hole", hole.entrance_id_, lx, ly,
469 absl::StrFormat(
"H%02X", hole.entrance_id_), 255, 150, 40);
473 for (
const auto& exit : *overworld.
exits()) {
474 if (exit.deleted_ || !owned(exit.map_id_))
476 const int lx = exit.x_ - origin_x;
477 const int ly = exit.y_ - origin_y;
478 if (!in_area(lx, ly))
480 mark(
"exit", exit.room_id_, lx, ly,
481 absl::StrFormat(
"X%02X", exit.room_id_), 200, 120, 255);
485 for (
int phase = 0; phase < 3; ++phase) {
488 const auto color = SpritePhaseColor(phase);
490 for (
const auto& sprite : overworld.
sprites(phase)) {
491 if (sprite.map_id() != parent)
493 const int index = list_index++;
494 if (sprite.deleted())
498 const int nudge = req.
phase < 0 ? phase * 2 : 0;
499 const int lx = sprite.x() - origin_x + nudge;
500 const int ly = sprite.y() - origin_y + nudge;
501 if (!in_area(lx, ly))
503 const std::string label =
504 req.
phase < 0 ? absl::StrFormat(
"%d:%02X", phase, sprite.id())
505 : absl::StrFormat(
"%02X", sprite.id());
506 mark(
"sprite", sprite.id(), lx, ly, label, color.r, color.g, color.b,
513 result.
width = out_w;
521 return absl::InvalidArgumentError(
522 absl::StrFormat(
"Invalid room_id 0x%02X", room_id));
525 std::lock_guard<std::mutex> lock(
mu_);
539 meta.
tag1 =
static_cast<int>(room.
tag1());
540 meta.
tag2 =
static_cast<int>(room.
tag2());
554 SDL_Surface* surface = bitmap.
surface();
556 return absl::InternalError(
"Bitmap has no SDL surface");
559 const int src_w = bitmap.
width();
560 const int src_h = bitmap.
height();
563 std::vector<uint8_t> rgba(
static_cast<size_t>(out_w) * out_h * 4, 0xFF);
566 for (
int oy = 0; oy < out_h; ++oy) {
567 const int sy = oy * src_h / out_h;
568 for (
int ox = 0; ox < out_w; ++ox) {
569 const int sx = ox * src_w / out_w;
570 if (sx >= src_w || sy >= src_h)
572 const SDL_Color color = bitmap.
ReadColor(sx, sy);
574 const size_t base =
static_cast<size_t>((oy * out_w + ox) * 4);
575 rgba[base + 0] = color.r;
576 rgba[base + 1] = color.g;
577 rgba[base + 2] = color.b;
578 rgba[base + 3] = color.a;
587 uint32_t flags,
float scale) {
592 for (
int tx = 0; tx < 64; ++tx) {
593 draw.
DrawLine(
static_cast<float>(tx * 8), 0,
static_cast<float>(tx * 8),
594 511, 60, 60, 60, 80);
596 for (
int ty = 0; ty < 64; ++ty) {
597 draw.
DrawLine(0,
static_cast<float>(ty * 8), 511,
598 static_cast<float>(ty * 8), 60, 60, 60, 80);
606 for (
int ty = 0; ty < 64; ++ty) {
607 for (
int tx = 0; tx < 64; ++tx) {
608 const uint8_t val = cc.
tiles[ty * 64 + tx];
613 const bool is_track =
614 (val >= 0xB0 && val <= 0xBE) || (val >= 0xD0 && val <= 0xD3);
618 const auto c = CollisionColor(val);
622 static_cast<float>(ty * 8), 8.0f, 8.0f, c.r, c.g,
635 for (
int ty = 0; ty < 64; ++ty) {
636 for (
int tx = 0; tx < 64; ++tx) {
637 const bool upper = maps.upper(tx, ty) == 0x20;
638 const bool lower = maps.lower(tx, ty) == 0x20;
639 if (!upper && !lower)
642 static_cast<float>(ty * 8), 8.0f, 8.0f, 255,
643 upper ? 0 : 140, 0, 150);
651 const float px =
static_cast<float>(obj.x() * 8);
652 const float py =
static_cast<float>(obj.y() * 8);
654 static_cast<float>(std::max(1,
static_cast<int>(obj.width_)) * 8);
656 static_cast<float>(std::max(1,
static_cast<int>(obj.height_)) * 8);
657 draw.
DrawRect(px, py, pw, ph, 255, 200, 0, 200);
665 const float px =
static_cast<float>(spr.nx() * 16);
666 const float py =
static_cast<float>(spr.ny() * 16);
668 draw.
DrawRect(px, py, 16.0f, 16.0f, 255, 60, 60, 230);
674 draw.
DrawLine(256, 0, 256, 511, 200, 200, 255, 120);
675 draw.
DrawLine(0, 256, 511, 256, 200, 200, 255, 120);
679#ifdef YAZE_CLI_HAS_PNG
681 const std::vector<uint8_t>& rgba,
int width,
int height) {
683 png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
685 return absl::InternalError(
"png_create_write_struct failed");
687 png_infop info = png_create_info_struct(png);
689 png_destroy_write_struct(&png,
nullptr);
690 return absl::InternalError(
"png_create_info_struct failed");
693 std::vector<uint8_t> output;
696 if (setjmp(png_jmpbuf(png))) {
697 png_destroy_write_struct(&png, &info);
698 return absl::InternalError(
"PNG encoding error");
701 png_set_write_fn(png, &ctx, PngWrite, PngFlush);
702 png_set_IHDR(png, info,
static_cast<uint32_t
>(width),
703 static_cast<uint32_t
>(height), 8, PNG_COLOR_TYPE_RGBA,
704 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
705 PNG_FILTER_TYPE_DEFAULT);
706 png_write_info(png, info);
708 std::vector<png_bytep> rows(
static_cast<size_t>(height));
709 for (
int row = 0; row < height; ++row) {
710 rows[
static_cast<size_t>(row)] =
const_cast<png_bytep
>(
711 rgba.data() +
static_cast<size_t>(row) * width * 4);
713 png_write_image(png, rows.data());
714 png_write_end(png,
nullptr);
715 png_destroy_write_struct(&png, &info);
721 const std::vector<uint8_t>& ,
int ,
int ) {
722 return absl::UnimplementedError(
"PNG encoding unavailable (libpng missing)");
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
void DrawRect(float x, float y, float w, float h, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
void DrawLine(float x0, float y0, float x1, float y1, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
void DrawFilledRect(float x, float y, float w, float h, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
zelda3::GameData * game_data_
absl::StatusOr< std::vector< uint8_t > > EncodePng(const std::vector< uint8_t > &rgba, int width, int height)
RenderService(Rom *rom, zelda3::GameData *game_data)
absl::StatusOr< RoomMetadata > GetDungeonRoomMetadata(int room_id)
absl::StatusOr< OverworldRenderResult > RenderOverworldArea(const OverworldRenderRequest &req)
void ApplyOverlays(std::vector< uint8_t > &rgba, int width, int height, const zelda3::Room &room, uint32_t flags, float scale)
absl::StatusOr< std::vector< uint8_t > > BitmapToRgba(const gfx::Bitmap &bitmap, int width, int height)
absl::StatusOr< RenderResult > RenderDungeonRoom(const RenderRequest &req)
Represents a bitmap image optimized for SNES ROM hacking.
SDL_Color ReadColor(int x, int y) const
SDL_Surface * surface() const
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
const std::vector< OverworldEntrance > & holes() const
auto sprites(int state) const
absl::StatusOr< SubscreenOverlayLayer > BuildSubscreenOverlayLayer(int map_index)
Build the subscreen overlay layer for a screen (builds the screen if needed). The overlay comes from ...
auto overworld_map(int i) const
void SetGraphicsPreview(OverworldGraphicsPreview preview)
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
const std::vector< OverworldEntrance > & entrances() const
RoomLayerManager - Manages layer visibility and compositing.
void ApplyGameLayerRegisters(const RoomLayerRegisters ®isters)
void ApplyLayerMerging(const LayerMergeType &merge_type)
void ApplyRoomEffect(EffectKey effect)
const CustomCollisionMap & custom_collision() const
uint16_t message_id() const
RoomLayerRegisters GameLayerRegisters(uint16_t room_flags=0) const
void SetGraphicsSheetOverrides(std::map< uint16_t, std::vector< uint8_t > > overrides)
void RenderRoomGraphics()
CollisionKey collision() const
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
gfx::Bitmap & GetCompositeBitmap(RoomLayerManager &layer_mgr)
Get a composite bitmap of all layers merged.
uint8_t spriteset() const
const std::vector< zelda3::Sprite > & GetSprites() const
const std::vector< RoomObject > & GetTileObjects() const
const LayerMergeType & layer_merging() const
uint8_t layout_id() const
bool has_custom_collision() const
void SetGameData(GameData *data)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
constexpr uint32_t kItems
constexpr uint32_t kHoles
constexpr uint32_t kSprites
constexpr uint32_t kEntrances
constexpr uint32_t kExits
constexpr uint32_t kTileTypes
constexpr uint32_t kCollision
constexpr uint32_t kSprites
constexpr uint32_t kCameraQuads
constexpr uint32_t kObjects
constexpr uint32_t kTrack
absl::Status ValidateRenderScale(float scale)
PhaseColor SpritePhaseColor(int phase)
TileColor CollisionColor(uint8_t tile)
void DrawLabel(HeadlessOverlayRenderer &draw, float x, float y, const std::string &text, uint8_t r, uint8_t g, uint8_t b)
const uint8_t * GlyphFor(char c)
absl::StatusOr< float > ParseRenderScale(absl::string_view value)
constexpr int kNumOverworldMaps
RoomCollisionMaps ComputeRoomCollisionMaps(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
Room LoadRoomFromRom(Rom *rom, int room_id)
constexpr int kNumberOfRooms
RoomCollisionInput MakeRoomCollisionInput(const Room &room, const RoomTilemaps &tilemaps)
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
zelda3::OverworldGraphicsPreview graphics_preview
std::vector< OverworldRenderMarker > markers
std::vector< int > screens
std::vector< uint8_t > png_data
std::map< uint16_t, std::vector< uint8_t > > sheet_overrides
std::vector< uint8_t > png_data
std::array< uint8_t, 64 *64 > tiles