diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index 1ca911098e..8a11f8c3a2 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -208,13 +208,6 @@ protected: uint8_t exclude_bit; uint8_t range_bit; uint8_t dash; - - void FillWithEmptyValues(uint num_valid) - { - assert(num_valid < std::size(values)); - std::fill(std::begin(this->values), std::begin(this->values) + num_valid, 0); - std::fill(std::begin(this->values) + num_valid, std::end(this->values), INVALID_DATAPOINT); - } }; std::vector data{}; @@ -1716,7 +1709,7 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { transported.dash = 2; auto transported_filler = Filler{transported, &Industry::ProducedHistory::transported}; - FillFromHistory(p.history, i->valid_history, produced_filler, transported_filler); + FillFromHistory(p.history, i->valid_history, 0, produced_filler, transported_filler); } for (const auto &a : i->accepted) { @@ -1740,9 +1733,9 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { auto waiting_filler = Filler{waiting, &Industry::AcceptedHistory::waiting}; if (a.history == nullptr) { - FillFromEmpty(i->valid_history, accepted_filler, waiting_filler); + FillFromEmpty(i->valid_history, 0, accepted_filler, waiting_filler); } else { - FillFromHistory(*a.history, i->valid_history, accepted_filler, waiting_filler); + FillFromHistory(*a.history, i->valid_history, 0, accepted_filler, waiting_filler); } } diff --git a/src/industry_cmd.cpp b/src/industry_cmd.cpp index b036fd9a47..e9a333b576 100644 --- a/src/industry_cmd.cpp +++ b/src/industry_cmd.cpp @@ -1842,7 +1842,7 @@ static void DoCreateNewIndustry(Industry *i, TileIndex tile, IndustryType type, p.history[LAST_MONTH].production += ScaleByCargoScale(p.rate * 8, false); } - UpdateValidHistory(i->valid_history); + UpdateValidHistory(i->valid_history, TimerGameEconomy::month); } if (indspec->callback_mask.Test(IndustryCallbackMask::DecideColour)) { @@ -2494,19 +2494,38 @@ void GenerateIndustries() _industry_builder.Reset(); } +template <> +Industry::ProducedHistory SumHistory(std::span history) +{ + uint32_t production = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const auto &p) { return r + p.production; }); + uint32_t transported = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const auto &p) { return r + p.transported; }); + auto count = std::size(history); + return {.production = ClampTo(production / count), .transported = ClampTo(transported / count)}; +} + +template <> +Industry::AcceptedHistory SumHistory(std::span history) +{ + uint32_t accepted = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const auto &a) { return r + a.accepted; }); + uint32_t waiting = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const auto &a) { return r + a.waiting; });; + auto count = std::size(history); + return {.accepted = ClampTo(accepted / count), .waiting = ClampTo(waiting / count)}; +} + /** * Monthly update of industry statistics. * @param i Industry to update. */ static void UpdateIndustryStatistics(Industry *i) { - UpdateValidHistory(i->valid_history); + auto month = TimerGameEconomy::month; + UpdateValidHistory(i->valid_history, month); for (auto &p : i->produced) { if (IsValidCargoType(p.cargo)) { if (p.history[THIS_MONTH].production != 0) i->last_prod_year = TimerGameEconomy::year; - RotateHistory(p.history); + RotateHistory(p.history, i->valid_history, month); } } @@ -2515,7 +2534,7 @@ static void UpdateIndustryStatistics(Industry *i) if (a.history == nullptr) continue; (*a.history)[THIS_MONTH].waiting = GetAndResetAccumulatedAverage(a.accumulated_waiting); - RotateHistory(*a.history); + RotateHistory(*a.history, i->valid_history, month); } } diff --git a/src/misc/CMakeLists.txt b/src/misc/CMakeLists.txt index d26249165c..0cadf41a94 100644 --- a/src/misc/CMakeLists.txt +++ b/src/misc/CMakeLists.txt @@ -8,6 +8,7 @@ add_files( getoptdata.cpp getoptdata.h hashtable.hpp + history.cpp history_func.hpp history_type.hpp lrucache.hpp diff --git a/src/misc/history.cpp b/src/misc/history.cpp new file mode 100644 index 0000000000..6188c8ae34 --- /dev/null +++ b/src/misc/history.cpp @@ -0,0 +1,71 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file history.cpp Implementation of functions for storing historical data. */ + +#ifndef HISTORY_CPP +#define HISTORY_CPP + +#include "../stdafx.h" + +#include "../core/bitmath_func.hpp" +#include "history_type.hpp" +#include "history_func.hpp" + +#include "../safeguards.h" + +static void UpdateValidHistory(ValidHistoryMask &valid_history, const HistoryRange &hr, uint cur_month) +{ + if (cur_month % hr.total_division != 0) return; + if (hr.division != 1 && !HasBit(valid_history, hr.first - hr.division)) return; + + SB(valid_history, hr.first, hr.records, GB(valid_history, hr.first, hr.records) << 1ULL | 1ULL); +} + +/** + * Update mask of valid records. + * @param[in,out] valid_history Valid history records. + * @param age Current economy month. + */ +void UpdateValidHistory(ValidHistoryMask &valid_history, uint cur_month) +{ + UpdateValidHistory(valid_history, HISTORY_MONTH, cur_month); + UpdateValidHistory(valid_history, HISTORY_QUARTER, cur_month); + UpdateValidHistory(valid_history, HISTORY_YEAR, cur_month); +} + +static bool IsValidHistory(ValidHistoryMask valid_history, const HistoryRange &hr, uint age) +{ + if (hr.hr == nullptr) { + if (age < hr.periods) { + uint slot = hr.first + age; + return HasBit(valid_history, slot); + } + } else { + if (age * hr.division < static_cast(hr.hr->periods - hr.division)) { + uint start = age * hr.division + ((TimerGameEconomy::month / hr.hr->division) % hr.division); + return IsValidHistory(valid_history, *hr.hr, start/* + hr.division - 1*/); + } + if (age < hr.periods) { + uint slot = hr.first + age - ((hr.hr->periods / hr.division) - 1); + return HasBit(valid_history, slot); + } + } + return false; +} + +bool IsValidHistory(ValidHistoryMask valid_history, uint period, uint age) +{ + switch (period) { + case 0: return IsValidHistory(valid_history, HISTORY_MONTH, age); + case 1: return IsValidHistory(valid_history, HISTORY_QUARTER, age); + case 2: return IsValidHistory(valid_history, HISTORY_YEAR, age); + default: NOT_REACHED(); + } +} + +#endif /* HISTORY_CPP */ diff --git a/src/misc/history_func.hpp b/src/misc/history_func.hpp index 5d8fa32919..b6147d94e7 100644 --- a/src/misc/history_func.hpp +++ b/src/misc/history_func.hpp @@ -15,14 +15,18 @@ #include "../timer/timer_game_economy.h" #include "history_type.hpp" +void UpdateValidHistory(ValidHistoryMask &valid_history, uint cur_month); +bool IsValidHistory(ValidHistoryMask valid_history, uint period, uint age); + /** - * Update mask of valid history records. - * @param[in,out] valid_history Valid history records. + * Sum history data elements. + * @note The summation should prevent overflowing, and perform transformations relevant to the type of data. + * @tparam T type of history data element. + * @param history History elements to sum. + * @return Sum of history elements. */ -inline void UpdateValidHistory(ValidHistoryMask &valid_history) -{ - SB(valid_history, LAST_MONTH, HISTORY_RECORDS - LAST_MONTH, GB(valid_history, LAST_MONTH, HISTORY_RECORDS - LAST_MONTH) << 1ULL | 1ULL); -} +template +T SumHistory(typename std::span history); /** * Rotate history. @@ -30,10 +34,28 @@ inline void UpdateValidHistory(ValidHistoryMask &valid_history) * @param history Historical data to rotate. */ template -void RotateHistory(HistoryData &history) +void RotateHistory(HistoryData &history, ValidHistoryMask valid_history, const HistoryRange &hr, uint cur_month) { - std::rotate(std::rbegin(history), std::rbegin(history) + 1, std::rend(history)); - history[THIS_MONTH] = {}; + if (cur_month % hr.total_division != 0) return; + + std::move_backward(std::next(std::begin(history), hr.first), std::next(std::begin(history), hr.last - 1), std::next(std::begin(history), hr.last)); + + if (hr.division == 1) { + history[hr.first] = history[hr.first - 1]; + } else if (HasBit(valid_history, hr.first - hr.division)) { + auto first = std::next(std::begin(history), hr.first - hr.division); + auto last = std::next(first, hr.division); + history[hr.first] = SumHistory(std::span{first, last}); + } +} + +template +void RotateHistory(HistoryData &history, ValidHistoryMask valid_history, uint cur_month) +{ + RotateHistory(history, valid_history, HISTORY_MONTH, cur_month); + RotateHistory(history, valid_history, HISTORY_QUARTER, cur_month); + RotateHistory(history, valid_history, HISTORY_YEAR, cur_month); + history.front() = {}; } /** @@ -49,18 +71,68 @@ T GetAndResetAccumulatedAverage(Taccrued &total) return result; } +template +bool GetHistory(const HistoryData &history, ValidHistoryMask valid_history, const HistoryRange &hr, uint age, T &result) +{ + if (hr.hr == nullptr) { + if (age < hr.periods) { + uint slot = hr.first + age; + result = history[slot]; + return HasBit(valid_history, slot); + } + } else { + if (age * hr.division < static_cast(hr.hr->periods - hr.division)) { + bool is_valid = false; + std::array tmp_result; // No need to clear as we fill every element we use. + uint start = age * hr.division + ((TimerGameEconomy::month / hr.hr->division) % hr.division); + for (auto i = start; i != start + hr.division; ++i) { + is_valid |= GetHistory(history, valid_history, *hr.hr, i, tmp_result[i - start]); + } + result = SumHistory(std::span{std::begin(tmp_result), hr.division}); + return is_valid; + } + if (age < hr.periods) { + uint slot = hr.first + age - ((hr.hr->periods / hr.division) - 1); + result = history[slot]; + return HasBit(valid_history, slot); + } + } + NOT_REACHED(); +} + +/** + * Get history data for the specified period and age within that period. + * @param history History data to extract from. + * @param valid_history Mask of valid history records. + * @param period Period to get. + * @param age Age of data to get. + * @param[out] result Variable to store historical value for period and age. + * @return True if the value is valid. + */ +template +bool GetHistory(const HistoryData &history, ValidHistoryMask valid_history, uint period, uint age, T &result) +{ + switch (period) { + case 0: return GetHistory(history, valid_history, HISTORY_MONTH, age, result); + case 1: return GetHistory(history, valid_history, HISTORY_QUARTER, age, result); + case 2: return GetHistory(history, valid_history, HISTORY_YEAR, age, result); + default: NOT_REACHED(); + } +} + /** * Fill some data with historical data. * @param history Historical data to fill from. * @param valid_history Mask of valid history records. + * @param period Period (monthly, quarterly, yearly) to fill with. * @param fillers Fillers to fill with history data. */ template -void FillFromHistory(const HistoryData &history, ValidHistoryMask valid_history, Tfillers... fillers) +void FillFromHistory(const HistoryData &history, ValidHistoryMask valid_history, uint period, Tfillers... fillers) { + T data{}; for (uint i = 0; i != N; ++i) { - if (HasBit(valid_history, N - i)) { - auto &data = history[N - i]; + if (GetHistory(history, valid_history, period, N - i - 1, data)) { (fillers.Fill(i, data), ...); } else { (fillers.MakeInvalid(i), ...); @@ -71,13 +143,14 @@ void FillFromHistory(const HistoryData &history, ValidHistoryMask valid_histo /** * Fill some data with empty records. * @param valid_history Mask of valid history records. + * @param period Period (monthly, quarterly, yearly) to fill with. * @param fillers Fillers to fill with history data. */ template -void FillFromEmpty(ValidHistoryMask valid_history, Tfillers... fillers) +void FillFromEmpty(ValidHistoryMask valid_history, uint period, Tfillers... fillers) { for (uint i = 0; i != N; ++i) { - if (HasBit(valid_history, N - i)) { + if (IsValidHistory(valid_history, period, N - i - 1)) { (fillers.MakeZero(i), ...); } else { (fillers.MakeInvalid(i), ...); diff --git a/src/misc/history_type.hpp b/src/misc/history_type.hpp index 98bcee97a2..81e71dff0e 100644 --- a/src/misc/history_type.hpp +++ b/src/misc/history_type.hpp @@ -10,7 +10,40 @@ #ifndef HISTORY_TYPE_HPP #define HISTORY_TYPE_HPP -static constexpr uint8_t HISTORY_RECORDS = 25; +#include "../stdafx.h" + +static constexpr uint8_t HISTORY_PERIODS = 24; + +struct HistoryRange { + const HistoryRange *hr; + const uint8_t periods; ///< Number of periods for this range. + const uint8_t records; ///< Number of records needed for this range. + const uint8_t first; ///< Index of first element in history data. + const uint8_t last; ///< Index of last element in history data. + const uint8_t division; ///< Number of divisions of the previous history range. + const uint8_t total_division; ///< Number of divisions of the initial history range. + + explicit constexpr HistoryRange(uint8_t periods) : + hr(nullptr), periods(periods), records(this->periods), first(1), last(this->first + this->records), division(1), total_division(1) + { + } + + constexpr HistoryRange(const HistoryRange &hr, uint8_t division, uint8_t periods) : + hr(&hr), periods(periods), records(this->periods - ((hr.periods / division) - 1)), first(hr.last), last(this->first + this->records), + division(division), total_division(division * hr.total_division) + { + } +}; + +static constexpr HistoryRange HISTORY_MONTH{HISTORY_PERIODS}; +static constexpr HistoryRange HISTORY_QUARTER{HISTORY_MONTH, 3, HISTORY_PERIODS}; +static constexpr HistoryRange HISTORY_YEAR{HISTORY_QUARTER, 4, HISTORY_PERIODS}; + +/** Maximum number of divisions from previous history range. */ +static constexpr uint8_t HISTORY_MAX_DIVISION = std::max({HISTORY_MONTH.division, HISTORY_QUARTER.division, HISTORY_YEAR.division}); + +/** Total number of records require for all history data. */ +static constexpr uint8_t HISTORY_RECORDS = HISTORY_YEAR.last; static constexpr uint8_t THIS_MONTH = 0; static constexpr uint8_t LAST_MONTH = 1; diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index c603eb99d3..32715c4431 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -3,6 +3,7 @@ add_test_files( bitmath_func.cpp enum_over_optimisation.cpp flatset_type.cpp + history_func.cpp landscape_partial_pixel_z.cpp math_func.cpp mock_environment.h diff --git a/src/tests/history_func.cpp b/src/tests/history_func.cpp new file mode 100644 index 0000000000..7390e56749 --- /dev/null +++ b/src/tests/history_func.cpp @@ -0,0 +1,87 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file history_func.cpp Test functionality for misc/history_func. */ + +#include "../stdafx.h" + +#include "../3rdparty/catch2/catch.hpp" + +#include "../misc/history_type.hpp" +#include "../misc/history_func.hpp" + +#include "../safeguards.h" + +template <> +uint16_t SumHistory(std::span history) +{ + uint32_t total = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const uint16_t &value) { return r + value; }); + return ClampTo(total); +} + +/** + * Helper to get history records and return the value, instead returning its validity. + * @param history History data to extract from. + * @param period Period to get. + * @param age Age of data to get. + * @return Historical value for the period and age. + */ +template +T GetHistory(const HistoryData &history, uint period, uint age) +{ + T result; + switch (period) { + case 0: GetHistory(history, 0, HISTORY_MONTH, age, result); break; + case 1: GetHistory(history, 0, HISTORY_QUARTER, age, result); break; + case 2: GetHistory(history, 0, HISTORY_YEAR, age, result); break; + default: NOT_REACHED(); + } + return result; +} + +TEST_CASE("History Rotation and Reporting tests") +{ + HistoryData history{}; + uint64_t valid_history = 0; + + /* Fill the history with decreasing data points for 24 years of history. This ensures that no data period should + * contain the same value as another period. */ + uint16_t i = 12 * HISTORY_PERIODS; + for (uint date = 1; date <= 12 * HISTORY_PERIODS; ++date, --i) { + history[THIS_MONTH] = i; + RotateHistory(history, valid_history, date % 12); + } + + /* With the decreasing sequence, the expected value is triangle number (x*x+n)/2 and the square of the total divisions. + * for quarters: 1 + 2 + 3 = 6, 4 + 5 + 6 = 15, 7 + 8 + 9 = 24, 10 + 11 + 12 = 33 + * 13 + 14 + 15 = 42, 16 + 17 + 18 = 51, 19 + 20 + 21 = 60, 22 + 23 + 24 = 69... + * for years: 6 + 15 + 24 + 33 = 78, 42 + 51 + 60 + 69 = 222... + */ + for (uint j = 0; j < HISTORY_PERIODS; ++j) { + CHECK(GetHistory(history, 0, j) == (( 1 * 1 + 1) / 2) + 1 * 1 * j); + CHECK(GetHistory(history, 1, j) == (( 3 * 3 + 3) / 2) + 3 * 3 * j); + CHECK(GetHistory(history, 2, j) == ((12 * 12 + 12) / 2) + 12 * 12 * j); + } + + /* Double-check quarter history matches summed month history. */ + CHECK(GetHistory(history, 0, 0) + GetHistory(history, 0, 1) + GetHistory(history, 0, 2) == GetHistory(history, 1, 0)); + CHECK(GetHistory(history, 0, 3) + GetHistory(history, 0, 4) + GetHistory(history, 0, 5) == GetHistory(history, 1, 1)); + CHECK(GetHistory(history, 0, 6) + GetHistory(history, 0, 7) + GetHistory(history, 0, 8) == GetHistory(history, 1, 2)); + CHECK(GetHistory(history, 0, 9) + GetHistory(history, 0, 10) + GetHistory(history, 0, 11) == GetHistory(history, 1, 3)); + CHECK(GetHistory(history, 0, 12) + GetHistory(history, 0, 13) + GetHistory(history, 0, 14) == GetHistory(history, 1, 4)); + CHECK(GetHistory(history, 0, 15) + GetHistory(history, 0, 16) + GetHistory(history, 0, 17) == GetHistory(history, 1, 5)); + CHECK(GetHistory(history, 0, 18) + GetHistory(history, 0, 19) + GetHistory(history, 0, 20) == GetHistory(history, 1, 6)); + CHECK(GetHistory(history, 0, 21) + GetHistory(history, 0, 22) + GetHistory(history, 0, 23) == GetHistory(history, 1, 7)); + + /* Double-check year history matches summed quarter history. */ + CHECK(GetHistory(history, 1, 0) + GetHistory(history, 1, 1) + GetHistory(history, 1, 2) + GetHistory(history, 1, 3) == GetHistory(history, 2, 0)); + CHECK(GetHistory(history, 1, 4) + GetHistory(history, 1, 5) + GetHistory(history, 1, 6) + GetHistory(history, 1, 7) == GetHistory(history, 2, 1)); + CHECK(GetHistory(history, 1, 8) + GetHistory(history, 1, 9) + GetHistory(history, 1, 10) + GetHistory(history, 1, 11) == GetHistory(history, 2, 2)); + CHECK(GetHistory(history, 1, 12) + GetHistory(history, 1, 13) + GetHistory(history, 1, 14) + GetHistory(history, 1, 15) == GetHistory(history, 2, 3)); + CHECK(GetHistory(history, 1, 16) + GetHistory(history, 1, 17) + GetHistory(history, 1, 18) + GetHistory(history, 1, 19) == GetHistory(history, 2, 4)); + CHECK(GetHistory(history, 1, 20) + GetHistory(history, 1, 21) + GetHistory(history, 1, 22) + GetHistory(history, 1, 23) == GetHistory(history, 2, 5)); +}