In previous versions of NetHack, setting -DUSE_TILES enabled the tile
support, while setting -DSUPPRESS_GRAPHICS produced a NetHack that
would write its TTY output to standard output, and rely on ANSI.SYS or
similar to do screen control. (USE_TILES is now TILES_IN_GLYPHMAP.)
This change ensures that the current NetHack can be built the same
ways.
One twist is that previous NetHacks would drop all support for graphical
modes when tiles were not supported. Thus sys/msdos/vid{vga,vesa}.c have
very disordered use of TILES_IN_GLYPHMAP. There was no need to check
this. But now, the graphical modes also support Unicode. A non-tiled
build should have the graphical modes, with only the text functions
present, provided that ENHANCED_SYMBOLS is defined.
Some unused and locally used symbols were cleaned up along the way.
600 lines
19 KiB
C
600 lines
19 KiB
C
/* NetHack 5.0 bmptiles.c $NHDT-Date: 1781973097 2026/06/20 16:31:37 $ $NHDT-Branch: NetHack-5.0 $:$NHDT-Revision: 1.6 $ */
|
|
/* Copyright (c) Ray Chason, 2016. */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
#include "config.h"
|
|
|
|
#ifdef TILES_IN_GLYPHMAP
|
|
|
|
#include "tileset.h"
|
|
|
|
/* First BMP file header */
|
|
struct BitmapHeader {
|
|
char magic[2];
|
|
uint32 bmp_size;
|
|
uint32 img_offset;
|
|
};
|
|
|
|
/* Color model information for larger BMP headers */
|
|
struct CIE_XYZ {
|
|
uint32 ciexyzX;
|
|
uint32 ciexyzY;
|
|
uint32 ciexyzZ;
|
|
};
|
|
|
|
struct CIE_XYZTriple {
|
|
struct CIE_XYZ ciexyzRed;
|
|
struct CIE_XYZ ciexyzGreen;
|
|
struct CIE_XYZ ciexyzBlue;
|
|
};
|
|
|
|
/* Second BMP file header */
|
|
/* This one can vary in size; contents can vary according to the size */
|
|
struct BitmapInfoHeader {
|
|
uint32 Size; /* 12 40 52 56 108 124 64 */
|
|
int32 Width; /* 12 40 52 56 108 124 64 */
|
|
int32 Height; /* 12 40 52 56 108 124 64 */
|
|
uint16 NumPlanes; /* 12 40 52 56 108 124 64 */
|
|
uint16 BitsPerPixel; /* 12 40 52 56 108 124 64 */
|
|
uint32 Compression; /* 40 52 56 108 124 64 */
|
|
uint32 ImageDataSize; /* 40 52 56 108 124 64 */
|
|
int32 XResolution; /* 40 52 56 108 124 64 */
|
|
int32 YResolution; /* 40 52 56 108 124 64 */
|
|
uint32 ColorsUsed; /* 40 52 56 108 124 64 */
|
|
uint32 ColorsImportant; /* 40 52 56 108 124 64 */
|
|
uint32 RedMask; /* 52 56 108 124 */
|
|
uint32 GreenMask; /* 52 56 108 124 */
|
|
uint32 BlueMask; /* 52 56 108 124 */
|
|
uint32 AlphaMask; /* 56 108 124 */
|
|
uint32 CSType; /* 108 124 */
|
|
struct CIE_XYZTriple Endpoints; /* 108 124 */
|
|
uint32 GammaRed; /* 108 124 */
|
|
uint32 GammaGreen; /* 108 124 */
|
|
uint32 GammaBlue; /* 108 124 */
|
|
uint32 Intent; /* 124 */
|
|
uint32 ProfileData; /* 124 */
|
|
uint32 ProfileSize; /* 124 */
|
|
};
|
|
/* Compression */
|
|
#define BI_RGB 0
|
|
#define BI_RLE8 1
|
|
#define BI_RLE4 2
|
|
#define BI_BITFIELDS 3
|
|
#define BI_JPEG 4
|
|
#define BI_PNG 5
|
|
|
|
static uint16 read_u16(const unsigned char buf[2]);
|
|
static uint32 read_u32(const unsigned char buf[4]);
|
|
static int32 read_s32(const unsigned char buf[4]);
|
|
static struct Pixel build_pixel(const struct BitmapInfoHeader *, uint32);
|
|
static unsigned char pixel_element(uint32, uint32);
|
|
static boolean read_header(FILE *, struct BitmapHeader *);
|
|
static boolean read_info_header(FILE *, struct BitmapInfoHeader *);
|
|
static boolean check_info_header(const struct BitmapInfoHeader *);
|
|
static unsigned get_palette_size(const struct BitmapInfoHeader *);
|
|
static boolean read_palette(FILE *, struct Pixel *, unsigned);
|
|
|
|
/* Read a .BMP file into the image structure */
|
|
/* Return TRUE if successful, FALSE on any error */
|
|
boolean
|
|
read_bmp_tiles(const char *filename, struct TileSetImage *image)
|
|
{
|
|
struct BitmapHeader header1;
|
|
struct BitmapInfoHeader header2;
|
|
unsigned palette_size;
|
|
size_t num_pixels, size;
|
|
unsigned x, y, y_start, y_end, y_inc;
|
|
unsigned bytes_per_row;
|
|
|
|
FILE *fp = NULL; /* custodial */
|
|
unsigned char *row_bytes = NULL; /* custodial */
|
|
|
|
image->width = 0;
|
|
image->height = 0;
|
|
image->pixels = NULL; /* custodial, returned */
|
|
image->indexes = NULL; /* custodial, returned */
|
|
image->image_desc = NULL; /* custodial, returned */
|
|
image->tile_width = 0;
|
|
image->tile_height = 0;
|
|
|
|
fp = fopen(filename, "rb");
|
|
if (fp == NULL) goto error;
|
|
|
|
/* Read the headers */
|
|
if (!read_header(fp, &header1)) goto error;
|
|
if (memcmp(header1.magic, "BM", 2) != 0) goto error;
|
|
|
|
if (!read_info_header(fp, &header2)) goto error;
|
|
if (!check_info_header(&header2)) goto error;
|
|
|
|
/* header2.Height < 0 means the Y coordinate is reversed; the origin is
|
|
* top left rather than bottom left */
|
|
image->width = header2.Width;
|
|
image->height = labs(header2.Height);
|
|
|
|
/* Allocate pixel area; watch out for overflow */
|
|
num_pixels = (size_t) image->width * (size_t) image->height;
|
|
if (num_pixels / image->width != image->height) goto error; /* overflow */
|
|
size = num_pixels * sizeof(image->pixels[0]);
|
|
if (size / sizeof(image->pixels[0]) != num_pixels) goto error; /* overflow */
|
|
image->pixels = (struct Pixel *) alloc(size);
|
|
if (header2.BitsPerPixel <= 8) {
|
|
image->indexes = (unsigned char *) alloc(num_pixels);
|
|
}
|
|
|
|
/* Read the palette */
|
|
palette_size = get_palette_size(&header2);
|
|
if (!read_palette(fp, image->palette, palette_size)) goto error;
|
|
|
|
/* Read the pixels */
|
|
fseek(fp, header1.img_offset, SEEK_SET);
|
|
if (header2.Height < 0) {
|
|
y_start = 0;
|
|
y_end = image->height;
|
|
y_inc = 1;
|
|
} else {
|
|
y_start = image->height - 1;
|
|
y_end = (unsigned) -1;
|
|
y_inc = -1;
|
|
}
|
|
if (header2.Compression == BI_RLE4 || header2.Compression == BI_RLE8) {
|
|
unsigned char *p;
|
|
p = image->indexes;
|
|
memset(p, 0, num_pixels);
|
|
x = 0;
|
|
y = image->height - 1;
|
|
while (TRUE) {
|
|
int b1, b2;
|
|
b1 = fgetc(fp);
|
|
if (b1 == EOF) goto error;
|
|
b2 = fgetc(fp);
|
|
if (b2 == EOF) goto error;
|
|
/*
|
|
* b1 b2
|
|
* 0 0 end of line
|
|
* 0 1 end of bitmap
|
|
* 0 2 next two bytes are x and y offset
|
|
* 0 >2 b2 is a count of bytes
|
|
* >0 any repeat b2, b1 times
|
|
*/
|
|
if (b1 == 0) {
|
|
if (b2 == 0) {
|
|
/* end of line */
|
|
--y;
|
|
x = 0;
|
|
} else if (b2 == 1) {
|
|
/* end of bitmap */
|
|
break;
|
|
} else if (b2 == 2) {
|
|
/* next two bytes are x and y offset */
|
|
b1 = fgetc(fp);
|
|
if (b1 == EOF) break;
|
|
b2 = fgetc(fp);
|
|
if (b2 == EOF) break;
|
|
x += b1;
|
|
y += b2;
|
|
} else {
|
|
/* get bytes */
|
|
int i;
|
|
if (y < image->height) {
|
|
p = image->indexes + y * image->width;
|
|
for (i = 0; i < b2; ++i) {
|
|
b1 = fgetc(fp);
|
|
if (b1 == EOF) break;
|
|
if (header2.BitsPerPixel == 8) {
|
|
if (x < image->width) {
|
|
p[x] = b1;
|
|
}
|
|
++x;
|
|
} else {
|
|
if (x < image->width) {
|
|
p[x] = b1 >> 4;
|
|
}
|
|
++x;
|
|
if (x < image->width) {
|
|
p[x] = b1 & 0xF;
|
|
}
|
|
++x;
|
|
}
|
|
}
|
|
if (b2 & 1) {
|
|
b1 = fgetc(fp);
|
|
if (b1 == EOF) break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/* repeat b2, b1 times */
|
|
int i;
|
|
if (y < image->height) {
|
|
p = image->indexes + y * image->width;
|
|
for (i = 0; i < b1; ++i) {
|
|
if (header2.BitsPerPixel == 8) {
|
|
if (x < image->width) {
|
|
p[x] = b2;
|
|
}
|
|
++x;
|
|
} else {
|
|
if (x < image->width) {
|
|
p[x] = b2 >> 4;
|
|
}
|
|
++x;
|
|
if (x < image->width) {
|
|
p[x] = b2 & 0xF;
|
|
}
|
|
++x;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
bytes_per_row = (image->width * header2.BitsPerPixel + 31) / 32 * 4;
|
|
row_bytes = (unsigned char *) alloc(bytes_per_row);
|
|
if (header2.Compression == BI_RGB) {
|
|
switch (header2.BitsPerPixel) {
|
|
case 16:
|
|
header2.RedMask = 0x001F;
|
|
header2.GreenMask = 0x07E0;
|
|
header2.BlueMask = 0xF800;
|
|
header2.AlphaMask = 0x0000;
|
|
break;
|
|
|
|
case 32:
|
|
header2.RedMask = 0x000000FF;
|
|
header2.GreenMask = 0x0000FF00;
|
|
header2.BlueMask = 0x00FF0000;
|
|
header2.AlphaMask = 0xFF000000;
|
|
break;
|
|
}
|
|
}
|
|
for (y = y_start; y != y_end; y += y_inc) {
|
|
struct Pixel *row = image->pixels + y * image->width;
|
|
unsigned char *ind = image->indexes + y * image->width;
|
|
size = fread(row_bytes, 1, bytes_per_row, fp);
|
|
if (size < bytes_per_row) goto error;
|
|
switch (header2.BitsPerPixel) {
|
|
case 1:
|
|
for (x = 0; x < image->width; ++x) {
|
|
unsigned byte = x / 8;
|
|
unsigned shift = x % 8;
|
|
unsigned color = (row_bytes[byte] >> shift) & 1;
|
|
ind[x] = color;
|
|
}
|
|
break;
|
|
case 4:
|
|
for (x = 0; x < image->width; ++x) {
|
|
unsigned byte = x / 2;
|
|
unsigned shift = (x % 2) * 4;
|
|
unsigned color = (row_bytes[byte] >> shift) & 1;
|
|
ind[x] = color;
|
|
}
|
|
break;
|
|
case 8:
|
|
for (x = 0; x < image->width; ++x) {
|
|
ind[x] = row_bytes[x];
|
|
}
|
|
break;
|
|
case 16:
|
|
for (x = 0; x < image->width; ++x) {
|
|
uint16 color = read_u16(row_bytes + x * 2);
|
|
row[x] = build_pixel(&header2, color);
|
|
}
|
|
break;
|
|
case 24:
|
|
for (x = 0; x < image->width; ++x) {
|
|
row[x].r = row_bytes[x * 3 + 2];
|
|
row[x].g = row_bytes[x * 3 + 1];
|
|
row[x].b = row_bytes[x * 3 + 0];
|
|
row[x].a = 255;
|
|
}
|
|
break;
|
|
case 32:
|
|
for (x = 0; x < image->width; ++x) {
|
|
uint32 color = read_u32(row_bytes + x * 4);
|
|
row[x] = build_pixel(&header2, color);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
free(row_bytes);
|
|
row_bytes = NULL;
|
|
}
|
|
|
|
if (image->indexes != NULL) {
|
|
size_t i;
|
|
for (i = 0; i < num_pixels; ++i) {
|
|
image->pixels[i] = image->palette[image->indexes[i]];
|
|
}
|
|
}
|
|
|
|
fclose(fp);
|
|
return TRUE;
|
|
|
|
error:
|
|
if (fp) fclose(fp);
|
|
free(row_bytes);
|
|
free(image->pixels);
|
|
image->pixels = NULL;
|
|
free(image->indexes);
|
|
image->indexes = NULL;
|
|
free(image->image_desc);
|
|
image->image_desc = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
/* Read and decode the first header */
|
|
static boolean
|
|
read_header(FILE *fp, struct BitmapHeader *header)
|
|
{
|
|
unsigned char buf[14];
|
|
size_t size;
|
|
|
|
size = fread(buf, 1, sizeof(buf), fp);
|
|
if (size < sizeof(buf)) return FALSE;
|
|
|
|
memcpy(header->magic, buf + 0, 2);
|
|
header->bmp_size = read_u32(buf + 2);
|
|
/* 6 and 8 are 16 bit integers giving the hotspot of a cursor */
|
|
header->img_offset = read_u32(buf + 10);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Read and decode the second header */
|
|
static boolean
|
|
read_info_header(FILE *fp, struct BitmapInfoHeader *header)
|
|
{
|
|
unsigned char buf[124]; /* maximum size */
|
|
size_t size;
|
|
boolean have_color_mask;
|
|
|
|
memset(header, 0, sizeof(*header));
|
|
|
|
/* Get the header size */
|
|
size = fread(buf, 1, 4, fp);
|
|
if (size < 4) return FALSE;
|
|
header->Size = read_u32(buf + 0);
|
|
if (header->Size > sizeof(buf)) return FALSE;
|
|
|
|
/* Get the rest of the header */
|
|
size = fread(buf + 4, 1, header->Size - 4, fp);
|
|
if (size < header->Size - 4) return FALSE;
|
|
|
|
have_color_mask = FALSE;
|
|
switch (header->Size) {
|
|
case 124: /* BITMAPV5INFOHEADER */
|
|
/* 120 is reserved */
|
|
header->ProfileSize = read_u32(buf + 116);
|
|
header->ProfileData = read_u32(buf + 112);
|
|
header->Intent = read_u32(buf + 108);
|
|
/* fall through */
|
|
|
|
case 108: /* BITMAPV4INFOHEADER */
|
|
header->GammaBlue = read_u32(buf + 104);
|
|
header->GammaGreen = read_u32(buf + 100);
|
|
header->GammaRed = read_u32(buf + 96);
|
|
header->Endpoints.ciexyzBlue.ciexyzZ = read_u32(buf + 92);
|
|
header->Endpoints.ciexyzBlue.ciexyzY = read_u32(buf + 88);
|
|
header->Endpoints.ciexyzBlue.ciexyzX = read_u32(buf + 84);
|
|
header->Endpoints.ciexyzGreen.ciexyzZ = read_u32(buf + 80);
|
|
header->Endpoints.ciexyzGreen.ciexyzY = read_u32(buf + 76);
|
|
header->Endpoints.ciexyzGreen.ciexyzX = read_u32(buf + 72);
|
|
header->Endpoints.ciexyzRed.ciexyzZ = read_u32(buf + 68);
|
|
header->Endpoints.ciexyzRed.ciexyzY = read_u32(buf + 64);
|
|
header->Endpoints.ciexyzRed.ciexyzX = read_u32(buf + 60);
|
|
header->CSType = read_u32(buf + 56);
|
|
/* fall through */
|
|
|
|
case 56: /* BITMAPV3INFOHEADER */
|
|
header->AlphaMask = read_u32(buf + 52);
|
|
/* fall through */
|
|
|
|
case 52: /* BITMAPV2INFOHEADER */
|
|
header->BlueMask = read_u32(buf + 48);
|
|
header->GreenMask = read_u32(buf + 44);
|
|
header->RedMask = read_u32(buf + 40);
|
|
have_color_mask = TRUE;
|
|
/* fall through */
|
|
|
|
case 40: /* BITMAPINFOHEADER */
|
|
case 64: /* OS22XBITMAPHEADER */
|
|
/* The last 24 bytes in OS22XBITMAPHEADER are incompatible with the
|
|
* later Microsoft versions of the header */
|
|
header->ColorsImportant = read_u32(buf + 36);
|
|
header->ColorsUsed = read_u32(buf + 32);
|
|
header->YResolution = read_s32(buf + 28);
|
|
header->XResolution = read_s32(buf + 24);
|
|
header->ImageDataSize = read_u32(buf + 20);
|
|
header->Compression = read_u32(buf + 16);
|
|
header->BitsPerPixel = read_u16(buf + 14);
|
|
header->NumPlanes = read_u16(buf + 12);
|
|
header->Height = read_s32(buf + 8);
|
|
header->Width = read_s32(buf + 4);
|
|
break;
|
|
|
|
case 12: /* BITMAPCOREHEADER */
|
|
header->BitsPerPixel = read_u16(buf + 10);
|
|
header->NumPlanes = read_u16(buf + 8);
|
|
header->Height = read_u16(buf + 6);
|
|
header->Width = read_u16(buf + 4);
|
|
break;
|
|
|
|
default:
|
|
return FALSE;
|
|
}
|
|
|
|
/* For BI_BITFIELDS, the next three 32 bit words are the color masks */
|
|
if (header->Compression == BI_BITFIELDS && !have_color_mask) {
|
|
size = fread(buf, 1, 12, fp);
|
|
if (size < 12) return FALSE;
|
|
header->RedMask = read_u32(buf + 0);
|
|
header->GreenMask = read_u32(buf + 4);
|
|
header->BlueMask = read_u32(buf + 8);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Check the second header for consistency and unsupported features */
|
|
static boolean
|
|
check_info_header(const struct BitmapInfoHeader *header)
|
|
{
|
|
if (header->NumPlanes != 1) return FALSE;
|
|
switch (header->BitsPerPixel) {
|
|
#if 0 /* TODO */
|
|
case 0:
|
|
if (header->Compression != BI_PNG) return FALSE;
|
|
/* JPEG not supported */
|
|
break;
|
|
#endif
|
|
|
|
case 1:
|
|
case 24:
|
|
if (header->Compression != BI_RGB) return FALSE;
|
|
break;
|
|
|
|
case 4:
|
|
if (header->Compression != BI_RGB
|
|
&& header->Compression != BI_RLE4) return FALSE;
|
|
break;
|
|
|
|
case 8:
|
|
if (header->Compression != BI_RGB
|
|
&& header->Compression != BI_RLE8) return FALSE;
|
|
break;
|
|
|
|
case 16:
|
|
case 32:
|
|
if (header->Compression != BI_RGB
|
|
&& header->Compression != BI_BITFIELDS) return FALSE;
|
|
/* Any of the color masks could conceivably be zero; the bitmap, though
|
|
* limited, would still be meaningful */
|
|
if (header->Compression == BI_BITFIELDS
|
|
&& header->RedMask == 0
|
|
&& header->GreenMask == 0
|
|
&& header->BlueMask == 0) return FALSE;
|
|
break;
|
|
|
|
default:
|
|
return FALSE;
|
|
}
|
|
|
|
if (header->Height < 0 && header->Compression != BI_RGB
|
|
&& header->Compression != BI_BITFIELDS) return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Return the number of palette entries to read from the file */
|
|
static unsigned
|
|
get_palette_size(const struct BitmapInfoHeader *header)
|
|
{
|
|
switch (header->BitsPerPixel) {
|
|
case 1:
|
|
return 2;
|
|
|
|
case 4:
|
|
return header->ColorsUsed ? header->ColorsUsed : 16;
|
|
|
|
case 8:
|
|
return header->ColorsUsed ? header->ColorsUsed : 256;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Read the palette from the file
|
|
* palette_size is the number of entries to read, but no more than 256 will
|
|
* be written into the palette array
|
|
* Return TRUE if successful, FALSE on any error
|
|
*/
|
|
static boolean
|
|
read_palette(FILE *fp, struct Pixel *palette, unsigned palette_size)
|
|
{
|
|
unsigned i;
|
|
unsigned char buf[4];
|
|
unsigned read_size;
|
|
|
|
read_size = (palette_size < 256) ? palette_size : 256;
|
|
for (i = 0; i < read_size; ++i) {
|
|
size_t size = fread(buf, 1, sizeof(buf), fp);
|
|
if (size < sizeof(buf)) return FALSE;
|
|
palette[i].b = buf[0];
|
|
palette[i].g = buf[1];
|
|
palette[i].r = buf[2];
|
|
palette[i].a = 255;
|
|
}
|
|
for (; i < 256; ++i) {
|
|
palette[i].b = 0;
|
|
palette[i].g = 0;
|
|
palette[i].r = 0;
|
|
palette[i].a = 255;
|
|
}
|
|
fseek(fp, 4 * (palette_size - read_size), SEEK_CUR);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Decode an unsigned 16 bit quantity */
|
|
static uint16
|
|
read_u16(const unsigned char buf[2])
|
|
{
|
|
return ((uint16)buf[0] << 0)
|
|
| ((uint16)buf[1] << 8);
|
|
}
|
|
|
|
/* Decode an unsigned 32 bit quantity */
|
|
static uint32
|
|
read_u32(const unsigned char buf[4])
|
|
{
|
|
return ((uint32)buf[0] << 0)
|
|
| ((uint32)buf[1] << 8)
|
|
| ((uint32)buf[2] << 16)
|
|
| ((uint32)buf[3] << 24);
|
|
}
|
|
|
|
/* Decode a signed 32 bit quantity */
|
|
static int32
|
|
read_s32(const unsigned char buf[4])
|
|
{
|
|
return (int32)((read_u32(buf) ^ 0x80000000) - 0x80000000);
|
|
}
|
|
|
|
/* Build a pixel structure, given the mask words in the second header and
|
|
* a packed 16 or 32 bit pixel */
|
|
static struct Pixel
|
|
build_pixel(const struct BitmapInfoHeader *header, uint32 color)
|
|
{
|
|
struct Pixel pixel;
|
|
|
|
pixel.r = pixel_element(header->RedMask, color);
|
|
pixel.g = pixel_element(header->GreenMask, color);
|
|
pixel.b = pixel_element(header->BlueMask, color);
|
|
pixel.a = header->AlphaMask ? pixel_element(header->AlphaMask, color) : 255;
|
|
return pixel;
|
|
}
|
|
|
|
/* Extract one element (red, green, blue or alpha) from a pixel */
|
|
static unsigned char
|
|
pixel_element(uint32 mask, uint32 color)
|
|
{
|
|
uint32 bits, shift;
|
|
|
|
if (mask == 0) return 0;
|
|
bits = 0xFFFF; /* 0xFF, 0xF, 0x3, 0x1 */
|
|
shift = 16; /* 8, 4, 2, 1 */
|
|
while (shift != 0) {
|
|
if ((mask & bits) == 0) {
|
|
mask >>= shift;
|
|
color >>= shift;
|
|
}
|
|
shift /= 2;
|
|
bits >>= shift;
|
|
}
|
|
color &= mask;
|
|
return color * 255 / mask;
|
|
}
|
|
|
|
#endif /* TILES_IN_GLYPHMAP */
|