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;