From 8f30ed910f1e0831c96e4edf6cd02132a46d8121 Mon Sep 17 00:00:00 2001 From: Ted John Date: Wed, 23 Feb 2022 02:02:07 +0000 Subject: [PATCH] Start working on custom image APIs --- distribution/openrct2.d.ts | 102 +++++++++++++ src/openrct2-ui/scripting/ScImageManager.hpp | 147 +++++++++++++++++++ src/openrct2/scripting/ScriptEngine.cpp | 5 + 3 files changed, 254 insertions(+) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 583e90b330..45b3e09212 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -2794,6 +2794,108 @@ declare global { * Useful for displaying how fragmented the allocated image list is. */ getAvailableAllocationRanges(): ImageIndexRange[]; + + /** + * Allocates one or more contigous image IDs. + * @param count The number of image IDs to allocate. + * @returns the range of allocated image IDs or null if the range could not be allocated. + */ + allocate(count: number): ImageIndexRange | null; + + /** + * Frees one or more contigous image IDs. + * An error will occur if attempting the given range contains an ID not owned by the plugin. + * @param range The range of images to free. + */ + free(range: ImageIndexRange): void; + + /** + * Sets the pixel data for a given image ID. + * + * Will error if given an ID of an image not owned by this plugin. + * @param id The id of the image to set the pixels of. + * @param data The pixel data. + */ + setPixelData(id: number, data: PixelData): void; + + /** + * Gets a {@link GraphicsContext} for the given image so that you can draw directly to it. + * Allocates or reallocates the image if not previously allocated or if the size is changed. + * The pixels of the image will persist between calls, so you can draw over the top of what + * is currently there. The default pixel colour will be 0 (transparent). + * + * Drawing a large number of pixels each frame can be expensive, so caching as many as you + * can in images is a good way to improve performance. + * + * Will error if given an ID of an image not owned by this plugin. + * @param id The id of the image to get a {@link GraphicsContext} for. + * @param size The size the image that should be allocated. + */ + getGraphicsContext(id: number, size: ScreenSize): GraphicsContext; + } + + type PixelData = RawPixelData | RlePixelData | PngPixelData; + + /** + * Raw pixel data that is not encoded. A contiguous sequence of bytes + * representing the 8bpp pixel values with a optional padding between + * each horizontal row. + */ + interface RawPixelData { + type: 'raw'; + width: number; + height: number; + + /** + * The length of each horizontal row in bytes. + */ + stride?: number; + + /** + * Data can either by a: + * - A base64 string. + * - An array of bytes + * - A {@link Uint8Array} of bytes + */ + data: string | number | Uint8Array; + } + + /** + * Pixel data that is encoded as RCT run-length encoded data. + */ + interface RlePixelData { + type: 'rle'; + width: number; + height: number; + + /** + * Data can either by a: + * - A base64 string. + * - An array of bytes + * - A {@link Uint8Array} of bytes + */ + data: string | number | Uint8Array; + } + + /** + * Pixel data that is encoded as a .png file. + */ + interface PngPixelData { + type: 'png'; + + /** + * If keep is specified for palette, the raw 8bpp .png bytes will be loaded. The palette + * in the .png will not be read. This will improve load performance. + */ + palette?: 'keep'; + + /** + * Data can either by a: + * - A base64 string. + * - An array of bytes + * - A {@link Uint8Array} of bytes + */ + data: string | number | Uint8Array; } interface ImageIndexRange { diff --git a/src/openrct2-ui/scripting/ScImageManager.hpp b/src/openrct2-ui/scripting/ScImageManager.hpp index cb456e4315..983887034b 100644 --- a/src/openrct2-ui/scripting/ScImageManager.hpp +++ b/src/openrct2-ui/scripting/ScImageManager.hpp @@ -32,6 +32,9 @@ namespace OpenRCT2::Scripting { dukglue_register_method(ctx, &ScImageManager::getPredefinedRange, "getPredefinedRange"); dukglue_register_method(ctx, &ScImageManager::getAvailableAllocationRanges, "getAvailableAllocationRanges"); + dukglue_register_method(ctx, &ScImageManager::allocate, "allocate"); + dukglue_register_method(ctx, &ScImageManager::free, "free"); + dukglue_register_method(ctx, &ScImageManager::setPixelData, "setPixelData"); } private: @@ -74,6 +77,150 @@ namespace OpenRCT2::Scripting return DukValue::take_from_stack(_ctx); } + DukValue allocate(int32_t count) + { + std::vector images; + images.resize(count); + + auto base = gfx_object_allocate_images(images.data(), count); + if (base == ImageIndexUndefined) + { + return ToDuk(_ctx, nullptr); + } + return CreateImageIndexRange(base, count); + } + + void free(const DukValue& range) + { + auto start = range["start"].as_int(); + auto count = range["count"].as_int(); + gfx_object_free_images(start, count); + } + + static void setPixelDataFromBuffer( + uint8_t* dst, const uint8_t* src, size_t srcLen, int32_t width, int32_t height, int32_t stride) + { + auto srcEnd = src + srcLen; + auto dstLen = static_cast(width) * height; + if (stride == width) + { + std::memcpy(dst, src, std::min(srcLen, dstLen)); + if (dstLen > srcLen) + { + std::memset(dst + srcLen, 0, dstLen - srcLen); + } + } + else + { + std::memset(dst, 0, dstLen); + auto srcLine = src; + for (int32_t y = 0; y < height; y++) + { + auto dstI = y * width; + auto lineWidth = std::min(srcEnd - srcLine, width); + std::memcpy(dst + dstI, srcLine, lineWidth); + if (lineWidth < width) + { + break; + } + srcLine += stride; + } + } + } + + void setPixelData(int32_t id, const DukValue& pixelData) + { + auto dukType = pixelData["type"]; + auto& type = dukType.as_string(); + if (type == "raw") + { + auto width = pixelData["width"].as_int(); + auto height = pixelData["height"].as_int(); + auto stride = AsOrDefault(pixelData["stride"], width); + auto data = pixelData["data"]; + auto padding = stride - width; + + auto imageData = new (std::nothrow) uint8_t[width * height]; + if (imageData == nullptr) + { + duk_pop(_ctx); + duk_error(_ctx, DUK_ERR_ERROR, "Unable to allocate memory for pixel data."); + } + else + { + // Setup the g1 element + rct_g1_element el{}; + auto* lastel = gfx_get_g1_element(id); + if (lastel != nullptr) + { + el = *lastel; + delete el.offset; + } + + el.offset = imageData; + el.width = width; + el.height = height; + gfx_set_g1_element(id, &el); + + // Set the pixel data + if (data.is_array()) + { + // From array of numbers + data.push(); + duk_uarridx_t i = 0; + for (int32_t y = 0; y < height; y++) + { + for (int32_t x = 0; x < width; x++) + { + auto dstI = y * width + x; + if (duk_get_prop_index(_ctx, -1, i)) + { + imageData[dstI] = duk_get_int(_ctx, -1) & 0xFF; + duk_pop(_ctx); + } + else + { + imageData[dstI] = 0; + } + i++; + } + i += padding; + } + duk_pop(_ctx); + } + else if (data.type() == DukValue::Type::STRING) + { + // From base64 string + data.push(); + duk_base64_decode(_ctx, -1); + duk_size_t bufferLen{}; + const auto* buffer = reinterpret_cast(duk_get_buffer_data(_ctx, -1, &bufferLen)); + if (buffer != nullptr) + { + setPixelDataFromBuffer(imageData, buffer, bufferLen, width, height, stride); + } + duk_pop(_ctx); + } + else if (data.type() == DukValue::Type::OBJECT) + { + // From Uint8Array + data.push(); + duk_size_t bufferLen{}; + const auto* buffer = reinterpret_cast(duk_get_buffer_data(_ctx, -1, &bufferLen)); + if (buffer != nullptr) + { + setPixelDataFromBuffer(imageData, buffer, bufferLen, width, height, stride); + } + duk_pop(_ctx); + } + } + } + else + { + duk_error(_ctx, DUK_ERR_ERROR, "Unsupported pixel data type."); + } + } + DukValue CreateImageIndexRange(size_t start, size_t count) const { DukObject obj(_ctx); diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 7d91c10924..fdc716dbd1 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -590,6 +590,11 @@ void ScriptEngine::StopUnloadRegisterAllPlugins() void ScriptEngine::LoadTransientPlugins() { + if (!_initialised) + { + Initialise(); + RefreshPlugins(); + } _transientPluginsEnabled = true; }