diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 24b2fb40f8..c87689564e 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -22,6 +22,7 @@ - Feature: [#16800] [Plugin] Add lift hill speed properties to API. - Feature: [#16806] Parkobj can load sprites from RCT image archives. - Feature: [#16831] Allow ternary colours for small and large scenery objects. +- Feature: [#16872] [Plugin] Add support for custom images. - Improved: [#3517] Cheats are now saved with the park. - Improved: [#10150] Ride stations are now properly checked if they’re sheltered. - Improved: [#10664, #16072] Visibility status can be modified directly in the Tile Inspector's list. diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 583e90b330..cb3b6329d1 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -2794,6 +2794,125 @@ 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; + + /** + * Gets the metadata for a given image. + */ + getImageInfo(id: number): ImageInfo | undefined; + + /** + * Gets the pixel data for a given image ID. + */ + getPixelData(id: number): PixelData | undefined; + + /** + * 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; + + /** + * Calls the given function with a {@link GraphicsContext} for the given image, allowing the + * ability to 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 draw to. + * @param size The size the image that should be allocated. + * @param callback The function that will draw to the image. + */ + draw(id: number, size: ScreenSize, callback: (g: GraphicsContext) => void): void; + } + + 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'; + + /** + * How the colours of the .png file are converted to the OpenRCT2 palette. + * 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. + * Closest will find the closest matching colour from the OpenRCT2 palette. + * Dither will add noise to reduce colour banding for images rich in colour. + * If undefined, only colours that are in OpenRCT2 palette will be imported. + */ + palette?: 'keep' | 'closest' | 'dither'; + + /** + * 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/libopenrct2ui.vcxproj b/src/openrct2-ui/libopenrct2ui.vcxproj index 7cc750c533..a0a67bf64c 100644 --- a/src/openrct2-ui/libopenrct2ui.vcxproj +++ b/src/openrct2-ui/libopenrct2ui.vcxproj @@ -51,6 +51,7 @@ + @@ -105,6 +106,7 @@ + diff --git a/src/openrct2-ui/scripting/CustomImages.cpp b/src/openrct2-ui/scripting/CustomImages.cpp new file mode 100644 index 0000000000..100cb02b57 --- /dev/null +++ b/src/openrct2-ui/scripting/CustomImages.cpp @@ -0,0 +1,471 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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 = gfx_get_g1_element(index); + if (g1 != nullptr) + { + // Free pixel data + delete[] g1->offset; + + // Replace slot with empty element + rct_g1_element empty{}; + gfx_set_g1_element(index, &empty); + } + } + gfx_object_free_images(range.BaseId, range.Count); + } + + std::optional AllocateCustomImages(const std::shared_ptr& plugin, uint32_t count) + { + std::vector images; + images.resize(count); + + auto base = gfx_object_allocate_images(images.data(), count); + if (base == ImageIndexUndefined) + { + 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 = gfx_get_g1_element(id); + if (g1 == nullptr) + { + return ToDuk(ctx, undefined); + } + + DukObject obj(ctx); + obj.Set("id", id); + obj.Set("offset", ToDuk(ctx, { g1->x_offset, g1->y_offset })); + obj.Set("width", g1->width); + obj.Set("height", g1->height); + + obj.Set("isBMP", (g1->flags & G1_FLAG_BMP) != 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->zoomed_offset); + } + else + { + obj.Set("nextZoomId", undefined); + } + return obj.Take(); + } + + static const char* GetPixelDataTypeForG1(const rct_g1_element& 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 = gfx_get_g1_element(id); + if (g1 == nullptr) + { + return ToDuk(ctx, undefined); + } + auto dataSize = g1_calculate_data_size(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 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); + break; + } + case PixelDataKind::Rle: + { + imageData = DukGetDataFromBufferLikeObject(pixelData.Data); + break; + } + case PixelDataKind::Png: + { + auto imageFormat = pixelData.Palette == PixelDataPaletteKind::Keep ? IMAGE_FORMAT::PNG : IMAGE_FORMAT::PNG_32; + auto palette = pixelData.Palette == PixelDataPaletteKind::Keep ? ImageImporter::Palette::KeepIndices + : ImageImporter::Palette::OpenRCT2; + auto importMode = ImageImporter::ImportMode::Default; + if (pixelData.Palette == PixelDataPaletteKind::Closest) + importMode = ImageImporter::ImportMode::Closest; + else if (pixelData.Palette == PixelDataPaletteKind::Dither) + importMode = ImageImporter::ImportMode::Dithering; + auto pngData = DukGetDataFromBufferLikeObject(pixelData.Data); + auto image = Imaging::ReadFromBuffer(pngData, imageFormat); + + ImageImporter importer; + auto importResult = importer.Import(image, 0, 0, palette, ImageImporter::ImportFlags::RLE, importMode); + + 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 + rct_g1_element el{}; + auto* lastel = gfx_get_g1_element(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; + } + gfx_set_g1_element(id, &el); + drawing_engine_invalidate_image(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()); + rct_drawpixelinfo dpi; + dpi.DrawingEngine = drawingEngine.get(); + dpi.width = size.width; + dpi.height = size.height; + + auto createNewImage = false; + auto g1 = gfx_get_g1_element(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; + dpi.bits = new uint8_t[bufferSize]; + std::memset(dpi.bits, 0, bufferSize); + + // Draw the original image if we are creating a new one + gfx_draw_sprite(&dpi, ImageId(id), { 0, 0 }); + } + else + { + dpi.bits = g1->offset; + } + + auto dukG = GetObjectAsDukValue(ctx, std::make_shared(ctx, dpi)); + scriptEngine.ExecutePluginCall(plugin, callback, { dukG }, false); + + if (createNewImage) + { + rct_g1_element newg1{}; + if (g1 != nullptr) + { + delete[] g1->offset; + newg1 = *g1; + } + newg1.offset = dpi.bits; + newg1.width = size.width; + newg1.height = size.height; + newg1.flags = 0; + gfx_set_g1_element(id, &newg1); + } + + drawing_engine_invalidate_image(id); + } + +} // namespace OpenRCT2::Scripting + +#endif diff --git a/src/openrct2-ui/scripting/CustomImages.h b/src/openrct2-ui/scripting/CustomImages.h new file mode 100644 index 0000000000..930586b8cc --- /dev/null +++ b/src/openrct2-ui/scripting/CustomImages.h @@ -0,0 +1,32 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 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. + *****************************************************************************/ + +#pragma once + +#ifdef ENABLE_SCRIPTING + +# include +# include +# include +# include + +namespace OpenRCT2::Scripting +{ + void InitialiseCustomImages(ScriptEngine& scriptEngine); + std::optional AllocateCustomImages(const std::shared_ptr& plugin, uint32_t count); + bool FreeCustomImages(const std::shared_ptr& plugin, ImageList range); + bool DoesPluginOwnImage(const std::shared_ptr& plugin, ImageIndex index); + DukValue DukGetImageInfo(duk_context* ctx, ImageIndex id); + DukValue DukGetImagePixelData(duk_context* ctx, ImageIndex id); + void DukSetPixelData(duk_context* ctx, ImageIndex id, const DukValue& dukPixelData); + void DukDrawCustomImage(ScriptEngine& scriptEngine, ImageIndex id, ScreenSize size, const DukValue& callback); + +} // namespace OpenRCT2::Scripting + +#endif diff --git a/src/openrct2-ui/scripting/ScGraphicsContext.hpp b/src/openrct2-ui/scripting/ScGraphicsContext.hpp index 63a7682666..7c7fcb8a1d 100644 --- a/src/openrct2-ui/scripting/ScGraphicsContext.hpp +++ b/src/openrct2-ui/scripting/ScGraphicsContext.hpp @@ -11,6 +11,8 @@ #ifdef ENABLE_SCRIPTING +# include "CustomImages.h" + # include # include @@ -149,32 +151,7 @@ namespace OpenRCT2::Scripting DukValue getImage(uint32_t id) { - auto* g1 = gfx_get_g1_element(id); - if (g1 == nullptr) - { - return ToDuk(_ctx, undefined); - } - - DukObject obj(_ctx); - obj.Set("id", id); - obj.Set("offset", ToDuk(_ctx, { g1->x_offset, g1->y_offset })); - obj.Set("width", g1->width); - obj.Set("height", g1->height); - - obj.Set("isBMP", (g1->flags & G1_FLAG_BMP) != 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->zoomed_offset); - } - else - { - obj.Set("nextZoomId", undefined); - } - return obj.Take(); + return DukGetImageInfo(_ctx, id); } DukValue measureText(const std::string& text) diff --git a/src/openrct2-ui/scripting/ScImageManager.hpp b/src/openrct2-ui/scripting/ScImageManager.hpp index cb456e4315..559ea1a646 100644 --- a/src/openrct2-ui/scripting/ScImageManager.hpp +++ b/src/openrct2-ui/scripting/ScImageManager.hpp @@ -11,6 +11,9 @@ #ifdef ENABLE_SCRIPTING +# include "CustomImages.h" + +# include # include # include # include @@ -32,6 +35,12 @@ 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::getImageInfo, "getImageInfo"); + dukglue_register_method(ctx, &ScImageManager::getPixelData, "getPixelData"); + dukglue_register_method(ctx, &ScImageManager::setPixelData, "setPixelData"); + dukglue_register_method(ctx, &ScImageManager::draw, "draw"); } private: @@ -74,6 +83,66 @@ namespace OpenRCT2::Scripting return DukValue::take_from_stack(_ctx); } + DukValue allocate(int32_t count) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + auto range = AllocateCustomImages(plugin, count); + return range ? CreateImageIndexRange(range->BaseId, range->Count) : ToDuk(_ctx, undefined); + } + + void free(const DukValue& dukRange) + { + auto start = dukRange["start"].as_int(); + auto count = dukRange["count"].as_int(); + + ImageList range(start, count); + + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + if (!FreeCustomImages(plugin, range)) + { + duk_error(_ctx, DUK_ERR_ERROR, "This plugin did not allocate the specified image range."); + } + } + + DukValue getImageInfo(int32_t id) + { + return DukGetImageInfo(_ctx, id); + } + + DukValue getPixelData(int32_t id) + { + return DukGetImagePixelData(_ctx, id); + } + + void setPixelData(int32_t id, const DukValue& pixelData) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + if (!DoesPluginOwnImage(plugin, id)) + { + duk_error(_ctx, DUK_ERR_ERROR, "This plugin did not allocate the specified image."); + } + + DukSetPixelData(_ctx, id, pixelData); + } + + void draw(int32_t id, const DukValue& dukSize, const DukValue& callback) + { + auto width = dukSize["width"].as_int(); + auto height = dukSize["height"].as_int(); + + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + if (!DoesPluginOwnImage(plugin, id)) + { + duk_error(_ctx, DUK_ERR_ERROR, "This plugin did not allocate the specified image."); + } + + DukDrawCustomImage(scriptEngine, id, { width, height }, callback); + } + DukValue CreateImageIndexRange(size_t start, size_t count) const { DukObject obj(_ctx); diff --git a/src/openrct2-ui/scripting/UiExtensions.cpp b/src/openrct2-ui/scripting/UiExtensions.cpp index 9d43a6430d..3e74f27fe5 100644 --- a/src/openrct2-ui/scripting/UiExtensions.cpp +++ b/src/openrct2-ui/scripting/UiExtensions.cpp @@ -11,6 +11,7 @@ # include "UiExtensions.h" +# include "CustomImages.h" # include "CustomMenu.h" # include "ScGraphicsContext.hpp" # include "ScImageManager.hpp" @@ -55,6 +56,7 @@ void UiScriptExtensions::Extend(ScriptEngine& scriptEngine) ScTitleSequencePark::Register(ctx); ScWindow::Register(ctx); + InitialiseCustomImages(scriptEngine); InitialiseCustomMenuItems(scriptEngine); scriptEngine.SubscribeToPluginStoppedEvent( [](std::shared_ptr plugin) -> void { CloseWindowsOwnedByPlugin(plugin); }); diff --git a/src/openrct2/drawing/Image.h b/src/openrct2/drawing/Image.h index 359caaa271..992b3b6d50 100644 --- a/src/openrct2/drawing/Image.h +++ b/src/openrct2/drawing/Image.h @@ -9,6 +9,8 @@ #pragma once +#include "ImageId.hpp" + #include #include #include @@ -17,10 +19,42 @@ struct rct_g1_element; struct ImageList { - uint32_t BaseId; - uint32_t Count; + ImageIndex BaseId{}; + ImageIndex Count{}; + + ImageList() = default; + ImageList(ImageIndex baseId, ImageIndex count) + : BaseId(baseId) + , Count(count) + { + } + + bool Contains(ImageIndex index) const + { + return index >= BaseId && index < GetEnd(); + } + + ImageIndex GetEnd() const + { + return BaseId + Count; + } + + static ImageList FromBeginEnd(ImageIndex begin, ImageIndex end) + { + return ImageList(begin, end - begin); + } }; +constexpr bool operator==(const ImageList& lhs, const ImageList& rhs) +{ + return lhs.BaseId == rhs.BaseId && lhs.Count == rhs.Count; +} + +constexpr bool operator!=(const ImageList& lhs, const ImageList& rhs) +{ + return !(lhs == rhs); +} + uint32_t gfx_object_allocate_images(const rct_g1_element* images, uint32_t count); void gfx_object_free_images(uint32_t baseImageId, uint32_t count); void gfx_object_check_all_images_freed(); diff --git a/src/openrct2/scripting/Plugin.cpp b/src/openrct2/scripting/Plugin.cpp index 89afb8f446..17a9e47094 100644 --- a/src/openrct2/scripting/Plugin.cpp +++ b/src/openrct2/scripting/Plugin.cpp @@ -89,17 +89,18 @@ void Plugin::Start() throw std::runtime_error("No main function specified."); } + _hasStarted = true; + mainFunc.push(); auto result = duk_pcall(_context, 0); if (result != DUK_ERR_NONE) { auto val = std::string(duk_safe_to_string(_context, -1)); duk_pop(_context); + _hasStarted = false; throw std::runtime_error("[" + _metadata.Name + "] " + val); } duk_pop(_context); - - _hasStarted = true; } void Plugin::StopBegin() 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; } diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index a2a16e4c3f..cfeb5505ec 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -46,7 +46,7 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { - static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 51; + static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 52; // Versions marking breaking changes. static constexpr int32_t API_VERSION_33_PEEP_DEPRECATION = 33;