OpenRCT2/src/openrct2/paint/tile_element/Paint.LargeScenery.cpp

420 lines
14 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2023 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 "../Paint.h"
#include "../../Game.h"
#include "../../config/Config.h"
#include "../../core/Numerics.hpp"
#include "../../core/String.hpp"
#include "../../interface/Viewport.h"
#include "../../localisation/Formatter.h"
#include "../../localisation/Formatting.h"
#include "../../localisation/Localisation.h"
#include "../../object/LargeSceneryObject.h"
#include "../../profiling/Profiling.h"
#include "../../ride/Ride.h"
#include "../../ride/TrackDesign.h"
#include "../../util/Util.h"
#include "../../world/Banner.h"
#include "../../world/Map.h"
#include "../../world/Scenery.h"
#include "../../world/TileInspector.h"
#include "../Boundbox.h"
#include "../Supports.h"
#include "Paint.TileElement.h"
// clang-format off
static constexpr BoundBoxXY LargeSceneryBoundBoxes[] = {
{ { 3, 3 }, { 26, 26 } },
{ { 17, 17 }, { 12, 12 } },
{ { 17, 3 }, { 12, 12 } },
{ { 17, 3 }, { 12, 26 } },
{ { 3, 3 }, { 12, 12 } },
{ { 3, 3 }, { 26, 26 } },
{ { 3, 3 }, { 28, 12 } },
{ { 3, 3 }, { 26, 26 } },
{ { 3, 17 }, { 12, 12 } },
{ { 3, 17 }, { 26, 12 } },
{ { 3, 3 }, { 26, 26 } },
{ { 3, 3 }, { 26, 26 } },
{ { 3, 3 }, { 12, 28 } },
{ { 3, 3 }, { 26, 26 } },
{ { 3, 3 }, { 26, 26 } },
{ { 3, 3 }, { 26, 26 } },
{ { 1, 1 }, { 30, 30 } },
};
// clang-format on
static void PaintLargeScenerySupports(
PaintSession& session, uint8_t direction, uint16_t height, const LargeSceneryElement& tileElement, ImageId imageTemplate,
const LargeSceneryTile& tile)
{
PROFILED_FUNCTION();
if (tile.flags & LARGE_SCENERY_TILE_FLAG_NO_SUPPORTS)
return;
auto transitionType = WoodenSupportTransitionType::None;
auto supportHeight = height;
if (supportHeight & 0xF)
{
supportHeight &= ~0xF;
transitionType = WoodenSupportTransitionType::Scenery;
}
WoodenBSupportsPaintSetupRotated(
session, WoodenSupportType::Truss, WoodenSupportSubType::NeSw, direction, supportHeight, imageTemplate, transitionType);
int32_t clearanceHeight = Ceil2(tileElement.GetClearanceZ() + 15, 16);
if (tile.flags & LARGE_SCENERY_TILE_FLAG_ALLOW_SUPPORTS_ABOVE)
{
PaintUtilSetSegmentSupportHeight(session, SEGMENTS_ALL, clearanceHeight, 0x20);
}
else
{
PaintUtilSetSegmentSupportHeight(session, SEGMENTS_ALL, 0xFFFF, 0);
}
PaintUtilSetGeneralSupportHeight(session, clearanceHeight, 0x20);
}
static std::string_view LargeSceneryCalculateDisplayText(const LargeSceneryText& text, std::string_view s, bool height)
{
size_t totalSize = 0;
CodepointView view(s);
auto it = view.begin();
while (it != view.end() && totalSize <= text.max_width)
{
auto glyph = text.GetGlyph(*it, ' ');
totalSize += height ? glyph.height : glyph.width;
it++;
}
auto totalLength = it.GetIndex();
return s.substr(0, totalLength);
}
static int32_t DivToMinusInfinity(int32_t a, int32_t b)
{
return (a / b) - (a % b < 0);
}
static void PaintLargeScenery3DTextLine(
PaintSession& session, const LargeSceneryEntry& sceneryEntry, const LargeSceneryText& text, std::string_view line,
ImageId imageTemplate, Direction direction, int32_t offsetY)
{
PROFILED_FUNCTION();
line = LargeSceneryCalculateDisplayText(text, line, false);
auto width = text.MeasureWidth(line);
auto offsetX = text.offset[(direction & 1)].x;
auto acc = offsetY * ((direction & 1) ? -1 : 1);
if (!(text.flags & LARGE_SCENERY_TEXT_FLAG_VERTICAL))
{
// sign is horizontal, centre text:
offsetX -= (width / 2);
acc -= (width / 2);
}
for (auto codepoint : CodepointView(line))
{
auto glyph = text.GetGlyph(codepoint, ' ');
// Upcasting from uint8_t to uint32_t to avoid an overflow.
uint32_t glyphOffset = glyph.image_offset;
auto glyphType = direction & 1;
if (text.flags & LARGE_SCENERY_TEXT_FLAG_VERTICAL)
{
glyphOffset *= 2;
}
else
{
glyphOffset *= 4;
// set slightly different glyph on horizontal sign, which was rendered 1/2 pixel lower to deal with aliasing:
if (direction & 1)
{
if (!(acc & 1))
{
glyphType += 2;
}
}
else
{
if ((acc & 1))
{
glyphType += 2;
}
}
}
auto imageIndex = sceneryEntry.text_image + glyphOffset + glyphType;
auto imageId = imageTemplate.WithIndex(imageIndex);
if (direction == 3)
{
PaintAttachToPreviousPS(session, imageId, offsetX, -DivToMinusInfinity(acc, 2));
}
else if (text.flags & LARGE_SCENERY_TEXT_FLAG_VERTICAL)
{
PaintAttachToPreviousPS(session, imageId, offsetX, DivToMinusInfinity(acc, 2));
}
else
{
PaintAttachToPreviousAttach(session, imageId, offsetX, DivToMinusInfinity(acc, 2));
}
offsetX += glyph.width;
acc += glyph.width;
}
}
static bool Is3DTextSingleLine(const LargeSceneryText& text, std::string_view s)
{
if (text.flags & LARGE_SCENERY_TEXT_FLAG_TWO_LINE)
{
auto width = text.MeasureWidth(s);
return width <= text.max_width;
}
return true;
}
static void PaintLargeScenery3DText(
PaintSession& session, const LargeSceneryEntry& sceneryEntry, const LargeSceneryTile& tile,
const LargeSceneryElement& tileElement, uint8_t direction, uint16_t height, bool isGhost)
{
PROFILED_FUNCTION();
if (sceneryEntry.tiles[1].x_offset != -1)
{
auto sequenceDirection = (tileElement.GetSequenceIndex() - 1) & 3;
if (sequenceDirection != direction)
{
return;
}
}
if (session.DPI.zoom_level > ZoomLevel{ 1 })
return;
auto banner = tileElement.GetBanner();
if (banner == nullptr)
return;
const auto* text = sceneryEntry.text;
if (text == nullptr)
return;
auto textColour = isGhost ? static_cast<colour_t>(COLOUR_GREY) : tileElement.GetSecondaryColour();
auto imageTemplate = ImageId().WithPrimary(textColour);
char signString[256];
auto ft = Formatter();
banner->FormatTextTo(ft);
OpenRCT2::FormatStringLegacy(signString, sizeof(signString), STR_STRINGID, ft.Data());
auto offsetY = text->offset[(direction & 1)].y * 2;
if (text->flags & LARGE_SCENERY_TEXT_FLAG_VERTICAL)
{
// Vertical sign
offsetY++;
auto displayText = LargeSceneryCalculateDisplayText(*text, signString, true);
auto displayTextHeight = text->MeasureHeight(displayText);
for (auto codepoint : CodepointView(displayText))
{
char line[8]{};
UTF8WriteCodepoint(line, codepoint);
PaintLargeScenery3DTextLine(
session, sceneryEntry, *text, line, imageTemplate, direction, offsetY - displayTextHeight);
auto glyph = text->GetGlyph(codepoint, ' ');
offsetY += glyph.height * 2;
}
}
else
{
// Horizontal sign
offsetY -= (direction & 1);
if (Is3DTextSingleLine(*text, signString))
{
PaintLargeScenery3DTextLine(session, sceneryEntry, *text, signString, imageTemplate, direction, offsetY);
}
else
{
auto lineHeight = text->GetGlyph('A')->height + 1;
offsetY -= lineHeight;
// Split the string into two lines at best position
auto current = std::string_view(signString);
std::string_view next;
for (int32_t lineIndex = 0; lineIndex < 2; lineIndex++)
{
std::string_view best;
CodepointView view(current);
auto lineWidth = 0;
auto it = view.begin();
while (it != view.end() && lineWidth < text->max_width)
{
// Trim any leading spaces
auto codepoint = *it;
if (codepoint != ' ' || lineWidth != 0)
{
// Determine if this is a good place to split
if (codepoint == ' ' || codepoint == '\n')
{
auto index = it.GetIndex();
best = current.substr(0, index);
next = current.substr(index + 1);
if (codepoint == '\n')
break;
}
auto glyph = text->GetGlyph(*it, ' ');
lineWidth += glyph.width;
}
it++;
}
if (best.empty())
{
// No good split found, or reached end of string
auto index = it.GetIndex();
best = current.substr(0, index);
next = current.substr(index);
}
PaintLargeScenery3DTextLine(session, sceneryEntry, *text, best, imageTemplate, direction, offsetY);
current = next;
offsetY += lineHeight * 2;
}
}
}
}
static void PaintLargeSceneryScrollingText(
PaintSession& session, const LargeSceneryEntry& sceneryEntry, const LargeSceneryElement& tileElement, uint8_t direction,
uint16_t height, const CoordsXYZ& bbOffset, bool isGhost)
{
PROFILED_FUNCTION();
auto textColour = isGhost ? static_cast<colour_t>(COLOUR_GREY) : tileElement.GetSecondaryColour();
auto textPaletteIndex = direction == 0 ? ColourMapA[textColour].mid_dark : ColourMapA[textColour].light;
auto banner = tileElement.GetBanner();
if (banner == nullptr)
return;
auto ft = Formatter();
banner->FormatTextTo(ft);
char text[256];
if (gConfigGeneral.UpperCaseBanners)
{
FormatStringToUpper(text, sizeof(text), STR_SCROLLING_SIGN_TEXT, ft.Data());
}
else
{
OpenRCT2::FormatStringLegacy(text, sizeof(text), STR_SCROLLING_SIGN_TEXT, ft.Data());
}
auto scrollMode = sceneryEntry.scrolling_mode + ((direction + 1) & 3);
auto stringWidth = GfxGetStringWidth(text, FontStyle::Tiny);
auto scroll = stringWidth > 0 ? (gCurrentTicks / 2) % stringWidth : 0;
auto imageId = ScrollingTextSetup(session, STR_SCROLLING_SIGN_TEXT, ft, scroll, scrollMode, textPaletteIndex);
PaintAddImageAsChild(session, imageId, { 0, 0, height + 25 }, { bbOffset, { 1, 1, 21 } });
}
void PaintLargeScenery(PaintSession& session, uint8_t direction, uint16_t height, const LargeSceneryElement& tileElement)
{
PROFILED_FUNCTION();
if (session.ViewFlags & VIEWPORT_FLAG_HIGHLIGHT_PATH_ISSUES)
return;
auto sequenceNum = tileElement.GetSequenceIndex();
const auto* object = tileElement.GetObject();
if (object == nullptr)
return;
const auto* sceneryEntry = tileElement.GetEntry();
if (sceneryEntry == nullptr)
return;
const auto* tile = object->GetTileForSequence(sequenceNum);
if (tile == nullptr)
return;
session.InteractionType = ViewportInteractionItem::LargeScenery;
auto isGhost = false;
ImageId imageTemplate;
if (gTrackDesignSaveMode && !TrackDesignSaveContainsTileElement(reinterpret_cast<const TileElement*>(&tileElement)))
{
imageTemplate = ImageId().WithRemap(FilterPaletteID::Palette46);
isGhost = true;
}
else if (tileElement.IsGhost())
{
session.InteractionType = ViewportInteractionItem::None;
imageTemplate = ImageId().WithRemap(FilterPaletteID::PaletteGhost);
isGhost = true;
}
else if (session.SelectedElement == reinterpret_cast<const TileElement*>(&tileElement))
{
imageTemplate = ImageId().WithRemap(FilterPaletteID::PaletteGhost);
isGhost = true;
}
else
{
if (sceneryEntry->flags & LARGE_SCENERY_FLAG_HAS_PRIMARY_COLOUR)
{
imageTemplate = imageTemplate.WithPrimary(tileElement.GetPrimaryColour());
}
if (sceneryEntry->flags & LARGE_SCENERY_FLAG_HAS_SECONDARY_COLOUR)
{
imageTemplate = imageTemplate.WithSecondary(tileElement.GetSecondaryColour());
}
if (sceneryEntry->flags & LARGE_SCENERY_FLAG_HAS_TERTIARY_COLOUR)
{
imageTemplate = imageTemplate.WithTertiary(tileElement.GetTertiaryColour());
}
}
auto boxlengthZ = std::min<uint8_t>(tile->z_clearance, 128) - 3;
auto flags = tile->flags;
auto bbIndex = 16;
if (flags & 0xF00)
{
flags &= 0xF000;
flags = Numerics::rol16(flags, direction);
bbIndex = (flags & 0xF) | (flags >> 12);
}
const CoordsXYZ& bbOffset = { LargeSceneryBoundBoxes[bbIndex].offset, height };
const CoordsXYZ& bbLength = { LargeSceneryBoundBoxes[bbIndex].length, boxlengthZ };
auto imageIndex = sceneryEntry->image + 4 + (sequenceNum << 2) + direction;
PaintAddImageAsParent(session, imageTemplate.WithIndex(imageIndex), { 0, 0, height }, { bbOffset, bbLength });
if (sceneryEntry->scrolling_mode != SCROLLING_MODE_NONE && direction != 1 && direction != 2)
{
if (sceneryEntry->flags & LARGE_SCENERY_FLAG_3D_TEXT)
{
PaintLargeScenery3DText(session, *sceneryEntry, *tile, tileElement, direction, height, isGhost);
}
else if (session.DPI.zoom_level <= ZoomLevel{ 0 })
{
auto sequenceDirection2 = (tileElement.GetSequenceIndex() - 1) & 3;
if (sequenceDirection2 == direction)
{
PaintLargeSceneryScrollingText(session, *sceneryEntry, tileElement, direction, height, bbOffset, isGhost);
}
}
}
PaintLargeScenerySupports(
session, direction, height, tileElement, isGhost ? imageTemplate : ImageId(0, COLOUR_BLACK), *tile);
}