OpenLoco/src/openloco/Station.cpp

783 lines
24 KiB
C++

#include "Station.h"
#include "CompanyManager.h"
#include "IndustryManager.h"
#include "MessageManager.h"
#include "OpenLoco.h"
#include "ViewportManager.h"
#include "interop/interop.hpp"
#include "localisation/string_ids.h"
#include "map/tilemgr.h"
#include "objects/airport_object.h"
#include "objects/building_object.h"
#include "objects/cargo_object.h"
#include "objects/industry_object.h"
#include "objects/objectmgr.h"
#include "objects/road_station_object.h"
#include "ui/WindowManager.h"
#include <algorithm>
#include <cassert>
using namespace openloco::interop;
using namespace openloco::map;
using namespace openloco::ui;
namespace openloco
{
constexpr uint8_t min_cargo_rating = 0;
constexpr uint8_t max_cargo_rating = 200;
constexpr uint8_t catchmentSize = 4;
struct CargoSearchState
{
private:
inline static loco_global<uint8_t[map_size], 0x00F00484> _map;
inline static loco_global<uint32_t, 0x0112C68C> _filter;
inline static loco_global<uint32_t[max_cargo_stats], 0x0112C690> _score;
inline static loco_global<uint32_t, 0x0112C710> _producedCargoTypes;
inline static loco_global<industry_id_t[max_cargo_stats], 0x0112C7D2> _industry;
inline static loco_global<uint8_t, 0x0112C7F2> _byte_112C7F2;
public:
bool mapHas2(const tile_coord_t x, const tile_coord_t y) const
{
return (_map[y * map_columns + x] & (1 << 1)) != 0;
}
void mapRemove2(const tile_coord_t x, const tile_coord_t y)
{
_map[y * map_columns + x] &= ~(1 << 1);
}
void setTile(const tile_coord_t x, const tile_coord_t y, const uint8_t flag)
{
_map[y * map_columns + x] |= (1 << flag);
}
void resetTile(const tile_coord_t x, const tile_coord_t y, const uint8_t flag)
{
_map[y * map_columns + x] &= ~(1 << flag);
}
void setTileRegion(tile_coord_t x, tile_coord_t y, int16_t xTileCount, int16_t yTileCount, const uint8_t flag)
{
auto xStart = x;
auto xTileStartCount = xTileCount;
while (yTileCount > 0)
{
while (xTileCount > 0)
{
setTile(x, y, flag);
x++;
xTileCount--;
}
x = xStart;
xTileCount = xTileStartCount;
y++;
yTileCount--;
}
}
void resetTileRegion(tile_coord_t x, tile_coord_t y, int16_t xTileCount, int16_t yTileCount, const uint8_t flag)
{
auto xStart = x;
auto xTileStartCount = xTileCount;
while (yTileCount > 0)
{
while (xTileCount > 0)
{
resetTile(x, y, flag);
x++;
xTileCount--;
}
x = xStart;
xTileCount = xTileStartCount;
y++;
yTileCount--;
}
}
uint32_t filter() const
{
return _filter;
}
void filter(const uint32_t value)
{
_filter = value;
}
void resetScores()
{
std::fill_n(_score.get(), max_cargo_stats, 0);
}
uint32_t score(const uint8_t cargo)
{
return _score[cargo];
}
void addScore(const uint8_t cargo, const int32_t value)
{
_score[cargo] += value;
}
uint32_t producedCargoTypes() const
{
return _producedCargoTypes;
}
void resetProducedCargoTypes()
{
_producedCargoTypes = 0;
}
void addProducedCargoType(const uint8_t cargoId)
{
_producedCargoTypes = _producedCargoTypes | (1 << cargoId);
}
void byte_112C7F2(const uint8_t value)
{
_byte_112C7F2 = value;
}
void resetIndustryMap()
{
std::fill_n(_industry.get(), max_cargo_stats, industry_id::null);
}
industry_id_t getIndustry(const uint8_t cargo) const
{
return _industry[cargo];
}
void setIndustry(const uint8_t cargo, const industry_id_t id)
{
_industry[cargo] = id;
}
};
static void sub_491BF5(const map_pos& pos, const uint8_t flag);
static station_element* getStationElement(const map_pos3& pos);
station_id_t station::id() const
{
// TODO check if this is stored in station structure
// otherwise add it when possible
static loco_global<station[1024], 0x005E6EDC> _stations;
auto index = (size_t)(this - _stations);
if (index > 1024)
{
index = station_id::null;
}
return (station_id_t)index;
}
// 0x0048B23E
void station::update()
{
updateCargoAcceptance();
}
// 0x00492640
void station::updateCargoAcceptance()
{
CargoSearchState cargoSearchState;
uint32_t currentAcceptedCargo = calcAcceptedCargo(cargoSearchState);
uint32_t originallyAcceptedCargo = 0;
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
auto& cs = cargo_stats[cargoId];
cs.industry_id = cargoSearchState.getIndustry(cargoId);
if (cs.isAccepted())
{
originallyAcceptedCargo |= (1 << cargoId);
}
bool isNowAccepted = (currentAcceptedCargo & (1 << cargoId)) != 0;
cs.isAccepted(isNowAccepted);
}
if (originallyAcceptedCargo != currentAcceptedCargo)
{
if (owner == companymgr::getControllingId())
{
alertCargoAcceptanceChange(originallyAcceptedCargo, currentAcceptedCargo);
}
invalidateWindow();
}
}
// 0x00492683
void station::alertCargoAcceptanceChange(uint32_t oldCargoAcc, uint32_t newCargoAcc)
{
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
bool acceptedBefore = (oldCargoAcc & (1 << cargoId)) != 0;
bool acceptedNow = (newCargoAcc & (1 << cargoId)) != 0;
if (acceptedBefore && !acceptedNow)
{
messagemgr::post(
messageType::cargoNoLongerAccepted,
owner,
id(),
cargoId);
}
else if (!acceptedBefore && acceptedNow)
{
messagemgr::post(
messageType::cargoNowAccepted,
owner,
id(),
cargoId);
}
}
}
// 0x00491FE0
// WARNING: this may be called with station (ebp) = -1
// filter only used if location.x != -1
uint32_t station::calcAcceptedCargo(CargoSearchState& cargoSearchState, const map_pos& location, const uint32_t filter)
{
cargoSearchState.byte_112C7F2(1);
cargoSearchState.filter(0);
if (location.x != -1)
{
cargoSearchState.filter(filter);
}
cargoSearchState.resetIndustryMap();
setCatchmentDisplay(1);
if (location.x != -1)
{
sub_491BF5(location, 1);
}
cargoSearchState.resetScores();
cargoSearchState.resetProducedCargoTypes();
if (this != (station*)0xFFFFFFFF)
{
for (uint16_t i = 0; i < stationTileSize; i++)
{
auto pos = stationTiles[i];
auto stationElement = getStationElement(pos);
if (stationElement == nullptr)
{
continue;
}
cargoSearchState.byte_112C7F2(0);
if (stationElement->stationType() == stationType::roadStation)
{
auto obj = objectmgr::get<road_station_object>(stationElement->objectId());
if (obj->flags & road_station_flags::passenger)
{
cargoSearchState.filter(cargoSearchState.filter() | (1 << obj->var_2C));
}
else if (obj->flags & road_station_flags::freight)
{
cargoSearchState.filter(cargoSearchState.filter() | ~(1 << obj->var_2C));
}
}
else
{
cargoSearchState.filter(~0);
}
}
}
if (cargoSearchState.filter() == 0)
{
cargoSearchState.filter(~0);
}
for (tile_coord_t ty = 0; ty < map_columns; ty++)
{
for (tile_coord_t tx = 0; tx < map_rows; tx++)
{
if (cargoSearchState.mapHas2(tx, ty))
{
auto pos = map_pos(tx * tile_size, ty * tile_size);
auto tile = tilemgr::get(pos);
for (auto& el : tile)
{
if (el.isFlag4())
{
continue;
}
switch (el.type())
{
case element_type::industry:
{
auto industryEl = el.asIndustry();
auto industry = industryEl->industry();
if (industry == nullptr || industry->under_construction != 0xFF)
{
break;
}
auto obj = industry->object();
if (obj == nullptr)
{
break;
}
for (auto cargoId : obj->required_cargo_type)
{
if (cargoId != 0xFF && (cargoSearchState.filter() & (1 << cargoId)))
{
cargoSearchState.addScore(cargoId, 8);
cargoSearchState.setIndustry(cargoId, industry->id());
}
}
for (auto cargoId : obj->produced_cargo_type)
{
if (cargoId != 0xFF && (cargoSearchState.filter() & (1 << cargoId)))
{
cargoSearchState.addProducedCargoType(cargoId);
}
}
break;
}
case element_type::building:
{
auto buildingEl = el.asBuilding();
if (buildingEl == nullptr || buildingEl->has_40() || !buildingEl->hasStationElement())
{
break;
}
auto obj = buildingEl->object();
if (obj == nullptr)
{
break;
}
for (int i = 0; i < 2; i++)
{
const auto cargoId = obj->producedCargoType[i];
if (cargoId != 0xFF && (cargoSearchState.filter() & (1 << cargoId)))
{
cargoSearchState.addScore(cargoId, obj->var_A6[i]);
if (obj->var_A0[i] != 0)
{
cargoSearchState.addProducedCargoType(cargoId);
}
}
}
for (int i = 0; i < 2; i++)
{
if (obj->var_A4[i] != 0xFF && (cargoSearchState.filter() & (1 << obj->var_A4[i])))
{
cargoSearchState.addScore(obj->var_A4[i], obj->var_A8[i]);
}
}
// Multi tile buildings should only be counted once so remove the other tiles from the search
if (obj->flags & building_object_flags::large_tile)
{
// 0x004F9296, 0x4F9298
static const map_pos offsets[4] = { { 0, 0 }, { 0, 32 }, { 32, 32 }, { 32, 0 } };
auto index = buildingEl->multiTileIndex();
tile_coord_t xPos = (pos.x - offsets[index].x) / tile_size;
tile_coord_t yPos = (pos.y - offsets[index].y) / tile_size;
cargoSearchState.mapRemove2(xPos + 0, yPos + 0);
cargoSearchState.mapRemove2(xPos + 0, yPos + 1);
cargoSearchState.mapRemove2(xPos + 1, yPos + 0);
cargoSearchState.mapRemove2(xPos + 1, yPos + 1);
}
break;
}
default:
continue;
}
}
}
}
}
uint32_t acceptedCargos = 0;
for (uint8_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
if (cargoSearchState.score(cargoId) >= 8)
{
acceptedCargos |= (1 << cargoId);
}
}
return acceptedCargos;
}
static void setStationCatchmentRegion(CargoSearchState& cargoSearchState, TilePos minPos, TilePos maxPos, const uint8_t flags);
// 0x00491D70
// catchment flag should not be shifted (1, 2, 3, 4) and NOT (1 << 0, 1 << 1)
void station::setCatchmentDisplay(const uint8_t catchmentFlag)
{
CargoSearchState cargoSearchState;
cargoSearchState.resetTileRegion(0, 0, map_columns, map_rows, catchmentFlag);
if (this == (station*)0xFFFFFFFF)
return;
if (stationTileSize == 0)
return;
for (uint16_t i = 0; i < stationTileSize; i++)
{
auto pos = stationTiles[i];
pos.z &= ~((1 << 1) | (1 << 0));
auto stationElement = getStationElement(pos);
if (stationElement == nullptr)
continue;
switch (stationElement->stationType())
{
case stationType::airport:
{
auto airportObject = objectmgr::get<airport_object>(stationElement->objectId());
map_pos minPos, maxPos;
minPos.x = airportObject->min_x;
minPos.y = airportObject->min_y;
maxPos.x = airportObject->max_x;
maxPos.y = airportObject->max_y;
minPos = rotate2dCoordinate(minPos, stationElement->rotation());
maxPos = rotate2dCoordinate(maxPos, stationElement->rotation());
minPos.x += pos.x;
minPos.y += pos.y;
maxPos.x += pos.x;
maxPos.y += pos.y;
if (minPos.x > maxPos.x)
{
std::swap(minPos.x, maxPos.x);
}
if (minPos.y > maxPos.y)
{
std::swap(minPos.y, maxPos.y);
}
TilePos tileMinPos(minPos);
TilePos tileMaxPos(maxPos);
tileMinPos.x -= catchmentSize;
tileMinPos.y -= catchmentSize;
tileMaxPos.x += catchmentSize;
tileMaxPos.y += catchmentSize;
setStationCatchmentRegion(cargoSearchState, tileMinPos, tileMaxPos, catchmentFlag);
}
break;
case stationType::docks:
{
TilePos minPos(pos);
auto maxPos = minPos;
minPos.x -= catchmentSize;
minPos.y -= catchmentSize;
// Docks are always size 2x2
maxPos.x += catchmentSize + 1;
maxPos.y += catchmentSize + 1;
setStationCatchmentRegion(cargoSearchState, minPos, maxPos, catchmentFlag);
}
break;
default:
{
TilePos minPos(pos);
auto maxPos = minPos;
minPos.x -= catchmentSize;
minPos.y -= catchmentSize;
maxPos.x += catchmentSize;
maxPos.y += catchmentSize;
setStationCatchmentRegion(cargoSearchState, minPos, maxPos, catchmentFlag);
}
}
}
}
// 0x0048F7D1
void station::sub_48F7D1()
{
registers regs;
regs.ebx = id();
call(0x0048F7D1, regs);
}
// 0x00492A98
void station::getStatusString(const char* buffer)
{
char* ptr = (char*)buffer;
*ptr = '\0';
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
auto& stats = cargo_stats[cargoId];
if (stats.quantity == 0)
continue;
if (*buffer != '\0')
ptr = stringmgr::formatString(ptr, string_ids::waiting_cargo_separator);
loco_global<uint32_t, 0x112C826> _common_format_args;
*_common_format_args = stats.quantity;
auto cargo = objectmgr::get<cargo_object>(cargoId);
string_id unit_name = stats.quantity == 1 ? cargo->unit_name_singular : cargo->unit_name_plural;
ptr = stringmgr::formatString(ptr, unit_name, &*_common_format_args);
}
string_id suffix = *buffer == '\0' ? string_ids::nothing_waiting : string_ids::waiting;
ptr = stringmgr::formatString(ptr, suffix);
}
// 0x00492793
bool station::updateCargo()
{
bool atLeastOneGoodRating = false;
bool quantityUpdated = false;
var_3B0 = std::min(var_3B0 + 1, 255);
var_3B1 = std::min(var_3B1 + 1, 255);
auto& rng = gPrng();
for (uint32_t i = 0; i < max_cargo_stats; i++)
{
auto& cargo = cargo_stats[i];
if (!cargo.empty())
{
if (cargo.quantity != 0 && cargo.origin != id())
{
cargo.enroute_age = std::min(cargo.enroute_age + 1, 255);
}
cargo.age = std::min(cargo.age + 1, 255);
auto targetRating = calculateCargoRating(cargo);
// Limit to +/- 2 minimum change
auto ratingDelta = std::clamp(targetRating - cargo.rating, -2, 2);
cargo.rating += ratingDelta;
if (cargo.rating <= 50)
{
// Rating < 25%, decrease cargo
if (cargo.quantity >= 400)
{
cargo.quantity -= rng.randNext(1, 32);
quantityUpdated = true;
}
else if (cargo.quantity >= 200)
{
cargo.quantity -= rng.randNext(1, 8);
quantityUpdated = true;
}
}
if (cargo.rating >= 100)
{
atLeastOneGoodRating = true;
}
if (cargo.rating <= 100 && cargo.quantity != 0)
{
if (cargo.rating <= rng.randNext(0, 127))
{
cargo.quantity = std::max(0, cargo.quantity - rng.randNext(1, 4));
quantityUpdated = true;
}
}
}
}
sub_4929DB();
auto w = WindowManager::find(WindowType::station, id());
if (w != nullptr && (w->current_tab == 2 || w->current_tab == 1 || quantityUpdated))
{
w->invalidate();
}
return atLeastOneGoodRating;
}
// 0x004927F6
int32_t station::calculateCargoRating(const station_cargo_stats& cargo) const
{
int32_t rating = 0;
// Bonus if cargo is fresh
if (cargo.age <= 45)
{
rating += 40;
if (cargo.age <= 30)
{
rating += 45;
if (cargo.age <= 15)
{
rating += 45;
if (cargo.age <= 7)
{
rating += 35;
}
}
}
}
// Penalty if lots of cargo waiting
rating -= 130;
if (cargo.quantity <= 1000)
{
rating += 30;
if (cargo.quantity <= 500)
{
rating += 30;
if (cargo.quantity <= 300)
{
rating += 30;
if (cargo.quantity <= 200)
{
rating += 20;
if (cargo.quantity <= 100)
{
rating += 20;
}
}
}
}
}
if ((flags & (station_flags::flag_7 | station_flags::flag_8)) == 0 && !isPlayerCompany(owner))
{
rating = 120;
}
int32_t unk3 = std::min<uint8_t>(cargo.var_36, 250);
if (unk3 < 35)
{
rating += unk3 / 4;
}
if (cargo.var_38 < 4)
{
rating += 10;
if (cargo.var_38 < 2)
{
rating += 10;
if (cargo.var_38 < 1)
{
rating += 13;
}
}
}
return std::clamp<int32_t>(rating, min_cargo_rating, max_cargo_rating);
}
void station::sub_4929DB()
{
registers regs;
regs.ebp = (int32_t)this;
call(0x004929DB, regs);
}
// 0x004CBA2D
void station::invalidate()
{
ui::viewportmgr::invalidate(this);
}
void station::invalidateWindow()
{
WindowManager::invalidate(WindowType::station, id());
}
// 0x0048F6D4
static station_element* getStationElement(const map_pos3& pos)
{
auto tile = tilemgr::get(pos.x, pos.y);
auto baseZ = pos.z / 4;
for (auto& element : tile)
{
auto stationElement = element.asStation();
if (stationElement == nullptr)
{
continue;
}
if (stationElement->baseZ() != baseZ)
{
continue;
}
if (!stationElement->isFlag5())
{
return stationElement;
}
else
{
return nullptr;
}
}
return nullptr;
}
// 0x00491EDC
static void setStationCatchmentRegion(CargoSearchState& cargoSearchState, TilePos minPos, TilePos maxPos, const uint8_t flag)
{
minPos.x = std::max(minPos.x, static_cast<coord_t>(0));
minPos.y = std::max(minPos.y, static_cast<coord_t>(0));
maxPos.x = std::min(maxPos.x, static_cast<coord_t>(map_columns - 1));
maxPos.y = std::min(maxPos.y, static_cast<coord_t>(map_rows - 1));
maxPos.x -= minPos.x;
maxPos.y -= minPos.y;
maxPos.x++;
maxPos.y++;
cargoSearchState.setTileRegion(minPos.x, minPos.y, maxPos.x, maxPos.y, flag);
}
// 0x00491BF5
static void sub_491BF5(const map_pos& pos, const uint8_t flag)
{
TilePos minPos(pos);
auto maxPos = minPos;
maxPos.x += catchmentSize;
maxPos.y += catchmentSize;
minPos.x -= catchmentSize;
minPos.y -= catchmentSize;
CargoSearchState cargoSearchState;
setStationCatchmentRegion(cargoSearchState, minPos, maxPos, flag);
}
}