From d56312d3449e929e13e62c8299a508dbb2eef602 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Thu, 17 Jul 2025 18:32:21 +0100 Subject: [PATCH 1/5] Codechange: Allow unused graph ranges to be masked. --- src/graph_gui.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index 3e744f5670..fd7a73cc0a 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -182,8 +182,9 @@ protected: static const int MIN_GRAPH_NUM_LINES_Y = 9; ///< Minimal number of horizontal lines to draw. static const int MIN_GRID_PIXEL_SIZE = 20; ///< Minimum distance between graph lines. - uint64_t excluded_data = 0; ///< bitmask of the datasets that shouldn't be displayed. - uint64_t excluded_range = 0; ///< bitmask of ranges that should not be displayed. + uint64_t excluded_data = 0; ///< bitmask of datasets hidden by the player. + uint64_t excluded_range = 0; ///< bitmask of ranges hidden by the player. + uint64_t masked_range = 0; ///< bitmask of ranges that are not available for the current data. uint8_t num_on_x_axis = 0; uint8_t num_vert_lines = GRAPH_NUM_MONTHS; @@ -675,13 +676,17 @@ public: uint index = 0; Rect line = r.WithHeight(line_height); for (const auto &str : this->ranges) { - bool lowered = !HasBit(this->excluded_range, index); + bool lowered = !HasBit(this->excluded_range, index) && !HasBit(this->masked_range, index); /* Redraw frame if lowered */ if (lowered) DrawFrameRect(line, COLOUR_BROWN, FrameFlag::Lowered); const Rect text = line.Shrink(WidgetDimensions::scaled.framerect); - DrawString(text, str, TC_BLACK, SA_CENTER, false, FS_SMALL); + DrawString(text, str, (this->highlight_state && this->highlight_range == index) ? TC_WHITE : TC_BLACK, SA_CENTER, false, FS_SMALL); + + if (HasBit(this->masked_range, index)) { + GfxFillRect(line.Shrink(WidgetDimensions::scaled.bevel), GetColourGradient(COLOUR_BROWN, SHADE_DARKER), FILLRECT_CHECKER); + } line = line.Translate(0, line_height); ++index; @@ -704,6 +709,7 @@ public: case WID_GRAPH_RANGE_MATRIX: { int row = GetRowFromWidget(pt.y, widget, 0, GetCharacterHeight(FS_SMALL) + WidgetDimensions::scaled.framerect.Vertical()); + if (HasBit(this->masked_range, row)) break; ToggleBit(this->excluded_range, row); this->SetDirty(); break; @@ -1115,6 +1121,7 @@ struct BaseCargoGraphWindow : BaseGraphWindow { { this->CreateNestedTree(); + this->excluded_range = this->masked_range; this->cargo_types = this->GetCargoTypes(number); this->vscroll = this->GetScrollbar(WID_GRAPH_MATRIX_SCROLLBAR); From db7dd4b03f47d605ad62ea8339fc035c331a7502 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Thu, 17 Jul 2025 18:32:21 +0100 Subject: [PATCH 2/5] Add: Industry accepted and waiting history graphs. Records amount of cargo accepted, and a rolling average of the waiting amount. Average waiting samples the waiting amount once per day for each industry, spread out over an economy day. --- src/economy.cpp | 1 + src/graph_gui.cpp | 53 +++++++++++++++++++++++++++++--- src/industry.h | 19 +++++++++++- src/industry_cmd.cpp | 12 ++++++++ src/industry_gui.cpp | 4 +-- src/lang/english.txt | 8 +++-- src/misc/history_func.hpp | 32 +++++++++++++++++++ src/saveload/industry_sl.cpp | 38 +++++++++++++++++++++++ src/saveload/oldloader_sl.cpp | 2 +- src/saveload/saveload.h | 1 + src/timer/timer_game_economy.cpp | 4 +++ src/timer/timer_game_economy.h | 2 ++ 12 files changed, 164 insertions(+), 12 deletions(-) diff --git a/src/economy.cpp b/src/economy.cpp index 50bdc26a62..f3529cd8e3 100644 --- a/src/economy.cpp +++ b/src/economy.cpp @@ -1067,6 +1067,7 @@ static uint DeliverGoodsToIndustry(const Station *st, CargoType cargo_type, uint uint amount = std::min(num_pieces, 0xFFFFu - it->waiting); it->waiting += amount; + it->GetOrCreateHistory()[THIS_MONTH].accepted += amount; it->last_accepted = TimerGameEconomy::date; num_pieces -= amount; accepted += amount; diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index fd7a73cc0a..f94c1bd159 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -217,13 +217,20 @@ protected: uint8_t highlight_range = UINT8_MAX; ///< Data range that should be highlighted, or UINT8_MAX for none. bool highlight_state = false; ///< Current state of highlight, toggled every TIMER_BLINK_INTERVAL period. - template - struct Filler { + struct BaseFiller { DataSet &dataset; ///< Dataset to fill. + + inline void MakeZero(uint i) const { this->dataset.values[i] = 0; } + inline void MakeInvalid(uint i) const { this->dataset.values[i] = INVALID_DATAPOINT; } + }; + + template + struct Filler : BaseFiller { const Tprojection &proj; ///< Projection to apply. + constexpr Filler(DataSet &dataset, const Tprojection &proj) : BaseFiller(dataset), proj(proj) {} + inline void Fill(uint i, const auto &data) const { this->dataset.values[i] = std::invoke(this->proj, data); } - inline void MakeInvalid(uint i) const { this->dataset.values[i] = INVALID_DATAPOINT; } }; /** @@ -1615,7 +1622,9 @@ CompanyID PerformanceRatingDetailWindow::company = CompanyID::Invalid(); struct IndustryProductionGraphWindow : BaseCargoGraphWindow { static inline constexpr StringID RANGE_LABELS[] = { STR_GRAPH_INDUSTRY_RANGE_PRODUCED, - STR_GRAPH_INDUSTRY_RANGE_TRANSPORTED + STR_GRAPH_INDUSTRY_RANGE_TRANSPORTED, + STR_GRAPH_INDUSTRY_RANGE_DELIVERED, + STR_GRAPH_INDUSTRY_RANGE_WAITING, }; static inline CargoTypes excluded_cargo_types{}; @@ -1630,6 +1639,10 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); this->ranges = RANGE_LABELS; + const Industry *i = Industry::Get(window_number); + if (!i->IsCargoProduced()) this->masked_range = (1U << 0) | (1U << 1); + if (!i->IsCargoAccepted()) this->masked_range = (1U << 2) | (1U << 3); + this->InitializeWindow(window_number, STR_GRAPH_LAST_24_MINUTES_TIME_LABEL); } @@ -1637,6 +1650,9 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { { CargoTypes cargo_types{}; const Industry *i = Industry::Get(window_number); + for (const auto &a : i->accepted) { + if (IsValidCargoType(a.cargo)) SetBit(cargo_types, a.cargo); + } for (const auto &p : i->produced) { if (IsValidCargoType(p.cargo)) SetBit(cargo_types, p.cargo); } @@ -1650,7 +1666,7 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { std::string GetWidgetString(WidgetID widget, StringID stringid) const override { - if (widget == WID_GRAPH_CAPTION) return GetString(STR_GRAPH_INDUSTRY_PRODUCTION_CAPTION, this->window_number); + if (widget == WID_GRAPH_CAPTION) return GetString(STR_GRAPH_INDUSTRY_CAPTION, this->window_number); return this->Window::GetWidgetString(widget, stringid); } @@ -1698,6 +1714,33 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { FillFromHistory(p.history, i->valid_history, produced_filler, transported_filler); } + for (const auto &a : i->accepted) { + if (!IsValidCargoType(a.cargo)) continue; + const CargoSpec *cs = CargoSpec::Get(a.cargo); + + this->data.reserve(this->data.size() + 2); + + DataSet &accepted = this->data.emplace_back(); + accepted.colour = cs->legend_colour; + accepted.exclude_bit = cs->Index(); + accepted.range_bit = 2; + accepted.dash = 1; + auto accepted_filler = Filler{accepted, &Industry::AcceptedHistory::accepted}; + + DataSet &waiting = this->data.emplace_back(); + waiting.colour = cs->legend_colour; + waiting.exclude_bit = cs->Index(); + waiting.range_bit = 3; + waiting.dash = 4; + auto waiting_filler = Filler{waiting, &Industry::AcceptedHistory::waiting}; + + if (a.history == nullptr) { + FillFromEmpty(i->valid_history, accepted_filler, waiting_filler); + } else { + FillFromHistory(*a.history, i->valid_history, accepted_filler, waiting_filler); + } + } + this->SetDirty(); } }; diff --git a/src/industry.h b/src/industry.h index 0122de124c..27296410e3 100644 --- a/src/industry.h +++ b/src/industry.h @@ -77,10 +77,27 @@ struct Industry : IndustryPool::PoolItem<&_industry_pool> { HistoryData history{}; ///< History of cargo produced and transported for this month and 24 previous months }; + struct AcceptedHistory { + uint16_t accepted = 0; /// Total accepted. + uint16_t waiting = 0; /// Average waiting. + }; + struct AcceptedCargo { CargoType cargo = 0; ///< Cargo type uint16_t waiting = 0; ///< Amount of cargo waiting to processed + uint32_t accumulated_waiting = 0; ///< Accumulated waiting total over the last month, used to calculate average. TimerGameEconomy::Date last_accepted{}; ///< Last day cargo was accepted by this industry + std::unique_ptr> history{}; ///< History of accepted and waiting cargo. + + /** + * Get history data, creating it if necessary. + * @return Accepted history data. + */ + inline HistoryData &GetOrCreateHistory() + { + if (this->history == nullptr) this->history = std::make_unique>(); + return *this->history; + } }; using ProducedCargoes = std::vector; @@ -151,7 +168,7 @@ struct Industry : IndustryPool::PoolItem<&_industry_pool> { */ inline const AcceptedCargo &GetAccepted(size_t slot) const { - static const AcceptedCargo empty{INVALID_CARGO, 0, {}}; + static const AcceptedCargo empty{INVALID_CARGO, 0, 0, {}, {}}; return slot < this->accepted.size() ? this->accepted[slot] : empty; } diff --git a/src/industry_cmd.cpp b/src/industry_cmd.cpp index 18f4d94a48..b036fd9a47 100644 --- a/src/industry_cmd.cpp +++ b/src/industry_cmd.cpp @@ -1245,6 +1245,10 @@ void OnTick_Industry() for (Industry *i : Industry::Iterate()) { ProduceIndustryGoods(i); + + if ((TimerGameTick::counter + i->index) % Ticks::DAY_TICKS == 0) { + for (auto &a : i->accepted) a.accumulated_waiting += a.waiting; + } } } @@ -2505,6 +2509,14 @@ static void UpdateIndustryStatistics(Industry *i) RotateHistory(p.history); } } + + for (auto &a : i->accepted) { + if (!IsValidCargoType(a.cargo)) continue; + if (a.history == nullptr) continue; + + (*a.history)[THIS_MONTH].waiting = GetAndResetAccumulatedAverage(a.accumulated_waiting); + RotateHistory(*a.history); + } } /** diff --git a/src/industry_gui.cpp b/src/industry_gui.cpp index 6119c486dd..c8cc21f83e 100644 --- a/src/industry_gui.cpp +++ b/src/industry_gui.cpp @@ -845,7 +845,7 @@ public: nvp->InitializeViewport(this, Industry::Get(window_number)->location.GetCenterTile(), ScaleZoomGUI(ZoomLevel::Industry)); const Industry *i = Industry::Get(window_number); - if (!i->IsCargoProduced()) this->DisableWidget(WID_IV_GRAPH); + if (!i->IsCargoProduced() && !i->IsCargoAccepted()) this->DisableWidget(WID_IV_GRAPH); this->InvalidateData(); } @@ -1242,7 +1242,7 @@ static constexpr NWidgetPart _nested_industry_view_widgets[] = { EndContainer(), NWidget(NWID_HORIZONTAL), NWidget(WWT_PUSHTXTBTN, COLOUR_CREAM, WID_IV_DISPLAY), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_INDUSTRY_DISPLAY_CHAIN, STR_INDUSTRY_DISPLAY_CHAIN_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_CREAM, WID_IV_GRAPH), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_INDUSTRY_VIEW_PRODUCTION_GRAPH, STR_INDUSTRY_VIEW_PRODUCTION_GRAPH_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_CREAM, WID_IV_GRAPH), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_INDUSTRY_VIEW_CARGO_GRAPH, STR_INDUSTRY_VIEW_CARGO_GRAPH_TOOLTIP), NWidget(WWT_RESIZEBOX, COLOUR_CREAM), EndContainer(), }; diff --git a/src/lang/english.txt b/src/lang/english.txt index 5ebe998a1a..41531939f3 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -634,9 +634,11 @@ STR_GRAPH_CARGO_TOOLTIP_DISABLE_ALL :{BLACK}Display STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO :{BLACK}Toggle graph of this cargo type STR_GRAPH_CARGO_PAYMENT_CARGO :{TINY_FONT}{BLACK}{STRING} -STR_GRAPH_INDUSTRY_PRODUCTION_CAPTION :{WHITE}{INDUSTRY} - Production History +STR_GRAPH_INDUSTRY_CAPTION :{WHITE}{INDUSTRY} - Cargo History STR_GRAPH_INDUSTRY_RANGE_PRODUCED :Produced STR_GRAPH_INDUSTRY_RANGE_TRANSPORTED :Transported +STR_GRAPH_INDUSTRY_RANGE_DELIVERED :Delivered +STR_GRAPH_INDUSTRY_RANGE_WAITING :Waiting STR_GRAPH_PERFORMANCE_DETAIL_TOOLTIP :{BLACK}Show detailed performance ratings @@ -4024,8 +4026,8 @@ STR_INDUSTRY_VIEW_PRODUCTION_LAST_MONTH_TITLE :{BLACK}Producti STR_INDUSTRY_VIEW_PRODUCTION_LAST_MINUTE_TITLE :{BLACK}Production last minute: STR_INDUSTRY_VIEW_TRANSPORTED :{YELLOW}{CARGO_LONG}{RAW_STRING}{BLACK} ({COMMA}% transported) STR_INDUSTRY_VIEW_LOCATION_TOOLTIP :{BLACK}Centre the main view on industry location. Ctrl+Click to open a new viewport on industry location -STR_INDUSTRY_VIEW_PRODUCTION_GRAPH :{BLACK}Production Graph -STR_INDUSTRY_VIEW_PRODUCTION_GRAPH_TOOLTIP :{BLACK}Shows the graph of industry production history +STR_INDUSTRY_VIEW_CARGO_GRAPH :{BLACK}Cargo Graph +STR_INDUSTRY_VIEW_CARGO_GRAPH_TOOLTIP :{BLACK}Shows the graph of industry cargo history STR_INDUSTRY_VIEW_PRODUCTION_LEVEL :{BLACK}Production level: {YELLOW}{COMMA}% STR_INDUSTRY_VIEW_INDUSTRY_ANNOUNCED_CLOSURE :{YELLOW}The industry has announced imminent closure! diff --git a/src/misc/history_func.hpp b/src/misc/history_func.hpp index b8c5b33050..5d8fa32919 100644 --- a/src/misc/history_func.hpp +++ b/src/misc/history_func.hpp @@ -11,6 +11,8 @@ #define HISTORY_FUNC_HPP #include "../core/bitmath_func.hpp" +#include "../core/math_func.hpp" +#include "../timer/timer_game_economy.h" #include "history_type.hpp" /** @@ -34,6 +36,19 @@ void RotateHistory(HistoryData &history) history[THIS_MONTH] = {}; } +/** + * Get an average value for the previous month, as reset for the next month. + * @param total Accrued total to average. Will be reset to zero. + * @return Average value for the month. + */ +template +T GetAndResetAccumulatedAverage(Taccrued &total) +{ + T result = ClampTo(total / std::max(1U, TimerGameEconomy::days_since_last_month)); + total = 0; + return result; +} + /** * Fill some data with historical data. * @param history Historical data to fill from. @@ -53,4 +68,21 @@ void FillFromHistory(const HistoryData &history, ValidHistoryMask valid_histo } } +/** + * Fill some data with empty records. + * @param valid_history Mask of valid history records. + * @param fillers Fillers to fill with history data. + */ +template +void FillFromEmpty(ValidHistoryMask valid_history, Tfillers... fillers) +{ + for (uint i = 0; i != N; ++i) { + if (HasBit(valid_history, N - i)) { + (fillers.MakeZero(i), ...); + } else { + (fillers.MakeInvalid(i), ...); + } + } +} + #endif /* HISTORY_FUNC_HPP */ diff --git a/src/saveload/industry_sl.cpp b/src/saveload/industry_sl.cpp index 6fc5dfe258..7d81e2bf95 100644 --- a/src/saveload/industry_sl.cpp +++ b/src/saveload/industry_sl.cpp @@ -19,12 +19,50 @@ static OldPersistentStorage _old_ind_persistent_storage; +class SlIndustryAcceptedHistory : public DefaultSaveLoadHandler { +public: + static inline const SaveLoad description[] = { + SLE_VAR(Industry::AcceptedHistory, accepted, SLE_UINT16), + SLE_VAR(Industry::AcceptedHistory, waiting, SLE_UINT16), + }; + static inline const SaveLoadCompatTable compat_description = _industry_produced_history_sl_compat; + + void Save(Industry::AcceptedCargo *a) const override + { + if (!IsValidCargoType(a->cargo) || a->history == nullptr) { + /* Don't save any history if cargo slot isn't used. */ + SlSetStructListLength(0); + return; + } + + SlSetStructListLength(a->history->size()); + + for (auto &h : *a->history) { + SlObject(&h, this->GetDescription()); + } + } + + void Load(Industry::AcceptedCargo *a) const override + { + size_t len = SlGetStructListLength(UINT32_MAX); + if (len == 0) return; + + auto &history = a->GetOrCreateHistory(); + for (auto &h : history) { + if (--len > history.size()) break; // unsigned so wraps after hitting zero. + SlObject(&h, this->GetDescription()); + } + } +}; + class SlIndustryAccepted : public VectorSaveLoadHandler { public: static inline const SaveLoad description[] = { SLE_VAR(Industry::AcceptedCargo, cargo, SLE_UINT8), SLE_VAR(Industry::AcceptedCargo, waiting, SLE_UINT16), SLE_VAR(Industry::AcceptedCargo, last_accepted, SLE_INT32), + SLE_CONDVAR(Industry::AcceptedCargo, accumulated_waiting, SLE_UINT32, SLV_INDUSTRY_ACCEPTED_HISTORY, SL_MAX_VERSION), + SLEG_CONDSTRUCTLIST("history", SlIndustryAcceptedHistory, SLV_INDUSTRY_ACCEPTED_HISTORY, SL_MAX_VERSION), }; static inline const SaveLoadCompatTable compat_description = _industry_accepts_sl_compat; diff --git a/src/saveload/oldloader_sl.cpp b/src/saveload/oldloader_sl.cpp index 8ba02478cc..d3a716dc0d 100644 --- a/src/saveload/oldloader_sl.cpp +++ b/src/saveload/oldloader_sl.cpp @@ -858,7 +858,7 @@ static bool LoadOldIndustry(LoadgameState &ls, int num) if (i->location.tile != 0) { /* Copy data from old fixed arrays to industry. */ - std::copy(std::begin(_old_accepted), std::end(_old_accepted), std::back_inserter(i->accepted)); + std::move(std::begin(_old_accepted), std::end(_old_accepted), std::back_inserter(i->accepted)); std::copy(std::begin(_old_produced), std::end(_old_produced), std::back_inserter(i->produced)); i->town = RemapTown(i->location.tile); diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index 8a150e468b..002e27fef2 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -405,6 +405,7 @@ enum SaveLoadVersion : uint16_t { SLV_FACE_STYLES, ///< 355 PR#14319 Addition of face styles, replacing gender and ethnicity. SLV_INDUSTRY_NUM_VALID_HISTORY, ///< 356 PR#14416 Store number of valid history records for industries. + SLV_INDUSTRY_ACCEPTED_HISTORY, ///< 357 PR#14321 Add per-industry history of cargo delivered and waiting. SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/timer/timer_game_economy.cpp b/src/timer/timer_game_economy.cpp index 8ac08a1ba2..607c2ae856 100644 --- a/src/timer/timer_game_economy.cpp +++ b/src/timer/timer_game_economy.cpp @@ -37,6 +37,7 @@ TimerGameEconomy::Year TimerGameEconomy::year = {}; TimerGameEconomy::Month TimerGameEconomy::month = {}; TimerGameEconomy::Date TimerGameEconomy::date = {}; TimerGameEconomy::DateFract TimerGameEconomy::date_fract = {}; +uint TimerGameEconomy::days_since_last_month = {}; /** * Converts a Date to a Year, Month & Day. @@ -133,6 +134,7 @@ bool TimerManager::Elapsed([[maybe_unused]] TimerGameEconomy:: /* increase day counter */ TimerGameEconomy::date++; + ++TimerGameEconomy::days_since_last_month; TimerGameEconomy::YearMonthDay ymd = TimerGameEconomy::ConvertDateToYMD(TimerGameEconomy::date); @@ -177,6 +179,8 @@ bool TimerManager::Elapsed([[maybe_unused]] TimerGameEconomy:: } } + if (new_month) TimerGameEconomy::days_since_last_month = 0; + /* check if we reached the maximum year, decrement dates by a year */ if (TimerGameEconomy::year == EconomyTime::MAX_YEAR + 1) { TimerGameEconomy::year--; diff --git a/src/timer/timer_game_economy.h b/src/timer/timer_game_economy.h index 81df5ff179..9c51086e9e 100644 --- a/src/timer/timer_game_economy.h +++ b/src/timer/timer_game_economy.h @@ -37,6 +37,8 @@ public: static Date date; ///< Current date in days (day counter). static DateFract date_fract; ///< Fractional part of the day. + static uint days_since_last_month; ///< Number of days that have elapsed since the last month. + static YearMonthDay ConvertDateToYMD(Date date); static Date ConvertYMDToDate(Year year, Month month, Day day); static void SetDate(Date date, DateFract fract); From 7feaac53918fd0e006da6c79cd75f74d22595199 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Fri, 18 Jul 2025 22:28:09 +0100 Subject: [PATCH 3/5] Codechange: Extend industry cargo history to 24 years. Monthly data is stored for the current 24 months. Quarterly data is stored for a further 2-6 years. Yearly data is stored for a further 6-24 years. --- src/graph_gui.cpp | 6 +-- src/industry_cmd.cpp | 27 ++++++++-- src/misc/CMakeLists.txt | 1 + src/misc/history.cpp | 71 ++++++++++++++++++++++++++ src/misc/history_func.hpp | 101 ++++++++++++++++++++++++++++++++----- src/misc/history_type.hpp | 35 ++++++++++++- src/tests/CMakeLists.txt | 1 + src/tests/history_func.cpp | 88 ++++++++++++++++++++++++++++++++ 8 files changed, 308 insertions(+), 22 deletions(-) create mode 100644 src/misc/history.cpp create mode 100644 src/tests/history_func.cpp diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index f94c1bd159..3069b1488a 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -1711,7 +1711,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) { @@ -1735,9 +1735,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..26170b6d4d --- /dev/null +++ b/src/tests/history_func.cpp @@ -0,0 +1,88 @@ +/* + * 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{}; + ValidHistoryMask 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; + UpdateValidHistory(valid_history, date % 12); + 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)); +} From b24ceb27f5f04382bf163cb5553ea4b147bcc1f2 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Fri, 18 Jul 2025 22:28:09 +0100 Subject: [PATCH 4/5] Change: Add support for different horizontal graph scales. --- src/graph_gui.cpp | 104 ++++++++++++++++++++++++++++++------- src/lang/english.txt | 8 +++ src/widgets/graph_widget.h | 1 + 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index 3069b1488a..45b66f4366 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -8,6 +8,7 @@ /** @file graph_gui.cpp GUI that shows performance graphs. */ #include "stdafx.h" +#include #include "misc/history_func.hpp" #include "graph_gui.h" #include "window_gui.h" @@ -174,6 +175,7 @@ protected: static const int GRAPH_PAYMENT_RATE_STEPS = 20; ///< Number of steps on Payment rate graph. static const int PAYMENT_GRAPH_X_STEP_DAYS = 10; ///< X-axis step label for cargo payment rates "Days in transit". static const int PAYMENT_GRAPH_X_STEP_SECONDS = 20; ///< X-axis step label for cargo payment rates "Seconds in transit". + static const int ECONOMY_YEAR_MINUTES = 12; ///< Minutes per economic year. static const int ECONOMY_QUARTER_MINUTES = 3; ///< Minutes per economic quarter. static const int ECONOMY_MONTH_MINUTES = 1; ///< Minutes per economic month. @@ -182,6 +184,24 @@ protected: static const int MIN_GRAPH_NUM_LINES_Y = 9; ///< Minimal number of horizontal lines to draw. static const int MIN_GRID_PIXEL_SIZE = 20; ///< Minimum distance between graph lines. + struct GraphScale { + StringID label = STR_NULL; + uint8_t month_increment = 0; + int16_t x_values_increment = 0; + }; + + static inline constexpr GraphScale MONTHLY_SCALE_WALLCLOCK[] = { + {STR_GRAPH_LAST_24_MINUTES_TIME_LABEL, HISTORY_MONTH.total_division, ECONOMY_MONTH_MINUTES}, + {STR_GRAPH_LAST_72_MINUTES_TIME_LABEL, HISTORY_QUARTER.total_division, ECONOMY_QUARTER_MINUTES}, + {STR_GRAPH_LAST_288_MINUTES_TIME_LABEL, HISTORY_YEAR.total_division, ECONOMY_YEAR_MINUTES}, + }; + + static inline constexpr GraphScale MONTHLY_SCALE_CALENDAR[] = { + {STR_GRAPH_LAST_24_MONTHS, HISTORY_MONTH.total_division, ECONOMY_MONTH_MINUTES}, + {STR_GRAPH_LAST_24_QUARTERS, HISTORY_QUARTER.total_division, ECONOMY_QUARTER_MINUTES}, + {STR_GRAPH_LAST_24_YEARS, HISTORY_YEAR.total_division, ECONOMY_YEAR_MINUTES}, + }; + uint64_t excluded_data = 0; ///< bitmask of datasets hidden by the player. uint64_t excluded_range = 0; ///< bitmask of ranges hidden by the player. uint64_t masked_range = 0; ///< bitmask of ranges that are not available for the current data. @@ -211,7 +231,9 @@ protected: }; std::vector data{}; - std::span ranges = {}; + std::span ranges{}; + std::span scales{}; + uint8_t selected_scale = 0; uint8_t highlight_data = UINT8_MAX; ///< Data set that should be highlighted, or UINT8_MAX for none. uint8_t highlight_range = UINT8_MAX; ///< Data range that should be highlighted, or UINT8_MAX for none. @@ -617,24 +639,34 @@ protected: this->SetDirty(); }}; + void UpdateMatrixSize(WidgetID widget, Dimension &size, Dimension &resize, auto labels) + { + size = {}; + for (const StringID &str : labels) { + size = maxdim(size, GetStringBoundingBox(str, FS_SMALL)); + } + + size.width += WidgetDimensions::scaled.framerect.Horizontal(); + size.height += WidgetDimensions::scaled.framerect.Vertical(); + + /* Set fixed height for number of ranges. */ + size.height *= static_cast(std::size(labels)); + + resize.width = 0; + resize.height = 0; + this->GetWidget(widget)->SetMatrixDimension(1, ClampTo(std::size(labels))); + } + public: void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override { switch (widget) { case WID_GRAPH_RANGE_MATRIX: - for (const StringID &str : this->ranges) { - size = maxdim(size, GetStringBoundingBox(str, FS_SMALL)); - } + this->UpdateMatrixSize(widget, size, resize, this->ranges); + break; - size.width += WidgetDimensions::scaled.framerect.Horizontal(); - size.height += WidgetDimensions::scaled.framerect.Vertical(); - - /* Set fixed height for number of ranges. */ - size.height *= static_cast(std::size(this->ranges)); - - resize.width = 0; - resize.height = 0; - this->GetWidget(WID_GRAPH_RANGE_MATRIX)->SetMatrixDimension(1, ClampTo(std::size(this->ranges))); + case WID_GRAPH_SCALE_MATRIX: + this->UpdateMatrixSize(widget, size, resize, this->scales | std::views::transform(&GraphScale::label)); break; case WID_GRAPH_GRAPH: { @@ -701,6 +733,21 @@ public: break; } + case WID_GRAPH_SCALE_MATRIX: { + uint line_height = GetCharacterHeight(FS_SMALL) + WidgetDimensions::scaled.framerect.Vertical(); + uint8_t selected_month_increment = this->scales[this->selected_scale].month_increment; + Rect line = r.WithHeight(line_height); + for (const auto &scale : this->scales) { + /* Redraw frame if selected */ + if (selected_month_increment == scale.month_increment) DrawFrameRect(line, COLOUR_BROWN, FrameFlag::Lowered); + + DrawString(line.Shrink(WidgetDimensions::scaled.framerect), scale.label, TC_BLACK, SA_CENTER, false, FS_SMALL); + + line = line.Translate(0, line_height); + } + break; + } + default: break; } } @@ -722,6 +769,18 @@ public: break; } + case WID_GRAPH_SCALE_MATRIX: { + int row = GetRowFromWidget(pt.y, widget, 0, GetCharacterHeight(FS_SMALL) + WidgetDimensions::scaled.framerect.Vertical()); + const auto &scale = this->scales[row]; + if (this->selected_scale != row) { + this->selected_scale = row; + this->month_increment = scale.month_increment; + this->x_values_increment = scale.x_values_increment; + this->InvalidateData(); + } + break; + } + default: break; } } @@ -1646,6 +1705,13 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { this->InitializeWindow(window_number, STR_GRAPH_LAST_24_MINUTES_TIME_LABEL); } + void OnInit() override + { + this->BaseCargoGraphWindow::OnInit(); + + this->scales = TimerGameEconomy::UsingWallclockUnits() ? MONTHLY_SCALE_WALLCLOCK : MONTHLY_SCALE_CALENDAR; + } + CargoTypes GetCargoTypes(WindowNumber window_number) const override { CargoTypes cargo_types{}; @@ -1673,7 +1739,7 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { void UpdateStatistics(bool initialize) override { - int mo = TimerGameEconomy::month - this->num_vert_lines; + int mo = (TimerGameEconomy::month / this->month_increment - this->num_vert_lines) * this->month_increment; auto yr = TimerGameEconomy::year; while (mo < 0) { yr--; @@ -1711,7 +1777,7 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { transported.dash = 2; auto transported_filler = Filler{transported, &Industry::ProducedHistory::transported}; - FillFromHistory(p.history, i->valid_history, 0, produced_filler, transported_filler); + FillFromHistory(p.history, i->valid_history, this->selected_scale, produced_filler, transported_filler); } for (const auto &a : i->accepted) { @@ -1735,9 +1801,9 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow { auto waiting_filler = Filler{waiting, &Industry::AcceptedHistory::waiting}; if (a.history == nullptr) { - FillFromEmpty(i->valid_history, 0, accepted_filler, waiting_filler); + FillFromEmpty(i->valid_history, this->selected_scale, accepted_filler, waiting_filler); } else { - FillFromHistory(*a.history, i->valid_history, 0, accepted_filler, waiting_filler); + FillFromHistory(*a.history, i->valid_history, this->selected_scale, accepted_filler, waiting_filler); } } @@ -1758,7 +1824,7 @@ static constexpr NWidgetPart _nested_industry_production_widgets[] = { NWidget(WWT_EMPTY, INVALID_COLOUR, WID_GRAPH_GRAPH), SetMinimalSize(495, 0), SetFill(1, 1), SetResize(1, 1), NWidget(NWID_VERTICAL), NWidget(NWID_SPACER), SetMinimalSize(0, 24), SetFill(0, 1), - NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_RANGE_MATRIX), SetFill(1, 0), SetResize(0, 0), SetMatrixDataTip(1, 0, STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO), + NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_RANGE_MATRIX), SetFill(1, 0), SetResize(0, 0), SetMatrixDataTip(1, 0, STR_GRAPH_TOGGLE_RANGE), NWidget(NWID_SPACER), SetMinimalSize(0, 4), NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GRAPH_ENABLE_CARGOES), SetStringTip(STR_GRAPH_CARGO_ENABLE_ALL, STR_GRAPH_CARGO_TOOLTIP_ENABLE_ALL), SetFill(1, 0), NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GRAPH_DISABLE_CARGOES), SetStringTip(STR_GRAPH_CARGO_DISABLE_ALL, STR_GRAPH_CARGO_TOOLTIP_DISABLE_ALL), SetFill(1, 0), @@ -1767,6 +1833,8 @@ static constexpr NWidgetPart _nested_industry_production_widgets[] = { NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_MATRIX), SetFill(1, 0), SetResize(0, 2), SetMatrixDataTip(1, 0, STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO), SetScrollbar(WID_GRAPH_MATRIX_SCROLLBAR), NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_GRAPH_MATRIX_SCROLLBAR), EndContainer(), + NWidget(NWID_SPACER), SetMinimalSize(0, 4), + NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_SCALE_MATRIX), SetFill(1, 0), SetResize(0, 0), SetMatrixDataTip(1, 0, STR_GRAPH_SELECT_SCALE), NWidget(NWID_SPACER), SetMinimalSize(0, 24), SetFill(0, 1), EndContainer(), NWidget(NWID_SPACER), SetMinimalSize(5, 0), SetFill(0, 1), SetResize(0, 1), diff --git a/src/lang/english.txt b/src/lang/english.txt index 41531939f3..6b36d8aef6 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -622,6 +622,14 @@ STR_GRAPH_COMPANY_VALUES_CAPTION :{WHITE}Company STR_GRAPH_LAST_24_MINUTES_TIME_LABEL :{TINY_FONT}{BLACK}Last 24 minutes STR_GRAPH_LAST_72_MINUTES_TIME_LABEL :{TINY_FONT}{BLACK}Last 72 minutes +STR_GRAPH_LAST_288_MINUTES_TIME_LABEL :{TINY_FONT}{BLACK}Last 288 minutes + +STR_GRAPH_LAST_24_MONTHS :{TINY_FONT}{BLACK}2 years (monthly) +STR_GRAPH_LAST_24_QUARTERS :{TINY_FONT}{BLACK}6 years (quarterly) +STR_GRAPH_LAST_24_YEARS :{TINY_FONT}{BLACK}24 years (yearly) + +STR_GRAPH_TOGGLE_RANGE :Toggle graph for this data range +STR_GRAPH_SELECT_SCALE :Change horizontal scale of graph STR_GRAPH_CARGO_PAYMENT_RATES_CAPTION :{WHITE}Cargo Payment Rates STR_GRAPH_CARGO_PAYMENT_RATES_DAYS :{TINY_FONT}{BLACK}Days in transit diff --git a/src/widgets/graph_widget.h b/src/widgets/graph_widget.h index 496410eb8a..f6c1ae56bc 100644 --- a/src/widgets/graph_widget.h +++ b/src/widgets/graph_widget.h @@ -37,6 +37,7 @@ enum GraphWidgets : WidgetID { WID_GRAPH_MATRIX_SCROLLBAR,///< Cargo list scrollbar. WID_GRAPH_RANGE_MATRIX, ///< Range list. + WID_GRAPH_SCALE_MATRIX, ///< Horizontal axis scale list. WID_PHG_DETAILED_PERFORMANCE, ///< Detailed performance. }; From 09347cc5b6fe4be68b401df9cc38e0b89b9d4b90 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sun, 22 Jun 2025 17:52:55 +0100 Subject: [PATCH 5/5] Add: Town cargo history graphs. --- src/graph_gui.cpp | 143 +++++++++++++++++++++++++++++++++ src/graph_gui.h | 1 + src/lang/english.txt | 6 ++ src/newgrf_town.cpp | 42 ++++++---- src/saveload/oldloader_sl.cpp | 26 ++++-- src/saveload/saveload.h | 1 + src/saveload/town_sl.cpp | 116 +++++++++++++++++++------- src/script/api/script_town.cpp | 10 ++- src/town.h | 47 ++++++++++- src/town_cmd.cpp | 35 ++++++-- src/town_gui.cpp | 14 +++- src/widgets/town_widget.h | 1 + src/window_type.h | 6 ++ 13 files changed, 385 insertions(+), 63 deletions(-) diff --git a/src/graph_gui.cpp b/src/graph_gui.cpp index 45b66f4366..a8f949f0ec 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -27,6 +27,7 @@ #include "timer/timer_game_economy.h" #include "zoom_func.h" #include "industry.h" +#include "town.h" #include "widgets/graph_widget.h" @@ -1858,6 +1859,148 @@ void ShowIndustryProductionGraph(WindowNumber window_number) AllocateWindowDescFront(_industry_production_desc, window_number); } +struct TownCargoGraphWindow : BaseCargoGraphWindow { + static inline constexpr StringID RANGE_LABELS[] = { + STR_GRAPH_TOWN_RANGE_PRODUCED, + STR_GRAPH_TOWN_RANGE_TRANSPORTED, + }; + + static inline CargoTypes excluded_cargo_types{}; + + TownCargoGraphWindow(WindowDesc &desc, WindowNumber window_number) : BaseCargoGraphWindow(desc, STR_JUST_COMMA) + { + this->num_on_x_axis = GRAPH_NUM_MONTHS; + this->num_vert_lines = GRAPH_NUM_MONTHS; + this->month_increment = 1; + this->x_values_reversed = true; + this->x_values_increment = ECONOMY_MONTH_MINUTES; + this->draw_dates = !TimerGameEconomy::UsingWallclockUnits(); + this->ranges = RANGE_LABELS; + + this->InitializeWindow(window_number, STR_GRAPH_LAST_24_MINUTES_TIME_LABEL); + } + + void OnInit() override + { + this->BaseCargoGraphWindow::OnInit(); + + this->scales = TimerGameEconomy::UsingWallclockUnits() ? MONTHLY_SCALE_WALLCLOCK : MONTHLY_SCALE_CALENDAR; + } + + CargoTypes GetCargoTypes(WindowNumber window_number) const override + { + CargoTypes cargo_types{}; + const Town *t = Town::Get(window_number); + for (const auto &s : t->supplied) { + if (IsValidCargoType(s.cargo)) SetBit(cargo_types, s.cargo); + } + return cargo_types; + } + + CargoTypes &GetExcludedCargoTypes() const override + { + return TownCargoGraphWindow::excluded_cargo_types; + } + + std::string GetWidgetString(WidgetID widget, StringID stringid) const override + { + if (widget == WID_GRAPH_CAPTION) return GetString(STR_GRAPH_TOWN_CARGO_CAPTION, this->window_number); + + return this->Window::GetWidgetString(widget, stringid); + } + + void UpdateStatistics(bool initialize) override + { + int mo = TimerGameEconomy::month - this->num_vert_lines; + auto yr = TimerGameEconomy::year; + while (mo < 0) { + yr--; + mo += 12; + } + + if (!initialize && this->excluded_data == this->GetExcludedCargoTypes() && this->num_on_x_axis == this->num_vert_lines && this->year == yr && this->month == mo) { + /* There's no reason to get new stats */ + return; + } + + this->excluded_data = this->GetExcludedCargoTypes(); + this->year = yr; + this->month = mo; + + const Town *t = Town::Get(this->window_number); + + this->data.clear(); + for (const auto &s : t->supplied) { + if (!IsValidCargoType(s.cargo)) continue; + const CargoSpec *cs = CargoSpec::Get(s.cargo); + + this->data.reserve(this->data.size() + 2); + + DataSet &produced = this->data.emplace_back(); + produced.colour = cs->legend_colour; + produced.exclude_bit = cs->Index(); + produced.range_bit = 0; + + DataSet &transported = this->data.emplace_back(); + transported.colour = cs->legend_colour; + transported.exclude_bit = cs->Index(); + transported.range_bit = 1; + transported.dash = 2; + + FillFromHistory(s.history, t->valid_history, this->selected_scale, Filler{produced, &Town::SuppliedHistory::production}, Filler{transported, &Town::SuppliedHistory::transported}); + } + + this->SetDirty(); + } +}; + +static constexpr NWidgetPart _nested_town_cargo_graph_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_BROWN), + NWidget(WWT_CAPTION, COLOUR_BROWN, WID_GRAPH_CAPTION), + NWidget(WWT_SHADEBOX, COLOUR_BROWN), + NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN), + NWidget(WWT_STICKYBOX, COLOUR_BROWN), + EndContainer(), + NWidget(WWT_PANEL, COLOUR_BROWN, WID_GRAPH_BACKGROUND), SetMinimalSize(568, 128), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_EMPTY, INVALID_COLOUR, WID_GRAPH_GRAPH), SetMinimalSize(495, 0), SetFill(1, 1), SetResize(1, 1), + NWidget(NWID_VERTICAL), + NWidget(NWID_SPACER), SetMinimalSize(0, 24), SetFill(0, 1), + NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_RANGE_MATRIX), SetFill(1, 0), SetResize(0, 0), SetMatrixDataTip(1, 0, STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO), + NWidget(NWID_SPACER), SetMinimalSize(0, 4), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GRAPH_ENABLE_CARGOES), SetStringTip(STR_GRAPH_CARGO_ENABLE_ALL, STR_GRAPH_CARGO_TOOLTIP_ENABLE_ALL), SetFill(1, 0), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GRAPH_DISABLE_CARGOES), SetStringTip(STR_GRAPH_CARGO_DISABLE_ALL, STR_GRAPH_CARGO_TOOLTIP_DISABLE_ALL), SetFill(1, 0), + NWidget(NWID_SPACER), SetMinimalSize(0, 4), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_MATRIX), SetFill(1, 0), SetResize(0, 2), SetMatrixDataTip(1, 0, STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO), SetScrollbar(WID_GRAPH_MATRIX_SCROLLBAR), + NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_GRAPH_MATRIX_SCROLLBAR), + EndContainer(), + NWidget(NWID_SPACER), SetMinimalSize(0, 4), + NWidget(WWT_MATRIX, COLOUR_BROWN, WID_GRAPH_SCALE_MATRIX), SetFill(1, 0), SetResize(0, 0), SetMatrixDataTip(1, 0, STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO), + NWidget(NWID_SPACER), SetMinimalSize(0, 24), SetFill(0, 1), + EndContainer(), + NWidget(NWID_SPACER), SetMinimalSize(5, 0), SetFill(0, 1), SetResize(0, 1), + EndContainer(), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_TEXT, INVALID_COLOUR, WID_GRAPH_FOOTER), SetFill(1, 0), SetResize(1, 0), SetPadding(2, 0, 2, 0), SetTextStyle(TC_BLACK, FS_SMALL), SetAlignment(SA_CENTER), + NWidget(WWT_RESIZEBOX, COLOUR_BROWN, WID_GRAPH_RESIZE), SetResizeWidgetTypeTip(RWV_HIDE_BEVEL, STR_TOOLTIP_RESIZE), + EndContainer(), + EndContainer(), +}; + +static WindowDesc _town_cargo_graph_desc( + WDP_AUTO, "graph_town_cargo", 0, 0, + WC_TOWN_CARGO_GRAPH, WC_TOWN_VIEW, + {}, + _nested_town_cargo_graph_widgets +); + +void ShowTownCargoGraph(WindowNumber window_number) +{ + AllocateWindowDescFront(_town_cargo_graph_desc, window_number); +} + /** * Make a vertical list of panels for outputting score details. * @return Panel with performance details. diff --git a/src/graph_gui.h b/src/graph_gui.h index 59a0cb7a8a..4f11807a25 100644 --- a/src/graph_gui.h +++ b/src/graph_gui.h @@ -20,5 +20,6 @@ void ShowCompanyValueGraph(); void ShowCargoPaymentRates(); void ShowPerformanceRatingDetail(); void ShowIndustryProductionGraph(WindowNumber window_number); +void ShowTownCargoGraph(WindowNumber window_number); #endif /* GRAPH_GUI_H */ diff --git a/src/lang/english.txt b/src/lang/english.txt index 6b36d8aef6..a2479d4159 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -648,6 +648,10 @@ STR_GRAPH_INDUSTRY_RANGE_TRANSPORTED :Transported STR_GRAPH_INDUSTRY_RANGE_DELIVERED :Delivered STR_GRAPH_INDUSTRY_RANGE_WAITING :Waiting +STR_GRAPH_TOWN_CARGO_CAPTION :{WHITE}{TOWN} - Cargo History +STR_GRAPH_TOWN_RANGE_PRODUCED :Supply +STR_GRAPH_TOWN_RANGE_TRANSPORTED :Transported + STR_GRAPH_PERFORMANCE_DETAIL_TOOLTIP :{BLACK}Show detailed performance ratings # Graph key window @@ -3702,6 +3706,8 @@ STR_TOWN_VIEW_CENTER_TOOLTIP :{BLACK}Centre t STR_TOWN_VIEW_LOCAL_AUTHORITY_BUTTON :{BLACK}Local Authority STR_TOWN_VIEW_LOCAL_AUTHORITY_TOOLTIP :{BLACK}Show information on local authority STR_TOWN_VIEW_RENAME_TOOLTIP :{BLACK}Change town name +STR_TOWN_VIEW_CARGO_GRAPH :Cargo Graph +STR_TOWN_VIEW_CARGO_GRAPH_TOOLTIP :Show graph of town's cargo history STR_TOWN_VIEW_EXPAND_BUTTON :{BLACK}Expand STR_TOWN_VIEW_EXPAND_TOOLTIP :{BLACK}Increase size of town diff --git a/src/newgrf_town.cpp b/src/newgrf_town.cpp index 64ed6a800e..eb581d9328 100644 --- a/src/newgrf_town.cpp +++ b/src/newgrf_town.cpp @@ -15,6 +15,15 @@ #include "safeguards.h" +template +static uint16_t TownHistoryHelper(const Town *t, CargoLabel label, uint period, Tproj proj) +{ + auto it = t->GetCargoSupplied(GetCargoTypeByLabel(label)); + if (it == std::end(t->supplied)) return 0; + + return ClampTo(std::invoke(proj, it->history[period])); +} + /* virtual */ uint32_t TownScopeResolver::GetVariable(uint8_t variable, [[maybe_unused]] uint32_t parameter, bool &available) const { if (this->t == nullptr) { @@ -22,7 +31,6 @@ return UINT_MAX; } - CargoType cargo_type; switch (variable) { /* Larger towns */ case 0x40: @@ -87,22 +95,22 @@ case 0xB2: return this->t->statues.base(); case 0xB6: return ClampTo(this->t->cache.num_houses); case 0xB9: return this->t->growth_rate / Ticks::TOWN_GROWTH_TICKS; - case 0xBA: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].new_max) : 0; - case 0xBB: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].new_max), 8, 8) : 0; - case 0xBC: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].new_max) : 0; - case 0xBD: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].new_max), 8, 8) : 0; - case 0xBE: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].new_act) : 0; - case 0xBF: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].new_act), 8, 8) : 0; - case 0xC0: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].new_act) : 0; - case 0xC1: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].new_act), 8, 8) : 0; - case 0xC2: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].old_max) : 0; - case 0xC3: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].old_max), 8, 8) : 0; - case 0xC4: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].old_max) : 0; - case 0xC5: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].old_max), 8, 8) : 0; - case 0xC6: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].old_act) : 0; - case 0xC7: cargo_type = GetCargoTypeByLabel(CT_PASSENGERS); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].old_act), 8, 8) : 0; - case 0xC8: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? ClampTo(this->t->supplied[cargo_type].old_act) : 0; - case 0xC9: cargo_type = GetCargoTypeByLabel(CT_MAIL); return IsValidCargoType(cargo_type) ? GB(ClampTo(this->t->supplied[cargo_type].old_act), 8, 8) : 0; + case 0xBA: return TownHistoryHelper(this->t, CT_PASSENGERS, THIS_MONTH, &Town::SuppliedHistory::production); + case 0xBB: return TownHistoryHelper(this->t, CT_PASSENGERS, THIS_MONTH, &Town::SuppliedHistory::production) >> 8; + case 0xBC: return TownHistoryHelper(this->t, CT_MAIL, THIS_MONTH, &Town::SuppliedHistory::production); + case 0xBD: return TownHistoryHelper(this->t, CT_MAIL, THIS_MONTH, &Town::SuppliedHistory::production) >> 8; + case 0xBE: return TownHistoryHelper(this->t, CT_PASSENGERS, THIS_MONTH, &Town::SuppliedHistory::transported); + case 0xBF: return TownHistoryHelper(this->t, CT_PASSENGERS, THIS_MONTH, &Town::SuppliedHistory::transported) >> 8; + case 0xC0: return TownHistoryHelper(this->t, CT_MAIL, THIS_MONTH, &Town::SuppliedHistory::transported); + case 0xC1: return TownHistoryHelper(this->t, CT_MAIL, THIS_MONTH, &Town::SuppliedHistory::transported) >> 8; + case 0xC2: return TownHistoryHelper(this->t, CT_PASSENGERS, LAST_MONTH, &Town::SuppliedHistory::production); + case 0xC3: return TownHistoryHelper(this->t, CT_PASSENGERS, LAST_MONTH, &Town::SuppliedHistory::production) >> 8; + case 0xC4: return TownHistoryHelper(this->t, CT_MAIL, LAST_MONTH, &Town::SuppliedHistory::production); + case 0xC5: return TownHistoryHelper(this->t, CT_MAIL, LAST_MONTH, &Town::SuppliedHistory::production) >> 8; + case 0xC6: return TownHistoryHelper(this->t, CT_PASSENGERS, LAST_MONTH, &Town::SuppliedHistory::transported); + case 0xC7: return TownHistoryHelper(this->t, CT_PASSENGERS, LAST_MONTH, &Town::SuppliedHistory::transported) >> 8; + case 0xC8: return TownHistoryHelper(this->t, CT_MAIL, LAST_MONTH, &Town::SuppliedHistory::transported); + case 0xC9: return TownHistoryHelper(this->t, CT_MAIL, LAST_MONTH, &Town::SuppliedHistory::transported) >> 8; case 0xCA: return this->t->GetPercentTransported(GetCargoTypeByLabel(CT_PASSENGERS)); case 0xCB: return this->t->GetPercentTransported(GetCargoTypeByLabel(CT_MAIL)); case 0xCC: return this->t->received[TAE_FOOD].new_act; diff --git a/src/saveload/oldloader_sl.cpp b/src/saveload/oldloader_sl.cpp index d3a716dc0d..ea059def0f 100644 --- a/src/saveload/oldloader_sl.cpp +++ b/src/saveload/oldloader_sl.cpp @@ -564,6 +564,9 @@ static void ReadTTDPatchFlags(LoadgameState &ls) Debug(oldloader, 3, "Vehicle-multiplier is set to {} ({} vehicles)", ls.vehicle_multiplier, ls.vehicle_multiplier * 850); } +static std::array _old_pass_supplied{}; +static std::array _old_mail_supplied{}; + static const OldChunks town_chunk[] = { OCL_SVAR( OC_TILE, Town, xy ), OCL_NULL( 2 ), ///< population, no longer in use @@ -592,14 +595,14 @@ static const OldChunks town_chunk[] = { OCL_SVAR( OC_FILE_U8 | OC_VAR_U16, Town, growth_rate ), /* Slots 0 and 2 are passengers and mail respectively for old saves. */ - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[0].new_max ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[2].new_max ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[0].new_act ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[2].new_act ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[0].old_max ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[2].old_max ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[0].old_act ), - OCL_SVAR( OC_FILE_U16 | OC_VAR_U32, Town, supplied[2].old_act ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_pass_supplied[THIS_MONTH].production ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_mail_supplied[THIS_MONTH].production ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_pass_supplied[THIS_MONTH].transported ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_mail_supplied[THIS_MONTH].transported ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_pass_supplied[LAST_MONTH].production ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_mail_supplied[LAST_MONTH].production ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_pass_supplied[LAST_MONTH].transported ), + OCL_VAR( OC_FILE_U16 | OC_VAR_U32, 1, &_old_mail_supplied[LAST_MONTH].transported ), OCL_NULL( 2 ), ///< pct_pass_transported / pct_mail_transported, now computed on the fly @@ -626,6 +629,13 @@ static bool LoadOldTown(LoadgameState &ls, int num) /* 0x10B6 is auto-generated name, others are custom names */ t->townnametype = t->townnametype == 0x10B6 ? 0x20C1 : t->townnametype + 0x2A00; } + /* Passengers and mail were always treated as slots 0 and 2 in older saves. */ + auto &pass = t->supplied.emplace_back(0); + pass.history[LAST_MONTH] = _old_pass_supplied[LAST_MONTH]; + pass.history[THIS_MONTH] = _old_pass_supplied[THIS_MONTH]; + auto &mail = t->supplied.emplace_back(2); + mail.history[LAST_MONTH] = _old_mail_supplied[LAST_MONTH]; + mail.history[THIS_MONTH] = _old_mail_supplied[THIS_MONTH]; } else { delete t; } diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index 002e27fef2..c89f62b9ac 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -406,6 +406,7 @@ enum SaveLoadVersion : uint16_t { SLV_FACE_STYLES, ///< 355 PR#14319 Addition of face styles, replacing gender and ethnicity. SLV_INDUSTRY_NUM_VALID_HISTORY, ///< 356 PR#14416 Store number of valid history records for industries. SLV_INDUSTRY_ACCEPTED_HISTORY, ///< 357 PR#14321 Add per-industry history of cargo delivered and waiting. + SLV_TOWN_SUPPLY_HISTORY, ///< 358 PR#..... Town supply history. SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/saveload/town_sl.cpp b/src/saveload/town_sl.cpp index fdf92738a6..95b94d9143 100644 --- a/src/saveload/town_sl.cpp +++ b/src/saveload/town_sl.cpp @@ -18,6 +18,7 @@ #include "../landscape.h" #include "../subsidy_func.h" #include "../strings_func.h" +#include "../misc/history_func.hpp" #include "../safeguards.h" @@ -112,7 +113,8 @@ void UpdateHousesAndTowns() RebuildTownCaches(); } -class SlTownSupplied : public DefaultSaveLoadHandler { + +class SlTownOldSupplied : public DefaultSaveLoadHandler { public: static inline const SaveLoad description[] = { SLE_CONDVAR(TransportedCargoStat, old_max, SLE_UINT32, SLV_165, SL_MAX_VERSION), @@ -134,23 +136,64 @@ public: return SlGetStructListLength(NUM_CARGO); } - void Save(Town *t) const override - { - SlSetStructListLength(std::size(t->supplied)); - for (auto &supplied : t->supplied) { - SlObject(&supplied, this->GetDescription()); - } - } - void Load(Town *t) const override { size_t num_cargo = this->GetNumCargo(); for (size_t i = 0; i < num_cargo; i++) { - SlObject(&t->supplied[i], this->GetLoadDescription()); + TransportedCargoStat cargo_stat; + SlObject(&cargo_stat, this->GetLoadDescription()); + + /* Ignore empty statistics. */ + if (cargo_stat.new_act == 0 && cargo_stat.new_max == 0 && cargo_stat.old_act == 0 && cargo_stat.old_max == 0) continue; + + auto &s = t->supplied.emplace_back(static_cast(i)); + s.history[LAST_MONTH].production = cargo_stat.old_max; + s.history[LAST_MONTH].transported = cargo_stat.old_act; + s.history[THIS_MONTH].production = cargo_stat.new_max; + s.history[THIS_MONTH].transported = cargo_stat.new_act; } } }; +class SlTownSuppliedHistory : public DefaultSaveLoadHandler { +public: + static inline const SaveLoad description[] = { + SLE_VAR(Town::SuppliedHistory, production, SLE_UINT32), + SLE_VAR(Town::SuppliedHistory, transported, SLE_UINT32), + }; + static inline const SaveLoadCompatTable compat_description = {}; + + void Save(Town::SuppliedCargo *p) const override + { + SlSetStructListLength(p->history.size()); + + for (auto &h : p->history) { + SlObject(&h, this->GetDescription()); + } + } + + void Load(Town::SuppliedCargo *p) const override + { + size_t len = SlGetStructListLength(p->history.size()); + + for (auto &h : p->history) { + if (--len > p->history.size()) break; // unsigned so wraps after hitting zero. + SlObject(&h, this->GetDescription()); + } + } +}; + +class SlTownSupplied : public VectorSaveLoadHandler { +public: + inline static const SaveLoad description[] = { + SLE_VAR(Town::SuppliedCargo, cargo, SLE_UINT8), + SLEG_STRUCTLIST("history", SlTownSuppliedHistory), + }; + inline const static SaveLoadCompatTable compat_description = {}; + + std::vector &GetVector(Town *t) const override { return t->supplied; } +}; + class SlTownReceived : public DefaultSaveLoadHandler { public: static inline const SaveLoad description[] = { @@ -205,6 +248,9 @@ public: } }; +static std::array _old_pass_supplied{}; +static std::array _old_mail_supplied{}; + static const SaveLoad _town_desc[] = { SLE_CONDVAR(Town, xy, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_6), SLE_CONDVAR(Town, xy, SLE_UINT32, SLV_6, SL_MAX_VERSION), @@ -226,22 +272,22 @@ static const SaveLoad _town_desc[] = { SLE_CONDARR(Town, unwanted, SLE_INT8, MAX_COMPANIES, SLV_104, SL_MAX_VERSION), /* Slots 0 and 2 are passengers and mail respectively for old saves. */ - SLE_CONDVARNAME(Town, supplied[0].old_max, "supplied[CT_PASSENGERS].old_max", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[0].old_max, "supplied[CT_PASSENGERS].old_max", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[2].old_max, "supplied[CT_MAIL].old_max", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[2].old_max, "supplied[CT_MAIL].old_max", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[0].new_max, "supplied[CT_PASSENGERS].new_max", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[0].new_max, "supplied[CT_PASSENGERS].new_max", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[2].new_max, "supplied[CT_MAIL].new_max", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[2].new_max, "supplied[CT_MAIL].new_max", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[0].old_act, "supplied[CT_PASSENGERS].old_act", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[0].old_act, "supplied[CT_PASSENGERS].old_act", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[2].old_act, "supplied[CT_MAIL].old_act", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[2].old_act, "supplied[CT_MAIL].old_act", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[0].new_act, "supplied[CT_PASSENGERS].new_act", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[0].new_act, "supplied[CT_PASSENGERS].new_act", SLE_UINT32, SLV_9, SLV_165), - SLE_CONDVARNAME(Town, supplied[2].new_act, "supplied[CT_MAIL].new_act", SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), - SLE_CONDVARNAME(Town, supplied[2].new_act, "supplied[CT_MAIL].new_act", SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR("supplied[CT_PASSENGERS].old_max", _old_pass_supplied[LAST_MONTH].production, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR("supplied[CT_PASSENGERS].old_max", _old_pass_supplied[LAST_MONTH].production, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR( "supplied[CT_MAIL].old_max", _old_mail_supplied[LAST_MONTH].production, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR( "supplied[CT_MAIL].old_max", _old_mail_supplied[LAST_MONTH].production, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR("supplied[CT_PASSENGERS].new_max", _old_pass_supplied[THIS_MONTH].production, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR("supplied[CT_PASSENGERS].new_max", _old_pass_supplied[THIS_MONTH].production, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR( "supplied[CT_MAIL].new_max", _old_mail_supplied[THIS_MONTH].production, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR( "supplied[CT_MAIL].new_max", _old_mail_supplied[THIS_MONTH].production, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR("supplied[CT_PASSENGERS].old_act", _old_pass_supplied[LAST_MONTH].transported, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR("supplied[CT_PASSENGERS].old_act", _old_pass_supplied[LAST_MONTH].transported, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR( "supplied[CT_MAIL].old_act", _old_mail_supplied[LAST_MONTH].transported, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR( "supplied[CT_MAIL].old_act", _old_mail_supplied[LAST_MONTH].transported, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR("supplied[CT_PASSENGERS].new_act", _old_pass_supplied[THIS_MONTH].transported, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR("supplied[CT_PASSENGERS].new_act", _old_pass_supplied[THIS_MONTH].transported, SLE_UINT32, SLV_9, SLV_165), + SLEG_CONDVAR( "supplied[CT_MAIL].new_act", _old_mail_supplied[THIS_MONTH].transported, SLE_FILE_U16 | SLE_VAR_U32, SL_MIN_VERSION, SLV_9), + SLEG_CONDVAR( "supplied[CT_MAIL].new_act", _old_mail_supplied[THIS_MONTH].transported, SLE_UINT32, SLV_9, SLV_165), SLE_CONDVARNAME(Town, received[TAE_FOOD].old_act, "received[TE_FOOD].old_act", SLE_UINT16, SL_MIN_VERSION, SLV_165), SLE_CONDVARNAME(Town, received[TAE_WATER].old_act, "received[TE_WATER].old_act", SLE_UINT16, SL_MIN_VERSION, SLV_165), @@ -268,10 +314,12 @@ static const SaveLoad _town_desc[] = { SLE_CONDVAR(Town, larger_town, SLE_BOOL, SLV_56, SL_MAX_VERSION), SLE_CONDVAR(Town, layout, SLE_UINT8, SLV_113, SL_MAX_VERSION), + SLE_CONDVAR(Town, valid_history, SLE_UINT64, SLV_TOWN_SUPPLY_HISTORY, SL_MAX_VERSION), SLE_CONDREFVECTOR(Town, psa_list, REF_STORAGE, SLV_161, SL_MAX_VERSION), - SLEG_CONDSTRUCTLIST("supplied", SlTownSupplied, SLV_165, SL_MAX_VERSION), + SLEG_CONDSTRUCTLIST("supplied", SlTownOldSupplied, SLV_165, SLV_TOWN_SUPPLY_HISTORY), + SLEG_CONDSTRUCTLIST("supplied", SlTownSupplied, SLV_TOWN_SUPPLY_HISTORY, SL_MAX_VERSION), SLEG_CONDSTRUCTLIST("received", SlTownReceived, SLV_165, SL_MAX_VERSION), SLEG_CONDSTRUCTLIST("acceptance_matrix", SlTownAcceptanceMatrix, SLV_166, SLV_REMOVE_TOWN_CARGO_CACHE), }; @@ -303,6 +351,20 @@ struct CITYChunkHandler : ChunkHandler { Town *t = new (TownID(index)) Town(); SlObject(t, slt); + if (IsSavegameVersionBefore(SLV_165)) { + /* Passengers and mail were always treated as slots 0 and 2 in older saves. */ + auto &pass = t->supplied.emplace_back(0); + pass.history[LAST_MONTH] = _old_pass_supplied[LAST_MONTH]; + pass.history[THIS_MONTH] = _old_pass_supplied[THIS_MONTH]; + auto &mail = t->supplied.emplace_back(2); + mail.history[LAST_MONTH] = _old_mail_supplied[LAST_MONTH]; + mail.history[THIS_MONTH] = _old_mail_supplied[THIS_MONTH]; + } + + if (IsSavegameVersionBefore(SLV_TOWN_SUPPLY_HISTORY)) { + t->valid_history = 1U << LAST_MONTH; + } + if (t->townnamegrfid == 0 && !IsInsideMM(t->townnametype, SPECSTR_TOWNNAME_START, SPECSTR_TOWNNAME_END) && GetStringTab(t->townnametype) != TEXT_TAB_OLD_CUSTOM) { SlErrorCorrupt("Invalid town name generator"); } diff --git a/src/script/api/script_town.cpp b/src/script/api/script_town.cpp index 35b84d84af..dc47da6fd9 100644 --- a/src/script/api/script_town.cpp +++ b/src/script/api/script_town.cpp @@ -93,7 +93,10 @@ const Town *t = ::Town::Get(town_id); - return t->supplied[cargo_type].old_max; + auto it = t->GetCargoSupplied(cargo_type); + if (it == std::end(t->supplied)) return 0; + + return it->history[LAST_MONTH].production; } /* static */ SQInteger ScriptTown::GetLastMonthSupplied(TownID town_id, CargoType cargo_type) @@ -103,7 +106,10 @@ const Town *t = ::Town::Get(town_id); - return t->supplied[cargo_type].old_act; + auto it = t->GetCargoSupplied(cargo_type); + if (it == std::end(t->supplied)) return 0; + + return it->history[LAST_MONTH].transported; } /* static */ SQInteger ScriptTown::GetLastMonthTransportedPercentage(TownID town_id, CargoType cargo_type) diff --git a/src/town.h b/src/town.h index 7894d19f86..11168ba287 100644 --- a/src/town.h +++ b/src/town.h @@ -10,6 +10,7 @@ #ifndef TOWN_H #define TOWN_H +#include "misc/history_type.hpp" #include "viewport_type.h" #include "timer/timer_game_tick.h" #include "town_map.h" @@ -74,16 +75,56 @@ struct Town : TownPool::PoolItem<&_town_pool> { uint8_t exclusive_counter = 0; ///< months till the exclusivity expires TypedIndexContainer, CompanyID> ratings{}; ///< ratings of each company for this town - std::array, NUM_CARGO> supplied{}; ///< Cargo statistics about supplied cargo. + struct SuppliedHistory { + uint32_t production = 0; ///< Total produced + uint32_t transported = 0; ///< Total transported + + uint8_t PctTransported() const + { + if (this->production == 0) return 0; + return ClampTo(this->transported * 256 / this->production); + } + }; + + struct SuppliedCargo { + CargoType cargo = INVALID_CARGO; + HistoryData history{}; + + SuppliedCargo() = default; + SuppliedCargo(CargoType cargo) : cargo(cargo) {} + }; + + using SuppliedCargoes = std::vector; + + SuppliedCargoes supplied{}; ///< Cargo statistics about supplied cargo. std::array, NUM_TAE> received{}; ///< Cargo statistics about received cargotypes. std::array goal{}; ///< Amount of cargo required for the town to grow. + ValidHistoryMask valid_history = 0; ///< Mask of valid history records. EncodedString text{}; ///< General text with additional information. + inline SuppliedCargo &GetOrCreateCargoSupplied(CargoType cargo) + { + assert(IsValidCargoType(cargo)); + auto it = std::ranges::lower_bound(this->supplied, cargo, std::less{}, &SuppliedCargo::cargo); + if (it == std::end(this->supplied) || it->cargo != cargo) it = this->supplied.emplace(it, cargo); + return *it; + } + + inline SuppliedCargoes::const_iterator GetCargoSupplied(CargoType cargo) const + { + if (!IsValidCargoType(cargo)) return std::end(this->supplied); + auto it = std::ranges::lower_bound(this->supplied, cargo, std::less{}, &SuppliedCargo::cargo); + if (it == std::end(this->supplied) || it->cargo != cargo) return std::end(supplied); + return it; + } + inline uint8_t GetPercentTransported(CargoType cargo_type) const { - if (!IsValidCargoType(cargo_type)) return 0; - return this->supplied[cargo_type].old_act * 256 / (this->supplied[cargo_type].old_max + 1); + auto it = this->GetCargoSupplied(cargo_type); + if (it == std::end(this->supplied)) return 0; + + return it->history[LAST_MONTH].PctTransported(); } StationList stations_near{}; ///< NOSAVE: List of nearby stations. diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp index 1bc2befd08..71ed4e6762 100644 --- a/src/town_cmd.cpp +++ b/src/town_cmd.cpp @@ -8,6 +8,8 @@ /** @file town_cmd.cpp Handling of town tiles. */ #include "stdafx.h" +#include "misc/history_type.hpp" +#include "misc/history_func.hpp" #include "road.h" #include "road_internal.h" /* Cleaning up road bits */ #include "road_cmd.h" @@ -114,6 +116,7 @@ Town::~Town() /* Delete town authority window * and remove from list of sorted towns */ CloseWindowById(WC_TOWN_VIEW, this->index); + CloseWindowById(WC_TOWN_CARGO_GRAPH, this->index); #ifdef WITH_ASSERT /* Check no industry is related to us. */ @@ -536,10 +539,12 @@ static void TownGenerateCargo(Town *t, CargoType cargo, uint amount, StationFind /* Scale by cargo scale setting. */ amount = ScaleByCargoScale(amount, true); + if (amount == 0) return; /* Actually generate cargo and update town statistics. */ - t->supplied[cargo].new_max += amount; - t->supplied[cargo].new_act += MoveGoodsToStation(cargo, amount, {t->index, SourceType::Town}, stations.GetStations());; + auto &supplied = t->GetOrCreateCargoSupplied(cargo); + supplied.history[THIS_MONTH].production += amount; + supplied.history[THIS_MONTH].transported += MoveGoodsToStation(cargo, amount, {t->index, SourceType::Town}, stations.GetStations());; } /** @@ -2004,10 +2009,19 @@ void UpdateTownRadius(Town *t) void UpdateTownMaxPass(Town *t) { for (const CargoSpec *cs : CargoSpec::town_production_cargoes[TPE_PASSENGERS]) { - t->supplied[cs->Index()].old_max = ScaleByCargoScale(t->cache.population >> 3, true); + uint32_t production = ScaleByCargoScale(t->cache.population >> 3, true); + if (production == 0) continue; + + auto &supplied = t->GetOrCreateCargoSupplied(cs->Index()); + supplied.history[LAST_MONTH].production = production; } + for (const CargoSpec *cs : CargoSpec::town_production_cargoes[TPE_MAIL]) { - t->supplied[cs->Index()].old_max = ScaleByCargoScale(t->cache.population >> 4, true); + uint32_t production = ScaleByCargoScale(t->cache.population >> 4, true); + if (production == 0) continue; + + auto &supplied = t->GetOrCreateCargoSupplied(cs->Index()); + supplied.history[LAST_MONTH].production = production; } } @@ -4058,6 +4072,15 @@ CommandCost CheckforTownRating(DoCommandFlags flags, Town *t, TownRatingCheckTyp return CommandCost(); } +template <> +Town::SuppliedHistory SumHistory(std::span history) +{ + uint32_t production = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const auto &s) { return r + s.production; }); + uint32_t transported = std::accumulate(std::begin(history), std::end(history), 0, [](uint32_t r, const auto &s) { return r + s.transported; }); + auto count = std::size(history); + return {.production = ClampTo(production / count), .transported = ClampTo(transported / count)}; +} + static const IntervalTimer _economy_towns_monthly({TimerGameEconomy::MONTH, TimerGameEconomy::Priority::TOWN}, [](auto) { for (Town *t : Town::Iterate()) { @@ -4074,8 +4097,10 @@ static const IntervalTimer _economy_towns_monthly({TimerGameEc if (t->unwanted[c->index] > 0) t->unwanted[c->index]--; } + UpdateValidHistory(t->valid_history, TimerGameEconomy::month); + /* Update cargo statistics. */ - for (auto &supplied : t->supplied) supplied.NewMonth(); + for (auto &s : t->supplied) RotateHistory(s.history, t->valid_history, TimerGameEconomy::month); for (auto &received : t->received) received.NewMonth(); UpdateTownGrowth(t); diff --git a/src/town_gui.cpp b/src/town_gui.cpp index e1f85e8024..bde11df0fd 100644 --- a/src/town_gui.cpp +++ b/src/town_gui.cpp @@ -46,6 +46,7 @@ #include "timer/timer_window.h" #include "zoom_func.h" #include "hotkeys.h" +#include "graph_gui.h" #include "widgets/town_widget.h" @@ -400,7 +401,12 @@ public: for (auto tpe : {TPE_PASSENGERS, TPE_MAIL}) { for (const CargoSpec *cs : CargoSpec::town_production_cargoes[tpe]) { CargoType cargo_type = cs->Index(); - DrawString(tr, GetString(str_last_period, 1ULL << cargo_type, this->town->supplied[cargo_type].old_act, this->town->supplied[cargo_type].old_max)); + auto it = this->town->GetCargoSupplied(cargo_type); + if (it == std::end(this->town->supplied)) { + DrawString(tr, GetString(str_last_period, 1ULL << cargo_type, 0, 0)); + } else { + DrawString(tr, GetString(str_last_period, 1ULL << cargo_type, it->history[LAST_MONTH].transported, it->history[LAST_MONTH].production)); + } tr.top += GetCharacterHeight(FS_NORMAL); } } @@ -503,6 +509,11 @@ public: case WID_TV_DELETE: // delete town - only available on Scenario editor Command::Post(STR_ERROR_TOWN_CAN_T_DELETE, static_cast(this->window_number)); break; + + case WID_TV_GRAPH: { + ShowTownCargoGraph(this->window_number); + break; + } } } @@ -618,6 +629,7 @@ static constexpr NWidgetPart _nested_town_game_view_widgets[] = { NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_TV_SHOW_AUTHORITY), SetMinimalSize(80, 12), SetFill(1, 1), SetResize(1, 0), SetStringTip(STR_TOWN_VIEW_LOCAL_AUTHORITY_BUTTON, STR_TOWN_VIEW_LOCAL_AUTHORITY_TOOLTIP), NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_TV_CATCHMENT), SetMinimalSize(40, 12), SetFill(1, 1), SetResize(1, 0), SetStringTip(STR_BUTTON_CATCHMENT, STR_TOOLTIP_CATCHMENT), + NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_TV_GRAPH), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_TOWN_VIEW_CARGO_GRAPH, STR_TOWN_VIEW_CARGO_GRAPH_TOOLTIP), NWidget(WWT_RESIZEBOX, COLOUR_BROWN), EndContainer(), }; diff --git a/src/widgets/town_widget.h b/src/widgets/town_widget.h index fe901339ac..ac60a1ae3a 100644 --- a/src/widgets/town_widget.h +++ b/src/widgets/town_widget.h @@ -45,6 +45,7 @@ enum TownViewWidgets : WidgetID { WID_TV_EXPAND_BUILDINGS, ///< Expand number of buildings this town (scenario editor only). WID_TV_EXPAND_ROADS, ///< Expand roads of this town (scenario editor only). WID_TV_DELETE, ///< Delete this town (scenario editor only). + WID_TV_GRAPH, }; /** Widgets of the #FoundTownWindow class. */ diff --git a/src/window_type.h b/src/window_type.h index 28f72486b7..585ad7a83f 100644 --- a/src/window_type.h +++ b/src/window_type.h @@ -582,6 +582,12 @@ enum WindowClass : uint16_t { */ WC_INDUSTRY_PRODUCTION, + /** + * Town cargo history graph; %Window numbers: + * - #TownID = #GraphWidgets + */ + WC_TOWN_CARGO_GRAPH, + /** * Company infrastructure overview; %Window numbers: * - #CompanyID = #CompanyInfrastructureWidgets