Implement Tree and Wall toolUpdate Down (#1078)

* Implement Wall toolUpdate Down

* Start implementation of tree tool Update Down

* Continue

* Finish scatter down

* Refactor trig function into Math file

* Implement second cluster tool function

* Name tree flags where possible

* Add land object flags

* Try alternative constant

* Add missing error titles and widget checks

* Add missing error title from industrylist

* Clang format

* Simplify sine wave
This commit is contained in:
Duncan 2021-08-05 16:06:58 +01:00 committed by GitHub
parent c82281540f
commit b756c2e440
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 587 additions and 63 deletions

View File

@ -69,7 +69,7 @@ namespace OpenLoco::GameCommands
{ GameCommand::pauseGame, togglePause, 0x00431E32, false },
{ GameCommand::loadSaveQuitGame, loadSaveQuit, 0x0043BFCB, false },
{ GameCommand::removeTree, nullptr, 0x004BB392, true },
{ GameCommand::gc_unk_23, nullptr, 0x004BB138, true },
{ GameCommand::createTree, nullptr, 0x004BB138, true },
{ GameCommand::changeLandMaterial, nullptr, 0x00468EDD, true },
{ GameCommand::raiseLand, nullptr, 0x00463702, true },
{ GameCommand::lowerLand, nullptr, 0x004638C6, true },
@ -78,7 +78,7 @@ namespace OpenLoco::GameCommands
{ GameCommand::lowerWater, nullptr, 0x004C5126, true },
{ GameCommand::changeCompanyName, nullptr, 0x00434914, false },
{ GameCommand::changeCompanyOwnerName, nullptr, 0x00434A58, false },
{ GameCommand::gc_unk_32, nullptr, 0x004C436C, true },
{ GameCommand::createWall, nullptr, 0x004C436C, true },
{ GameCommand::removeWall, nullptr, 0x004C466C, true },
{ GameCommand::gc_unk_34, nullptr, 0x004C4717, false },
{ GameCommand::vehicleOrderInsert, nullptr, 0x0047036E, false },

View File

@ -56,7 +56,7 @@ namespace OpenLoco::GameCommands
pauseGame = 20,
loadSaveQuitGame = 21,
removeTree = 22,
gc_unk_23 = 23,
createTree = 23,
changeLandMaterial = 24,
raiseLand = 25,
lowerLand = 26,
@ -65,7 +65,7 @@ namespace OpenLoco::GameCommands
lowerWater = 29,
changeCompanyName = 30,
changeCompanyOwnerName = 31,
gc_unk_32 = 32,
createWall = 32,
removeWall = 33,
gc_unk_34 = 34,
vehicleOrderInsert = 35,
@ -433,6 +433,48 @@ namespace OpenLoco::GameCommands
doCommand(GameCommand::removeTree, regs);
}
struct TreePlacementArgs
{
TreePlacementArgs() = default;
explicit TreePlacementArgs(const registers& regs)
: pos(regs.ax, regs.cx)
, rotation(regs.di & 0x3)
, type(regs.bh)
, quadrant(regs.dl)
, colour(regs.dh)
, buildImmediately(regs.di & 0x8000)
, requiresFullClearance(regs.di & 0x4000)
{
}
Map::Pos2 pos;
uint8_t rotation;
uint8_t type;
uint8_t quadrant;
Colour_t colour;
bool buildImmediately = false;
bool requiresFullClearance = false;
explicit operator registers() const
{
registers regs;
regs.ax = pos.x;
regs.cx = pos.y;
regs.dl = quadrant;
regs.dh = colour;
regs.di = rotation | (buildImmediately ? 0x8000 : 0) | (requiresFullClearance ? 0x4000 : 0);
regs.bh = type;
return regs;
}
};
inline uint32_t do_23(uint8_t flags, const TreePlacementArgs& args)
{
registers regs = registers(args);
regs.bl = flags;
return doCommand(GameCommand::createTree, regs);
}
// Change Land Material
inline void do_24(Map::Pos2 pointA, Map::Pos2 pointB, uint8_t landType, uint8_t flags)
{
@ -535,10 +577,51 @@ namespace OpenLoco::GameCommands
return doCommand(GameCommand::changeCompanyOwnerName, regs) != FAILURE;
}
struct WallPlacementArgs
{
WallPlacementArgs() = default;
explicit WallPlacementArgs(const registers& regs)
: pos(regs.ax, regs.cx, regs.di)
, rotation(regs.dl)
, type(regs.bh)
, unk(regs.dh)
, primaryColour(regs.bp & 0xFF)
, secondaryColour((regs.bp >> 8) & 0xFF)
{
}
Map::Pos3 pos;
uint8_t rotation;
uint8_t type;
uint8_t unk;
Colour_t primaryColour;
Colour_t secondaryColour;
explicit operator registers() const
{
registers regs;
regs.ax = pos.x;
regs.cx = pos.y;
regs.dl = rotation;
regs.dh = unk;
regs.di = pos.z;
regs.bp = primaryColour | (secondaryColour << 8);
regs.bh = type;
return regs;
}
};
inline bool do_32(uint8_t flags, const WallPlacementArgs& args)
{
registers regs = registers(args);
regs.bl = flags;
return doCommand(GameCommand::createWall, regs) != FAILURE;
}
struct WallRemovalArgs
{
WallRemovalArgs() = default;
explicit WallRemovalArgs(const registers regs)
explicit WallRemovalArgs(const registers& regs)
: pos(regs.ax, regs.cx, regs.dh * 4)
, rotation(regs.dl)
{

View File

@ -380,6 +380,7 @@ namespace OpenLoco::StringIds
constexpr string_id title_build_walls = 446;
constexpr string_id title_plant_trees = 447;
constexpr string_id cant_plant_this_here = 449;
constexpr string_id outlined_wcolour2_stringid = 450;
constexpr string_id black_tiny_font = 453;

View File

@ -0,0 +1,23 @@
#include "Trigonometry.hpp"
#include "../Interop/Interop.hpp"
namespace OpenLoco::Math::Trigonometry
{
// Excel Function =SIN((A1/16384)*2*PI())*32768
// Where A1 is 0 : 4095
static Interop::loco_global<int16_t[directionPrecisionHigh / 4], 0x00501B50> _quarterSine;
int32_t integerSinePrecisionHigh(uint16_t direction, int32_t magnitude)
{
// Build the full sine wave from the quarter wave by subtraction of direction/magnitude
const auto sineIndex = ((direction & (1 << 12)) ? -direction : direction) & 0xFFF;
const auto value = (direction & (1 << 13)) ? -_quarterSine[sineIndex] : _quarterSine[sineIndex];
return value * magnitude / 0x8000;
}
int32_t integerCosinePrecisionHigh(uint16_t direction, int32_t magnitude)
{
// Cosine is Sine plus pi/2
return integerSinePrecisionHigh(direction + directionPrecisionHigh / 4, magnitude);
}
}

View File

@ -113,4 +113,8 @@ namespace OpenLoco::Math::Trigonometry
{
return computeXYVector(computeXYMagnitude(height, pitch), yaw);
}
constexpr auto directionPrecisionHigh = 0x4000;
int32_t integerSinePrecisionHigh(uint16_t direction, int32_t magnitude);
int32_t integerCosinePrecisionHigh(uint16_t direction, int32_t magnitude);
}

View File

@ -9,13 +9,23 @@ namespace OpenLoco
struct Context;
}
namespace LandObjectFlags
{
constexpr uint8_t unk0 = (1 << 0);
constexpr uint8_t unk1 = (1 << 1);
constexpr uint8_t isDesert = (1 << 2);
constexpr uint8_t noTrees = (1 << 3);
}
#pragma pack(push, 1)
struct LandObject
{
string_id name;
uint8_t cost_index; // 0x02
uint8_t var_03;
uint8_t pad_04[0x8 - 0x04];
uint8_t pad_04;
uint8_t flags; //0x05
uint8_t pad_06[0x8 - 0x6];
uint8_t cost_factor; // 0x08
uint8_t pad_09[0x0A - 0x09];
uint32_t image; // 0x0A

View File

@ -28,7 +28,7 @@ namespace OpenLoco
Gfx::point_t treePos = Gfx::point_t{ x, y } + Gfx::point_t{ 0, 48 };
if (var_08 & (1 << 0))
if (flags & TreeObjectFlags::hasSnowVariation)
{
auto snowImage = treeGrowth[growth] * num_rotations;
snowImage += rotation;

View File

@ -24,6 +24,18 @@ namespace OpenLoco
0,
} };
namespace TreeObjectFlags
{
constexpr uint16_t hasSnowVariation = (1 << 0);
constexpr uint16_t unk1 = (1 << 1);
constexpr uint16_t veryHighAltitude = (1 << 2);
constexpr uint16_t highAltitude = (1 << 3);
constexpr uint16_t requiresWater = (1 << 4);
constexpr uint16_t unk5 = (1 << 5);
constexpr uint16_t droughtResistant = (1 << 6);
constexpr uint16_t unk7 = (1 << 7);
}
#pragma pack(push, 1)
struct TreeObject
{
@ -34,7 +46,7 @@ namespace OpenLoco
uint8_t var_05;
uint8_t num_rotations; // 0x06 (1,2,4)
uint8_t growth; // 0x07 (number of tree size images)
uint16_t var_08; // 0x08
uint16_t flags; // 0x08
uint32_t sprites[12]; // 0x0A
uint8_t pad_3A[0x3D - 0x3A];
uint8_t season_state; // 0x3D (index for sprites, seasons + dying)

View File

@ -217,6 +217,8 @@ namespace OpenLoco::Ui
std::pair<ViewportInteraction::InteractionArg, Ui::Viewport*> getMapCoordinatesFromPos(int32_t screenX, int32_t screenY, int32_t flags);
std::optional<Map::Pos2> getSurfaceOrWaterLocFromUi(const xy32& screenCoords);
uint8_t getQuadrantOrCentreFromPos(const Map::Pos2& loc);
uint8_t getQuadrantFromPos(const Map::Pos2& loc);
uint8_t getSideFromPos(const Map::Pos2& loc);
std::optional<std::pair<Map::Pos2, Ui::Viewport*>> getSurfaceLocFromUi(const xy32& screenCoords);
}
}

View File

@ -551,17 +551,8 @@ namespace OpenLoco::Ui::ViewportInteraction
}
// Determine to which edge the cursor is closest
[[maybe_unused]] uint32_t closestEdge = 0; // ecx
const auto xNibble = mapPos.x & 0x1F;
const auto yNibble = mapPos.y & 0x1F;
if (xNibble < yNibble)
{
closestEdge = (xNibble + yNibble < 32) ? 0 : 1;
}
else
{
closestEdge = (xNibble + yNibble < 32) ? 3 : 2;
}
[[maybe_unused]] uint32_t closestEdge = getSideFromPos(mapPos); // ecx
return { Pos2(mapPos.x & 0xFFE0, mapPos.y & 0xFFE0) };
}
@ -595,7 +586,7 @@ namespace OpenLoco::Ui::ViewportInteraction
// NOTE: Original call getSurfaceLocFromUi within this function
// instead OpenLoco has split it in two. Also note that result of original
// was a Pos2 start i.e. (& 0xFFE0) both components
static uint8_t getQuadrantFromPos(const Map::Pos2& loc)
uint8_t getQuadrantFromPos(const Map::Pos2& loc)
{
const auto xNibble = loc.x & 0x1F;
const auto yNibble = loc.y & 0x1F;
@ -609,6 +600,24 @@ namespace OpenLoco::Ui::ViewportInteraction
}
}
// 0x0045FE4C
// NOTE: Original call getSurfaceLocFromUi within this function
// instead OpenLoco has split it in two. Also note that result of original
// was a Pos2 start i.e. (& 0xFFE0) both components
uint8_t getSideFromPos(const Map::Pos2& loc)
{
const auto xNibble = loc.x & 0x1F;
const auto yNibble = loc.y & 0x1F;
if (xNibble < yNibble)
{
return (xNibble + yNibble < 32) ? 0 : 1;
}
else
{
return (xNibble + yNibble < 32) ? 3 : 2;
}
}
// 0x0045FD8E
// NOTE: Original call getSurfaceLocFromUi within this function
// instead OpenLoco has split it in two. Also note that result of original

View File

@ -1023,6 +1023,7 @@ namespace OpenLoco::Ui::Windows::IndustryList
auto placementArgs = getIndustryPlacementArgsFromCursor(x, y);
if (placementArgs)
{
GameCommands::setErrorTitle(StringIds::error_cant_build_this_here);
if (GameCommands::do_47(GameCommands::Flags::apply, *placementArgs) != GameCommands::FAILURE)
{
Audio::playSound(Audio::SoundId::construct, GameCommands::getPosition());

View File

@ -1,4 +1,5 @@
#include "../Audio/Audio.h"
#include "../CompanyManager.h"
#include "../Economy/Economy.h"
#include "../GameCommands/GameCommands.h"
#include "../Graphics/Colour.h"
@ -10,6 +11,7 @@
#include "../Map/Map.hpp"
#include "../Map/Tile.h"
#include "../Map/TileManager.h"
#include "../Math/Trigonometry.hpp"
#include "../Objects/InterfaceSkinObject.h"
#include "../Objects/LandObject.h"
#include "../Objects/ObjectManager.h"
@ -41,18 +43,22 @@ namespace OpenLoco::Ui::Windows::Terraform
static loco_global<uint8_t, 0x00F003D2> _lastSelectedLand;
static loco_global<uint8_t, 0x01136496> _treeRotation;
static loco_global<uint8_t, 0x01136497> _treeColour;
static loco_global<uint8_t, 0x0113649A> _byte_113649A;
static loco_global<uint8_t, 0x0113649A> _terraformGhostPlaced;
static loco_global<uint8_t, 0x0113649E> _treeClusterType;
static loco_global<int16_t, 0x0050A000> _adjustToolSize;
static loco_global<uint32_t, 0x00F2530C> _raiseLandCost;
static loco_global<uint32_t, 0x00F25310> _lowerLandCost;
static loco_global<uint32_t, 0x01136484> _lastTreeCost;
static loco_global<uint16_t, 0x01136488> _word_1136488;
static loco_global<uint16_t, 0x0113648A> _word_113648A;
static loco_global<Map::TileElement*, 0x01136470> _lastPlacedWall;
static loco_global<Map::TileElement*, 0x01136470> _lastPlacedTree;
static loco_global<Map::Pos2, 0x01136488> _terraformGhostPos;
static loco_global<uint16_t, 0x01136490> _lastTreeColourFlag;
static loco_global<uint8_t, 0x01136499> _byte_1136499;
static loco_global<uint8_t, 0x0113649B> _byte_113649B;
static loco_global<uint8_t, 0x0113649C> _byte_113649C;
static loco_global<uint16_t, 0x01136492> _terraformGhostTreeRotationFlag;
static loco_global<uint8_t, 0x01136499> _terraformGhostBaseZ;
static loco_global<uint8_t, 0x0113649B> _terraformGhostTreeElementType;
static loco_global<uint8_t, 0x0113649C> _terraformGhostType;
static loco_global<uint8_t, 0x0113649D> _terraformGhostRotation; // wall
static loco_global<uint8_t, 0x0113649D> _terraformGhostQuadrant; // tree
static loco_global<uint32_t, 0x0113652C> _raiseWaterCost;
static loco_global<uint32_t, 0x01136528> _lowerWaterCost;
@ -87,7 +93,6 @@ namespace OpenLoco::Ui::Windows::Terraform
static WindowEventList _events;
static void initEvents();
static void sub_4BD297();
static void switchTab(Window* self, WidgetIndex_t widgetIndex);
static void repositionTabs(Window* self);
static void drawTabs(Window* self, Gfx::Context* context);
@ -96,6 +101,12 @@ namespace OpenLoco::Ui::Windows::Terraform
static void onResize(Window* self, uint8_t height);
static void onMouseUp(Window* self, WidgetIndex_t widgetIndex);
static void sub_4A69DD();
namespace GhostPlaced
{
constexpr uint8_t tree = (1 << 0);
constexpr uint8_t wall = (1 << 1);
}
}
namespace PlantTrees
@ -213,10 +224,12 @@ namespace OpenLoco::Ui::Windows::Terraform
updateTreeColours(self);
}
static void removeTreeGhost();
// 0x004BBB0A
static void onClose(Window* self)
{
Common::sub_4BD297();
removeTreeGhost();
Ui::Windows::hideGridlines();
}
@ -225,7 +238,7 @@ namespace OpenLoco::Ui::Windows::Terraform
{
Input::toolSet(self, Common::widx::panel, CursorId::plantTree);
Input::setFlag(Input::Flags::flag6);
_byte_113649A = 0;
_terraformGhostPlaced = 0;
_lastTreeCost = 0x80000000;
self->var_83C = 0;
self->row_hover = -1;
@ -394,27 +407,306 @@ namespace OpenLoco::Ui::Windows::Terraform
self->invalidate();
}
}
// 0x004BD297 (bits of)
static void removeTreeGhost()
{
if (_terraformGhostPlaced & Common::GhostPlaced::tree)
{
_terraformGhostPlaced = _terraformGhostPlaced & ~Common::GhostPlaced::tree;
GameCommands::TreeRemovalArgs args;
args.pos = Map::Pos3((*_terraformGhostPos).x, (*_terraformGhostPos).y, _terraformGhostBaseZ * 4);
args.type = _terraformGhostType;
args.elementType = _terraformGhostTreeElementType;
GameCommands::do_22(GameCommands::Flags::apply | GameCommands::Flags::flag_3 | GameCommands::Flags::flag_5 | GameCommands::Flags::flag_6, args);
}
}
// 0x004BD237
static currency32_t placeTreeGhost(const GameCommands::TreePlacementArgs& placementArgs)
{
removeTreeGhost();
auto res = GameCommands::do_23(GameCommands::Flags::apply | GameCommands::Flags::flag_3 | GameCommands::Flags::flag_5 | GameCommands::Flags::flag_6, placementArgs);
if (res != GameCommands::FAILURE)
{
_terraformGhostPos = placementArgs.pos;
_terraformGhostTreeElementType = (*_lastPlacedTree)->rawData()[0];
_terraformGhostType = placementArgs.type;
_terraformGhostBaseZ = (*_lastPlacedTree)->baseZ();
_terraformGhostPlaced |= Common::GhostPlaced::tree;
_terraformGhostQuadrant = placementArgs.quadrant;
_terraformGhostTreeRotationFlag = placementArgs.rotation | (placementArgs.buildImmediately ? 0x8000 : 0);
}
return res;
}
// 0x004BD1D9
static std::optional<GameCommands::TreePlacementArgs> getTreePlacementArgsFromCursor(const int16_t x, const int16_t y)
{
auto* self = WindowManager::find(WindowType::terraform);
if (self == nullptr)
{
return {};
}
auto res = ViewportInteraction::getSurfaceLocFromUi({ x, y });
if (!res)
{
return {};
}
if (self->row_hover == -1)
{
return {};
}
// TODO: modify getSurfaceOrWaterLocFromUi to return the viewport then use its rotation
static loco_global<int32_t, 0x00E3F0B8> gCurrentRotation;
GameCommands::TreePlacementArgs args;
// 0 for Z value means game command finds first available height
args.pos = Map::Pos3(res->first.x & 0xFFE0, res->first.y & 0xFFE0, 0);
args.type = self->row_hover;
args.quadrant = ViewportInteraction::getQuadrantFromPos(res->first) ^ (1 << 1);
args.colour = _treeColour;
args.rotation = (_treeRotation - gCurrentRotation) & 0x3;
if (isEditorMode())
{
args.buildImmediately = true;
}
return { args };
}
// 0x004BBB15
static void onToolUpdate(Window& self, const WidgetIndex_t widgetIndex, const int16_t x, const int16_t y)
{
registers regs;
regs.esi = X86Pointer(&self);
regs.dx = widgetIndex;
regs.ax = x;
regs.bx = y;
call(0x004BBB15, regs);
if (widgetIndex != Common::widx::panel)
{
return;
}
Map::TileManager::mapInvalidateSelectionRect();
Input::resetMapSelectionFlag(Input::MapSelectionFlags::enable);
auto placementArgs = getTreePlacementArgsFromCursor(x, y);
if (!placementArgs)
{
removeTreeGhost();
return;
}
Input::setMapSelectionFlags(Input::MapSelectionFlags::enable);
Map::TileManager::setMapSelectionCorner((placementArgs->quadrant ^ (1 << 1)) + 6);
Map::TileManager::setMapSelectionArea(placementArgs->pos, placementArgs->pos);
Map::TileManager::mapInvalidateSelectionRect();
if (_terraformGhostPlaced & Common::GhostPlaced::tree)
{
if (*_terraformGhostPos == placementArgs->pos
&& _terraformGhostQuadrant == placementArgs->quadrant
&& _terraformGhostType == placementArgs->type
&& _terraformGhostTreeRotationFlag == (placementArgs->rotation | (placementArgs->buildImmediately ? 0x8000 : 0)))
{
return;
}
}
removeTreeGhost();
_terraformGhostQuadrant = placementArgs->quadrant;
_terraformGhostTreeRotationFlag = placementArgs->rotation | (placementArgs->buildImmediately ? 0x8000 : 0);
_lastTreeCost = placeTreeGhost(*placementArgs);
}
// 0x004BDDC6
static void clusterSelectedTreeToolDown(const GameCommands::TreePlacementArgs& baseArgs, const uint16_t range, const uint16_t density)
{
const auto numPlacements = (range * range * density) / 8192;
for (auto i = 0; i < numPlacements; ++i)
{
// Choose a random offset in a circle
auto& rng = gPrng();
auto randomMagnitude = rng.randNext(std::numeric_limits<uint16_t>::max()) * range / 65536;
auto randomDirection = rng.randNext(Math::Trigonometry::directionPrecisionHigh - 1);
Map::Pos2 randomOffset(
Math::Trigonometry::integerSinePrecisionHigh(randomDirection, randomMagnitude),
Math::Trigonometry::integerCosinePrecisionHigh(randomDirection, randomMagnitude));
GameCommands::TreePlacementArgs args;
Map::Pos2 newLoc = randomOffset + baseArgs.pos;
args.quadrant = ViewportInteraction::getQuadrantFromPos(newLoc);
args.pos = Map::Pos2(newLoc.x & 0xFFE0, newLoc.y & 0xFFE0);
// Note: this is not the same as the randomDirection above as it is the trees rotation
args.rotation = rng.randNext(3);
args.colour = 0;
args.type = baseArgs.type;
args.buildImmediately = true;
args.requiresFullClearance = true;
do_23(GameCommands::Flags::apply, args);
}
}
static loco_global<uint8_t, 0x00525FB4> _currentSnowLine;
// 0x004BDF19
static std::optional<uint8_t> getRandomTreeTypeFromSurface(const Map::TilePos2& loc, bool unk)
{
if (!Map::validCoords(loc))
{
return {};
}
auto* surface = Map::TileManager::get(loc).surface();
if (surface == nullptr)
{
return {};
}
uint16_t mustNotTreeFlags = 0;
if (unk)
{
mustNotTreeFlags |= TreeObjectFlags::unk1;
}
uint16_t mustTreeFlags = 0;
if (surface->baseZ() - 4 > _currentSnowLine)
{
mustTreeFlags |= TreeObjectFlags::hasSnowVariation;
}
if (surface->baseZ() > 68)
{
mustTreeFlags |= TreeObjectFlags::veryHighAltitude;
}
if (surface->baseZ() > 48)
{
mustTreeFlags |= TreeObjectFlags::highAltitude;
}
auto* landObj = ObjectManager::get<LandObject>(surface->terrain());
mustNotTreeFlags |= TreeObjectFlags::droughtResistant;
if (landObj->flags & LandObjectFlags::isDesert)
{
mustTreeFlags |= TreeObjectFlags::droughtResistant;
mustNotTreeFlags &= ~TreeObjectFlags::droughtResistant;
}
if (landObj->flags & LandObjectFlags::noTrees)
{
return {};
}
mustNotTreeFlags |= TreeObjectFlags::requiresWater;
const uint16_t numSameTypeSurfaces = TileManager::countSurroundingWaterTiles(loc);
if (numSameTypeSurfaces >= 8)
{
mustNotTreeFlags &= ~TreeObjectFlags::requiresWater;
}
std::vector<uint8_t> selectableTrees;
for (uint8_t i = 0; i < ObjectManager::getMaxObjects(ObjectType::tree); ++i)
{
auto* treeObj = ObjectManager::get<TreeObject>(i);
if (treeObj == nullptr)
{
continue;
}
if (treeObj->flags & mustNotTreeFlags)
{
continue;
}
if ((treeObj->flags & mustTreeFlags) != mustTreeFlags)
{
continue;
}
selectableTrees.push_back(i);
}
if (selectableTrees.empty())
{
return {};
}
auto& rng = gPrng();
return { selectableTrees[rng.randNext(selectableTrees.size() - 1)] };
}
// 0x004BDC67
static void clusterSurfaceTypeTreeToolDown(const GameCommands::TreePlacementArgs& baseArgs, const uint16_t range, const uint16_t density)
{
const auto numPlacements = (range * range * density) / 8192;
for (auto i = 0; i < numPlacements; ++i)
{
// Choose a random offset in a circle
auto& rng = gPrng();
auto randomMagnitude = rng.randNext(std::numeric_limits<uint16_t>::max()) * range / 65536;
auto randomDirection = rng.randNext(Math::Trigonometry::directionPrecisionHigh - 1);
Map::Pos2 randomOffset(
Math::Trigonometry::integerSinePrecisionHigh(randomDirection, randomMagnitude),
Math::Trigonometry::integerCosinePrecisionHigh(randomDirection, randomMagnitude));
GameCommands::TreePlacementArgs args;
Map::Pos2 newLoc = randomOffset + baseArgs.pos;
args.quadrant = ViewportInteraction::getQuadrantFromPos(newLoc);
args.pos = Map::Pos2(newLoc.x & 0xFFE0, newLoc.y & 0xFFE0);
// Note: this is not the same as the randomDirection above as it is the trees rotation
args.rotation = rng.randNext(3);
args.colour = 0;
auto type = getRandomTreeTypeFromSurface(newLoc, false);
if (type)
{
args.type = *type;
args.buildImmediately = true;
args.requiresFullClearance = true;
do_23(GameCommands::Flags::apply, args);
}
}
}
// 0x004BBB20
static void onToolDown(Window& self, const WidgetIndex_t widgetIndex, const int16_t x, const int16_t y)
{
registers regs;
regs.esi = X86Pointer(&self);
regs.dx = widgetIndex;
regs.ax = x;
regs.bx = y;
call(0x004BBB20, regs);
if (widgetIndex != Common::widx::panel)
{
return;
}
removeTreeGhost();
auto placementArgs = getTreePlacementArgsFromCursor(x, y);
if (placementArgs)
{
switch (_treeClusterType)
{
case treeCluster::none:
GameCommands::setErrorTitle(StringIds::cant_plant_this_here);
if (GameCommands::do_23(GameCommands::Flags::apply, *placementArgs) != GameCommands::FAILURE)
{
Audio::playSound(Audio::SoundId::construct, GameCommands::getPosition());
}
break;
case treeCluster::selected:
{
// TODO: Lock behind sandbox/editor
auto previousId = CompanyManager::updatingCompanyId();
CompanyManager::updatingCompanyId(CompanyId::neutral);
auto height = TileManager::getHeight(placementArgs->pos);
Audio::playSound(Audio::SoundId::construct, Map::Pos3{ placementArgs->pos.x, placementArgs->pos.y, height.landHeight });
clusterSelectedTreeToolDown(*placementArgs, 320, 3);
// TODO: Lock behind sandbox/editor
CompanyManager::updatingCompanyId(previousId);
break;
}
case treeCluster::random:
// TODO: Lock behind sandbox/editor
auto previousId = CompanyManager::updatingCompanyId();
CompanyManager::updatingCompanyId(CompanyId::neutral);
auto height = TileManager::getHeight(placementArgs->pos);
Audio::playSound(Audio::SoundId::construct, Map::Pos3{ placementArgs->pos.x, placementArgs->pos.y, height.landHeight });
clusterSurfaceTypeTreeToolDown(*placementArgs, 384, 4);
// TODO: Lock behind sandbox/editor
CompanyManager::updatingCompanyId(previousId);
break;
}
}
}
// 0x004BBEC1
@ -689,7 +981,7 @@ namespace OpenLoco::Ui::Windows::Terraform
window->number = 0;
window->current_tab = Common::widx::tab_plant_trees - Common::widx::tab_clear_area;
window->frame_no = 0;
_byte_113649A = 0;
_terraformGhostPlaced = 0;
_lastTreeCost = 0x80000000;
window->owner = _player_company;
window->var_846 = 0xFFFF;
@ -1648,10 +1940,12 @@ namespace OpenLoco::Ui::Windows::Terraform
updateActiveThumb(self);
}
static void removeWallGhost();
// 0x004BC21C
static void onClose(Window* self)
{
Common::sub_4BD297();
removeWallGhost();
Ui::Windows::hideGridlines();
}
@ -1660,7 +1954,7 @@ namespace OpenLoco::Ui::Windows::Terraform
{
Input::toolSet(self, Common::widx::panel, CursorId::placeFence);
Input::setFlag(Input::Flags::flag6);
_byte_113649A = 0;
_terraformGhostPlaced = 0;
self->var_83C = 0;
self->row_hover = -1;
refreshWallList(self);
@ -1760,26 +2054,117 @@ namespace OpenLoco::Ui::Windows::Terraform
}
}
// 0x004BD297 (bits of)
static void removeWallGhost()
{
if (_terraformGhostPlaced & Common::GhostPlaced::wall)
{
_terraformGhostPlaced = _terraformGhostPlaced & ~Common::GhostPlaced::wall;
GameCommands::WallRemovalArgs args;
args.pos = Map::Pos3((*_terraformGhostPos).x, (*_terraformGhostPos).y, _terraformGhostBaseZ * 4);
args.rotation = _terraformGhostRotation;
GameCommands::do_33(GameCommands::Flags::apply | GameCommands::Flags::flag_3 | GameCommands::Flags::flag_5 | GameCommands::Flags::flag_6, args);
}
}
// 0x004BD4C8
static void placeWallGhost(const GameCommands::WallPlacementArgs& placementArgs)
{
removeWallGhost();
if (GameCommands::do_32(GameCommands::Flags::apply | GameCommands::Flags::flag_3 | GameCommands::Flags::flag_5 | GameCommands::Flags::flag_6, placementArgs))
{
_terraformGhostPos = placementArgs.pos;
_terraformGhostRotation = placementArgs.rotation;
_terraformGhostTreeElementType = placementArgs.rotation; // Unsure why duplicated not used
_terraformGhostType = placementArgs.type;
_terraformGhostBaseZ = (*_lastPlacedWall)->baseZ();
_terraformGhostPlaced |= Common::GhostPlaced::wall;
}
}
// 0x004BD48E
static std::optional<GameCommands::WallPlacementArgs> getWallPlacementArgsFromCursor(const int16_t x, const int16_t y)
{
auto* self = WindowManager::find(WindowType::terraform);
if (self == nullptr)
{
return {};
}
auto res = ViewportInteraction::getSurfaceLocFromUi({ x, y });
if (!res)
{
return {};
}
if (self->row_hover == -1)
{
return {};
}
GameCommands::WallPlacementArgs args;
// 0 for Z value means game command finds first available height
args.pos = Map::Pos3(res->first.x & 0xFFE0, res->first.y & 0xFFE0, 0);
args.type = self->row_hover;
args.rotation = ViewportInteraction::getSideFromPos(res->first);
args.primaryColour = Colour::black;
args.secondaryColour = Colour::black;
args.unk = 0;
return { args };
}
// 0x004BC227
static void onToolUpdate(Window& self, const WidgetIndex_t widgetIndex, const int16_t x, const int16_t y)
{
registers regs;
regs.esi = X86Pointer(&self);
regs.dx = widgetIndex;
regs.ax = x;
regs.bx = y;
call(0x004BC227, regs);
if (widgetIndex != Common::widx::panel)
{
return;
}
Map::TileManager::mapInvalidateSelectionRect();
Input::resetMapSelectionFlag(Input::MapSelectionFlags::enable);
auto placementArgs = getWallPlacementArgsFromCursor(x, y);
if (!placementArgs)
{
removeWallGhost();
return;
}
Input::setMapSelectionFlags(Input::MapSelectionFlags::enable);
Map::TileManager::setMapSelectionCorner(placementArgs->rotation + 10);
Map::TileManager::setMapSelectionArea(placementArgs->pos, placementArgs->pos);
Map::TileManager::mapInvalidateSelectionRect();
if (_terraformGhostPlaced & Common::GhostPlaced::wall)
{
if (*_terraformGhostPos == placementArgs->pos && _terraformGhostRotation == placementArgs->rotation && _terraformGhostType == placementArgs->type)
{
return;
}
}
removeWallGhost();
_terraformGhostRotation = placementArgs->rotation;
placeWallGhost(*placementArgs);
}
// 0x004BC232
static void onToolDown(Window& self, const WidgetIndex_t widgetIndex, const int16_t x, const int16_t y)
{
registers regs;
regs.esi = X86Pointer(&self);
regs.dx = widgetIndex;
regs.ax = x;
regs.bx = y;
call(0x004BC232, regs);
if (widgetIndex != Common::widx::panel)
{
return;
}
removeWallGhost();
auto placementArgs = getWallPlacementArgsFromCursor(x, y);
if (placementArgs)
{
GameCommands::setErrorTitle(StringIds::error_cant_build_this_here);
if (GameCommands::do_32(GameCommands::Flags::apply, *placementArgs))
{
Audio::playSound(Audio::SoundId::construct, GameCommands::getPosition());
}
}
}
// 0x004BC359
@ -2097,13 +2482,6 @@ namespace OpenLoco::Ui::Windows::Terraform
}
}
// 0x004BD297
static void sub_4BD297()
{
registers regs;
call(0x004BD297, regs);
}
// 0x004BBB2B
static void switchTab(Window* self, WidgetIndex_t widgetIndex)
{

View File

@ -65,6 +65,7 @@
<ClCompile Include="Map\SurfaceTile.cpp" />
<ClCompile Include="Map\Tile.cpp" />
<ClCompile Include="Map\TileManager.cpp" />
<ClCompile Include="Math\Trigonometry.cpp" />
<ClCompile Include="Math\Vector.cpp" />
<ClCompile Include="MessageManager.cpp" />
<ClCompile Include="MultiPlayer.cpp" />