From 45d32bec6893baa54fdab77231ca8b75babd0ce1 Mon Sep 17 00:00:00 2001 From: Ted John Date: Sat, 9 May 2020 12:55:24 +0100 Subject: [PATCH] [Plugin] Add API for taking captures and giant captures of the park --- distribution/openrct2.d.ts | 43 +++++++++++++ src/openrct2/core/FileSystem.hpp | 15 +++++ src/openrct2/interface/Screenshot.cpp | 88 ++++++++++++++++++++++++++- src/openrct2/interface/Screenshot.h | 23 ++++++- src/openrct2/scripting/ScContext.hpp | 35 +++++++++++ 5 files changed, 202 insertions(+), 2 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index e6eace8c3b..a118062035 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -138,6 +138,13 @@ declare global { */ sharedStorage: Configuration; + /** + * Render the current state of the map and save to disc. + * Useful for server administration and timelapse creation. + * @param options Options that control the capture and output file. + */ + captureImage(options: CaptureOptions): void; + /** * Gets the loaded object at the given index. * @param type The object type. @@ -214,6 +221,42 @@ declare global { has(key: string): boolean; } + interface CaptureOptions { + /** + * A relative filename from the screenshot directory to save the capture as. + * By default, the filename will be automatically generated using the system date and time. + */ + filename?: string; + + /** + * Width of the capture in pixels. + * Do not set if you would like a giant screenshot. + */ + width?: number; + + /** + * Height of the capture in pixels. + * Do not set if you would like a giant screenshot. + */ + height?: number; + + /** + * Map position to centre the view on in map units. + * Do not set if you would like a giant screenshot. + */ + position?: CoordsXY; + + /** + * The zoom level, 0 is 1:1, 1 is 2:1, 2 is 4:1 etc. + */ + zoom: number; + + /** + * Rotation of the camera from 0 to 3. + */ + rotation: number; + } + type ObjectType = "ride" | "small_scenery" | diff --git a/src/openrct2/core/FileSystem.hpp b/src/openrct2/core/FileSystem.hpp index e33f6fc4cf..2f1c1fd6b8 100644 --- a/src/openrct2/core/FileSystem.hpp +++ b/src/openrct2/core/FileSystem.hpp @@ -35,7 +35,22 @@ # include namespace fs = std::filesystem; #else +# ifdef _WIN32 +# ifndef NOMINMAX +# define NOMINMAX +# endif +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# define BITMAP WIN32_BITMAP +# endif # include +# ifdef _WIN32 +# undef CreateDirectory +# undef CreateWindow +# undef GetMessage +# undef BITMAP +# endif namespace fs = ghc::filesystem; #endif diff --git a/src/openrct2/interface/Screenshot.cpp b/src/openrct2/interface/Screenshot.cpp index a27fcd15db..0feeaaf2f3 100644 --- a/src/openrct2/interface/Screenshot.cpp +++ b/src/openrct2/interface/Screenshot.cpp @@ -1,5 +1,5 @@ /***************************************************************************** - * Copyright (c) 2014-2019 OpenRCT2 developers + * Copyright (c) 2014-2020 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 @@ -758,3 +758,89 @@ int32_t cmdline_for_screenshot(const char** argv, int32_t argc, ScreenshotOption return exitCode; } + +static bool IsPathChildOf(fs::path x, const fs::path& parent) +{ + auto xp = x.parent_path(); + while (xp != x) + { + if (xp == parent) + { + return true; + } + x = xp; + xp = x.parent_path(); + } + return false; +} + +static std::string ResolveFilenameForCapture(const fs::path& filename) +{ + if (filename.empty()) + { + // Automatic filename + auto path = screenshot_get_next_path(); + if (!path) + { + throw std::runtime_error("Unable to generate a filename for capture."); + } + return *path; + } + else + { + auto screenshotDirectory = fs::u8path(screenshot_get_directory()); + auto screenshotPath = fs::absolute(screenshotDirectory / filename); + + // Check the filename isn't attempting to leave the screenshot directory for security + if (!IsPathChildOf(screenshotPath, screenshotDirectory)) + { + throw std::runtime_error("Filename is not a child of the screenshot directory."); + } + + auto directory = screenshotPath.parent_path(); + if (!fs::is_directory(directory)) + { + if (!fs::create_directory(directory, screenshotDirectory)) + { + throw std::runtime_error("Unable to create directory."); + } + } + + return screenshotPath.string(); + } +} + +void CaptureImage(const CaptureOptions& options) +{ + rct_viewport viewport{}; + if (options.View) + { + viewport.width = options.View->Width; + viewport.height = options.View->Height; + viewport.view_width = viewport.width; + viewport.view_height = viewport.height; + + auto z = tile_element_height(options.View->Position); + CoordsXYZ coords3d(options.View->Position, z); + auto coords2d = translate_3d_to_2d_with_z(options.Rotation, coords3d); + viewport.viewPos = { coords2d.x - ((viewport.view_width * options.Zoom) / 2), + coords2d.y - ((viewport.view_height * options.Zoom) / 2) }; + viewport.zoom = options.Zoom; + } + else + { + viewport = GetGiantViewport(gMapSize, options.Rotation, options.Zoom); + } + + auto backupRotation = gCurrentRotation; + gCurrentRotation = options.Rotation; + + auto outputPath = ResolveFilenameForCapture(options.Filename); + auto dpi = CreateDPI(viewport); + RenderViewport(nullptr, viewport, dpi); + auto renderedPalette = screenshot_get_rendered_palette(); + WriteDpiToFile(outputPath, &dpi, renderedPalette); + ReleaseDPI(dpi); + + gCurrentRotation = backupRotation; +} diff --git a/src/openrct2/interface/Screenshot.h b/src/openrct2/interface/Screenshot.h index f0e29bf8ba..fa2306b307 100644 --- a/src/openrct2/interface/Screenshot.h +++ b/src/openrct2/interface/Screenshot.h @@ -1,5 +1,5 @@ /***************************************************************************** - * Copyright (c) 2014-2019 OpenRCT2 developers + * Copyright (c) 2014-2020 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 @@ -10,7 +10,11 @@ #pragma once #include "../common.h" +#include "../core/FileSystem.hpp" +#include "../world/Location.hpp" +#include "ZoomLevel.hpp" +#include #include struct rct_drawpixelinfo; @@ -31,6 +35,21 @@ struct ScreenshotOptions bool transparent = false; }; +struct CaptureView +{ + int32_t Width{}; + int32_t Height{}; + CoordsXY Position; +}; + +struct CaptureOptions +{ + fs::path Filename; + std::optional View; + ZoomLevel Zoom; + uint8_t Rotation{}; +}; + void screenshot_check(); std::string screenshot_dump(); std::string screenshot_dump_png(rct_drawpixelinfo* dpi); @@ -39,3 +58,5 @@ std::string screenshot_dump_png_32bpp(int32_t width, int32_t height, const void* void screenshot_giant(); int32_t cmdline_for_screenshot(const char** argv, int32_t argc, ScreenshotOptions* options); int32_t cmdline_for_gfxbench(const char** argv, int32_t argc); + +void CaptureImage(const CaptureOptions& options); diff --git a/src/openrct2/scripting/ScContext.hpp b/src/openrct2/scripting/ScContext.hpp index 2d6c17ac1d..6b0725ad6d 100644 --- a/src/openrct2/scripting/ScContext.hpp +++ b/src/openrct2/scripting/ScContext.hpp @@ -12,6 +12,7 @@ #ifdef ENABLE_SCRIPTING # include "../actions/GameAction.h" +# include "../interface/Screenshot.h" # include "../object/ObjectManager.h" # include "../scenario/Scenario.h" # include "Duktape.hpp" @@ -51,6 +52,39 @@ namespace OpenRCT2::Scripting return std::make_shared(scriptEngine.GetSharedStorage()); } + void captureImage(const DukValue& options) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + try + { + CaptureOptions captureOptions; + captureOptions.Filename = fs::u8path(AsOrDefault(options["filename"], "")); + captureOptions.Rotation = options["rotation"].as_int() & 3; + captureOptions.Zoom = ZoomLevel(options["zoom"].as_int()); + + auto dukPosition = options["position"]; + if (dukPosition.type() == DukValue::Type::OBJECT) + { + CaptureView view; + view.Width = options["width"].as_int(); + view.Height = options["height"].as_int(); + view.Position.x = dukPosition["x"].as_int(); + view.Position.y = dukPosition["y"].as_int(); + captureOptions.View = view; + } + + CaptureImage(captureOptions); + } + catch (const DukException&) + { + duk_error(ctx, DUK_ERR_ERROR, "Invalid options."); + } + catch (const std::exception& ex) + { + duk_error(ctx, DUK_ERR_ERROR, ex.what()); + } + } + static DukValue CreateScObject(duk_context* ctx, uint8_t type, int32_t index) { switch (type) @@ -247,6 +281,7 @@ namespace OpenRCT2::Scripting { dukglue_register_property(ctx, &ScContext::configuration_get, nullptr, "configuration"); dukglue_register_property(ctx, &ScContext::sharedStorage_get, nullptr, "sharedStorage"); + dukglue_register_method(ctx, &ScContext::captureImage, "captureImage"); dukglue_register_method(ctx, &ScContext::getObject, "getObject"); dukglue_register_method(ctx, &ScContext::getAllObjects, "getAllObjects"); dukglue_register_method(ctx, &ScContext::getRandom, "getRandom");