1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2025-12-10 01:22:25 +01:00

Merge pull request #22646 from AaronVanGeffen/scenario-preview

Show minimap in scenario list for new .park scenarios
This commit is contained in:
Aaron van Geffen
2025-03-30 18:34:47 +02:00
committed by GitHub
3 changed files with 144 additions and 37 deletions

View File

@@ -1,5 +1,6 @@
0.4.21 (in development)
------------------------------------------------------------------------
- Feature: [#22646] New scenario files now contain a minimap image, shown in the scenario selection window.
- Feature: [#23774] Climates can now be customised using objects.
- Feature: [#23876] New park save files now contain a preview image, shown in the load/save window.
- Change: [#23932] The land rights window now checks “Land Owned” by default.

View File

@@ -13,15 +13,20 @@
#include <openrct2-ui/interface/Widget.h>
#include <openrct2-ui/windows/Windows.h>
#include <openrct2/Context.h>
#include <openrct2/Diagnostic.h>
#include <openrct2/FileClassifier.h>
#include <openrct2/ParkImporter.h>
#include <openrct2/SpriteIds.h>
#include <openrct2/audio/Audio.h>
#include <openrct2/config/Config.h>
#include <openrct2/core/FileStream.h>
#include <openrct2/core/String.hpp>
#include <openrct2/drawing/Drawing.h>
#include <openrct2/localisation/Formatter.h>
#include <openrct2/localisation/Formatting.h>
#include <openrct2/localisation/Localisation.Date.h>
#include <openrct2/localisation/LocalisationService.h>
#include <openrct2/park/ParkPreview.h>
#include <openrct2/ride/RideData.h>
#include <openrct2/scenario/Scenario.h>
#include <openrct2/scenario/ScenarioRepository.h>
@@ -31,17 +36,18 @@
namespace OpenRCT2::Ui::Windows
{
static constexpr StringId kWindowTitle = STR_SELECT_SCENARIO;
static constexpr int32_t kWindowWidth = 734;
static constexpr int32_t kWindowHeight = 384;
static constexpr int32_t kInitialNumUnlockedScenarios = 5;
static constexpr uint8_t kNumTabs = 10;
static constexpr int32_t kPreviewPaneWidth = 179;
static constexpr int32_t kSidebarWidth = 180;
static constexpr int32_t kTabWidth = 92;
static constexpr int32_t kTabHeight = 34;
static constexpr int32_t kTrueFontSize = 24;
static constexpr int32_t kWidgetsStart = 17;
static constexpr int32_t kTabsStart = kWidgetsStart;
static constexpr int32_t kInitialNumUnlockedScenarios = 5;
constexpr uint8_t kNumTabs = 10;
static constexpr int32_t kTabWidth = 92;
static constexpr int32_t kTrueFontSize = 24;
static constexpr int32_t kWindowHeight = 384;
static constexpr int32_t kWindowWidth = 734;
static constexpr StringId kWindowTitle = STR_SELECT_SCENARIO;
enum class ListItemType : uint8_t
{
@@ -117,6 +123,7 @@ namespace OpenRCT2::Ui::Windows
std::function<void(std::string_view)> _callback;
std::vector<ScenarioListItem> _listItems;
const ScenarioIndexEntry* _highlightedScenario = nullptr;
ParkPreview _preview;
public:
ScenarioSelectWindow(std::function<void(std::string_view)> callback)
@@ -150,9 +157,12 @@ namespace OpenRCT2::Ui::Windows
if (widgetIndex >= WIDX_TAB1 && widgetIndex <= WIDX_TAB10)
{
selected_tab = widgetIndex - 4;
_highlightedScenario = nullptr;
Config::Get().interface.ScenarioselectLastTab = selected_tab;
Config::Save();
_highlightedScenario = nullptr;
_preview = {};
InitialiseListItems();
Invalidate();
OnResize();
@@ -162,10 +172,69 @@ namespace OpenRCT2::Ui::Windows
}
}
void LoadPreview()
{
_preview = {};
if (_highlightedScenario == nullptr)
return;
auto& path = _highlightedScenario->Path;
auto fs = FileStream(path, FileMode::open);
ClassifiedFileInfo info;
if (!TryClassifyFile(&fs, &info) || info.Type != FileType::park)
{
// TODO: try loading a preview from a 'scenario meta' object file
return;
}
try
{
auto& objectRepository = GetContext()->GetObjectRepository();
auto parkImporter = ParkImporter::CreateParkFile(objectRepository);
parkImporter->LoadFromStream(&fs, false, true, path.c_str());
_preview = parkImporter->GetParkPreview();
}
catch (const std::exception& e)
{
LOG_ERROR("Could not get preview:", e.what());
_preview = {};
return;
}
}
ScreenCoordsXY DrawPreview(DrawPixelInfo& dpi, ScreenCoordsXY screenPos)
{
// Find minimap image to draw, if available
PreviewImage* image = nullptr;
for (auto& candidate : _preview.images)
{
if (candidate.type == PreviewImageType::miniMap)
{
image = &candidate;
break;
}
}
if (image == nullptr)
return screenPos;
// Draw frame
auto startFrameX = width - (kPreviewPaneWidth / 2) - (image->width / 2);
auto frameStartPos = ScreenCoordsXY(windowPos.x + startFrameX, screenPos.y + 15);
auto frameEndPos = frameStartPos + ScreenCoordsXY(image->width + 1, image->height + 1);
GfxFillRectInset(dpi, { frameStartPos, frameEndPos }, colours[1], INSET_RECT_F_60 | INSET_RECT_FLAG_FILL_MID_LIGHT);
// Draw image, if available
auto imagePos = frameStartPos + ScreenCoordsXY(1, 1);
drawPreviewImage(*image, dpi, imagePos);
return frameEndPos;
}
void OnDraw(DrawPixelInfo& dpi) override
{
const ScenarioIndexEntry* scenario;
DrawWidgets(dpi);
StringId format = STR_WINDOW_COLOUR_2_STRINGID;
@@ -199,7 +268,7 @@ namespace OpenRCT2::Ui::Windows
}
// Return if no scenario highlighted
scenario = _highlightedScenario;
auto* scenario = _highlightedScenario;
if (scenario == nullptr)
{
if (_showLockedInformation)
@@ -242,7 +311,10 @@ namespace OpenRCT2::Ui::Windows
ft.Add<const char*>(scenario->Name.c_str());
DrawTextEllipsised(
dpi, screenPos + ScreenCoordsXY{ 85, 0 }, 170, STR_WINDOW_COLOUR_2_STRINGID, ft, { TextAlignment::CENTRE });
screenPos.y += 15;
// Draw preview
auto previewEnd = DrawPreview(dpi, screenPos);
screenPos.y = previewEnd.y + 15;
// Scenario details
ft = Formatter();
@@ -289,7 +361,7 @@ namespace OpenRCT2::Ui::Windows
ResizeFrameWithPage();
const int32_t bottomMargin = Config::Get().general.DebuggingTools ? 17 : 5;
widgets[WIDX_SCENARIOLIST].right = width - 179;
widgets[WIDX_SCENARIOLIST].right = width - kPreviewPaneWidth;
widgets[WIDX_SCENARIOLIST].bottom = height - bottomMargin;
}
@@ -353,6 +425,7 @@ namespace OpenRCT2::Ui::Windows
if (_highlightedScenario != selected)
{
_highlightedScenario = selected;
LoadPreview();
Invalidate();
}
else if (_showLockedInformation != originalShowLockedInformation)

View File

@@ -51,14 +51,18 @@ namespace OpenRCT2
return preview;
}
static uint8_t _tileColourIndex = 0;
static PaletteIndex getPreviewColourByTilePos(const TileCoordsXY& pos)
{
PaletteIndex colour = PALETTE_INDEX_0;
PaletteIndex paletteIndex = PALETTE_INDEX_0;
auto tileElement = MapGetFirstElementAt(pos);
if (tileElement == nullptr)
return colour;
return paletteIndex;
PaletteIndex surfaceColour = paletteIndex;
bool isOutsidePark = false;
do
{
switch (tileElement->GetType())
@@ -68,39 +72,42 @@ namespace OpenRCT2
auto* surfaceElement = tileElement->AsSurface();
if (surfaceElement == nullptr)
{
colour = PALETTE_INDEX_0;
surfaceColour = paletteIndex = PALETTE_INDEX_0;
break;
}
if (surfaceElement->GetWaterHeight() > 0)
{
colour = PALETTE_INDEX_195;
break;
surfaceColour = paletteIndex = PALETTE_INDEX_195;
}
else
{
const auto* surfaceObject = surfaceElement->GetSurfaceObject();
if (surfaceObject != nullptr)
{
surfaceColour = paletteIndex = surfaceObject->MapColours[_tileColourIndex];
}
}
const auto* surfaceObject = surfaceElement->GetSurfaceObject();
if (surfaceObject != nullptr)
{
colour = surfaceObject->MapColours[1];
}
isOutsidePark |= !(surfaceElement->GetOwnership() & OWNERSHIP_OWNED);
break;
}
case TileElementType::Path:
colour = PALETTE_INDEX_17;
paletteIndex = PALETTE_INDEX_17;
break;
case TileElementType::Track:
colour = PALETTE_INDEX_183;
paletteIndex = PALETTE_INDEX_183;
break;
case TileElementType::SmallScenery:
case TileElementType::LargeScenery:
colour = PALETTE_INDEX_99; // 64
paletteIndex = PALETTE_INDEX_99; // 64
break;
case TileElementType::Entrance:
colour = PALETTE_INDEX_186;
paletteIndex = PALETTE_INDEX_186;
break;
default:
@@ -108,32 +115,58 @@ namespace OpenRCT2
}
} while (!(tileElement++)->IsLastForTile());
return colour;
// Darken every other tile that's outside of the park, unless it's a path
if (isOutsidePark && _tileColourIndex == 1 && paletteIndex != PALETTE_INDEX_17)
paletteIndex = PALETTE_INDEX_10;
// For rides, every other tile should use the surface colour
else if (_tileColourIndex == 1 && paletteIndex == PALETTE_INDEX_183)
paletteIndex = surfaceColour;
_tileColourIndex = (_tileColourIndex + 1) % 2;
return paletteIndex;
}
// 0x0046DB4C
static std::optional<PreviewImage> generatePreviewMap()
{
const auto& gameState = getGameState();
const auto previewSize = 128;
const auto longEdgeSize = std::max(gameState.mapSize.x, gameState.mapSize.y);
const auto nearestPower = Numerics::ceil2(longEdgeSize, previewSize);
const auto mapSkipFactor = nearestPower / previewSize;
const auto offset = mapSkipFactor > 0 ? (nearestPower - longEdgeSize) / mapSkipFactor : 1;
const auto drawableMapSize = TileCoordsXY{ gameState.mapSize.x - 2, gameState.mapSize.y - 2 };
const auto longEdgeSize = std::max(drawableMapSize.x, drawableMapSize.y);
const auto idealPreviewSize = 150;
auto longEdgeSizeLeft = longEdgeSize;
uint8_t mapSkipFactor = 1;
while (longEdgeSizeLeft > idealPreviewSize)
{
longEdgeSizeLeft -= idealPreviewSize;
mapSkipFactor++;
}
const uint8_t previewWidth = std::max(1, drawableMapSize.x / mapSkipFactor);
const uint8_t previewHeight = std::max(1, drawableMapSize.y / mapSkipFactor);
PreviewImage image{
.type = PreviewImageType::miniMap,
.width = previewSize,
.height = previewSize,
.width = previewWidth,
.height = previewHeight,
};
for (auto y = 0u; y < image.height; y++)
{
int32_t mapY = 1 + (y * mapSkipFactor);
if (mapY >= drawableMapSize.y)
break;
_tileColourIndex = y % 2;
for (auto x = 0u; x < image.width; x++)
{
auto pos = TileCoordsXY(gameState.mapSize.x - (x + 1) * mapSkipFactor + 1, y * mapSkipFactor + 1);
int32_t mapX = drawableMapSize.x - (x * mapSkipFactor);
if (mapX < 1)
break;
image.pixels[(y + offset) * previewSize + (x + offset)] = getPreviewColourByTilePos(pos);
image.pixels[y * image.width + x] = getPreviewColourByTilePos({ mapX, mapY });
}
}