From e7b0ab3bfc55a93515f2c71670a679dd3d41ba83 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Wed, 9 Jul 2025 23:14:50 +0100 Subject: [PATCH] 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 | 60 +++++++++++++++++++++++++++++--- 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, 171 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 d91babc25f..1ca911098e 100644 --- a/src/graph_gui.cpp +++ b/src/graph_gui.cpp @@ -208,6 +208,13 @@ 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{}; @@ -217,13 +224,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; } }; /** @@ -1613,7 +1627,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{}; @@ -1628,6 +1644,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); } @@ -1635,6 +1655,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); } @@ -1648,7 +1671,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); } @@ -1696,6 +1719,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 fd36e64c90..bdad6cd5ad 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);