diff --git a/src/economy_func.h b/src/economy_func.h index da6ac090ff..eb9a04f487 100644 --- a/src/economy_func.h +++ b/src/economy_func.h @@ -15,6 +15,8 @@ #include "cargo_type.h" #include "vehicle_type.h" #include "company_type.h" +#include "settings_type.h" +#include "core/random_func.hpp" void ResetPriceBaseMultipliers(); void SetPriceBaseMultiplier(Price price, int factor); @@ -49,4 +51,48 @@ inline bool EconomyIsInRecession() return _economy.fluct <= 0; } +/** + * Scale a number by the inverse of the cargo scale setting, e.g. a scale of 25% multiplies the number by 4. + * @param num The number to scale. + * @param town Are we scaling town production, or industry production? + * @return The number scaled by the inverse of the cargo scale setting, minimum of 1. + */ +static uint ScaleByInverseCargoScale(uint num, bool town) +{ + uint16_t percentage = (town ? _settings_game.economy.town_cargo_scale : _settings_game.economy.industry_cargo_scale); + + /* We might not need to do anything. */ + if (percentage == 100) return num; + + /* Never return 0, since we often divide by this number. */ + return std::max((num * 100) / percentage, 1u); +} + +/** + * Scale a number by the cargo scale setting. + * @param num The number to scale. + * @param town Are we scaling town production, or industry production? + * @return The number scaled by the current cargo scale setting. May be 0. + */ +inline uint ScaleByCargoScale(uint num, bool town) +{ + /* Don't bother scaling in the menu, especially since settings don't exist when starting OpenTTD and trying to read them crashes the game. */ + if (_game_mode == GM_MENU) return num; + + if (num == 0) return num; + + uint16_t percentage = (town ? _settings_game.economy.town_cargo_scale : _settings_game.economy.industry_cargo_scale); + + /* We might not need to do anything. */ + if (percentage == 100) return num; + + uint scaled = (num * percentage) / 100; + + /* We might round down to 0, so we compensate with a random chance approximately equal to the economy scale, + * e.g. at 25% scale there's a 1/4 chance to round up to 1. */ + if (scaled == 0 && Chance16(1, ScaleByInverseCargoScale(1, town))) return 1; + + return scaled; +} + #endif /* ECONOMY_FUNC_H */ diff --git a/src/economy_type.h b/src/economy_type.h index c565e3257b..26474b7b9d 100644 --- a/src/economy_type.h +++ b/src/economy_type.h @@ -24,6 +24,21 @@ enum EconomyType : uint8_t { ET_END = 3, }; +/** + * Minimum allowed value of town_cargo_scale/industry_cargo_scale. + * Below 13, callback-based industries would produce less than once per month. We round up to 15% because it's a nicer number. + * Towns use the same minimum to match, and because below this small towns often produce no cargo. + */ +static const int MIN_CARGO_SCALE = 15; +/** + * Maximum allowed value of town_cargo_scale/industry_cargo_scale. + * Above 340, callback-based industries would produce more than once per day, which GRFs do not expect. + * Towns use the same maximum to match. + */ +static const int MAX_CARGO_SCALE = 300; +/** Default value of town_cargo_scale/industry_cargo_scale. */ +static const int DEF_CARGO_SCALE = 100; + /** Data of the economy. */ struct Economy { Money max_loan; ///< NOSAVE: Maximum possible loan diff --git a/src/industry_cmd.cpp b/src/industry_cmd.cpp index 82dd51cc88..7a4fc70d42 100644 --- a/src/industry_cmd.cpp +++ b/src/industry_cmd.cpp @@ -1132,7 +1132,24 @@ static void ChopLumberMillTrees(Industry *i) TileIndex tile = i->location.tile; if (CircularTileSearch(&tile, 40, SearchLumberMillTrees, nullptr)) { // 40x40 tiles to search. - i->produced[0].waiting = ClampTo(i->produced[0].waiting + 45); // Found a tree, add according value to waiting cargo. + i->produced[0].waiting = ClampTo(i->produced[0].waiting + ScaleByCargoScale(45, false)); // Found a tree, add according value to waiting cargo. + } +} + +/** + * Helper for ProduceIndustryGoods that scales and produces cargos. + * @param i The industry + * @param scale Should we scale production of this cargo directly? + */ +static void ProduceIndustryGoodsHelper(Industry *i, bool scale) +{ + for (auto &p : i->produced) { + if (!IsValidCargoID(p.cargo)) continue; + + uint16_t amount = p.rate; + if (scale) amount = ScaleByCargoScale(amount, false); + + p.waiting = ClampTo(p.waiting + amount); } } @@ -1155,15 +1172,23 @@ static void ProduceIndustryGoods(Industry *i) i->counter--; - /* produce some cargo */ + /* If using an industry callback, scale the callback interval by cargo scale percentage. */ + if (HasBit(indsp->callback_mask, CBM_IND_PRODUCTION_256_TICKS)) { + if (i->counter % ScaleByInverseCargoScale(Ticks::INDUSTRY_PRODUCE_TICKS, false) == 0) { + IndustryProductionCallback(i, 1); + ProduceIndustryGoodsHelper(i, false); + } + } + + /* + * All other production and special effects happen every 256 ticks, and cargo production is just scaled by the cargo scale percentage. + * This keeps a slow trickle of production to avoid confusion at low scale factors when the industry seems to be doing nothing for a long period of time. + */ if ((i->counter % Ticks::INDUSTRY_PRODUCE_TICKS) == 0) { - if (HasBit(indsp->callback_mask, CBM_IND_PRODUCTION_256_TICKS)) IndustryProductionCallback(i, 1); + /* Handle non-callback cargo production. */ + if (!HasBit(indsp->callback_mask, CBM_IND_PRODUCTION_256_TICKS)) ProduceIndustryGoodsHelper(i, true); IndustryBehaviour indbehav = indsp->behaviour; - for (auto &p : i->produced) { - if (!IsValidCargoID(p.cargo)) continue; - p.waiting = ClampTo(p.waiting + p.rate); - } if ((indbehav & INDUSTRYBEH_PLANT_FIELDS) != 0) { uint16_t cb_res = CALLBACK_FAILED; @@ -1825,7 +1850,7 @@ static void DoCreateNewIndustry(Industry *i, TileIndex tile, IndustryType type, } for (auto &p : i->produced) { - p.history[LAST_MONTH].production += p.rate * 8; + p.history[LAST_MONTH].production += ScaleByCargoScale(p.rate * 8, false); } } diff --git a/src/industry_gui.cpp b/src/industry_gui.cpp index d3127ef1ae..672cd03c85 100644 --- a/src/industry_gui.cpp +++ b/src/industry_gui.cpp @@ -1169,7 +1169,7 @@ static void UpdateIndustryProduction(Industry *i) for (auto &p : i->produced) { if (IsValidCargoID(p.cargo)) { - p.history[LAST_MONTH].production = 8 * p.rate; + p.history[LAST_MONTH].production = ScaleByCargoScale(8 * p.rate, false); } } } diff --git a/src/lang/english.txt b/src/lang/english.txt index eb9121d90f..3d5aa502a1 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -1524,6 +1524,12 @@ STR_CONFIG_SETTING_MINUTES_PER_YEAR_VALUE :{NUM} ###setting-zero-is-special STR_CONFIG_SETTING_MINUTES_PER_YEAR_FROZEN :0 (calendar time frozen) +STR_CONFIG_SETTING_TOWN_CARGO_SCALE :Scale town cargo production: {STRING2} +STR_CONFIG_SETTING_TOWN_CARGO_SCALE_HELPTEXT :Scale the cargo production of towns by this percentage. +STR_CONFIG_SETTING_INDUSTRY_CARGO_SCALE :Scale industry cargo production: {STRING2} +STR_CONFIG_SETTING_INDUSTRY_CARGO_SCALE_HELPTEXT :Scale the cargo production of industries by this percentage. +STR_CONFIG_SETTING_CARGO_SCALE_VALUE :{NUM}% + STR_CONFIG_SETTING_AUTORENEW_VEHICLE :Autorenew vehicle when it gets old: {STRING2} STR_CONFIG_SETTING_AUTORENEW_VEHICLE_HELPTEXT :When enabled, a vehicle nearing its end of life gets automatically replaced when the renew conditions are fulfilled diff --git a/src/object_cmd.cpp b/src/object_cmd.cpp index 81764fccf2..ab4907eb1f 100644 --- a/src/object_cmd.cpp +++ b/src/object_cmd.cpp @@ -688,7 +688,13 @@ static void TileLoop_Object(TileIndex tile) /* Top town buildings generate 250, so the top HQ type makes 256. */ if (GB(r, 0, 8) < (256 / 4 / (6 - level))) { uint amt = GB(r, 0, 8) / 8 / 4 + 1; + + /* Production is halved during recessions. */ if (EconomyIsInRecession()) amt = (amt + 1) >> 1; + + /* Scale by cargo scale setting. */ + amt = ScaleByCargoScale(amt, true); + MoveGoodsToStation(CT_PASSENGERS, amt, SourceType::Headquarters, GetTileOwner(tile), stations.GetStations()); } @@ -697,7 +703,13 @@ static void TileLoop_Object(TileIndex tile) * equations. */ if (GB(r, 8, 8) < (196 / 4 / (6 - level))) { uint amt = GB(r, 8, 8) / 8 / 4 + 1; + + /* Production is halved during recessions. */ if (EconomyIsInRecession()) amt = (amt + 1) >> 1; + + /* Scale by cargo scale setting. */ + amt = ScaleByCargoScale(amt, true); + MoveGoodsToStation(CT_MAIL, amt, SourceType::Headquarters, GetTileOwner(tile), stations.GetStations()); } } diff --git a/src/settings_gui.cpp b/src/settings_gui.cpp index 9156f05aef..26e56fcdbd 100644 --- a/src/settings_gui.cpp +++ b/src/settings_gui.cpp @@ -2201,6 +2201,7 @@ static SettingsContainer &GetSettingsTree() SettingsPage *towns = environment->Add(new SettingsPage(STR_CONFIG_SETTING_ENVIRONMENT_TOWNS)); { + towns->Add(new SettingEntry("economy.town_cargo_scale")); towns->Add(new SettingEntry("economy.town_growth_rate")); towns->Add(new SettingEntry("economy.allow_town_roads")); towns->Add(new SettingEntry("economy.allow_town_level_crossings")); @@ -2213,6 +2214,7 @@ static SettingsContainer &GetSettingsTree() SettingsPage *industries = environment->Add(new SettingsPage(STR_CONFIG_SETTING_ENVIRONMENT_INDUSTRIES)); { + industries->Add(new SettingEntry("economy.industry_cargo_scale")); industries->Add(new SettingEntry("difficulty.industry_density")); industries->Add(new SettingEntry("construction.raw_industry_construction")); industries->Add(new SettingEntry("construction.industry_platform")); diff --git a/src/settings_type.h b/src/settings_type.h index 8a601b28b7..afe506619c 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -561,6 +561,8 @@ struct EconomySettings { bool infrastructure_maintenance; ///< enable monthly maintenance fee for owner infrastructure TimekeepingUnits timekeeping_units; ///< time units to use for the game economy, either calendar or wallclock uint16_t minutes_per_calendar_year; ///< minutes per calendar year. Special value 0 means that calendar time is frozen. + uint16_t town_cargo_scale; ///< scale cargo production of towns by this percentage. + uint16_t industry_cargo_scale; ///< scale cargo production of industries by this percentage. }; struct LinkGraphSettings { diff --git a/src/table/settings/economy_settings.ini b/src/table/settings/economy_settings.ini index 96cf16eaf3..eb920b7ceb 100644 --- a/src/table/settings/economy_settings.ini +++ b/src/table/settings/economy_settings.ini @@ -315,3 +315,29 @@ strval = STR_CONFIG_SETTING_MINUTES_PER_YEAR_VALUE pre_cb = [](auto) { return _game_mode == GM_MENU || _settings_game.economy.timekeeping_units == 1; } post_cb = ChangeMinutesPerYear cat = SC_BASIC + +[SDT_VAR] +var = economy.town_cargo_scale +type = SLE_UINT16 +flags = SF_NO_NETWORK +def = DEF_CARGO_SCALE +min = MIN_CARGO_SCALE +max = MAX_CARGO_SCALE +interval = 1 +str = STR_CONFIG_SETTING_TOWN_CARGO_SCALE +strhelp = STR_CONFIG_SETTING_TOWN_CARGO_SCALE_HELPTEXT +strval = STR_CONFIG_SETTING_CARGO_SCALE_VALUE +cat = SC_BASIC + +[SDT_VAR] +var = economy.industry_cargo_scale +type = SLE_UINT16 +flags = SF_NO_NETWORK +def = DEF_CARGO_SCALE +min = MIN_CARGO_SCALE +max = MAX_CARGO_SCALE +interval = 1 +str = STR_CONFIG_SETTING_INDUSTRY_CARGO_SCALE +strhelp = STR_CONFIG_SETTING_INDUSTRY_CARGO_SCALE_HELPTEXT +strval = STR_CONFIG_SETTING_CARGO_SCALE_VALUE +cat = SC_BASIC diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp index eaf58cfdb3..8f6e8adc93 100644 --- a/src/town_cmd.cpp +++ b/src/town_cmd.cpp @@ -521,6 +521,34 @@ static void AdvanceHouseConstruction(TileIndex tile) } /** + * Generate cargo for a house, scaled by the current economy scale. + * @param t The current town. + * @param ct Type of cargo to generate, usually CT_PASSENGERS or CT_MAIL. + * @param amount The number of cargo units. + * @param stations Available stations for this house. + * @param affected_by_recession Is this cargo halved during recessions? + */ +static void TownGenerateCargo(Town *t, CargoID ct, uint amount, StationFinder &stations, bool affected_by_recession) +{ + if (amount == 0) return; + + /* All production is halved during a recession (except for NewGRF-supplied town cargo). */ + if (affected_by_recession && EconomyIsInRecession()) { + amount = (amount + 1) >> 1; + } + + /* Scale by cargo scale setting. */ + amount = ScaleByCargoScale(amount, true); + + /* Actually generate cargo and update town statistics. */ + uint moved = MoveGoodsToStation(ct, amount, SourceType::Town, t->index, stations.GetStations()); + t->supplied[ct].new_max += amount; + t->supplied[ct].new_act += moved; +} + +/** + * Tile callback function. + * * Tile callback function. Periodic tick handler for the tiles of a town. * @param tile been asked to do its stuff */ @@ -565,11 +593,8 @@ static void TileLoop_Town(TileIndex tile) uint amt = GB(callback, 0, 8); if (amt == 0) continue; - uint moved = MoveGoodsToStation(cargo, amt, SourceType::Town, t->index, stations.GetStations()); - - const CargoSpec *cs = CargoSpec::Get(cargo); - t->supplied[cs->Index()].new_max += amt; - t->supplied[cs->Index()].new_act += moved; + /* NewGRF-supplied town cargos are not affected by recessions. */ + TownGenerateCargo(t, cargo, amt, stations, false); } } else { switch (_settings_game.economy.town_cargogen_mode) { @@ -577,18 +602,12 @@ static void TileLoop_Town(TileIndex tile) /* Original (quadratic) cargo generation algorithm */ if (GB(r, 0, 8) < hs->population) { uint amt = GB(r, 0, 8) / 8 + 1; - - if (EconomyIsInRecession()) amt = (amt + 1) >> 1; - t->supplied[CT_PASSENGERS].new_max += amt; - t->supplied[CT_PASSENGERS].new_act += MoveGoodsToStation(CT_PASSENGERS, amt, SourceType::Town, t->index, stations.GetStations()); + TownGenerateCargo(t, CT_PASSENGERS, amt, stations, true); } if (GB(r, 8, 8) < hs->mail_generation) { uint amt = GB(r, 8, 8) / 8 + 1; - - if (EconomyIsInRecession()) amt = (amt + 1) >> 1; - t->supplied[CT_MAIL].new_max += amt; - t->supplied[CT_MAIL].new_act += MoveGoodsToStation(CT_MAIL, amt, SourceType::Town, t->index, stations.GetStations()); + TownGenerateCargo(t, CT_MAIL, amt, stations, true); } break; @@ -603,18 +622,14 @@ static void TileLoop_Town(TileIndex tile) /* Mask random value by potential pax and count number of actual pax */ uint amt = CountBits(r & genmask); /* Adjust and apply */ - if (EconomyIsInRecession()) amt = (amt + 1) >> 1; - t->supplied[CT_PASSENGERS].new_max += amt; - t->supplied[CT_PASSENGERS].new_act += MoveGoodsToStation(CT_PASSENGERS, amt, SourceType::Town, t->index, stations.GetStations()); + TownGenerateCargo(t, CT_PASSENGERS, amt, stations, true); /* Do the same for mail, with a fresh random */ r = Random(); genmax = (hs->mail_generation + 7) / 8; genmask = (genmax >= 32) ? 0xFFFFFFFF : ((1 << genmax) - 1); amt = CountBits(r & genmask); - if (EconomyIsInRecession()) amt = (amt + 1) >> 1; - t->supplied[CT_MAIL].new_max += amt; - t->supplied[CT_MAIL].new_act += MoveGoodsToStation(CT_MAIL, amt, SourceType::Town, t->index, stations.GetStations()); + TownGenerateCargo(t, CT_MAIL, amt, stations, true); } break; @@ -1868,8 +1883,8 @@ void UpdateTownRadius(Town *t) */ void UpdateTownMaxPass(Town *t) { - t->supplied[CT_PASSENGERS].old_max = t->cache.population >> 3; - t->supplied[CT_MAIL].old_max = t->cache.population >> 4; + t->supplied[CT_PASSENGERS].old_max = ScaleByCargoScale(t->cache.population >> 3, true); + t->supplied[CT_MAIL].old_max = ScaleByCargoScale(t->cache.population >> 4, true); } static void UpdateTownGrowthRate(Town *t);