#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 #include 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 _map; inline static loco_global _filter; inline static loco_global _score; inline static loco_global _producedCargoTypes; inline static loco_global _industry; inline static loco_global _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 _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(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(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 _common_format_args; *_common_format_args = stats.quantity; auto cargo = objectmgr::get(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(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(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(0)); minPos.y = std::max(minPos.y, static_cast(0)); maxPos.x = std::min(maxPos.x, static_cast(map_columns - 1)); maxPos.y = std::min(maxPos.y, static_cast(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); } }