/***************************************************************************** * Copyright (c) 2014-2025 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. *****************************************************************************/ #ifdef ENABLE_SCRIPTING #include "CustomImages.h" #include "ScGraphicsContext.hpp" #include #include #include #include #include using namespace OpenRCT2::Drawing; namespace OpenRCT2::Scripting { enum class PixelDataKind { Unknown, Raw, Rle, Palette, Png }; enum class PixelDataPaletteKind { None, Keep, Closest, Dither }; struct PixelData { PixelDataKind Type; int32_t Width; int32_t Height; int32_t Stride; PixelDataPaletteKind Palette; DukValue Data; }; struct AllocatedImageList { std::shared_ptr Owner; ImageList Range; }; static std::vector _allocatedImages; static void FreeImages(ImageList range) { for (ImageIndex i = 0; i < range.Count; i++) { auto index = range.BaseId + i; auto g1 = GfxGetG1Element(index); if (g1 != nullptr) { // Free pixel data delete[] g1->offset; // Replace slot with empty element G1Element empty{}; GfxSetG1Element(index, &empty); } } GfxObjectFreeImages(range.BaseId, range.Count); } std::optional AllocateCustomImages(const std::shared_ptr& plugin, uint32_t count) { std::vector images; images.resize(count); auto base = GfxObjectAllocateImages(images.data(), count); if (base == kImageIndexUndefined) { return {}; } auto range = ImageList(base, count); AllocatedImageList item; item.Owner = plugin; item.Range = range; _allocatedImages.push_back(std::move(item)); return range; } bool FreeCustomImages(const std::shared_ptr& plugin, ImageList range) { auto it = std::find_if( _allocatedImages.begin(), _allocatedImages.end(), [&plugin, range](const AllocatedImageList& item) { return item.Owner == plugin && item.Range == range; }); if (it == _allocatedImages.end()) { return false; } FreeImages(it->Range); _allocatedImages.erase(it); return true; } bool DoesPluginOwnImage(const std::shared_ptr& plugin, ImageIndex index) { auto it = std::find_if( _allocatedImages.begin(), _allocatedImages.end(), [&plugin, index](const AllocatedImageList& item) { return item.Owner == plugin && item.Range.Contains(index); }); return it != _allocatedImages.end(); } static void FreeCustomImages(const std::shared_ptr& plugin) { auto it = _allocatedImages.begin(); while (it != _allocatedImages.end()) { if (it->Owner == plugin) { FreeImages(it->Range); it = _allocatedImages.erase(it); } else { it++; } } } void InitialiseCustomImages(ScriptEngine& scriptEngine) { scriptEngine.SubscribeToPluginStoppedEvent([](std::shared_ptr plugin) -> void { FreeCustomImages(plugin); }); } DukValue DukGetImageInfo(duk_context* ctx, ImageIndex id) { auto* g1 = GfxGetG1Element(id); if (g1 == nullptr) { return ToDuk(ctx, undefined); } DukObject obj(ctx); obj.Set("id", id); obj.Set("offset", ToDuk(ctx, { g1->xOffset, g1->yOffset })); obj.Set("width", g1->width); obj.Set("height", g1->height); obj.Set("hasTransparent", (g1->flags & G1_FLAG_HAS_TRANSPARENCY) != 0); obj.Set("isRLE", (g1->flags & G1_FLAG_RLE_COMPRESSION) != 0); obj.Set("isPalette", (g1->flags & G1_FLAG_PALETTE) != 0); obj.Set("noZoom", (g1->flags & G1_FLAG_NO_ZOOM_DRAW) != 0); if (g1->flags & G1_FLAG_HAS_ZOOM_SPRITE) { obj.Set("nextZoomId", id - g1->zoomedOffset); } else { obj.Set("nextZoomId", undefined); } return obj.Take(); } static const char* GetPixelDataTypeForG1(const G1Element& g1) { if (g1.flags & G1_FLAG_RLE_COMPRESSION) return "rle"; else if (g1.flags & G1_FLAG_PALETTE) return "palette"; return "raw"; } DukValue DukGetImagePixelData(duk_context* ctx, ImageIndex id) { auto* g1 = GfxGetG1Element(id); if (g1 == nullptr) { return ToDuk(ctx, undefined); } auto dataSize = G1CalculateDataSize(g1); auto* type = GetPixelDataTypeForG1(*g1); // Copy the G1 data to a JS buffer wrapped in a Uint8Array duk_push_fixed_buffer(ctx, dataSize); duk_size_t bufferSize{}; auto* buffer = duk_get_buffer_data(ctx, -1, &bufferSize); if (buffer != nullptr && bufferSize == dataSize) { std::memcpy(buffer, g1->offset, dataSize); } duk_push_buffer_object(ctx, -1, 0, dataSize, DUK_BUFOBJ_UINT8ARRAY); duk_remove(ctx, -2); auto data = DukValue::take_from_stack(ctx, -1); DukObject obj(ctx); obj.Set("type", type); obj.Set("width", g1->width); obj.Set("height", g1->height); obj.Set("data", data); return obj.Take(); } static std::vector GetBufferFromDukStack(duk_context* ctx) { std::vector result; duk_size_t bufferLen{}; const auto* buffer = reinterpret_cast(duk_get_buffer_data(ctx, -1, &bufferLen)); if (buffer != nullptr) { result.resize(bufferLen); std::memcpy(result.data(), buffer, bufferLen); } return result; } static std::vector DukGetDataFromBufferLikeObject(const DukValue& data) { std::vector result; auto ctx = data.context(); if (data.is_array()) { // From array of numbers data.push(); auto len = duk_get_length(ctx, -1); result.resize(len); for (duk_uarridx_t i = 0; i < len; i++) { if (duk_get_prop_index(ctx, -1, i)) { result[i] = duk_get_int(ctx, -1) & 0xFF; duk_pop(ctx); } } duk_pop(ctx); } else if (data.type() == DukValue::Type::STRING) { // From base64 string data.push(); duk_base64_decode(ctx, -1); result = GetBufferFromDukStack(ctx); duk_pop(ctx); } else if (data.type() == DukValue::Type::OBJECT) { // From Uint8Array data.push(); result = GetBufferFromDukStack(ctx); duk_pop(ctx); } return result; } static std::vector RemovePadding(const std::vector& srcData, const PixelData& pixelData) { std::vector unpadded(pixelData.Width * pixelData.Height, 0); auto* src = srcData.data(); auto* dst = unpadded.data(); for (int32_t y = 0; y < pixelData.Height; y++) { std::memcpy(dst, src, pixelData.Width); src += pixelData.Stride; dst += pixelData.Width; } return unpadded; } static ImportMode getImportModeFromPalette(const PixelDataPaletteKind& palette) { switch (palette) { case PixelDataPaletteKind::Closest: return ImportMode::Closest; case PixelDataPaletteKind::Dither: return ImportMode::Dithering; case PixelDataPaletteKind::None: case PixelDataPaletteKind::Keep: default: return ImportMode::Default; } } static std::vector GetBufferFromPixelData(duk_context* ctx, PixelData& pixelData) { std::vector imageData; switch (pixelData.Type) { case PixelDataKind::Raw: { auto data = DukGetDataFromBufferLikeObject(pixelData.Data); if (pixelData.Stride != pixelData.Width) { // Make sure data is expected size for RemovePadding data.resize(pixelData.Stride * pixelData.Height); data = RemovePadding(data, pixelData); } // Make sure data is expected size data.resize(pixelData.Width * pixelData.Height); imageData = std::move(data); break; } case PixelDataKind::Rle: { imageData = DukGetDataFromBufferLikeObject(pixelData.Data); break; } case PixelDataKind::Png: { auto imageFormat = pixelData.Palette == PixelDataPaletteKind::Keep ? ImageFormat::png : ImageFormat::png32; auto palette = pixelData.Palette == PixelDataPaletteKind::Keep ? Palette::KeepIndices : Palette::OpenRCT2; auto importMode = getImportModeFromPalette(pixelData.Palette); auto pngData = DukGetDataFromBufferLikeObject(pixelData.Data); auto image = Imaging::ReadFromBuffer(pngData, imageFormat); constexpr uint8_t flags = EnumToFlag(ImportFlags::RLE); ImageImportMeta meta = { { 0, 0 }, palette, flags, importMode }; ImageImporter importer; auto importResult = importer.Import(image, meta); pixelData.Type = PixelDataKind::Rle; pixelData.Width = importResult.Element.width; pixelData.Height = importResult.Element.height; imageData = std::move(importResult.Buffer); break; } default: throw std::runtime_error("Unsupported pixel data type."); } return imageData; } template<> PixelDataKind FromDuk(const DukValue& d) { if (d.type() == DukValue::Type::STRING) { auto& s = d.as_string(); if (s == "raw") return PixelDataKind::Raw; if (s == "rle") return PixelDataKind::Rle; if (s == "palette") return PixelDataKind::Palette; if (s == "png") return PixelDataKind::Png; } return PixelDataKind::Unknown; } template<> PixelDataPaletteKind FromDuk(const DukValue& d) { if (d.type() == DukValue::Type::STRING) { auto& s = d.as_string(); if (s == "keep") return PixelDataPaletteKind::Keep; if (s == "closest") return PixelDataPaletteKind::Closest; if (s == "dither") return PixelDataPaletteKind::Dither; } return PixelDataPaletteKind::None; } static PixelData GetPixelDataFromDuk(const DukValue& dukPixelData) { PixelData pixelData; pixelData.Type = FromDuk(dukPixelData["type"]); pixelData.Palette = FromDuk(dukPixelData["palette"]); pixelData.Width = AsOrDefault(dukPixelData["width"], 0); pixelData.Height = AsOrDefault(dukPixelData["height"], 0); pixelData.Stride = AsOrDefault(dukPixelData["stride"], pixelData.Width); pixelData.Data = dukPixelData["data"]; return pixelData; } static void ReplacePixelDataForImage(ImageIndex id, const PixelData& pixelData, std::vector&& data) { // Setup the g1 element G1Element el{}; auto* lastel = GfxGetG1Element(id); if (lastel != nullptr) { el = *lastel; delete[] el.offset; } // Copy data into new unmanaged uint8_t[] auto newData = new uint8_t[data.size()]; std::memcpy(newData, data.data(), data.size()); el.offset = newData; el.width = pixelData.Width; el.height = pixelData.Height; el.flags = 0; if (pixelData.Type == PixelDataKind::Rle) { el.flags |= G1_FLAG_RLE_COMPRESSION; } GfxSetG1Element(id, &el); DrawingEngineInvalidateImage(id); } void DukSetPixelData(duk_context* ctx, ImageIndex id, const DukValue& dukPixelData) { auto pixelData = GetPixelDataFromDuk(dukPixelData); try { auto newData = GetBufferFromPixelData(ctx, pixelData); ReplacePixelDataForImage(id, pixelData, std::move(newData)); } catch (const std::runtime_error& e) { duk_error(ctx, DUK_ERR_ERROR, e.what()); } } void DukDrawCustomImage(ScriptEngine& scriptEngine, ImageIndex id, ScreenSize size, const DukValue& callback) { auto* ctx = scriptEngine.GetContext(); auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); auto drawingEngine = std::make_unique(GetContext()->GetUiContext()); RenderTarget rt; rt.DrawingEngine = drawingEngine.get(); rt.width = size.width; rt.height = size.height; auto createNewImage = false; auto g1 = GfxGetG1Element(id); if (g1 == nullptr || g1->width != size.width || g1->height != size.height || (g1->flags & G1_FLAG_RLE_COMPRESSION)) { createNewImage = true; } if (createNewImage) { auto bufferSize = size.width * size.height; rt.bits = new uint8_t[bufferSize]; std::memset(rt.bits, 0, bufferSize); drawingEngine->BeginDraw(); // Draw the original image if we are creating a new one GfxDrawSprite(rt, ImageId(id), { 0, 0 }); drawingEngine->EndDraw(); } else { rt.bits = g1->offset; } auto dukG = GetObjectAsDukValue(ctx, std::make_shared(ctx, rt)); scriptEngine.ExecutePluginCall(plugin, callback, { dukG }, false); if (createNewImage) { G1Element newg1{}; if (g1 != nullptr) { delete[] g1->offset; newg1 = *g1; } newg1.offset = rt.bits; newg1.width = size.width; newg1.height = size.height; newg1.flags = 0; GfxSetG1Element(id, &newg1); } DrawingEngineInvalidateImage(id); } } // namespace OpenRCT2::Scripting #endif