mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-15 19:13:07 +01:00
961 lines
29 KiB
C++
961 lines
29 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2018 OpenRCT2 developers
|
|
*
|
|
* For a complete list of all authors, please refer to contributors.md
|
|
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
|
*
|
|
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
|
*****************************************************************************/
|
|
|
|
#include "../Context.h"
|
|
#include "../OpenRCT2.h"
|
|
#include "../PlatformEnvironment.h"
|
|
#include "../config/Config.h"
|
|
#include "../core/FileStream.hpp"
|
|
#include "../core/Path.hpp"
|
|
#include "../platform/platform.h"
|
|
#include "../sprites.h"
|
|
#include "../ui/UiContext.h"
|
|
#include "../util/Util.h"
|
|
#include "Drawing.h"
|
|
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
using namespace OpenRCT2;
|
|
using namespace OpenRCT2::Ui;
|
|
|
|
#pragma pack(push, 1)
|
|
struct rct_g1_header
|
|
{
|
|
uint32_t num_entries;
|
|
uint32_t total_size;
|
|
};
|
|
assert_struct_size(rct_g1_header, 8);
|
|
#pragma pack(pop)
|
|
|
|
struct rct_gx
|
|
{
|
|
rct_g1_header header;
|
|
std::vector<rct_g1_element> elements;
|
|
void* data;
|
|
};
|
|
|
|
// clang-format off
|
|
constexpr struct
|
|
{
|
|
int start;
|
|
int32_t x_offset;
|
|
int32_t y_offset;
|
|
}
|
|
sprite_peep_pickup_starts[15] =
|
|
{
|
|
{SPR_PEEP_PICKUP_GUEST_START, 0, 15},
|
|
{SPR_PEEP_PICKUP_HANDYMAN_START, 1, 18},
|
|
{SPR_PEEP_PICKUP_MECHANIC_START, 3, 22},
|
|
{SPR_PEEP_PICKUP_GUARD_START, 3, 15},
|
|
{SPR_PEEP_PICKUP_PANDA_START, -1, 19},
|
|
{SPR_PEEP_PICKUP_TIGER_START, -1, 17},
|
|
{SPR_PEEP_PICKUP_ELEPHANT_START, -1, 17},
|
|
{SPR_PEEP_PICKUP_GORILLA_START, 0, 17},
|
|
{SPR_PEEP_PICKUP_SNOWMAN_START, -1, 16},
|
|
{SPR_PEEP_PICKUP_KNIGHT_START, -2, 17},
|
|
{SPR_PEEP_PICKUP_BANDIT_START, 0, 16},
|
|
{SPR_PEEP_PICKUP_PIRATE_START, 0, 16},
|
|
{SPR_PEEP_PICKUP_SHERIFF_START, 0, 16},
|
|
{SPR_PEEP_PICKUP_ASTRONAUT_START, 0, 16},
|
|
{SPR_PEEP_PICKUP_ROMAN_START, -1, 17},
|
|
};
|
|
|
|
static inline uint32_t rctc_to_rct2_index(uint32_t image)
|
|
{
|
|
if ( image < 1542) return image;
|
|
else if (image >= 1574 && image < 4983) return image - 32;
|
|
else if (image >= 4986 && image < 17189) return image - 35;
|
|
else if (image >= 17191 && image < 18121) return image - 37;
|
|
else if (image >= 18123 && image < 23800) return image - 39;
|
|
else if (image >= 23804 && image < 24670) return image - 43;
|
|
else if (image >= 24674 && image < 28244) return image - 47;
|
|
else if (image >= 28246 ) return image - 49;
|
|
else throw std::runtime_error("Invalid RCTC g1.dat file");
|
|
}
|
|
// clang-format on
|
|
|
|
static void read_and_convert_gxdat(IStream* stream, size_t count, bool is_rctc, rct_g1_element* elements)
|
|
{
|
|
auto g1Elements32 = std::make_unique<rct_g1_element_32bit[]>(count);
|
|
stream->Read(g1Elements32.get(), count * sizeof(rct_g1_element_32bit));
|
|
if (is_rctc)
|
|
{
|
|
// Process RCTC's g1.dat file
|
|
uint32_t rctc = 0;
|
|
for (size_t i = 0; i < SPR_G1_END; ++i)
|
|
{
|
|
// RCTC's g1.dat has a number of additional elements
|
|
// added between the RCT2 elements. This switch
|
|
// statement skips over the elements we don't want.
|
|
switch (i)
|
|
{
|
|
case 1542:
|
|
rctc += 32;
|
|
break;
|
|
case 23761:
|
|
case 24627:
|
|
rctc += 4;
|
|
break;
|
|
case 4951:
|
|
rctc += 3;
|
|
break;
|
|
case 17154:
|
|
case 18084:
|
|
case 28197:
|
|
rctc += 2;
|
|
break;
|
|
}
|
|
|
|
const rct_g1_element_32bit& src = g1Elements32[rctc];
|
|
|
|
// Double cast to silence compiler warning about casting to
|
|
// pointer from integer of mismatched length.
|
|
elements[i].offset = (uint8_t*)(uintptr_t)src.offset;
|
|
elements[i].width = src.width;
|
|
elements[i].height = src.height;
|
|
elements[i].x_offset = src.x_offset;
|
|
elements[i].y_offset = src.y_offset;
|
|
elements[i].flags = src.flags;
|
|
|
|
if (src.flags & G1_FLAG_HAS_ZOOM_SPRITE)
|
|
{
|
|
elements[i].zoomed_offset = (uint16_t)(i - rctc_to_rct2_index(rctc - src.zoomed_offset));
|
|
}
|
|
else
|
|
{
|
|
elements[i].zoomed_offset = src.zoomed_offset;
|
|
}
|
|
|
|
++rctc;
|
|
}
|
|
|
|
// The pincer graphic for picking up peeps is different in
|
|
// RCTC, and the sprites have different offsets to accommodate
|
|
// the change. This reverts the offsets to their RCT2 values.
|
|
for (const auto& animation : sprite_peep_pickup_starts)
|
|
{
|
|
for (int i = 0; i < SPR_PEEP_PICKUP_COUNT; ++i)
|
|
{
|
|
elements[animation.start + i].x_offset -= animation.x_offset;
|
|
elements[animation.start + i].y_offset -= animation.y_offset;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < count; i++)
|
|
{
|
|
const rct_g1_element_32bit& src = g1Elements32[i];
|
|
|
|
// Double cast to silence compiler warning about casting to
|
|
// pointer from integer of mismatched length.
|
|
elements[i].offset = (uint8_t*)(uintptr_t)src.offset;
|
|
elements[i].width = src.width;
|
|
elements[i].height = src.height;
|
|
elements[i].x_offset = src.x_offset;
|
|
elements[i].y_offset = src.y_offset;
|
|
elements[i].flags = src.flags;
|
|
elements[i].zoomed_offset = src.zoomed_offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
void mask_scalar(
|
|
int32_t width,
|
|
int32_t height,
|
|
const uint8_t* RESTRICT maskSrc,
|
|
const uint8_t* RESTRICT colourSrc,
|
|
uint8_t* RESTRICT dst,
|
|
int32_t maskWrap,
|
|
int32_t colourWrap,
|
|
int32_t dstWrap)
|
|
{
|
|
for (int32_t yy = 0; yy < height; yy++)
|
|
{
|
|
for (int32_t xx = 0; xx < width; xx++)
|
|
{
|
|
uint8_t colour = (*colourSrc) & (*maskSrc);
|
|
if (colour != 0)
|
|
{
|
|
*dst = colour;
|
|
}
|
|
|
|
maskSrc++;
|
|
colourSrc++;
|
|
dst++;
|
|
}
|
|
maskSrc += maskWrap;
|
|
colourSrc += colourWrap;
|
|
dst += dstWrap;
|
|
}
|
|
}
|
|
|
|
static std::string gfx_get_csg_header_path()
|
|
{
|
|
auto path = Path::ResolveCasing(Path::Combine(gConfigGeneral.rct1_path, "Data", "csg1i.dat"));
|
|
if (path.empty())
|
|
{
|
|
path = Path::ResolveCasing(Path::Combine(gConfigGeneral.rct1_path, "RCTdeluxe_install", "Data", "csg1i.dat"));
|
|
}
|
|
return path;
|
|
}
|
|
|
|
static std::string gfx_get_csg_data_path()
|
|
{
|
|
// csg1.1 and csg1.dat are the same file.
|
|
// In the CD version, it's called csg1.1 on the CD and csg1.dat on the disk.
|
|
// In the GOG version, it's always called csg1.1.
|
|
// In the Steam version, it's called csg1.dat in the "disk" folder and csg1.1 in the "CD" folder.
|
|
auto path = Path::ResolveCasing(Path::Combine(gConfigGeneral.rct1_path, "Data", "csg1.1"));
|
|
if (path.empty())
|
|
{
|
|
path = Path::ResolveCasing(Path::Combine(gConfigGeneral.rct1_path, "Data", "csg1.dat"));
|
|
}
|
|
return path;
|
|
}
|
|
|
|
static rct_gx _g1 = {};
|
|
static rct_gx _g2 = {};
|
|
static rct_gx _csg = {};
|
|
static bool _csgLoaded = false;
|
|
|
|
static rct_g1_element _g1Temp = {};
|
|
bool gTinyFontAntiAliased = false;
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00678998
|
|
*/
|
|
bool gfx_load_g1(const IPlatformEnvironment& env)
|
|
{
|
|
log_verbose("gfx_load_g1(...)");
|
|
try
|
|
{
|
|
auto path = Path::Combine(env.GetDirectoryPath(DIRBASE::RCT2, DIRID::DATA), "g1.dat");
|
|
auto fs = FileStream(path, FILE_MODE_OPEN);
|
|
_g1.header = fs.ReadValue<rct_g1_header>();
|
|
|
|
log_verbose("g1.dat, number of entries: %u", _g1.header.num_entries);
|
|
|
|
if (_g1.header.num_entries < SPR_G1_END)
|
|
{
|
|
throw std::runtime_error("Not enough elements in g1.dat");
|
|
}
|
|
|
|
// Read element headers
|
|
_g1.elements.resize(324206);
|
|
bool is_rctc = _g1.header.num_entries == SPR_RCTC_G1_END;
|
|
read_and_convert_gxdat(&fs, _g1.header.num_entries, is_rctc, _g1.elements.data());
|
|
gTinyFontAntiAliased = is_rctc;
|
|
|
|
// Read element data
|
|
_g1.data = fs.ReadArray<uint8_t>(_g1.header.total_size);
|
|
|
|
// Fix entry data offsets
|
|
for (uint32_t i = 0; i < _g1.header.num_entries; i++)
|
|
{
|
|
_g1.elements[i].offset += (uintptr_t)_g1.data;
|
|
}
|
|
return true;
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
_g1.elements.clear();
|
|
_g1.elements.shrink_to_fit();
|
|
|
|
log_fatal("Unable to load g1 graphics");
|
|
if (!gOpenRCT2Headless)
|
|
{
|
|
auto uiContext = GetContext()->GetUiContext();
|
|
uiContext->ShowMessageBox("Unable to load g1.dat. Your RollerCoaster Tycoon 2 path may be incorrectly set.");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void gfx_unload_g1()
|
|
{
|
|
SafeFree(_g1.data);
|
|
_g1.elements.clear();
|
|
_g1.elements.shrink_to_fit();
|
|
}
|
|
|
|
void gfx_unload_g2()
|
|
{
|
|
SafeFree(_g2.data);
|
|
_g2.elements.clear();
|
|
_g2.elements.shrink_to_fit();
|
|
}
|
|
|
|
void gfx_unload_csg()
|
|
{
|
|
SafeFree(_csg.data);
|
|
_csg.elements.clear();
|
|
_csg.elements.shrink_to_fit();
|
|
}
|
|
|
|
bool gfx_load_g2()
|
|
{
|
|
log_verbose("gfx_load_g2()");
|
|
|
|
char path[MAX_PATH];
|
|
|
|
platform_get_openrct_data_path(path, sizeof(path));
|
|
safe_strcat_path(path, "g2.dat", MAX_PATH);
|
|
try
|
|
{
|
|
auto fs = FileStream(path, FILE_MODE_OPEN);
|
|
_g2.header = fs.ReadValue<rct_g1_header>();
|
|
|
|
// Read element headers
|
|
_g2.elements.resize(_g2.header.num_entries);
|
|
read_and_convert_gxdat(&fs, _g2.header.num_entries, false, _g2.elements.data());
|
|
|
|
// Read element data
|
|
_g2.data = fs.ReadArray<uint8_t>(_g2.header.total_size);
|
|
|
|
// Fix entry data offsets
|
|
for (uint32_t i = 0; i < _g2.header.num_entries; i++)
|
|
{
|
|
_g2.elements[i].offset += (uintptr_t)_g2.data;
|
|
}
|
|
return true;
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
_g2.elements.clear();
|
|
_g2.elements.shrink_to_fit();
|
|
|
|
log_fatal("Unable to load g2 graphics");
|
|
if (!gOpenRCT2Headless)
|
|
{
|
|
auto uiContext = GetContext()->GetUiContext();
|
|
uiContext->ShowMessageBox("Unable to load g2.dat");
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool gfx_load_csg()
|
|
{
|
|
log_verbose("gfx_load_csg()");
|
|
|
|
if (str_is_null_or_empty(gConfigGeneral.rct1_path))
|
|
{
|
|
log_verbose(" unable to load CSG, RCT1 path not set");
|
|
return false;
|
|
}
|
|
|
|
auto pathHeaderPath = gfx_get_csg_header_path();
|
|
auto pathDataPath = gfx_get_csg_data_path();
|
|
try
|
|
{
|
|
auto fileHeader = FileStream(pathHeaderPath, FILE_MODE_OPEN);
|
|
auto fileData = FileStream(pathDataPath, FILE_MODE_OPEN);
|
|
size_t fileHeaderSize = fileHeader.GetLength();
|
|
size_t fileDataSize = fileData.GetLength();
|
|
|
|
_csg.header.num_entries = (uint32_t)(fileHeaderSize / sizeof(rct_g1_element_32bit));
|
|
_csg.header.total_size = (uint32_t)fileDataSize;
|
|
|
|
if (_csg.header.num_entries < 69917)
|
|
{
|
|
log_warning("Cannot load CSG1.DAT, it has too few entries. Only CSG1.DAT from Loopy Landscapes will work.");
|
|
return false;
|
|
}
|
|
|
|
// Read element headers
|
|
_csg.elements.resize(_csg.header.num_entries);
|
|
read_and_convert_gxdat(&fileHeader, _csg.header.num_entries, false, _csg.elements.data());
|
|
|
|
// Read element data
|
|
_csg.data = fileData.ReadArray<uint8_t>(_csg.header.total_size);
|
|
|
|
// Fix entry data offsets
|
|
for (uint32_t i = 0; i < _csg.header.num_entries; i++)
|
|
{
|
|
_csg.elements[i].offset += (uintptr_t)_csg.data;
|
|
// RCT1 used zoomed offsets that counted from the beginning of the file, rather than from the current sprite.
|
|
if (_csg.elements[i].zoomed_offset != 0)
|
|
{
|
|
_csg.elements[i].zoomed_offset = i - (SPR_CSG_BEGIN + _csg.elements[i].zoomed_offset);
|
|
}
|
|
}
|
|
_csgLoaded = true;
|
|
return true;
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
_csg.elements.clear();
|
|
_csg.elements.shrink_to_fit();
|
|
|
|
log_error("Unable to load csg graphics");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies a sprite onto the buffer. There is no compression used on the sprite
|
|
* image.
|
|
* rct2: 0x0067A690
|
|
*/
|
|
void FASTCALL gfx_bmp_sprite_to_buffer(
|
|
const uint8_t* palette_pointer,
|
|
uint8_t* source_pointer,
|
|
uint8_t* dest_pointer,
|
|
const rct_g1_element* source_image,
|
|
rct_drawpixelinfo* dest_dpi,
|
|
int32_t height,
|
|
int32_t width,
|
|
int32_t image_type)
|
|
{
|
|
uint16_t zoom_level = dest_dpi->zoom_level;
|
|
uint8_t zoom_amount = 1 << zoom_level;
|
|
uint32_t dest_line_width = (dest_dpi->width / zoom_amount) + dest_dpi->pitch;
|
|
uint32_t source_line_width = source_image->width * zoom_amount;
|
|
|
|
// Image uses the palette pointer to remap the colours of the image
|
|
if (image_type & IMAGE_TYPE_REMAP)
|
|
{
|
|
assert(palette_pointer != nullptr);
|
|
|
|
// Image with remaps
|
|
for (; height > 0; height -= zoom_amount)
|
|
{
|
|
uint8_t* next_source_pointer = source_pointer + source_line_width;
|
|
uint8_t* next_dest_pointer = dest_pointer + dest_line_width;
|
|
for (int32_t no_pixels = width; no_pixels > 0;
|
|
no_pixels -= zoom_amount, source_pointer += zoom_amount, dest_pointer++)
|
|
{
|
|
uint8_t pixel = *source_pointer;
|
|
pixel = palette_pointer[pixel];
|
|
if (pixel)
|
|
{
|
|
*dest_pointer = pixel;
|
|
}
|
|
}
|
|
|
|
source_pointer = next_source_pointer;
|
|
dest_pointer = next_dest_pointer;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Image is transparent. It only uses source pointer for
|
|
// telling if it needs to be drawn not for colour. Colour provided
|
|
// by the palette pointer.
|
|
if (image_type & IMAGE_TYPE_TRANSPARENT)
|
|
{ // Not tested
|
|
assert(palette_pointer != nullptr);
|
|
for (; height > 0; height -= zoom_amount)
|
|
{
|
|
uint8_t* next_source_pointer = source_pointer + source_line_width;
|
|
uint8_t* next_dest_pointer = dest_pointer + dest_line_width;
|
|
|
|
for (int32_t no_pixels = width; no_pixels > 0;
|
|
no_pixels -= zoom_amount, source_pointer += zoom_amount, dest_pointer++)
|
|
{
|
|
uint8_t pixel = *source_pointer;
|
|
if (pixel)
|
|
{
|
|
pixel = *dest_pointer;
|
|
pixel = palette_pointer[pixel];
|
|
*dest_pointer = pixel;
|
|
}
|
|
}
|
|
|
|
source_pointer = next_source_pointer;
|
|
dest_pointer = next_dest_pointer;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Basic bitmap no fancy stuff
|
|
if (!(source_image->flags & G1_FLAG_BMP))
|
|
{ // Not tested
|
|
for (; height > 0; height -= zoom_amount)
|
|
{
|
|
uint8_t* next_source_pointer = source_pointer + source_line_width;
|
|
uint8_t* next_dest_pointer = dest_pointer + dest_line_width;
|
|
|
|
for (int32_t no_pixels = width; no_pixels > 0;
|
|
no_pixels -= zoom_amount, dest_pointer++, source_pointer += zoom_amount)
|
|
{
|
|
*dest_pointer = *source_pointer;
|
|
}
|
|
|
|
dest_pointer = next_dest_pointer;
|
|
source_pointer = next_source_pointer;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Basic bitmap with no draw pixels
|
|
for (; height > 0; height -= zoom_amount)
|
|
{
|
|
uint8_t* next_source_pointer = source_pointer + source_line_width;
|
|
uint8_t* next_dest_pointer = dest_pointer + dest_line_width;
|
|
|
|
for (int32_t no_pixels = width; no_pixels > 0; no_pixels -= zoom_amount, dest_pointer++, source_pointer += zoom_amount)
|
|
{
|
|
uint8_t pixel = *source_pointer;
|
|
if (pixel)
|
|
{
|
|
*dest_pointer = pixel;
|
|
}
|
|
}
|
|
dest_pointer = next_dest_pointer;
|
|
source_pointer = next_source_pointer;
|
|
}
|
|
}
|
|
|
|
uint8_t* FASTCALL gfx_draw_sprite_get_palette(int32_t image_id, uint32_t tertiary_colour)
|
|
{
|
|
int32_t image_type = (image_id & 0xE0000000);
|
|
if (image_type == 0)
|
|
return nullptr;
|
|
|
|
if (!(image_type & IMAGE_TYPE_REMAP_2_PLUS))
|
|
{
|
|
uint8_t palette_ref = (image_id >> 19) & 0xFF;
|
|
if (!(image_type & IMAGE_TYPE_TRANSPARENT))
|
|
{
|
|
palette_ref &= 0x7F;
|
|
}
|
|
|
|
uint16_t palette_offset = palette_to_g1_offset[palette_ref];
|
|
auto g1 = gfx_get_g1_element(palette_offset);
|
|
if (g1 == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
else
|
|
{
|
|
return g1->offset;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uint8_t* palette_pointer = gPeepPalette;
|
|
|
|
uint32_t primary_offset = palette_to_g1_offset[(image_id >> 19) & 0x1F];
|
|
uint32_t secondary_offset = palette_to_g1_offset[(image_id >> 24) & 0x1F];
|
|
|
|
if (!(image_type & IMAGE_TYPE_REMAP))
|
|
{
|
|
palette_pointer = gOtherPalette;
|
|
#if defined(DEBUG_LEVEL_2) && DEBUG_LEVEL_2
|
|
assert(tertiary_colour < PALETTE_TO_G1_OFFSET_COUNT);
|
|
#endif // DEBUG_LEVEL_2
|
|
uint32_t tertiary_offset = palette_to_g1_offset[tertiary_colour];
|
|
auto tertiary_palette = gfx_get_g1_element(tertiary_offset);
|
|
if (tertiary_palette != nullptr)
|
|
{
|
|
memcpy(palette_pointer + 0x2E, &tertiary_palette->offset[0xF3], 12);
|
|
}
|
|
}
|
|
auto primary_palette = gfx_get_g1_element(primary_offset);
|
|
if (primary_palette != nullptr)
|
|
{
|
|
memcpy(palette_pointer + 0xF3, &primary_palette->offset[0xF3], 12);
|
|
}
|
|
auto secondary_palette = gfx_get_g1_element(secondary_offset);
|
|
if (secondary_palette != nullptr)
|
|
{
|
|
memcpy(palette_pointer + 0xCA, &secondary_palette->offset[0xF3], 12);
|
|
}
|
|
|
|
return palette_pointer;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0067A28E
|
|
* image_id (ebx)
|
|
* image_id as below
|
|
* 0b_111X_XXXX_XXXX_XXXX_XXXX_XXXX_XXXX_XXXX image_type
|
|
* 0b_XXX1_11XX_XXXX_XXXX_XXXX_XXXX_XXXX_XXXX image_sub_type (unknown pointer)
|
|
* 0b_XXX1_1111_XXXX_XXXX_XXXX_XXXX_XXXX_XXXX secondary_colour
|
|
* 0b_XXXX_XXXX_1111_1XXX_XXXX_XXXX_XXXX_XXXX primary_colour
|
|
* 0b_XXXX_X111_1111_1XXX_XXXX_XXXX_XXXX_XXXX palette_ref
|
|
* 0b_XXXX_XXXX_XXXX_X111_1111_1111_1111_1111 image_id (offset to g1)
|
|
* x (cx)
|
|
* y (dx)
|
|
* dpi (esi)
|
|
* tertiary_colour (ebp)
|
|
*/
|
|
void FASTCALL gfx_draw_sprite_software(rct_drawpixelinfo* dpi, int32_t image_id, int32_t x, int32_t y, uint32_t tertiary_colour)
|
|
{
|
|
if (image_id != -1)
|
|
{
|
|
uint8_t* palette_pointer = gfx_draw_sprite_get_palette(image_id, tertiary_colour);
|
|
if (image_id & IMAGE_TYPE_REMAP_2_PLUS)
|
|
{
|
|
image_id |= IMAGE_TYPE_REMAP;
|
|
}
|
|
|
|
gfx_draw_sprite_palette_set_software(dpi, image_id, x, y, palette_pointer, nullptr);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* rct: 0x0067A46E
|
|
* image_id (ebx) and also (0x00EDF81C)
|
|
* palette_pointer (0x9ABDA4)
|
|
* unknown_pointer (0x9E3CDC)
|
|
* dpi (edi)
|
|
* x (cx)
|
|
* y (dx)
|
|
*/
|
|
void FASTCALL gfx_draw_sprite_palette_set_software(
|
|
rct_drawpixelinfo* dpi, int32_t image_id, int32_t x, int32_t y, uint8_t* palette_pointer, uint8_t* unknown_pointer)
|
|
{
|
|
int32_t image_element = image_id & 0x7FFFF;
|
|
int32_t image_type = image_id & 0xE0000000;
|
|
|
|
const rct_g1_element* g1 = gfx_get_g1_element(image_element);
|
|
if (g1 == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (dpi->zoom_level != 0 && (g1->flags & G1_FLAG_HAS_ZOOM_SPRITE))
|
|
{
|
|
rct_drawpixelinfo zoomed_dpi;
|
|
zoomed_dpi.bits = dpi->bits;
|
|
zoomed_dpi.x = dpi->x >> 1;
|
|
zoomed_dpi.y = dpi->y >> 1;
|
|
zoomed_dpi.height = dpi->height >> 1;
|
|
zoomed_dpi.width = dpi->width >> 1;
|
|
zoomed_dpi.pitch = dpi->pitch;
|
|
zoomed_dpi.zoom_level = dpi->zoom_level - 1;
|
|
gfx_draw_sprite_palette_set_software(
|
|
&zoomed_dpi, image_type | (image_element - g1->zoomed_offset), x >> 1, y >> 1, palette_pointer, unknown_pointer);
|
|
return;
|
|
}
|
|
|
|
if (dpi->zoom_level != 0 && (g1->flags & G1_FLAG_NO_ZOOM_DRAW))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Its used super often so we will define it to a separate variable.
|
|
int32_t zoom_level = dpi->zoom_level;
|
|
int32_t zoom_mask = 0xFFFFFFFF << zoom_level;
|
|
|
|
if (zoom_level && g1->flags & G1_FLAG_RLE_COMPRESSION)
|
|
{
|
|
x -= ~zoom_mask;
|
|
y -= ~zoom_mask;
|
|
}
|
|
|
|
// This will be the height of the drawn image
|
|
int32_t height = g1->height;
|
|
// This is the start y coordinate on the destination
|
|
int16_t dest_start_y = y + g1->y_offset;
|
|
|
|
// For whatever reason the RLE version does not use
|
|
// the zoom mask on the y coordinate but does on x.
|
|
if (g1->flags & G1_FLAG_RLE_COMPRESSION)
|
|
{
|
|
dest_start_y -= dpi->y;
|
|
}
|
|
else
|
|
{
|
|
dest_start_y = (dest_start_y & zoom_mask) - dpi->y;
|
|
}
|
|
// This is the start y coordinate on the source
|
|
int32_t source_start_y = 0;
|
|
|
|
if (dest_start_y < 0)
|
|
{
|
|
// If the destination y is negative reduce the height of the
|
|
// image as we will cut off the bottom
|
|
height += dest_start_y;
|
|
// If the image is no longer visible nothing to draw
|
|
if (height <= 0)
|
|
{
|
|
return;
|
|
}
|
|
// The source image will start a further up the image
|
|
source_start_y -= dest_start_y;
|
|
// The destination start is now reset to 0
|
|
dest_start_y = 0;
|
|
}
|
|
else
|
|
{
|
|
if (g1->flags & G1_FLAG_RLE_COMPRESSION && zoom_level)
|
|
{
|
|
source_start_y -= dest_start_y & ~zoom_mask;
|
|
height += dest_start_y & ~zoom_mask;
|
|
}
|
|
}
|
|
|
|
int32_t dest_end_y = dest_start_y + height;
|
|
|
|
if (dest_end_y > dpi->height)
|
|
{
|
|
// If the destination y is outside of the drawing
|
|
// image reduce the height of the image
|
|
height -= dest_end_y - dpi->height;
|
|
}
|
|
// If the image no longer has anything to draw
|
|
if (height <= 0)
|
|
return;
|
|
|
|
dest_start_y >>= zoom_level;
|
|
|
|
// This will be the width of the drawn image
|
|
int32_t width = g1->width;
|
|
// This is the source start x coordinate
|
|
int32_t source_start_x = 0;
|
|
// This is the destination start x coordinate
|
|
int16_t dest_start_x = ((x + g1->x_offset + ~zoom_mask) & zoom_mask) - dpi->x;
|
|
|
|
if (dest_start_x < 0)
|
|
{
|
|
// If the destination is negative reduce the width
|
|
// image will cut off the side
|
|
width += dest_start_x;
|
|
// If there is no image to draw
|
|
if (width <= 0)
|
|
{
|
|
return;
|
|
}
|
|
// The source start will also need to cut off the side
|
|
source_start_x -= dest_start_x;
|
|
// Reset the destination to 0
|
|
dest_start_x = 0;
|
|
}
|
|
else
|
|
{
|
|
if (g1->flags & G1_FLAG_RLE_COMPRESSION && zoom_level)
|
|
{
|
|
source_start_x -= dest_start_x & ~zoom_mask;
|
|
}
|
|
}
|
|
|
|
int32_t dest_end_x = dest_start_x + width;
|
|
|
|
if (dest_end_x > dpi->width)
|
|
{
|
|
// If the destination x is outside of the drawing area
|
|
// reduce the image width.
|
|
width -= dest_end_x - dpi->width;
|
|
// If there is no image to draw.
|
|
if (width <= 0)
|
|
return;
|
|
}
|
|
|
|
dest_start_x >>= zoom_level;
|
|
|
|
uint8_t* dest_pointer = dpi->bits;
|
|
// Move the pointer to the start point of the destination
|
|
dest_pointer += ((dpi->width >> zoom_level) + dpi->pitch) * dest_start_y + dest_start_x;
|
|
|
|
if (g1->flags & G1_FLAG_RLE_COMPRESSION)
|
|
{
|
|
// We have to use a different method to move the source pointer for
|
|
// rle encoded sprites so that will be handled within this function
|
|
gfx_rle_sprite_to_buffer(
|
|
g1->offset, dest_pointer, palette_pointer, dpi, image_type, source_start_y, height, source_start_x, width);
|
|
return;
|
|
}
|
|
uint8_t* source_pointer = g1->offset;
|
|
// Move the pointer to the start point of the source
|
|
source_pointer += g1->width * source_start_y + source_start_x;
|
|
|
|
if (!(g1->flags & G1_FLAG_1))
|
|
{
|
|
gfx_bmp_sprite_to_buffer(palette_pointer, source_pointer, dest_pointer, g1, dpi, height, width, image_type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws the given colour image masked out by the given mask image. This can currently only cope with bitmap formatted mask and
|
|
* colour images. Presumably the original game never used RLE images for masking. Colour 0 represents transparent.
|
|
*
|
|
* rct2: 0x00681DE2
|
|
*/
|
|
void FASTCALL
|
|
gfx_draw_sprite_raw_masked_software(rct_drawpixelinfo* dpi, int32_t x, int32_t y, int32_t maskImage, int32_t colourImage)
|
|
{
|
|
int32_t left, top, right, bottom, width, height;
|
|
auto imgMask = gfx_get_g1_element(maskImage & 0x7FFFF);
|
|
auto imgColour = gfx_get_g1_element(colourImage & 0x7FFFF);
|
|
if (imgMask == nullptr || imgColour == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Only BMP format is supported for masking
|
|
if (!(imgMask->flags & G1_FLAG_BMP) || !(imgColour->flags & G1_FLAG_BMP))
|
|
{
|
|
gfx_draw_sprite_software(dpi, colourImage, x, y, 0);
|
|
return;
|
|
}
|
|
|
|
if (dpi->zoom_level != 0)
|
|
{
|
|
// TODO: Implement other zoom levels (probably not used though)
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
width = std::min(imgMask->width, imgColour->width);
|
|
height = std::min(imgMask->height, imgColour->height);
|
|
|
|
x += imgMask->x_offset;
|
|
y += imgMask->y_offset;
|
|
|
|
left = std::max<int32_t>(dpi->x, x);
|
|
top = std::max<int32_t>(dpi->y, y);
|
|
right = std::min(dpi->x + dpi->width, x + width);
|
|
bottom = std::min(dpi->y + dpi->height, y + height);
|
|
|
|
width = right - left;
|
|
height = bottom - top;
|
|
if (width < 0 || height < 0)
|
|
return;
|
|
|
|
int32_t skipX = left - x;
|
|
int32_t skipY = top - y;
|
|
|
|
uint8_t const* maskSrc = imgMask->offset + (skipY * imgMask->width) + skipX;
|
|
uint8_t const* colourSrc = imgColour->offset + (skipY * imgColour->width) + skipX;
|
|
uint8_t* dst = dpi->bits + (left - dpi->x) + ((top - dpi->y) * (dpi->width + dpi->pitch));
|
|
|
|
int32_t maskWrap = imgMask->width - width;
|
|
int32_t colourWrap = imgColour->width - width;
|
|
int32_t dstWrap = ((dpi->width + dpi->pitch) - width);
|
|
|
|
mask_fn(width, height, maskSrc, colourSrc, dst, maskWrap, colourWrap, dstWrap);
|
|
}
|
|
|
|
const rct_g1_element* gfx_get_g1_element(int32_t image_id)
|
|
{
|
|
openrct2_assert(!gOpenRCT2NoGraphics, "gfx_get_g1_element called on headless instance");
|
|
|
|
if (image_id == (-1 & 0x7FFFF))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (image_id == SPR_TEMP)
|
|
{
|
|
return &_g1Temp;
|
|
}
|
|
else if (image_id < SPR_G2_BEGIN)
|
|
{
|
|
if (image_id >= (int32_t)_g1.elements.size())
|
|
{
|
|
return nullptr;
|
|
}
|
|
return &_g1.elements[image_id];
|
|
}
|
|
if (image_id < SPR_CSG_BEGIN)
|
|
{
|
|
const uint32_t idx = image_id - SPR_G2_BEGIN;
|
|
if (idx >= _g2.header.num_entries)
|
|
{
|
|
log_warning("Invalid entry in g2.dat requested, idx = %u. You may have to update your g2.dat.", idx);
|
|
return nullptr;
|
|
}
|
|
return &_g2.elements[idx];
|
|
}
|
|
|
|
if (is_csg_loaded())
|
|
{
|
|
const uint32_t idx = image_id - SPR_CSG_BEGIN;
|
|
if (idx >= _csg.header.num_entries)
|
|
{
|
|
openrct2_assert(idx < _csg.header.num_entries, "Invalid entry in csg.dat requested, idx = %u.", idx);
|
|
return nullptr;
|
|
}
|
|
return &_csg.elements[idx];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void gfx_set_g1_element(int32_t imageId, const rct_g1_element* g1)
|
|
{
|
|
openrct2_assert(!gOpenRCT2NoGraphics, "gfx_set_g1_element called on headless instance");
|
|
#ifdef DEBUG
|
|
openrct2_assert(
|
|
(imageId >= 0 && imageId < SPR_G2_BEGIN) || imageId == SPR_TEMP, "gfx_set_g1_element called with unexpected image id");
|
|
openrct2_assert(g1 != nullptr, "g1 was nullptr");
|
|
#endif
|
|
|
|
if (imageId == SPR_TEMP)
|
|
{
|
|
_g1Temp = *g1;
|
|
}
|
|
else if (imageId >= 0 && imageId < SPR_G2_BEGIN)
|
|
{
|
|
if (imageId < (int32_t)_g1.elements.size())
|
|
{
|
|
_g1.elements[imageId] = *g1;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool is_csg_loaded()
|
|
{
|
|
return _csgLoaded;
|
|
}
|
|
|
|
rct_size16 FASTCALL gfx_get_sprite_size(uint32_t image_id)
|
|
{
|
|
const rct_g1_element* g1 = gfx_get_g1_element(image_id & 0X7FFFF);
|
|
rct_size16 size = {};
|
|
if (g1 != nullptr)
|
|
{
|
|
size.width = g1->width;
|
|
size.height = g1->height;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
size_t g1_calculate_data_size(const rct_g1_element* g1)
|
|
{
|
|
if (g1->flags & G1_FLAG_PALETTE)
|
|
{
|
|
return g1->width * 3;
|
|
}
|
|
else if (g1->flags & G1_FLAG_RLE_COMPRESSION)
|
|
{
|
|
if (g1->offset == nullptr)
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
uint16_t* offsets = (uint16_t*)g1->offset;
|
|
uint8_t* ptr = g1->offset + offsets[g1->height - 1];
|
|
bool endOfLine = false;
|
|
do
|
|
{
|
|
uint8_t chunk0 = *ptr++;
|
|
ptr++; // offset
|
|
uint8_t chunkSize = chunk0 & 0x7F;
|
|
ptr += chunkSize;
|
|
endOfLine = (chunk0 & 0x80) != 0;
|
|
} while (!endOfLine);
|
|
return ptr - g1->offset;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return g1->width * g1->height;
|
|
}
|
|
}
|