diff --git a/src/lang/english.txt b/src/lang/english.txt index e1e18b4344..366594149a 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -1816,6 +1816,8 @@ STR_CONFIG_SETTING_LINKGRAPH_NOT_DAYLENGTH_SCALED :Do not scale th STR_CONFIG_SETTING_LINKGRAPH_NOT_DAYLENGTH_SCALED_HELPTEXT :When enabled, the linkgraph recalculation interval and time are in units of unscaled, original days, instead of day-length scaled calendar days. STR_CONFIG_SETTING_DISTRIBUTION_MANUAL :manual STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC :asymmetric +STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC_EQ :asymmetric (equal distribution) +STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC_NEAREST :asymmetric (nearest) STR_CONFIG_SETTING_DISTRIBUTION_SYMMETRIC :symmetric STR_CONFIG_SETTING_DISTRIBUTION_PAX :Distribution mode for passengers: {STRING2} STR_CONFIG_SETTING_DISTRIBUTION_PAX_HELPTEXT :"symmetric" means that roughly the same number of passengers will go from a station A to a station B as from B to A. "asymmetric" means that arbitrary numbers of passengers can go in either direction. "manual" means that no automatic distribution will take place for passengers. diff --git a/src/linkgraph/demands.cpp b/src/linkgraph/demands.cpp index f1b714820b..cd67f26f14 100644 --- a/src/linkgraph/demands.cpp +++ b/src/linkgraph/demands.cpp @@ -3,6 +3,8 @@ #include "../stdafx.h" #include "demands.h" #include +#include +#include #include "../safeguards.h" @@ -119,6 +121,58 @@ public: inline bool HasDemandLeft(const Node &to) { return to.Demand() > 0; } }; +/** + * A scaler for asymmetric distribution (equal supply). + */ +class AsymmetricScalerEq : public Scaler { +public: + /** + * Count a node's supply into the sum of supplies. + * @param node Node. + */ + inline void AddNode(const Node &node) + { + this->supply_sum += node.Supply(); + } + + /** + * Calculate the mean demand per node using the sum of supplies. + * @param num_demands Number of accepting nodes. + */ + inline void SetDemandPerNode(uint num_demands) + { + this->demand_per_node = CeilDiv(this->supply_sum, num_demands); + } + + /** + * Get the effective supply of one node towards another one. In symmetric + * distribution the supply of the other node is weighed in. + * @param from The supplying node. + * @param to The receiving node. + * @return Effective supply. + */ + inline uint EffectiveSupply(const Node &from, const Node &to) + { + return max(min(from.Supply(), ((int) this->demand_per_node) - ((int) to.ReceivedDemand())), 1); + } + + /** + * Check if there is any acceptance left for this node. In asymmetric (equal) distribution + * nodes accept as long as their demand > 0 and received_demand < demand_per_node. + * @param to The node to be checked. + */ + inline bool HasDemandLeft(const Node &to) + { + return to.Demand() > 0 && to.ReceivedDemand() < this->demand_per_node; + } + + void SetDemands(LinkGraphJob &job, NodeID from, NodeID to, uint demand_forw); + +private: + uint supply_sum; ///< Sum of all supplies in the component. + uint demand_per_node; ///< Mean demand associated with each node. +}; + /** * Set the demands between two nodes using the given base demand. In symmetric mode * this sets demands in both directions. @@ -142,6 +196,19 @@ void SymmetricScaler::SetDemands(LinkGraphJob &job, NodeID from_id, NodeID to_id this->Scaler::SetDemands(job, from_id, to_id, demand_forw); } +/** + * Set the demands between two nodes using the given base demand. + * @param job The link graph job. + * @param from_id The supplying node. + * @param to_id The receiving node. + * @param demand_forw Demand calculated for the "forward" direction. + */ +void AsymmetricScalerEq::SetDemands(LinkGraphJob &job, NodeID from_id, NodeID to_id, uint demand_forw) +{ + this->Scaler::SetDemands(job, from_id, to_id, demand_forw); + job[to_id].ReceiveDemand(demand_forw); +} + /** * Set the demands between two nodes using the given base demand. In asymmetric mode * this only sets demand in the "forward" direction. @@ -186,6 +253,7 @@ void DemandCalculator::CalcDemand(LinkGraphJob &job, Tscaler scaler) * symmetric this is relative to remote supply, otherwise it is * relative to remote demand. */ scaler.SetDemandPerNode(num_demands); + uint chance = 0; while (!supplies.empty() && !demands.empty()) { @@ -251,6 +319,56 @@ void DemandCalculator::CalcDemand(LinkGraphJob &job, Tscaler scaler) } } +/** + * Do the actual demand calculation, called from constructor. + * @param job Job to calculate the demands for. + * @tparam Tscaler Scaler to be used for scaling demands. + */ +template +void DemandCalculator::CalcMinimisedDistanceDemand(LinkGraphJob &job, Tscaler scaler) +{ + std::vector supplies; + std::vector demands; + + for (NodeID node = 0; node < job.Size(); node++) { + scaler.AddNode(job[node]); + if (job[node].Supply() > 0) { + supplies.push_back(node); + } + if (job[node].Demand() > 0) { + demands.push_back(node); + } + } + + if (supplies.empty() || demands.empty()) return; + + scaler.SetDemandPerNode(demands.size()); + + struct EdgeCandidate { + NodeID from_id; + NodeID to_id; + uint distance; + }; + std::vector candidates; + candidates.reserve(supplies.size() * demands.size() - min(supplies.size(), demands.size())); + for (NodeID from_id : supplies) { + for (NodeID to_id : demands) { + if (from_id != to_id) { + candidates.push_back({ from_id, to_id, DistanceMaxPlusManhattan(job[from_id].XY(), job[to_id].XY()) }); + } + } + } + std::sort(candidates.begin(), candidates.end(), [](const EdgeCandidate &a, const EdgeCandidate &b) { + return std::tie(a.distance, a.from_id, a.to_id) < std::tie(b.distance, b.from_id, b.to_id); + }); + for (const EdgeCandidate &candidate : candidates) { + if (job[candidate.from_id].UndeliveredSupply() == 0) continue; + if (!scaler.HasDemandLeft(job[candidate.to_id])) continue; + + scaler.SetDemands(job, candidate.from_id, candidate.to_id, min(job[candidate.from_id].UndeliveredSupply(), scaler.EffectiveSupply(job[candidate.from_id], job[candidate.to_id]))); + } +} + /** * Create the DemandCalculator and immediately do the calculation. * @param job Job to calculate the demands for. @@ -276,6 +394,12 @@ DemandCalculator::DemandCalculator(LinkGraphJob &job) : case DT_ASYMMETRIC: this->CalcDemand(job, AsymmetricScaler()); break; + case DT_ASYMMETRIC_EQ: + this->CalcMinimisedDistanceDemand(job, AsymmetricScalerEq()); + break; + case DT_ASYMMETRIC_NEAR: + this->CalcMinimisedDistanceDemand(job, AsymmetricScaler()); + break; default: /* Nothing to do. */ break; diff --git a/src/linkgraph/demands.h b/src/linkgraph/demands.h index 8a639b8b15..656ace2601 100644 --- a/src/linkgraph/demands.h +++ b/src/linkgraph/demands.h @@ -20,6 +20,9 @@ private: template void CalcDemand(LinkGraphJob &job, Tscaler scaler); + + template + void CalcMinimisedDistanceDemand(LinkGraphJob &job, Tscaler scaler); }; /** diff --git a/src/linkgraph/linkgraph_type.h b/src/linkgraph/linkgraph_type.h index 6a3239b089..9ee50f956d 100644 --- a/src/linkgraph/linkgraph_type.h +++ b/src/linkgraph/linkgraph_type.h @@ -21,23 +21,14 @@ static const LinkGraphID INVALID_LINK_GRAPH_JOB = UINT16_MAX; typedef uint16 NodeID; static const NodeID INVALID_NODE = UINT16_MAX; -enum DistributionType { - DT_BEGIN = 0, - DT_MIN = 0, +enum DistributionType : byte { DT_MANUAL = 0, ///< Manual distribution. No link graph calculations are run. DT_ASYMMETRIC = 1, ///< Asymmetric distribution. Usually cargo will only travel in one direction. - DT_MAX_NONSYMMETRIC = 1, ///< Maximum non-symmetric distribution. DT_SYMMETRIC = 2, ///< Symmetric distribution. The same amount of cargo travels in each direction between each pair of nodes. - DT_MAX = 2, - DT_NUM = 3, - DT_END = 3 -}; -/* It needs to be 8bits, because we save and load it as such - * Define basic enum properties - */ -template <> struct EnumPropsT : MakeEnumPropsT {}; -typedef TinyEnumT DistributionTypeByte; // typedefing-enumification of DistributionType + DT_ASYMMETRIC_EQ = 20, ///< Asymmetric distribution (equal). Usually cargo will only travel in one direction. Attempt to distribute the same amount of cargo to each sink. + DT_ASYMMETRIC_NEAR = 21, ///< Asymmetric distribution (nearest). Usually cargo will only travel in one direction. Attempt to distribute cargo to the nearest sink. +}; /** * Special modes for updating links. 'Restricted' means that vehicles with diff --git a/src/linkgraph/linkgraphjob.cpp b/src/linkgraph/linkgraphjob.cpp index d13500a935..d2df0fb71c 100644 --- a/src/linkgraph/linkgraphjob.cpp +++ b/src/linkgraph/linkgraphjob.cpp @@ -257,6 +257,7 @@ void LinkGraphJob::EdgeAnnotation::Init() void LinkGraphJob::NodeAnnotation::Init(uint supply) { this->undelivered_supply = supply; + this->received_demand = 0; } /** diff --git a/src/linkgraph/linkgraphjob.h b/src/linkgraph/linkgraphjob.h index 6c91bdc7b6..d6833a6778 100644 --- a/src/linkgraph/linkgraphjob.h +++ b/src/linkgraph/linkgraphjob.h @@ -48,6 +48,7 @@ private: */ struct NodeAnnotation { uint undelivered_supply; ///< Amount of supply that hasn't been distributed yet. + uint received_demand; ///< Received demand towards this node. PathList paths; ///< Paths through this node, sorted so that those with flow == 0 are in the back. FlowStatMap flows; ///< Planned flows to other nodes. void Init(uint supply); @@ -237,6 +238,12 @@ public: */ uint UndeliveredSupply() const { return this->node_anno.undelivered_supply; } + /** + * Get amount of supply that hasn't been delivered, yet. + * @return Undelivered supply. + */ + uint ReceivedDemand() const { return this->node_anno.received_demand; } + /** * Get the flows running through this node. * @return Flows. @@ -272,6 +279,15 @@ public: this->node_anno.undelivered_supply -= amount; (*this)[to].AddDemand(amount); } + + /** + * Receive some demand, adding demand to the respective edge. + * @param amount Amount of demand to be received. + */ + void ReceiveDemand(uint amount) + { + this->node_anno.received_demand += amount; + } }; /** diff --git a/src/settings_type.h b/src/settings_type.h index dfb7707772..7851651645 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -584,10 +584,10 @@ struct LinkGraphSettings { uint16 recalc_time; ///< time (in days) for recalculating each link graph component. uint16 recalc_interval; ///< time (in days) between subsequent checks for link graphs to be calculated. bool recalc_not_scaled_by_daylength; ///< whether the time should be in daylength-scaled days (false) or unscaled days (true) - DistributionTypeByte distribution_pax; ///< distribution type for passengers - DistributionTypeByte distribution_mail; ///< distribution type for mail - DistributionTypeByte distribution_armoured; ///< distribution type for armoured cargo class - DistributionTypeByte distribution_default; ///< distribution type for all other goods + DistributionType distribution_pax; ///< distribution type for passengers + DistributionType distribution_mail; ///< distribution type for mail + DistributionType distribution_armoured; ///< distribution type for armoured cargo class + DistributionType distribution_default; ///< distribution type for all other goods uint8 accuracy; ///< accuracy when calculating things on the link graph. low accuracy => low running time uint8 demand_size; ///< influence of supply ("station size") on the demand function uint8 demand_distance; ///< influence of distance between stations on the demand function diff --git a/src/table/settings.ini b/src/table/settings.ini index 0a8123dbd9..03f1f219ab 100644 --- a/src/table/settings.ini +++ b/src/table/settings.ini @@ -66,6 +66,22 @@ static int OrderTownGrowthRate(uint nth); /* End - GUI order callbacks */ +static const SettingDescEnumEntry _linkgraph_mode_symmetric[] = { +{ DT_MANUAL, STR_CONFIG_SETTING_DISTRIBUTION_MANUAL }, +{ DT_SYMMETRIC, STR_CONFIG_SETTING_DISTRIBUTION_SYMMETRIC }, +{ DT_ASYMMETRIC, STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC }, +{ DT_ASYMMETRIC_EQ, STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC_EQ }, +{ DT_ASYMMETRIC_NEAR, STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC_NEAREST }, +{ 0, STR_NULL } +}; +static const SettingDescEnumEntry _linkgraph_mode_asymmetric[] = { +{ DT_MANUAL, STR_CONFIG_SETTING_DISTRIBUTION_MANUAL }, +{ DT_ASYMMETRIC, STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC }, +{ DT_ASYMMETRIC_EQ, STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC_EQ }, +{ DT_ASYMMETRIC_NEAR, STR_CONFIG_SETTING_DISTRIBUTION_ASYMMETRIC_NEAREST }, +{ 0, STR_NULL } +}; + /* Some settings do not need to be synchronised when playing in multiplayer. * These include for example the GUI settings and will not be saved with the * savegame. @@ -814,60 +830,44 @@ strhelp = STR_CONFIG_SETTING_LINKGRAPH_NOT_DAYLENGTH_SCALED_HELPTEXT extver = SlXvFeatureTest(XSLFTO_AND, XSLFI_LINKGRAPH_DAY_SCALE) patxname = ""linkgraph_day_scale.linkgraph.recalc_not_scaled_by_daylength"" -[SDT_VAR] +[SDT_ENUM] base = GameSettings var = linkgraph.distribution_pax type = SLE_UINT8 from = SLV_183 -guiflags = SGF_MULTISTRING def = DT_MANUAL -min = DT_MIN -max = DT_MAX -interval = 1 +enumlist = _linkgraph_mode_symmetric str = STR_CONFIG_SETTING_DISTRIBUTION_PAX -strval = STR_CONFIG_SETTING_DISTRIBUTION_MANUAL strhelp = STR_CONFIG_SETTING_DISTRIBUTION_PAX_HELPTEXT -[SDT_VAR] +[SDT_ENUM] base = GameSettings var = linkgraph.distribution_mail type = SLE_UINT8 from = SLV_183 -guiflags = SGF_MULTISTRING def = DT_MANUAL -min = DT_MIN -max = DT_MAX -interval = 1 +enumlist = _linkgraph_mode_symmetric str = STR_CONFIG_SETTING_DISTRIBUTION_MAIL -strval = STR_CONFIG_SETTING_DISTRIBUTION_MANUAL strhelp = STR_CONFIG_SETTING_DISTRIBUTION_MAIL_HELPTEXT -[SDT_VAR] +[SDT_ENUM] base = GameSettings var = linkgraph.distribution_armoured type = SLE_UINT8 from = SLV_183 -guiflags = SGF_MULTISTRING def = DT_MANUAL -min = DT_MIN -max = DT_MAX -interval = 1 +enumlist = _linkgraph_mode_symmetric str = STR_CONFIG_SETTING_DISTRIBUTION_ARMOURED -strval = STR_CONFIG_SETTING_DISTRIBUTION_MANUAL strhelp = STR_CONFIG_SETTING_DISTRIBUTION_ARMOURED_HELPTEXT -[SDT_VAR] +[SDT_ENUM] base = GameSettings var = linkgraph.distribution_default type = SLE_UINT8 from = SLV_183 -guiflags = SGF_MULTISTRING def = DT_MANUAL -min = DT_BEGIN -max = DT_MAX_NONSYMMETRIC -interval = 1 +enumlist = _linkgraph_mode_asymmetric str = STR_CONFIG_SETTING_DISTRIBUTION_DEFAULT -strval = STR_CONFIG_SETTING_DISTRIBUTION_MANUAL strhelp = STR_CONFIG_SETTING_DISTRIBUTION_DEFAULT_HELPTEXT [SDT_VAR]