/*****************************************************************************
* Copyright (c) 2015 Ted John
* OpenRCT2, an open source clone of Roller Coaster Tycoon 2.
*
* This file is part of OpenRCT2.
*
* OpenRCT2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*****************************************************************************/
#include
#include "cmdline.h"
#include "drawing/drawing.h"
#include "platform/platform.h"
#include "util/util.h"
#include "openrct2.h"
#define MODE_DEFAULT 0
#define MODE_CLOSEST 1
#define MODE_DITHERING 2
typedef struct {
uint32 num_entries;
uint32 total_size;
} rct_sprite_file_header;
typedef struct { uint8 b, g, r, a; } rct_sprite_file_palette_entry;
rct_sprite_file_palette_entry spriteFilePalette[256];
static rct_sprite_file_palette_entry _standardPalette[256];
rct_sprite_file_header spriteFileHeader;
rct_g1_element *spriteFileEntries;
uint8 *spriteFileData;
void sprite_file_load_palette(int spriteIndex)
{
rct_g1_element *g1 = &spriteFileEntries[spriteIndex];
int numPaletteEntries = g1->width;
uint8* src = g1->offset;
rct_sprite_file_palette_entry *destPaletteEntry = &spriteFilePalette[g1->x_offset];
for (; numPaletteEntries > 0; numPaletteEntries--) {
destPaletteEntry->b = src[0];
destPaletteEntry->g = src[1];
destPaletteEntry->r = src[2];
src += 3;
destPaletteEntry++;
}
}
void sprite_entries_make_absolute()
{
for (uint32 i = 0; i < spriteFileHeader.num_entries; i++)
spriteFileEntries[i].offset += (int)spriteFileData;
}
void sprite_entries_make_relative()
{
for (uint32 i = 0; i < spriteFileHeader.num_entries; i++)
spriteFileEntries[i].offset -= (int)spriteFileData;
}
bool sprite_file_open(const utf8 *path)
{
SDL_RWops *file;
file = SDL_RWFromFile(path, "rb");
if (file == NULL)
return false;
if (SDL_RWread(file, &spriteFileHeader, sizeof(rct_sprite_file_header), 1) != 1) {
SDL_RWclose(file);
return false;
}
if (spriteFileHeader.num_entries > 0) {
int entryTableSize = spriteFileHeader.num_entries * sizeof(rct_g1_element);
spriteFileEntries = malloc(entryTableSize);
if (SDL_RWread(file, spriteFileEntries, entryTableSize, 1) != 1) {
SDL_RWclose(file);
return false;
}
spriteFileData = malloc(spriteFileHeader.total_size);
if (SDL_RWread(file, spriteFileData, spriteFileHeader.total_size, 1) != 1) {
SDL_RWclose(file);
return false;
}
sprite_entries_make_absolute();
}
SDL_RWclose(file);
return true;
}
bool sprite_file_save(const char *path)
{
SDL_RWops *file = SDL_RWFromFile(path, "wb");
if (file == NULL)
return false;
if (SDL_RWwrite(file, &spriteFileHeader, sizeof(rct_sprite_file_header), 1) != 1) {
SDL_RWclose(file);
return false;
}
if (spriteFileHeader.num_entries > 0) {
sprite_entries_make_relative();
int entryTableSize = spriteFileHeader.num_entries * sizeof(rct_g1_element);
if (SDL_RWwrite(file, spriteFileEntries, entryTableSize, 1) != 1) {
sprite_entries_make_absolute();
SDL_RWclose(file);
return false;
} else {
sprite_entries_make_absolute();
}
if (SDL_RWwrite(file, spriteFileData, spriteFileHeader.total_size, 1) != 1) {
SDL_RWclose(file);
return false;
}
}
SDL_RWclose(file);
return true;
}
void sprite_file_close()
{
free(spriteFileEntries);
free(spriteFileData);
}
bool sprite_file_export(int spriteIndex, const char *outPath)
{
rct_g1_element *spriteHeader;
rct_drawpixelinfo dpi;
uint8 *pixels;
int pixelBufferSize;
spriteHeader = &spriteFileEntries[spriteIndex];
pixelBufferSize = spriteHeader->width * spriteHeader->height;
pixels = malloc(pixelBufferSize);
memset(pixels, 0, pixelBufferSize);
dpi.bits = pixels;
dpi.x = 0;
dpi.y = 0;
dpi.width = spriteHeader->width;
dpi.height = spriteHeader->height;
dpi.pitch = 0;
dpi.zoom_level = 0;
memcpy(spriteFilePalette, _standardPalette, 256 * 4);
gfx_rle_sprite_to_buffer(spriteHeader->offset, pixels, (uint8*)spriteFilePalette, &dpi, IMAGE_TYPE_NO_BACKGROUND, 0, spriteHeader->height, 0, spriteHeader->width);
LodePNGState pngState;
unsigned int pngError;
unsigned char* pngData;
size_t pngSize;
lodepng_state_init(&pngState);
pngState.info_raw.colortype = LCT_PALETTE;
lodepng_palette_add(&pngState.info_raw, 0, 0, 0, 0);
for (int i = 1; i < 256; i++) {
lodepng_palette_add(
&pngState.info_raw,
spriteFilePalette[i].r,
spriteFilePalette[i].g,
spriteFilePalette[i].b,
255
);
}
pngError = lodepng_encode(&pngData, &pngSize, pixels, spriteHeader->width, spriteHeader->height, &pngState);
if (pngError != 0) {
free(pngData);
fprintf(stderr, "Error creating PNG data, %u: %s", pngError, lodepng_error_text(pngError));
return false;
} else {
lodepng_save_file(pngData, pngSize, outPath);
free(pngData);
return true;
}
}
bool is_transparent_pixel(sint16 *colour){
return colour[3] < 128;
}
// Returns true if pixel index is an index not used for remapping
bool is_changable_pixel(int palette_index) {
if (palette_index == -1)
return true;
if (palette_index == 0)
return false;
if (palette_index >= 203 && palette_index < 214)
return false;
if (palette_index == 226)
return false;
if (palette_index >= 227 && palette_index < 229)
return false;
if (palette_index >= 243)
return false;
return true;
}
int get_closest_palette_index(sint16 *colour){
uint32 smallest_error = -1;
int best_match = -1;
for (int x = 0; x < 256; x++){
if (is_changable_pixel(x)){
uint32 error =
((sint16)(spriteFilePalette[x].r) - colour[0]) * ((sint16)(spriteFilePalette[x].r) - colour[0]) +
((sint16)(spriteFilePalette[x].g) - colour[1]) * ((sint16)(spriteFilePalette[x].g) - colour[1]) +
((sint16)(spriteFilePalette[x].b) - colour[2]) * ((sint16)(spriteFilePalette[x].b) - colour[2]);
if (smallest_error == -1 || smallest_error > error){
best_match = x;
smallest_error = error;
}
}
}
return best_match;
}
int get_palette_index(sint16 *colour)
{
if (is_transparent_pixel(colour))
return -1;
for (int i = 0; i < 256; i++) {
if ((sint16)(spriteFilePalette[i].r) != colour[0]) continue;
if ((sint16)(spriteFilePalette[i].g) != colour[1]) continue;
if ((sint16)(spriteFilePalette[i].b) != colour[2]) continue;
return i;
}
return -1;
}
typedef struct {
uint8 num_pixels;
uint8 offset_x;
} rle_code;
bool sprite_file_import(const char *path, rct_g1_element *outElement, uint8 **outBuffer, int *outBufferLength, int mode)
{
unsigned char *pixels;
unsigned int width, height;
unsigned int pngError;
memcpy(spriteFilePalette, _standardPalette, 256 * 4);
pngError = lodepng_decode_file(&pixels, &width, &height, path, LCT_RGBA, 8);
if (pngError != 0) {
free(pixels);
fprintf(stderr, "Error creating PNG data, %u: %s", pngError, lodepng_error_text(pngError));
return false;
}
if (width > 256 || height > 256) {
fprintf(stderr, "Only images 256x256 or less are supported.");
free(pixels);
return false;
}
uint8 *buffer = malloc((height * 2) + (width * height * 16));
memset(buffer, 0, (height * 2) + (width * height * 16));
uint16 *yOffsets = (uint16*)buffer;
// A larger range is needed for proper dithering
sint16 *src = malloc(height * width * 4 * 2);
sint16 *src_orig = src;
for (uint32 x = 0; x < height * width * 4; x++){
src[x] = (sint16) pixels[x];
}
uint8 *dst = buffer + (height * 2);
for (unsigned int y = 0; y < height; y++) {
rle_code *previousCode, *currentCode;
yOffsets[y] = (dst - buffer);
previousCode = NULL;
currentCode = (rle_code*)dst;
dst += 2;
int startX = 0;
int pixels = 0;
bool pushRun = false;
for (unsigned int x = 0; x < width; x++) {
int paletteIndex = get_palette_index(src);
if (mode == MODE_CLOSEST || mode == MODE_DITHERING)
if (paletteIndex == -1 && !is_transparent_pixel(src))
paletteIndex = get_closest_palette_index(src);
if (mode == MODE_DITHERING)
if (!is_transparent_pixel(src) && is_changable_pixel(get_palette_index(src))){
sint16 dr = src[0] - (sint16)(spriteFilePalette[paletteIndex].r);
sint16 dg = src[1] - (sint16)(spriteFilePalette[paletteIndex].g);
sint16 db = src[2] - (sint16)(spriteFilePalette[paletteIndex].b);
if (x + 1 < width){
if (!is_transparent_pixel(src + 4) && is_changable_pixel(get_palette_index(src + 4))){
// Right
src[4] += dr * 7 / 16;
src[5] += dg * 7 / 16;
src[6] += db * 7 / 16;
}
}
if (y + 1 < height){
if (x > 0){
if (!is_transparent_pixel(src + 4 * (width - 1)) && is_changable_pixel(get_palette_index(src + 4 * (width - 1)))){
// Bottom left
src[4 * (width - 1)] += dr * 3 / 16;
src[4 * (width - 1) + 1] += dg * 3 / 16;
src[4 * (width - 1) + 2] += db * 3 / 16;
}
}
// Bottom
if (!is_transparent_pixel(src + 4 * width) && is_changable_pixel(get_palette_index(src + 4 * width))){
src[4 * width] += dr * 5 / 16;
src[4 * width + 1] += dg * 5 / 16;
src[4 * width + 2] += db * 5 / 16;
}
if (x + 1 < width){
if (!is_transparent_pixel(src + 4 * (width - 1)) && is_changable_pixel(get_palette_index(src + 4 * (width + 1)))){
// Bottom right
src[4 * (width + 1)] += dr * 1 / 16;
src[4 * (width + 1) + 1] += dg * 1 / 16;
src[4 * (width + 1) + 2] += db * 1 / 16;
}
}
}
}
src += 4;
if (paletteIndex == -1) {
if (pixels != 0) {
x--;
src -= 4;
pushRun = true;
}
} else {
if (pixels == 0)
startX = x;
pixels++;
*dst++ = (uint8)paletteIndex;
}
if (pixels == 127 || x == width - 1)
pushRun = true;
if (pushRun) {
if (pixels > 0) {
previousCode = currentCode;
currentCode->num_pixels = pixels;
currentCode->offset_x = startX;
if (x == width - 1)
currentCode->num_pixels |= 0x80;
currentCode = (rle_code*)dst;
dst += 2;
} else {
if (previousCode == NULL) {
currentCode->num_pixels = 0x80;
currentCode->offset_x = 0;
} else {
previousCode->num_pixels |= 0x80;
dst -= 2;
}
}
startX = 0;
pixels = 0;
pushRun = false;
}
}
}
free(pixels);
free(src_orig);
int bufferLength = (int)(dst - buffer);
buffer = realloc(buffer, bufferLength);
outElement->offset = buffer;
outElement->width = width;
outElement->height = height;
outElement->flags = G1_FLAG_RLE_COMPRESSION;
outElement->x_offset = 0;
outElement->y_offset = 0;
outElement->zoomed_offset = 0;
*outBuffer = buffer;
*outBufferLength = bufferLength;
return true;
}
int cmdline_for_sprite(const char **argv, int argc)
{
gOpenRCT2Headless = true;
if (argc == 0)
return -1;
if (_strcmpi(argv[0], "details") == 0) {
if (argc < 2) {
fprintf(stderr, "usage: sprite details [idx]\n");
return -1;
} else if (argc == 2) {
const char *spriteFilePath = argv[1];
if (!sprite_file_open(spriteFilePath)) {
fprintf(stderr, "Unable to open input sprite file.\n");
return -1;
}
printf("sprites: %lu\n", spriteFileHeader.num_entries);
printf("data size: %lu\n", spriteFileHeader.total_size);
sprite_file_close();
return 1;
} else {
const char *spriteFilePath = argv[1];
int spriteIndex = atoi(argv[2]);
if (!sprite_file_open(spriteFilePath)) {
fprintf(stderr, "Unable to open input sprite file.\n");
return -1;
}
if (spriteIndex < 0 || spriteIndex >= (int)spriteFileHeader.num_entries) {
sprite_file_close();
fprintf(stderr, "Sprite #%d does not exist in sprite file.\n", spriteIndex);
return -1;
}
rct_g1_element *g1 = &spriteFileEntries[spriteIndex];
printf("width: %d\n", g1->width);
printf("height: %d\n", g1->height);
printf("x offset: %d\n", g1->x_offset);
printf("y offset: %d\n", g1->y_offset);
printf("data offset: 0x%lX\n", (uint32)g1->offset);
sprite_file_close();
return 1;
}
} else if (_strcmpi(argv[0], "export") == 0) {
if (argc < 4) {
fprintf(stderr, "usage: sprite export