mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-23 06:44:38 +01:00
Merge pull request #16872 from IntelOrca/plugin/image-apis
[Plugin] Add support for custom images
This commit is contained in:
@@ -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.
|
||||
|
||||
119
distribution/openrct2.d.ts
vendored
119
distribution/openrct2.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<ClInclude Include="interface\Viewport.h" />
|
||||
<ClInclude Include="interface\Widget.h" />
|
||||
<ClInclude Include="interface\Window.h" />
|
||||
<ClInclude Include="scripting\CustomImages.h" />
|
||||
<ClInclude Include="scripting\CustomListView.h" />
|
||||
<ClInclude Include="scripting\CustomMenu.h" />
|
||||
<ClInclude Include="scripting\CustomWindow.h" />
|
||||
@@ -105,6 +106,7 @@
|
||||
<ClCompile Include="interface\ViewportInteraction.cpp" />
|
||||
<ClCompile Include="interface\Widget.cpp" />
|
||||
<ClCompile Include="interface\Window.cpp" />
|
||||
<ClCompile Include="scripting\CustomImages.cpp" />
|
||||
<ClCompile Include="scripting\CustomListView.cpp" />
|
||||
<ClCompile Include="scripting\CustomMenu.cpp" />
|
||||
<ClCompile Include="scripting\CustomWindow.cpp" />
|
||||
|
||||
471
src/openrct2-ui/scripting/CustomImages.cpp
Normal file
471
src/openrct2-ui/scripting/CustomImages.cpp
Normal file
@@ -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 <openrct2/Context.h>
|
||||
# include <openrct2/drawing/Image.h>
|
||||
# include <openrct2/drawing/ImageImporter.h>
|
||||
# include <openrct2/drawing/X8DrawingEngine.h>
|
||||
# include <openrct2/scripting/Plugin.h>
|
||||
|
||||
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<Plugin> Owner;
|
||||
ImageList Range;
|
||||
};
|
||||
|
||||
static std::vector<AllocatedImageList> _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<ImageList> AllocateCustomImages(const std::shared_ptr<Plugin>& plugin, uint32_t count)
|
||||
{
|
||||
std::vector<rct_g1_element> 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>& 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>& 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>& 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> 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<ScreenCoordsXY>(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<uint8_t> GetBufferFromDukStack(duk_context* ctx)
|
||||
{
|
||||
std::vector<uint8_t> result;
|
||||
duk_size_t bufferLen{};
|
||||
const auto* buffer = reinterpret_cast<uint8_t*>(duk_get_buffer_data(ctx, -1, &bufferLen));
|
||||
if (buffer != nullptr)
|
||||
{
|
||||
result.resize(bufferLen);
|
||||
std::memcpy(result.data(), buffer, bufferLen);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> DukGetDataFromBufferLikeObject(const DukValue& data)
|
||||
{
|
||||
std::vector<uint8_t> 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<uint8_t> RemovePadding(const std::vector<uint8_t>& srcData, const PixelData& pixelData)
|
||||
{
|
||||
std::vector<uint8_t> 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<uint8_t> GetBufferFromPixelData(duk_context* ctx, PixelData& pixelData)
|
||||
{
|
||||
std::vector<uint8_t> 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<PixelDataKind>(dukPixelData["type"]);
|
||||
pixelData.Palette = FromDuk<PixelDataPaletteKind>(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<uint8_t>&& 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<X8DrawingEngine>(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<ScGraphicsContext>(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
|
||||
32
src/openrct2-ui/scripting/CustomImages.h
Normal file
32
src/openrct2-ui/scripting/CustomImages.h
Normal file
@@ -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 <openrct2/drawing/Image.h>
|
||||
# include <openrct2/drawing/ImageId.hpp>
|
||||
# include <openrct2/scripting/Duktape.hpp>
|
||||
# include <openrct2/scripting/ScriptEngine.h>
|
||||
|
||||
namespace OpenRCT2::Scripting
|
||||
{
|
||||
void InitialiseCustomImages(ScriptEngine& scriptEngine);
|
||||
std::optional<ImageList> AllocateCustomImages(const std::shared_ptr<Plugin>& plugin, uint32_t count);
|
||||
bool FreeCustomImages(const std::shared_ptr<Plugin>& plugin, ImageList range);
|
||||
bool DoesPluginOwnImage(const std::shared_ptr<Plugin>& 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
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
#ifdef ENABLE_SCRIPTING
|
||||
|
||||
# include "CustomImages.h"
|
||||
|
||||
# include <openrct2/drawing/Drawing.h>
|
||||
# include <openrct2/scripting/Duktape.hpp>
|
||||
|
||||
@@ -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<ScreenCoordsXY>(_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)
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
#ifdef ENABLE_SCRIPTING
|
||||
|
||||
# include "CustomImages.h"
|
||||
|
||||
# include <openrct2/Context.h>
|
||||
# include <openrct2/drawing/Image.h>
|
||||
# include <openrct2/scripting/Duktape.hpp>
|
||||
# include <openrct2/sprites.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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> plugin) -> void { CloseWindowsOwnedByPlugin(plugin); });
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ImageId.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -590,6 +590,11 @@ void ScriptEngine::StopUnloadRegisterAllPlugins()
|
||||
|
||||
void ScriptEngine::LoadTransientPlugins()
|
||||
{
|
||||
if (!_initialised)
|
||||
{
|
||||
Initialise();
|
||||
RefreshPlugins();
|
||||
}
|
||||
_transientPluginsEnabled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user