/***************************************************************************** * Copyright (c) 2014-2024 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 "Drawing.h" #include "../Context.h" #include "../OpenRCT2.h" #include "../PlatformEnvironment.h" #include "../config/Config.h" #include "../core/FileStream.h" #include "../core/MemoryStream.h" #include "../core/Path.hpp" #include "../platform/Platform.h" #include "../rct1/Csg.h" #include "../sprites.h" #include "../ui/UiContext.h" #include "ScrollingText.h" #include #include #include using namespace OpenRCT2; using namespace OpenRCT2::Ui; /** * 12 elements from 0xF3 are the peep top colour, 12 elements from 0xCA are peep trouser colour * * rct2: 0x0009ABE0C */ // clang-format off static thread_local uint8_t secondaryRemapPalette[256] = { 0x00, 0xF3, 0xF4, 0xF5, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, }; /** rct2: 0x009ABF0C */ static thread_local uint8_t tertiaryRemapPalette[256] = { 0x00, 0xF3, 0xF4, 0xF5, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, }; 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; if (image >= 1574 && image < 4983) return image - 32; if (image >= 4986 && image < 17189) return image - 35; if (image >= 17191 && image < 18121) return image - 37; if (image >= 18123 && image < 23800) return image - 39; if (image >= 23804 && image < 24670) return image - 43; if (image >= 24674 && image < 28244) return image - 47; if (image >= 28246 ) return image - 49; throw std::runtime_error("Invalid RCTC g1.dat file"); } // clang-format on static void ReadAndConvertGxDat(IStream* stream, size_t count, bool is_rctc, G1Element* elements) { auto g1Elements32 = std::make_unique(count); stream->Read(g1Elements32.get(), count * sizeof(RCTG1Element)); 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 RCTG1Element& src = g1Elements32[rctc]; // Double cast to silence compiler warning about casting to // pointer from integer of mismatched length. elements[i].offset = reinterpret_cast(static_cast(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 = static_cast(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 RCTG1Element& src = g1Elements32[i]; // Double cast to silence compiler warning about casting to // pointer from integer of mismatched length. elements[i].offset = reinterpret_cast(static_cast(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 MaskScalar( 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 Gx _g1 = {}; static Gx _g2 = {}; static Gx _csg = {}; static G1Element _scrollingText[MaxScrollingTextEntries]{}; static bool _csgLoaded = false; static G1Element _g1Temp = {}; static std::vector _imageListElements; bool gTinyFontAntiAliased = false; /** * * rct2: 0x00678998 */ bool GfxLoadG1(const IPlatformEnvironment& env) { LOG_VERBOSE("GfxLoadG1(...)"); try { auto path = env.FindFile(DIRBASE::RCT2, DIRID::DATA, u8"g1.dat"); auto fs = FileStream(path, FILE_MODE_OPEN); _g1.header = fs.ReadValue(); 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 bool is_rctc = _g1.header.num_entries == SPR_RCTC_G1_END; _g1.elements.resize(_g1.header.num_entries); ReadAndConvertGxDat(&fs, _g1.header.num_entries, is_rctc, _g1.elements.data()); gTinyFontAntiAliased = is_rctc; // Read element data _g1.data = fs.ReadArray(_g1.header.total_size); // Fix entry data offsets for (uint32_t i = 0; i < _g1.header.num_entries; i++) { _g1.elements[i].offset += reinterpret_cast(_g1.data.get()); } 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 GfxUnloadG1() { _g1.data.reset(); _g1.elements.clear(); _g1.elements.shrink_to_fit(); } void GfxUnloadG2() { _g2.data.reset(); _g2.elements.clear(); _g2.elements.shrink_to_fit(); } void GfxUnloadCsg() { _csg.data.reset(); _csg.elements.clear(); _csg.elements.shrink_to_fit(); } bool GfxLoadG2() { LOG_VERBOSE("GfxLoadG2()"); auto env = GetContext()->GetPlatformEnvironment(); std::string path = Path::Combine(env->GetDirectoryPath(DIRBASE::OPENRCT2), u8"g2.dat"); try { auto fs = FileStream(path, FILE_MODE_OPEN); _g2.header = fs.ReadValue(); // Read element headers _g2.elements.resize(_g2.header.num_entries); ReadAndConvertGxDat(&fs, _g2.header.num_entries, false, _g2.elements.data()); // Read element data _g2.data = fs.ReadArray(_g2.header.total_size); if (_g2.header.num_entries != G2_SPRITE_COUNT) { std::string errorMessage = "Mismatched g2.dat size.\nExpected: " + std::to_string(G2_SPRITE_COUNT) + "\nActual: " + std::to_string(_g2.header.num_entries) + "\ng2.dat may be installed improperly.\nPath to g2.dat: " + path; LOG_ERROR(errorMessage.c_str()); if (!gOpenRCT2Headless) { auto uiContext = GetContext()->GetUiContext(); uiContext->ShowMessageBox(errorMessage); uiContext->ShowMessageBox("Warning: You may experience graphical glitches if you continue. It's recommended " "that you update g2.dat if you're seeing this message"); } } // Fix entry data offsets for (uint32_t i = 0; i < _g2.header.num_entries; i++) { _g2.elements[i].offset += reinterpret_cast(_g2.data.get()); } 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 GfxLoadCsg() { LOG_VERBOSE("GfxLoadCsg()"); if (Config::Get().general.RCT1Path.empty()) { LOG_VERBOSE(" unable to load CSG, RCT1 path not set"); return false; } auto pathHeaderPath = FindCsg1idatAtLocation(Config::Get().general.RCT1Path); auto pathDataPath = FindCsg1datAtLocation(Config::Get().general.RCT1Path); 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 = static_cast(fileHeaderSize / sizeof(RCTG1Element)); _csg.header.total_size = static_cast(fileDataSize); if (!CsgIsUsable(_csg)) { 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); ReadAndConvertGxDat(&fileHeader, _csg.header.num_entries, false, _csg.elements.data()); // Read element data _csg.data = fileData.ReadArray(_csg.header.total_size); // Fix entry data offsets for (uint32_t i = 0; i < _csg.header.num_entries; i++) { _csg.elements[i].offset += reinterpret_cast(_csg.data.get()); // RCT1 used zoomed offsets that counted from the beginning of the file, rather than from the current sprite. if (_csg.elements[i].flags & G1_FLAG_HAS_ZOOM_SPRITE) { _csg.elements[i].zoomed_offset = i - _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; } } std::optional GfxLoadGx(const std::vector& buffer) { try { OpenRCT2::MemoryStream istream(buffer.data(), buffer.size()); Gx gx; gx.header = istream.ReadValue(); // Read element headers gx.elements.resize(gx.header.num_entries); ReadAndConvertGxDat(&istream, gx.header.num_entries, false, gx.elements.data()); // Read element data gx.data = istream.ReadArray(gx.header.total_size); return std::make_optional(std::move(gx)); } catch (const std::exception&) { LOG_VERBOSE("Unable to load Gx graphics"); } return std::nullopt; } static std::optional FASTCALL GfxDrawSpriteGetPalette(ImageId imageId) { if (!imageId.HasSecondary()) { uint8_t paletteId = imageId.GetRemap(); if (!imageId.IsBlended()) { paletteId &= 0x7F; } return GetPaletteMapForColour(paletteId); } auto paletteMap = PaletteMap(secondaryRemapPalette); if (imageId.HasTertiary()) { paletteMap = PaletteMap(tertiaryRemapPalette); auto tertiaryPaletteMap = GetPaletteMapForColour(imageId.GetTertiary()); if (tertiaryPaletteMap.has_value()) { paletteMap.Copy( PALETTE_OFFSET_REMAP_TERTIARY, tertiaryPaletteMap.value(), PALETTE_OFFSET_REMAP_PRIMARY, PALETTE_LENGTH_REMAP); } } auto primaryPaletteMap = GetPaletteMapForColour(imageId.GetPrimary()); if (primaryPaletteMap.has_value()) { paletteMap.Copy( PALETTE_OFFSET_REMAP_PRIMARY, primaryPaletteMap.value(), PALETTE_OFFSET_REMAP_PRIMARY, PALETTE_LENGTH_REMAP); } auto secondaryPaletteMap = GetPaletteMapForColour(imageId.GetSecondary()); if (secondaryPaletteMap.has_value()) { paletteMap.Copy( PALETTE_OFFSET_REMAP_SECONDARY, secondaryPaletteMap.value(), PALETTE_OFFSET_REMAP_PRIMARY, PALETTE_LENGTH_REMAP); } return paletteMap; } void FASTCALL GfxDrawSpriteSoftware(DrawPixelInfo& dpi, const ImageId imageId, const ScreenCoordsXY& spriteCoords) { if (imageId.HasValue()) { auto palette = GfxDrawSpriteGetPalette(imageId); if (!palette) { palette = PaletteMap::GetDefault(); } GfxDrawSpritePaletteSetSoftware(dpi, imageId, spriteCoords, *palette); } } /* * rct: 0x0067A46E * image_id (ebx) and also (0x00EDF81C) * palette_pointer (0x9ABDA4) * unknown_pointer (0x9E3CDC) * dpi (edi) * x (cx) * y (dx) */ void FASTCALL GfxDrawSpritePaletteSetSoftware( DrawPixelInfo& dpi, const ImageId imageId, const ScreenCoordsXY& coords, const PaletteMap& paletteMap) { int32_t x = coords.x; int32_t y = coords.y; const auto* g1 = GfxGetG1Element(imageId); if (g1 == nullptr) { return; } if (dpi.zoom_level > ZoomLevel{ 0 } && (g1->flags & G1_FLAG_HAS_ZOOM_SPRITE)) { DrawPixelInfo zoomed_dpi = 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; const auto spriteCoords = ScreenCoordsXY{ x >> 1, y >> 1 }; GfxDrawSpritePaletteSetSoftware( zoomed_dpi, imageId.WithIndex(imageId.GetIndex() - g1->zoomed_offset), spriteCoords, paletteMap); return; } if (dpi.zoom_level > ZoomLevel{ 0 } && (g1->flags & G1_FLAG_NO_ZOOM_DRAW)) { return; } // Its used super often so we will define it to a separate variable. const auto zoom_level = dpi.zoom_level; const int32_t zoom_mask = zoom_level > ZoomLevel{ 0 } ? zoom_level.ApplyTo(0xFFFFFFFF) : 0xFFFFFFFF; if (zoom_level > ZoomLevel{ 0 } && 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 > ZoomLevel{ 0 }) { 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.ApplyInversedTo(dest_start_y); // 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 > ZoomLevel{ 0 }) { 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.ApplyInversedTo(dest_start_x); uint8_t* dest_pointer = dpi.bits; // Move the pointer to the start point of the destination dest_pointer += (zoom_level.ApplyInversedTo(dpi.width) + dpi.pitch) * dest_start_y + dest_start_x; DrawSpriteArgs args(imageId, paletteMap, *g1, source_start_x, source_start_y, width, height, dest_pointer); GfxSpriteToBuffer(dpi, args); } void FASTCALL GfxSpriteToBuffer(DrawPixelInfo& dpi, const DrawSpriteArgs& args) { if (args.SourceImage.flags & G1_FLAG_RLE_COMPRESSION) { GfxRleSpriteToBuffer(dpi, args); } else if (!(args.SourceImage.flags & G1_FLAG_1)) { GfxBmpSpriteToBuffer(dpi, args); } } /** * 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 GfxDrawSpriteRawMaskedSoftware( DrawPixelInfo& dpi, const ScreenCoordsXY& scrCoords, const ImageId maskImage, const ImageId colourImage) { int32_t left, top, right, bottom, width, height; auto imgMask = GfxGetG1Element(maskImage); auto imgColour = GfxGetG1Element(colourImage); if (imgMask == nullptr || imgColour == nullptr) { return; } // Must have transparency in order to pass check if (!(imgMask->flags & G1_FLAG_HAS_TRANSPARENCY) || !(imgColour->flags & G1_FLAG_HAS_TRANSPARENCY)) { GfxDrawSpriteSoftware(dpi, colourImage, scrCoords); return; } if (dpi.zoom_level != ZoomLevel{ 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); auto offsetCoords = scrCoords + ScreenCoordsXY{ imgMask->x_offset, imgMask->y_offset }; left = std::max(dpi.x, offsetCoords.x); top = std::max(dpi.y, offsetCoords.y); right = std::min(dpi.x + dpi.width, offsetCoords.x + width); bottom = std::min(dpi.y + dpi.height, offsetCoords.y + height); width = right - left; height = bottom - top; if (width < 0 || height < 0) return; int32_t skipX = left - offsetCoords.x; int32_t skipY = top - offsetCoords.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); MaskFn(width, height, maskSrc, colourSrc, dst, maskWrap, colourWrap, dstWrap); } const G1Element* GfxGetG1Element(const ImageId imageId) { return GfxGetG1Element(imageId.GetIndex()); } const G1Element* GfxGetG1Element(ImageIndex image_id) { Guard::Assert(!gOpenRCT2NoGraphics, "GfxGetG1Element called on headless instance"); auto offset = static_cast(image_id); if (offset == 0x7FFFF || offset == ImageIndexUndefined) { return nullptr; } if (offset == SPR_TEMP) { return &_g1Temp; } if (offset < SPR_RCTC_G1_END) { if (offset < _g1.elements.size()) { return &_g1.elements[offset]; } } else if (offset < SPR_G2_END) { size_t idx = offset - SPR_G2_BEGIN; if (idx < _g2.header.num_entries) { return &_g2.elements[idx]; } LOG_WARNING("Invalid entry in g2.dat requested, idx = %u. You may have to update your g2.dat.", idx); } else if (offset < SPR_CSG_END) { if (IsCsgLoaded()) { size_t idx = offset - SPR_CSG_BEGIN; if (idx < _csg.header.num_entries) { return &_csg.elements[idx]; } LOG_WARNING("Invalid entry in csg.dat requested, idx = %u.", idx); } } else if (offset < SPR_SCROLLING_TEXT_END) { size_t idx = offset - SPR_SCROLLING_TEXT_START; if (idx < std::size(_scrollingText)) { return &_scrollingText[idx]; } } else if (offset < SPR_IMAGE_LIST_END) { size_t idx = offset - SPR_IMAGE_LIST_BEGIN; if (idx < _imageListElements.size()) { return &_imageListElements[idx]; } } return nullptr; } void GfxSetG1Element(ImageIndex imageId, const G1Element* g1) { bool isTemp = imageId == SPR_TEMP; bool isValid = (imageId >= SPR_IMAGE_LIST_BEGIN && imageId < SPR_IMAGE_LIST_END) || (imageId >= SPR_SCROLLING_TEXT_START && imageId < SPR_SCROLLING_TEXT_END); #ifdef DEBUG Guard::Assert(!gOpenRCT2NoGraphics, "GfxSetG1Element called on headless instance"); Guard::Assert(isValid || isTemp, "GfxSetG1Element called with unexpected image id"); Guard::Assert(g1 != nullptr, "g1 was nullptr"); #endif if (g1 != nullptr) { if (isTemp) { _g1Temp = *g1; } else if (isValid) { if (imageId < SPR_RCTC_G1_END) { if (imageId < static_cast(_g1.elements.size())) { _g1.elements[imageId] = *g1; } } else if (imageId < SPR_SCROLLING_TEXT_END) { size_t idx = static_cast(imageId) - SPR_SCROLLING_TEXT_START; if (idx < std::size(_scrollingText)) { _scrollingText[idx] = *g1; } } else { size_t idx = static_cast(imageId) - SPR_IMAGE_LIST_BEGIN; // Grow the element buffer if necessary while (idx >= _imageListElements.size()) { _imageListElements.resize(std::max(256, _imageListElements.size() * 2)); } _imageListElements[idx] = *g1; } } } } bool IsCsgLoaded() { return _csgLoaded; } size_t G1CalculateDataSize(const G1Element* g1) { if (g1->flags & G1_FLAG_PALETTE) { return g1->width * 3; } if (g1->flags & G1_FLAG_RLE_COMPRESSION) { if (g1->offset == nullptr) { return 0; } auto idx = (g1->height - 1) * 2; uint16_t offset = g1->offset[idx] | (g1->offset[idx + 1] << 8); uint8_t* ptr = g1->offset + offset; 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; } return g1->width * g1->height; }