1
0
mirror of https://github.com/OpenTTD/OpenTTD.git synced 2025-08-16 19:19:09 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Kuhnovic
4d68ec16a5 Merge 643dcefc8f into 557fb0bd78 2025-07-30 00:13:14 +00:00
557fb0bd78 Change: Add support for different horizontal graph scales. 2025-07-30 01:13:05 +01:00
d09dfd843c 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.
2025-07-30 01:13:05 +01:00
Koen Bussemaker
643dcefc8f Change: Error window colour changes based on severity 2025-03-14 20:05:54 +01:00
14 changed files with 410 additions and 65 deletions

View File

@@ -134,7 +134,7 @@ bool DriverFactoryBase::SelectDriverImpl(const std::string &name, Driver::Type t
Debug(driver, 1, "Probing {} driver '{}' skipped due to earlier crash", GetDriverTypeName(type), d->name);
_video_hw_accel = false;
ErrorMessageData msg(GetEncodedString(STR_VIDEO_DRIVER_ERROR), GetEncodedString(STR_VIDEO_DRIVER_ERROR_HARDWARE_ACCELERATION_CRASH), true);
ErrorMessageData msg(GetEncodedString(STR_VIDEO_DRIVER_ERROR), GetEncodedString(STR_VIDEO_DRIVER_ERROR_HARDWARE_ACCELERATION_CRASH), WL_CRITICAL);
ScheduleErrorMessage(std::move(msg));
continue;
}
@@ -158,7 +158,7 @@ bool DriverFactoryBase::SelectDriverImpl(const std::string &name, Driver::Type t
if (type == Driver::DT_VIDEO && _video_hw_accel && d->UsesHardwareAcceleration()) {
_video_hw_accel = false;
ErrorMessageData msg(GetEncodedString(STR_VIDEO_DRIVER_ERROR), GetEncodedString(STR_VIDEO_DRIVER_ERROR_NO_HARDWARE_ACCELERATION), true);
ErrorMessageData msg(GetEncodedString(STR_VIDEO_DRIVER_ERROR), GetEncodedString(STR_VIDEO_DRIVER_ERROR_NO_HARDWARE_ACCELERATION), WL_CRITICAL);
ScheduleErrorMessage(std::move(msg));
}
}

View File

@@ -30,7 +30,7 @@ enum WarningLevel : uint8_t {
/** The data of the error message. */
class ErrorMessageData {
protected:
bool is_critical; ///< Whether the error message is critical.
WarningLevel level; ///< Level of the error message (e.g. whether it's a critical error).
EncodedString summary_msg; ///< General error message showed in first line. Must be valid.
EncodedString detailed_msg; ///< Detailed error message showed in second line. Can be #INVALID_STRING_ID.
EncodedString extra_msg; ///< Extra error message shown in third line. Can be #INVALID_STRING_ID.
@@ -38,7 +38,7 @@ protected:
CompanyID company; ///< Company belonging to the face being shown. #CompanyID::Invalid() if no face present.
public:
ErrorMessageData(EncodedString &&summary_msg, EncodedString &&detailed_msg, bool is_critical = false, int x = 0, int y = 0, EncodedString &&extra_msg = {}, CompanyID company = CompanyID::Invalid());
ErrorMessageData(EncodedString &&summary_msg, EncodedString &&detailed_msg, WarningLevel level = WL_ERROR, int x = 0, int y = 0, EncodedString &&extra_msg = {}, CompanyID company = CompanyID::Invalid());
/** Check whether error window shall display a company manager face */
bool HasFace() const { return company != CompanyID::Invalid(); }

View File

@@ -35,10 +35,10 @@
static constexpr NWidgetPart _nested_errmsg_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_RED),
NWidget(WWT_CLOSEBOX, COLOUR_RED, WID_EM_CLOSEBOX),
NWidget(WWT_CAPTION, COLOUR_RED, WID_EM_CAPTION), SetStringTip(STR_ERROR_MESSAGE_CAPTION),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_RED),
NWidget(WWT_PANEL, COLOUR_RED, WID_EM_PANEL),
NWidget(WWT_EMPTY, INVALID_COLOUR, WID_EM_MESSAGE), SetPadding(WidgetDimensions::unscaled.modalpopup), SetFill(1, 0), SetMinimalSize(236, 0),
EndContainer(),
};
@@ -52,10 +52,10 @@ static WindowDesc _errmsg_desc(
static constexpr NWidgetPart _nested_errmsg_face_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_RED),
NWidget(WWT_CLOSEBOX, COLOUR_RED, WID_EM_CLOSEBOX),
NWidget(WWT_CAPTION, COLOUR_RED, WID_EM_CAPTION),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_RED),
NWidget(WWT_PANEL, COLOUR_RED, WID_EM_PANEL),
NWidget(NWID_HORIZONTAL),
NWidget(WWT_EMPTY, INVALID_COLOUR, WID_EM_FACE), SetPadding(2, 0, 2, 2), SetFill(0, 1), SetMinimalSize(92, 119),
NWidget(WWT_EMPTY, INVALID_COLOUR, WID_EM_MESSAGE), SetPadding(WidgetDimensions::unscaled.modalpopup), SetFill(1, 1), SetMinimalSize(236, 0),
@@ -74,13 +74,13 @@ static WindowDesc _errmsg_face_desc(
* Display an error message in a window.
* @param summary_msg General error message showed in first line. Must be valid.
* @param detailed_msg Detailed error message showed in second line. Can be empty.
* @param is_critical Whether the error is critical. Critical messages never go away on their own.
* @param level Warning/error level of the message. NOTE: critical messages never go away on their own.
* @param x World X position (TileVirtX) of the error location. Set both x and y to 0 to just center the message when there is no related error tile.
* @param y World Y position (TileVirtY) of the error location. Set both x and y to 0 to just center the message when there is no related error tile.
* @param extra_msg Extra error message showed in third line. Can be empty.
*/
ErrorMessageData::ErrorMessageData(EncodedString &&summary_msg, EncodedString &&detailed_msg, bool is_critical, int x, int y, EncodedString &&extra_msg, CompanyID company) :
is_critical(is_critical),
ErrorMessageData::ErrorMessageData(EncodedString &&summary_msg, EncodedString &&detailed_msg, WarningLevel level, int x, int y, EncodedString &&extra_msg, CompanyID company) :
level(level),
summary_msg(std::move(summary_msg)),
detailed_msg(std::move(detailed_msg)),
extra_msg(std::move(extra_msg)),
@@ -113,8 +113,15 @@ public:
{
this->InitNested();
Colours window_colour = COLOUR_RED;
if (level == WL_INFO) window_colour = COLOUR_GREEN;
if (level == WL_WARNING) window_colour = COLOUR_ORANGE;
this->GetWidget<NWidgetCore>(WID_EM_CAPTION)->colour = window_colour;
this->GetWidget<NWidgetCore>(WID_EM_CLOSEBOX)->colour = window_colour;
this->GetWidget<NWidgetCore>(WID_EM_PANEL)->colour = window_colour;
/* Only start the timeout if the message is not critical. */
if (!this->is_critical) {
if (this->level != WL_CRITICAL) {
this->display_timeout.Reset();
}
}
@@ -225,7 +232,7 @@ public:
void OnMouseLoop() override
{
/* Disallow closing the window too easily, if timeout is disabled */
if (_right_button_down && !this->is_critical) this->Close();
if (_right_button_down && this->level != WL_CRITICAL) this->Close();
}
void Close([[maybe_unused]] int data = 0) override
@@ -241,7 +248,7 @@ public:
*/
bool IsCritical()
{
return this->is_critical;
return this->level == WL_CRITICAL;
}
};
@@ -281,7 +288,7 @@ void UnshowCriticalError()
/**
* Display an error message in a window.
* Note: CommandCost errors are always severity level WL_INFO.
* Note: CommandCost errors are always severity level WL_ERROR.
* @param summary_msg General error message showed in first line. Must be valid.
* @param x World X position (TileVirtX) of the error location. Set both x and y to 0 to just center the message when there is no related error tile.
* @param y World Y position (TileVirtY) of the error location. Set both x and y to 0 to just center the message when there is no related error tile.
@@ -292,7 +299,7 @@ void ShowErrorMessage(EncodedString &&summary_msg, int x, int y, CommandCost &cc
EncodedString error = std::move(cc.GetEncodedMessage());
if (error.empty()) error = GetEncodedStringIfValid(cc.GetErrorMessage());
ShowErrorMessage(std::move(summary_msg), std::move(error), WL_INFO, x, y,
ShowErrorMessage(std::move(summary_msg), std::move(error), WL_ERROR, x, y,
GetEncodedStringIfValid(cc.GetExtraErrorMessage()), cc.GetErrorOwner());
}
@@ -323,12 +330,10 @@ void ShowErrorMessage(EncodedString &&summary_msg, EncodedString &&detailed_msg,
IConsolePrint(wl == WL_WARNING ? CC_WARNING : CC_ERROR, message);
}
bool is_critical = wl == WL_CRITICAL;
if (_game_mode == GM_BOOTSTRAP) return;
if (_settings_client.gui.errmsg_duration == 0 && !is_critical) return;
if (_settings_client.gui.errmsg_duration == 0 && wl != WL_CRITICAL) return;
ErrorMessageData data(std::move(summary_msg), std::move(detailed_msg), is_critical, x, y, std::move(extra_msg), company);
ErrorMessageData data(std::move(summary_msg), std::move(detailed_msg), wl, x, y, std::move(extra_msg), company);
ErrmsgWindow *w = dynamic_cast<ErrmsgWindow *>(FindWindowById(WC_ERRMSG, 0));
if (w != nullptr) {

View File

@@ -8,6 +8,7 @@
/** @file graph_gui.cpp GUI that shows performance graphs. */
#include "stdafx.h"
#include <ranges>
#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,25 @@ 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;
const HistoryRange *history_range = nullptr;
};
static inline constexpr GraphScale MONTHLY_SCALE_WALLCLOCK[] = {
{STR_GRAPH_LAST_24_MINUTES_TIME_LABEL, HISTORY_MONTH.total_division, ECONOMY_MONTH_MINUTES, &HISTORY_MONTH},
{STR_GRAPH_LAST_72_MINUTES_TIME_LABEL, HISTORY_QUARTER.total_division, ECONOMY_QUARTER_MINUTES, &HISTORY_QUARTER},
{STR_GRAPH_LAST_288_MINUTES_TIME_LABEL, HISTORY_YEAR.total_division, ECONOMY_YEAR_MINUTES, &HISTORY_YEAR},
};
static inline constexpr GraphScale MONTHLY_SCALE_CALENDAR[] = {
{STR_GRAPH_LAST_24_MONTHS, HISTORY_MONTH.total_division, ECONOMY_MONTH_MINUTES, &HISTORY_MONTH},
{STR_GRAPH_LAST_24_QUARTERS, HISTORY_QUARTER.total_division, ECONOMY_QUARTER_MINUTES, &HISTORY_QUARTER},
{STR_GRAPH_LAST_24_YEARS, HISTORY_YEAR.total_division, ECONOMY_YEAR_MINUTES, &HISTORY_YEAR},
};
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 +232,9 @@ protected:
};
std::vector<DataSet> data{};
std::span<const StringID> ranges = {};
std::span<const StringID> ranges{};
std::span<const GraphScale> 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 +640,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<uint>(std::size(labels));
resize.width = 0;
resize.height = 0;
this->GetWidget<NWidgetCore>(widget)->SetMatrixDimension(1, ClampTo<uint32_t>(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<uint>(std::size(this->ranges));
resize.width = 0;
resize.height = 0;
this->GetWidget<NWidgetCore>(WID_GRAPH_RANGE_MATRIX)->SetMatrixDimension(1, ClampTo<uint32_t>(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 +734,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 +770,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 +1706,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 +1740,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 +1778,7 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow {
transported.dash = 2;
auto transported_filler = Filler{transported, &Industry::ProducedHistory::transported};
FillFromHistory<GRAPH_NUM_MONTHS>(p.history, i->valid_history, produced_filler, transported_filler);
FillFromHistory<GRAPH_NUM_MONTHS>(p.history, i->valid_history, *this->scales[this->selected_scale].history_range, produced_filler, transported_filler);
}
for (const auto &a : i->accepted) {
@@ -1735,9 +1802,9 @@ struct IndustryProductionGraphWindow : BaseCargoGraphWindow {
auto waiting_filler = Filler{waiting, &Industry::AcceptedHistory::waiting};
if (a.history == nullptr) {
FillFromEmpty<GRAPH_NUM_MONTHS>(i->valid_history, accepted_filler, waiting_filler);
FillFromEmpty<GRAPH_NUM_MONTHS>(i->valid_history, *this->scales[this->selected_scale].history_range, accepted_filler, waiting_filler);
} else {
FillFromHistory<GRAPH_NUM_MONTHS>(*a.history, i->valid_history, accepted_filler, waiting_filler);
FillFromHistory<GRAPH_NUM_MONTHS>(*a.history, i->valid_history, *this->scales[this->selected_scale].history_range, accepted_filler, waiting_filler);
}
}
@@ -1758,7 +1825,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 +1834,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),

View File

@@ -1836,7 +1836,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, HISTORY_YEAR, TimerGameEconomy::month);
}
if (indspec->callback_mask.Test(IndustryCallbackMask::DecideColour)) {
@@ -2488,19 +2488,38 @@ void GenerateIndustries()
_industry_builder.Reset();
}
template <>
Industry::ProducedHistory SumHistory(std::span<const Industry::ProducedHistory> 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<uint16_t>(production / count), .transported = ClampTo<uint16_t>(transported / count)};
}
template <>
Industry::AcceptedHistory SumHistory(std::span<const Industry::AcceptedHistory> 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<uint16_t>(accepted / count), .waiting = ClampTo<uint16_t>(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, HISTORY_YEAR, 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, HISTORY_YEAR, month);
}
}
@@ -2509,7 +2528,7 @@ static void UpdateIndustryStatistics(Industry *i)
if (a.history == nullptr) continue;
(*a.history)[THIS_MONTH].waiting = GetAndResetAccumulatedAverage<uint16_t>(a.accumulated_waiting);
RotateHistory(*a.history);
RotateHistory(*a.history, i->valid_history, HISTORY_YEAR, month);
}
}

View File

@@ -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

View File

@@ -8,6 +8,7 @@ add_files(
getoptdata.cpp
getoptdata.h
hashtable.hpp
history.cpp
history_func.hpp
history_type.hpp
lrucache.hpp

65
src/misc/history.cpp Normal file
View File

@@ -0,0 +1,65 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/** @file history.cpp Implementation of functions for storing historical data. */
#include "../stdafx.h"
#include "../core/bitmath_func.hpp"
#include "history_type.hpp"
#include "history_func.hpp"
#include "../safeguards.h"
/**
* Update mask of valid records for a historical data.
* @note Call only for the largest history range sub-division.
* @param[in,out] valid_history Valid history records.
* @param hr History range to update mask for.
* @param cur_month Current economy month.
*/
void UpdateValidHistory(ValidHistoryMask &valid_history, const HistoryRange &hr, uint cur_month)
{
/* Update for subdivisions first. */
if (hr.hr != nullptr) UpdateValidHistory(valid_history, *hr.hr, cur_month);
/* No need to update if our last entry is marked valid. */
if (HasBit(valid_history, hr.last - 1)) return;
/* Is it the right time for this history range? */
if (cur_month % hr.total_division != 0) return;
/* Is the previous history range valid yet? */
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);
}
/**
* Test if history data is valid, without extracting data.
* @param valid_history Mask of valid history records.
* @param hr History range to test.
* @param age Age of data to test.
* @return True iff the data for history range and age is valid.
*/
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<uint>(hr.hr->periods - hr.division)) {
uint start = age * hr.division + ((TimerGameEconomy::month / hr.hr->division) % hr.division);
return IsValidHistory(valid_history, *hr.hr, start);
}
if (age < hr.periods) {
uint slot = hr.first + age - ((hr.hr->periods / hr.division) - 1);
return HasBit(valid_history, slot);
}
}
return false;
}

View File

@@ -15,25 +15,44 @@
#include "../timer/timer_game_economy.h"
#include "history_type.hpp"
/**
* Update mask of valid history records.
* @param[in,out] valid_history Valid history records.
*/
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);
}
void UpdateValidHistory(ValidHistoryMask &valid_history, const HistoryRange &hr, uint cur_month);
bool IsValidHistory(ValidHistoryMask valid_history, const HistoryRange &hr, uint age);
/**
* Rotate history.
* 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 Historical data to rotate.
* @param history History elements to sum.
* @return Sum of history elements.
*/
template <typename T>
void RotateHistory(HistoryData<T> &history)
T SumHistory(typename std::span<const T> history);
/**
* Rotate historical data.
* @note Call only for the largest history range sub-division.
* @tparam T type of history data element.
* @param history Historical data to rotate.
* @param valid_history Mask of valid history records.
* @param hr History range to rotate..
* @param cur_month Current economy month.
*/
template <typename T>
void RotateHistory(HistoryData<T> &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 (hr.hr != nullptr) RotateHistory(history, valid_history, *hr.hr, cur_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.total_division == 1) {
history[hr.first] = history[hr.first - 1];
history.front() = {};
} 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<T>(std::span{first, last});
}
}
/**
@@ -49,19 +68,60 @@ T GetAndResetAccumulatedAverage(Taccrued &total)
return result;
}
/**
* Get historical data.
* @tparam T type of history data element.
* @param history History data to extract from.
* @param valid_history Mask of valid history records.
* @param hr History range to get.
* @param age Age of data to get.
* @param cur_month Current economy month.
* @param[out] result Extracted historical data.
* @return True iff the data for this history range and age is valid.
*/
template <typename T>
bool GetHistory(const HistoryData<T> &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<uint>(hr.hr->periods - hr.division)) {
bool is_valid = false;
std::array<T, HISTORY_MAX_DIVISION> 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<T>(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();
}
/**
* Fill some data with historical data.
* @param history Historical data to fill from.
* @param valid_history Mask of valid history records.
* @param hr History range to fill with.
* @param fillers Fillers to fill with history data.
*/
template <uint N, typename T, typename... Tfillers>
void FillFromHistory(const HistoryData<T> &history, ValidHistoryMask valid_history, Tfillers... fillers)
void FillFromHistory(const HistoryData<T> &history, ValidHistoryMask valid_history, const HistoryRange &hr, Tfillers... fillers)
{
T result{};
for (uint i = 0; i != N; ++i) {
if (HasBit(valid_history, N - i)) {
auto &data = history[N - i];
(fillers.Fill(i, data), ...);
if (GetHistory(history, valid_history, hr, N - i - 1, result)) {
(fillers.Fill(i, result), ...);
} else {
(fillers.MakeInvalid(i), ...);
}
@@ -71,13 +131,14 @@ void FillFromHistory(const HistoryData<T> &history, ValidHistoryMask valid_histo
/**
* Fill some data with empty records.
* @param valid_history Mask of valid history records.
* @param hr History range to fill with.
* @param fillers Fillers to fill with history data.
*/
template <uint N, typename... Tfillers>
void FillFromEmpty(ValidHistoryMask valid_history, Tfillers... fillers)
void FillFromEmpty(ValidHistoryMask valid_history, const HistoryRange &hr, Tfillers... fillers)
{
for (uint i = 0; i != N; ++i) {
if (HasBit(valid_history, N - i)) {
if (IsValidHistory(valid_history, hr, N - i - 1)) {
(fillers.MakeZero(i), ...);
} else {
(fillers.MakeInvalid(i), ...);

View File

@@ -10,7 +10,37 @@
#ifndef HISTORY_TYPE_HPP
#define HISTORY_TYPE_HPP
static constexpr uint8_t HISTORY_RECORDS = 25;
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 uint8_t HISTORY_PERIODS = 24;
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;

View File

@@ -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

View File

@@ -0,0 +1,83 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/** @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<const uint16_t> 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<uint16_t>(total);
}
/**
* Helper to get history records and return the value, instead returning its validity.
* @param history History data to extract from.
* @param hr History range to get.
* @param age Age of data to get.
* @return Historical value for the period and age.
*/
template <typename T>
T GetHistory(const HistoryData<T> &history, const HistoryRange &hr, uint age)
{
T result;
GetHistory(history, 0, hr, age, result);
return result;
}
TEST_CASE("History Rotation and Reporting tests")
{
HistoryData<uint16_t> 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, HISTORY_YEAR, date % 12);
RotateHistory(history, valid_history, HISTORY_YEAR, 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, HISTORY_MONTH, j) == (( 1 * 1 + 1) / 2) + 1 * 1 * j);
CHECK(GetHistory(history, HISTORY_QUARTER, j) == (( 3 * 3 + 3) / 2) + 3 * 3 * j);
CHECK(GetHistory(history, HISTORY_YEAR, j) == ((12 * 12 + 12) / 2) + 12 * 12 * j);
}
/* Double-check quarter history matches summed month history. */
CHECK(GetHistory(history, HISTORY_MONTH, 0) + GetHistory(history, HISTORY_MONTH, 1) + GetHistory(history, HISTORY_MONTH, 2) == GetHistory(history, HISTORY_QUARTER, 0));
CHECK(GetHistory(history, HISTORY_MONTH, 3) + GetHistory(history, HISTORY_MONTH, 4) + GetHistory(history, HISTORY_MONTH, 5) == GetHistory(history, HISTORY_QUARTER, 1));
CHECK(GetHistory(history, HISTORY_MONTH, 6) + GetHistory(history, HISTORY_MONTH, 7) + GetHistory(history, HISTORY_MONTH, 8) == GetHistory(history, HISTORY_QUARTER, 2));
CHECK(GetHistory(history, HISTORY_MONTH, 9) + GetHistory(history, HISTORY_MONTH, 10) + GetHistory(history, HISTORY_MONTH, 11) == GetHistory(history, HISTORY_QUARTER, 3));
CHECK(GetHistory(history, HISTORY_MONTH, 12) + GetHistory(history, HISTORY_MONTH, 13) + GetHistory(history, HISTORY_MONTH, 14) == GetHistory(history, HISTORY_QUARTER, 4));
CHECK(GetHistory(history, HISTORY_MONTH, 15) + GetHistory(history, HISTORY_MONTH, 16) + GetHistory(history, HISTORY_MONTH, 17) == GetHistory(history, HISTORY_QUARTER, 5));
CHECK(GetHistory(history, HISTORY_MONTH, 18) + GetHistory(history, HISTORY_MONTH, 19) + GetHistory(history, HISTORY_MONTH, 20) == GetHistory(history, HISTORY_QUARTER, 6));
CHECK(GetHistory(history, HISTORY_MONTH, 21) + GetHistory(history, HISTORY_MONTH, 22) + GetHistory(history, HISTORY_MONTH, 23) == GetHistory(history, HISTORY_QUARTER, 7));
/* Double-check year history matches summed quarter history. */
CHECK(GetHistory(history, HISTORY_QUARTER, 0) + GetHistory(history, HISTORY_QUARTER, 1) + GetHistory(history, HISTORY_QUARTER, 2) + GetHistory(history, HISTORY_QUARTER, 3) == GetHistory(history, HISTORY_YEAR, 0));
CHECK(GetHistory(history, HISTORY_QUARTER, 4) + GetHistory(history, HISTORY_QUARTER, 5) + GetHistory(history, HISTORY_QUARTER, 6) + GetHistory(history, HISTORY_QUARTER, 7) == GetHistory(history, HISTORY_YEAR, 1));
CHECK(GetHistory(history, HISTORY_QUARTER, 8) + GetHistory(history, HISTORY_QUARTER, 9) + GetHistory(history, HISTORY_QUARTER, 10) + GetHistory(history, HISTORY_QUARTER, 11) == GetHistory(history, HISTORY_YEAR, 2));
CHECK(GetHistory(history, HISTORY_QUARTER, 12) + GetHistory(history, HISTORY_QUARTER, 13) + GetHistory(history, HISTORY_QUARTER, 14) + GetHistory(history, HISTORY_QUARTER, 15) == GetHistory(history, HISTORY_YEAR, 3));
CHECK(GetHistory(history, HISTORY_QUARTER, 16) + GetHistory(history, HISTORY_QUARTER, 17) + GetHistory(history, HISTORY_QUARTER, 18) + GetHistory(history, HISTORY_QUARTER, 19) == GetHistory(history, HISTORY_YEAR, 4));
CHECK(GetHistory(history, HISTORY_QUARTER, 20) + GetHistory(history, HISTORY_QUARTER, 21) + GetHistory(history, HISTORY_QUARTER, 22) + GetHistory(history, HISTORY_QUARTER, 23) == GetHistory(history, HISTORY_YEAR, 5));
}

View File

@@ -12,9 +12,11 @@
/** Widgets of the #ErrmsgWindow class. */
enum ErrorMessageWidgets : WidgetID {
WID_EM_CAPTION, ///< Caption of the window.
WID_EM_FACE, ///< Error title.
WID_EM_MESSAGE, ///< Error message.
WID_EM_CAPTION, ///< Caption of the window.
WID_EM_FACE, ///< Error title.
WID_EM_MESSAGE, ///< Error message.
WID_EM_CLOSEBOX, ///< Close box button.
WID_EM_PANEL, ///< Coloured background panel.
};
#endif /* WIDGETS_ERROR_WIDGET_H */

View File

@@ -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.
};