mirror of
https://github.com/OpenRCT2/OpenRCT2.git
synced 2026-05-06 07:56:46 -04:00
45ff80d890
* Allow flag/bitmask coexistence in readers * Add factory helpers and bulk setters for per-widget flags * Switch coexistence from OR-both to per-window opt-in * Migrate seven small windows to per-widget flags * Migrate seven more windows to per-widget flags * Migrate Cheats, MapGen and Footpath to per-widget flags * Fix #26421: scenery tab highlight wraps when there are 64+ groups * Migrate Ride window to per-widget flags * Migrate ride and maze construction windows to per-widget flags * Migrate six more windows to per-widget flags * Migrate Banner, EditorScenarioOptions and RideList to per-widget flags * Migrate SceneryScatter and LoadSave to per-widget flags * Migrate EditorObjectSelection, GameBottomToolbar, Guest, Map, NewRide and Park to per-widget flags * Migrate TopToolbar, TrackList, Research, Player and Options to per-widget flags * Opt in remaining windows to per-widget flags * Narrow getters and setters to per-widget flags * Remove bitmask fields and useWidgetFlags switch * Restore switch statements for tab/density pressed state * Add makeHoldableSpinnerWidgets to drop per-page loops * Update tests for per-widget flag storage * Add changelog entry for #26421 * Reword narrative-tense comments from the migration * Fix clang-format violations * Remove dead holdable-widget tables from Park and Finances
431 lines
16 KiB
C++
431 lines
16 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2026 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.
|
|
*****************************************************************************/
|
|
|
|
#include "../UiStringIds.h"
|
|
|
|
#include <memory>
|
|
#include <openrct2-ui/interface/Widget.h>
|
|
#include <openrct2-ui/windows/Windows.h>
|
|
#include <openrct2/Context.h>
|
|
#include <openrct2/Diagnostic.h>
|
|
#include <openrct2/PlatformEnvironment.h>
|
|
#include <openrct2/SpriteIds.h>
|
|
#include <openrct2/audio/Audio.h>
|
|
#include <openrct2/core/File.h>
|
|
#include <openrct2/core/Path.hpp>
|
|
#include <openrct2/core/UnitConversion.h>
|
|
#include <openrct2/drawing/ColourMap.h>
|
|
#include <openrct2/drawing/Drawing.h>
|
|
#include <openrct2/drawing/Rectangle.h>
|
|
#include <openrct2/drawing/Text.h>
|
|
#include <openrct2/localisation/Formatter.h>
|
|
#include <openrct2/object/ObjectManager.h>
|
|
#include <openrct2/ride/RideConstruction.h>
|
|
#include <openrct2/ride/RideData.h>
|
|
#include <openrct2/ride/TrackDesign.h>
|
|
#include <openrct2/ride/TrackDesignRepository.h>
|
|
#include <openrct2/ui/WindowManager.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace OpenRCT2::Drawing;
|
|
|
|
namespace OpenRCT2::Ui::Windows
|
|
{
|
|
enum
|
|
{
|
|
WIDX_BACKGROUND,
|
|
WIDX_TITLE,
|
|
WIDX_CLOSE,
|
|
WIDX_TRACK_PREVIEW,
|
|
WIDX_ROTATE,
|
|
WIDX_TOGGLE_SCENERY,
|
|
WIDX_INSTALL,
|
|
WIDX_CANCEL
|
|
};
|
|
|
|
static constexpr StringId kWindowTitle = STR_TRACK_DESIGN_INSTALL_WINDOW_TITLE;
|
|
static constexpr ScreenSize kWindowSize = { 380, 460 };
|
|
constexpr int32_t kPreviewButtonsLeft = kWindowSize.width - 25;
|
|
constexpr int32_t kActionButtonsLeft = kWindowSize.width - 100;
|
|
|
|
// clang-format off
|
|
static constexpr auto window_install_track_widgets = makeWidgets(
|
|
makeWindowShim(kWindowTitle, kWindowSize),
|
|
makeWidget({ 4, 18}, {372, 219}, WidgetType::flatBtn, WindowColour::primary ),
|
|
makeWidget({kPreviewButtonsLeft, 422}, { 22, 24}, WidgetType::flatBtn, WindowColour::primary, ImageId(SPR_ROTATE_ARROW), STR_ROTATE_90_TIP ),
|
|
makeWidget({kPreviewButtonsLeft, 398}, { 22, 24}, WidgetType::flatBtn, WindowColour::primary, ImageId(SPR_SCENERY), STR_TOGGLE_SCENERY_TIP),
|
|
makeWidget({ kActionButtonsLeft, 241}, { 97, 15}, WidgetType::button, WindowColour::primary, STR_INSTALL_NEW_TRACK_DESIGN_INSTALL ),
|
|
makeWidget({ kActionButtonsLeft, 259}, { 97, 15}, WidgetType::button, WindowColour::primary, STR_INSTALL_NEW_TRACK_DESIGN_CANCEL )
|
|
);
|
|
// clang-format on
|
|
|
|
class InstallTrackWindow final : public Window
|
|
{
|
|
private:
|
|
std::unique_ptr<TrackDesign> _trackDesign;
|
|
std::string _trackPath;
|
|
std::string _trackName;
|
|
TrackDesignPreviewBuffer _trackDesignPreviewPixels{};
|
|
|
|
public:
|
|
void setupTrack(const utf8* path, std::unique_ptr<TrackDesign> trackDesign)
|
|
{
|
|
_trackDesign = std::move(trackDesign);
|
|
_trackPath = path;
|
|
_trackName = GetNameFromTrackPath(path);
|
|
std::fill(_trackDesignPreviewPixels.begin(), _trackDesignPreviewPixels.end(), PaletteIndex::transparent);
|
|
|
|
UpdatePreview();
|
|
invalidate();
|
|
}
|
|
|
|
void onOpen() override
|
|
{
|
|
setWidgets(window_install_track_widgets);
|
|
setWidgetPressed(WIDX_TRACK_PREVIEW, true);
|
|
|
|
WindowInitScrollWidgets(*this);
|
|
WindowPushOthersRight(*this);
|
|
}
|
|
|
|
void onClose() override
|
|
{
|
|
_trackPath.clear();
|
|
_trackName.clear();
|
|
_trackDesign = nullptr;
|
|
}
|
|
|
|
void onMouseUp(WidgetIndex widgetIndex) override
|
|
{
|
|
switch (widgetIndex)
|
|
{
|
|
case WIDX_CLOSE:
|
|
case WIDX_CANCEL:
|
|
close();
|
|
break;
|
|
case WIDX_ROTATE:
|
|
_currentTrackPieceDirection++;
|
|
_currentTrackPieceDirection %= 4;
|
|
invalidate();
|
|
break;
|
|
case WIDX_TOGGLE_SCENERY:
|
|
gTrackDesignSceneryToggle = !gTrackDesignSceneryToggle;
|
|
UpdatePreview();
|
|
invalidate();
|
|
break;
|
|
case WIDX_INSTALL:
|
|
InstallTrackDesign();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void onTextInput(WidgetIndex widgetIndex, std::string_view text) override
|
|
{
|
|
if (widgetIndex != WIDX_INSTALL || text.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_trackName = std::string(text);
|
|
|
|
onMouseUp(WIDX_INSTALL);
|
|
}
|
|
|
|
void onPrepareDraw() override
|
|
{
|
|
setWidgetPressed(WIDX_TOGGLE_SCENERY, !gTrackDesignSceneryToggle);
|
|
}
|
|
|
|
void onDraw(RenderTarget& rt) override
|
|
{
|
|
drawWidgets(rt);
|
|
|
|
// Track preview
|
|
Widget* widget = &widgets[WIDX_TRACK_PREVIEW];
|
|
auto screenPos = windowPos + ScreenCoordsXY{ widget->left + 1, widget->top + 1 };
|
|
auto colour = getColourMap(colours[0].colour).darkest;
|
|
Rectangle::fill(rt, { screenPos, screenPos + ScreenCoordsXY{ 369, 216 } }, colour);
|
|
|
|
G1Element g1temp = {};
|
|
g1temp.offset = reinterpret_cast<uint8_t*>(
|
|
_trackDesignPreviewPixels.data() + (_currentTrackPieceDirection * kTrackPreviewImageSize));
|
|
g1temp.width = 370;
|
|
g1temp.height = 217;
|
|
g1temp.flags = { G1Flag::hasTransparency };
|
|
GfxSetG1Element(SPR_TEMP_INSTALL_TRACK, &g1temp);
|
|
DrawingEngineInvalidateImage(SPR_TEMP_INSTALL_TRACK);
|
|
GfxDrawSprite(rt, ImageId(SPR_TEMP_INSTALL_TRACK), screenPos);
|
|
|
|
screenPos = windowPos + ScreenCoordsXY{ widget->midX(), widget->bottom - 12 };
|
|
|
|
// Warnings
|
|
const TrackDesign& td = *_trackDesign;
|
|
if (td.gameStateData.hasFlag(TrackDesignGameStateFlag::SceneryUnavailable))
|
|
{
|
|
if (!gTrackDesignSceneryToggle)
|
|
{
|
|
// Scenery not available
|
|
drawTextEllipsised(
|
|
rt, screenPos, 308, STR_DESIGN_INCLUDES_SCENERY_WHICH_IS_UNAVAILABLE, { TextAlignment::centre });
|
|
screenPos.y -= kListRowHeight;
|
|
}
|
|
}
|
|
|
|
// Information
|
|
screenPos = windowPos + ScreenCoordsXY{ widget->left + 1, widget->bottom + 4 };
|
|
// 0x006D3CF1 -- 0x006d3d71 missing
|
|
|
|
// Track design name & type
|
|
{
|
|
auto trackName = _trackName.c_str();
|
|
auto ft = Formatter();
|
|
ft.Add<const char*>(trackName);
|
|
drawText(rt, screenPos - ScreenCoordsXY{ 1, 0 }, STR_TRACK_DESIGN_NAME, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
|
|
// Friendly Track name
|
|
{
|
|
auto ft = Formatter();
|
|
|
|
const auto* objectEntry = ObjectManagerLoadObject(&td.trackAndVehicle.vehicleObject.Entry);
|
|
if (objectEntry != nullptr)
|
|
{
|
|
auto groupIndex = ObjectManagerGetLoadedObjectEntryIndex(objectEntry);
|
|
auto rideName = GetRideNaming(td.trackAndVehicle.rtdIndex, GetRideEntryByIndex(groupIndex));
|
|
ft.Add<StringId>(rideName.Name);
|
|
}
|
|
else
|
|
{
|
|
// Fall back on the technical track name if the vehicle object cannot be loaded
|
|
ft.Add<StringId>(GetRideTypeDescriptor(td.trackAndVehicle.rtdIndex).Naming.Name);
|
|
}
|
|
|
|
drawText(rt, screenPos, STR_TRACK_DESIGN_TYPE, ft);
|
|
screenPos.y += kListRowHeight + 4;
|
|
}
|
|
|
|
// Stats
|
|
{
|
|
fixed32_2dp rating = td.statistics.ratings.excitement;
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(rating);
|
|
drawText(rt, screenPos, STR_TRACK_LIST_EXCITEMENT_RATING, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
{
|
|
fixed32_2dp rating = td.statistics.ratings.intensity;
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(rating);
|
|
drawText(rt, screenPos, STR_TRACK_LIST_INTENSITY_RATING, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
{
|
|
fixed32_2dp rating = td.statistics.ratings.nausea;
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(rating);
|
|
drawText(rt, screenPos, STR_TRACK_LIST_NAUSEA_RATING, ft);
|
|
screenPos.y += kListRowHeight + 4;
|
|
}
|
|
|
|
const auto& rtd = GetRideTypeDescriptor(td.trackAndVehicle.rtdIndex);
|
|
if (rtd.specialType != RtdSpecialType::maze)
|
|
{
|
|
if (rtd.specialType == RtdSpecialType::miniGolf)
|
|
{
|
|
// Holes
|
|
auto ft = Formatter();
|
|
ft.Add<uint16_t>(td.statistics.holes);
|
|
drawText(rt, screenPos, STR_HOLES, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
else
|
|
{
|
|
// Maximum speed
|
|
{
|
|
uint16_t speed = ToHumanReadableSpeed(td.statistics.maxSpeed << 16);
|
|
auto ft = Formatter();
|
|
ft.Add<uint16_t>(speed);
|
|
drawText(rt, screenPos, STR_MAX_SPEED, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
// Average speed
|
|
{
|
|
uint16_t speed = ToHumanReadableSpeed(td.statistics.averageSpeed << 16);
|
|
auto ft = Formatter();
|
|
ft.Add<uint16_t>(speed);
|
|
drawText(rt, screenPos, STR_AVERAGE_SPEED, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
}
|
|
|
|
// Ride length
|
|
auto ft = Formatter();
|
|
ft.Add<StringId>(STR_RIDE_LENGTH_ENTRY);
|
|
ft.Add<uint16_t>(td.statistics.rideLength);
|
|
drawTextEllipsised(rt, screenPos, 214, STR_TRACK_LIST_RIDE_LENGTH, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
|
|
if (GetRideTypeDescriptor(td.trackAndVehicle.rtdIndex).flags.has(RtdFlag::hasGForces))
|
|
{
|
|
// Maximum positive vertical Gs
|
|
{
|
|
int32_t gForces = td.statistics.maxPositiveVerticalG;
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(gForces);
|
|
drawText(rt, screenPos, STR_MAX_POSITIVE_VERTICAL_G, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
// Maximum negative vertical Gs
|
|
{
|
|
int32_t gForces = td.statistics.maxNegativeVerticalG;
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(gForces);
|
|
drawText(rt, screenPos, STR_MAX_NEGATIVE_VERTICAL_G, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
// Maximum lateral Gs
|
|
{
|
|
int32_t gForces = td.statistics.maxLateralG;
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(gForces);
|
|
drawText(rt, screenPos, STR_MAX_LATERAL_G, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
if (td.statistics.totalAirTime != 0)
|
|
{
|
|
int32_t airTime = ToHumanReadableAirTime(td.statistics.totalAirTime);
|
|
auto ft = Formatter();
|
|
ft.Add<int32_t>(airTime);
|
|
drawText(rt, screenPos, STR_TOTAL_AIR_TIME, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
}
|
|
|
|
if (GetRideTypeDescriptor(td.trackAndVehicle.rtdIndex).flags.has(RtdFlag::hasDrops))
|
|
{
|
|
auto ft = Formatter();
|
|
ft.Add<uint16_t>(td.statistics.drops);
|
|
drawText(rt, screenPos, STR_DROPS, ft);
|
|
screenPos.y += kListRowHeight;
|
|
|
|
// Drop height is multiplied by 0.75
|
|
drawText(rt, screenPos, STR_HIGHEST_DROP_HEIGHT, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
|
|
if (td.statistics.inversions != 0)
|
|
{
|
|
// Inversions
|
|
auto ft = Formatter();
|
|
ft.Add<uint16_t>(td.statistics.inversions);
|
|
drawText(rt, screenPos, STR_INVERSIONS, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
|
|
screenPos.y += 4;
|
|
|
|
if (!td.statistics.spaceRequired.IsNull())
|
|
{
|
|
// Space required
|
|
auto ft = Formatter();
|
|
ft.Add<uint16_t>(td.statistics.spaceRequired.x);
|
|
ft.Add<uint16_t>(td.statistics.spaceRequired.y);
|
|
drawText(rt, screenPos, STR_TRACK_LIST_SPACE_REQUIRED, ft);
|
|
screenPos.y += kListRowHeight;
|
|
}
|
|
|
|
if (td.gameStateData.cost != 0)
|
|
{
|
|
auto ft = Formatter();
|
|
ft.Add<money64>(td.gameStateData.cost);
|
|
drawText(rt, screenPos, STR_TRACK_LIST_COST_AROUND, ft);
|
|
}
|
|
}
|
|
|
|
private:
|
|
void UpdatePreview()
|
|
{
|
|
TrackDesignDrawPreview(*_trackDesign, _trackDesignPreviewPixels, !gTrackDesignSceneryToggle);
|
|
}
|
|
|
|
void InstallTrackDesign()
|
|
{
|
|
auto& env = GetContext()->GetPlatformEnvironment();
|
|
auto destPath = env.GetDirectoryPath(DirBase::user, DirId::trackDesigns);
|
|
if (!Path::CreateDirectory(destPath))
|
|
{
|
|
LOG_ERROR("Unable to create directory '%s'", destPath.c_str());
|
|
ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, kStringIdNone, {});
|
|
return;
|
|
}
|
|
|
|
auto extension = trackDesignGetExtension(_trackDesign->version);
|
|
destPath = Path::Combine(destPath, _trackName + extension);
|
|
|
|
if (File::Exists(destPath))
|
|
{
|
|
LOG_INFO("%s already exists, prompting user for a different track design name", destPath.c_str());
|
|
ContextShowError(STR_UNABLE_TO_INSTALL_THIS_TRACK_DESIGN, kStringIdNone, {});
|
|
WindowTextInputRawOpen(
|
|
this, WIDX_INSTALL, STR_SELECT_NEW_NAME_FOR_TRACK_DESIGN,
|
|
STR_AN_EXISTING_TRACK_DESIGN_ALREADY_HAS_THIS_NAME, {}, _trackName.c_str(), 255);
|
|
}
|
|
else
|
|
{
|
|
if (TrackRepositoryInstall(_trackPath.c_str(), _trackName.c_str()))
|
|
{
|
|
close();
|
|
}
|
|
else
|
|
{
|
|
ContextShowError(STR_CANT_SAVE_TRACK_DESIGN, kStringIdNone, {});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
WindowBase* InstallTrackOpen(const utf8* path)
|
|
{
|
|
auto trackDesign = TrackDesignImport(path);
|
|
if (trackDesign == nullptr)
|
|
{
|
|
ContextShowError(STR_UNABLE_TO_LOAD_FILE, kStringIdNone, {});
|
|
return nullptr;
|
|
}
|
|
|
|
ObjectManagerUnloadAllObjects();
|
|
if (trackDesign->trackAndVehicle.rtdIndex == kRideTypeNull)
|
|
{
|
|
LOG_ERROR("Failed to load track (ride type null): %s", path);
|
|
return nullptr;
|
|
}
|
|
if (ObjectManagerLoadObject(&trackDesign->trackAndVehicle.vehicleObject.Entry) == nullptr)
|
|
{
|
|
LOG_ERROR("Failed to load track (vehicle load fail): %s", path);
|
|
return nullptr;
|
|
}
|
|
|
|
auto* windowMgr = GetWindowManager();
|
|
windowMgr->ForceClose(WindowClass::editorObjectSelection);
|
|
windowMgr->CloseConstructionWindows();
|
|
|
|
gTrackDesignSceneryToggle = false;
|
|
_currentTrackPieceDirection = 2;
|
|
|
|
auto* window = windowMgr->FocusOrCreate<InstallTrackWindow>(
|
|
WindowClass::installTrack, kWindowSize, { WindowFlag::autoPosition, WindowFlag::centreScreen });
|
|
window->setupTrack(path, std::move(trackDesign));
|
|
|
|
return window;
|
|
}
|
|
} // namespace OpenRCT2::Ui::Windows
|