mirror of
https://github.com/JGRennison/OpenTTD-patches.git
synced 2024-11-11 13:10:45 +00:00
Departures: Add scheduled dispatch 24 hour timetable mode
This commit is contained in:
parent
3b5eada729
commit
f1bf3e9ca2
@ -32,6 +32,7 @@
|
||||
#include "departures_func.h"
|
||||
#include "departures_type.h"
|
||||
#include "tracerestrict.h"
|
||||
#include "scope.h"
|
||||
#include "3rdparty/cpp-btree/btree_set.h"
|
||||
#include "3rdparty/cpp-btree/btree_map.h"
|
||||
|
||||
@ -115,13 +116,19 @@ bool DepartureCallingSettings::IsArrival(const Order *order, const DepartureOrde
|
||||
});
|
||||
}
|
||||
|
||||
static uint8_t GetDepartureConditionalOrderMode(const Order *order, const Vehicle *v, StateTicks eval_tick, const ScheduledDispatchVehicleRecords &records)
|
||||
static uint8_t GetNonScheduleDepartureConditionalOrderMode(const Order *order, const Vehicle *v, StateTicks eval_tick)
|
||||
{
|
||||
if (order->GetConditionVariable() == OCV_UNCONDITIONALLY) return 1;
|
||||
if (order->GetConditionVariable() == OCV_TIME_DATE) {
|
||||
int value = GetTraceRestrictTimeDateValueFromStateTicks(static_cast<TraceRestrictTimeDateValueField>(order->GetConditionValue()), eval_tick);
|
||||
return OrderConditionCompare(order->GetConditionComparator(), value, order->GetXData()) ? 1 : 2;
|
||||
}
|
||||
|
||||
return _settings_client.gui.departure_conditionals;
|
||||
}
|
||||
|
||||
static uint8_t GetDepartureConditionalOrderMode(const Order *order, const Vehicle *v, StateTicks eval_tick, const ScheduledDispatchVehicleRecords &records)
|
||||
{
|
||||
if (order->GetConditionVariable() == OCV_DISPATCH_SLOT) {
|
||||
if (GB(order->GetConditionValue(), ODCB_SRC_START, ODCB_SRC_COUNT) == ODCS_VEH) {
|
||||
auto record = records.find(std::make_pair(order->GetConditionDispatchScheduleID(), v->index));
|
||||
@ -134,8 +141,9 @@ static uint8_t GetDepartureConditionalOrderMode(const Order *order, const Vehicl
|
||||
|
||||
extern bool EvaluateDispatchSlotConditionalOrder(const Order *order, const Vehicle *v, StateTicks state_ticks, bool *predicted);
|
||||
return EvaluateDispatchSlotConditionalOrder(order, v, eval_tick, nullptr) ? 1 : 2;
|
||||
} else {
|
||||
return GetNonScheduleDepartureConditionalOrderMode(order, v, eval_tick);
|
||||
}
|
||||
return _settings_client.gui.departure_conditionals;
|
||||
}
|
||||
|
||||
static bool VehicleSetNextDepartureTime(Ticks *previous_departure, Ticks *waiting_time, const StateTicks state_ticks_base,
|
||||
@ -570,6 +578,71 @@ static bool IsIgnorableCallingAtOrder(const Order *order, DepartureCallingSettin
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process arrival history, returns true if a valid arrival was found.
|
||||
*/
|
||||
static bool ProcessArrivalHistory(Departure *d, std::span<const Order *> arrival_history, DepartureOrderDestinationDetector source, DepartureCallingSettings calling_settings)
|
||||
{
|
||||
std::vector<std::pair<StationID, uint>> possible_origins;
|
||||
|
||||
for (uint i = 0; i < (uint)arrival_history.size(); i++) {
|
||||
const Order *o = arrival_history[i];
|
||||
|
||||
if ((o->GetType() == OT_GOTO_STATION ||
|
||||
o->GetType() == OT_IMPLICIT) &&
|
||||
(o->GetNonStopType() & ONSF_NO_STOP_AT_DESTINATION_STATION) == 0) {
|
||||
if (source.StationMatches(o->GetDestination())) {
|
||||
/* Remove all possible origins */
|
||||
possible_origins.clear();
|
||||
} else {
|
||||
/* Remove all possible origins of this station */
|
||||
for (auto &item : possible_origins) {
|
||||
if (item.first == o->GetDestination()) {
|
||||
item.first = INVALID_STATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((o->GetLoadType() != OLFB_NO_LOAD ||
|
||||
calling_settings.show_all_stops) &&
|
||||
(o->GetType() == OT_GOTO_STATION ||
|
||||
o->GetType() == OT_IMPLICIT) &&
|
||||
!source.StationMatches(o->GetDestination()) &&
|
||||
(o->GetNonStopType() & ONSF_NO_STOP_AT_DESTINATION_STATION) == 0) {
|
||||
possible_origins.push_back({ o->GetDestination(), i });
|
||||
}
|
||||
}
|
||||
|
||||
const Order *origin = nullptr;
|
||||
uint origin_arrival_history_index = 0;
|
||||
for (const auto &item : possible_origins) {
|
||||
if (item.first != INVALID_STATION) {
|
||||
origin_arrival_history_index = item.second;
|
||||
origin = arrival_history[item.second];
|
||||
break;
|
||||
}
|
||||
}
|
||||
possible_origins.clear();
|
||||
if (origin != nullptr) {
|
||||
for (uint i = origin_arrival_history_index + 1; i < (uint)arrival_history.size(); i++) {
|
||||
const Order *o = arrival_history[i];
|
||||
if (o->GetType() == OT_GOTO_STATION &&
|
||||
(o->GetLoadType() != OLFB_NO_LOAD ||
|
||||
calling_settings.show_all_stops) &&
|
||||
(o->GetNonStopType() & ONSF_NO_STOP_AT_DESTINATION_STATION) == 0) {
|
||||
d->calling_at.push_back(CallAt((StationID)o->GetDestination()));
|
||||
}
|
||||
}
|
||||
|
||||
d->terminus = CallAt((StationID)origin->GetDestination());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute an up-to-date list of departures for a station.
|
||||
* @param source the station/etc to compute the departures of
|
||||
@ -578,7 +651,7 @@ static bool IsIgnorableCallingAtOrder(const Order *order, DepartureCallingSettin
|
||||
* @param calling_settings departure calling settings
|
||||
* @return a list of departures, which is empty if an error occurred
|
||||
*/
|
||||
DepartureList MakeDepartureList(DepartureOrderDestinationDetector source, const std::vector<const Vehicle *> &vehicles, DepartureType type, DepartureCallingSettings calling_settings)
|
||||
static DepartureList MakeDepartureListLiveMode(DepartureOrderDestinationDetector source, const std::vector<const Vehicle *> &vehicles, DepartureType type, DepartureCallingSettings calling_settings)
|
||||
{
|
||||
/* This function is the meat of the departure boards functionality. */
|
||||
/* As an overview, it works by repeatedly considering the best possible next departure to show. */
|
||||
@ -729,13 +802,9 @@ DepartureList MakeDepartureList(DepartureOrderDestinationDetector source, const
|
||||
c.station = (StationID)order->GetDestination();
|
||||
|
||||
/* We're not interested in this order any further if we're not calling at it. */
|
||||
if (IsIgnorableCallingAtOrder(order, calling_settings)) {
|
||||
if (c.scheduled_tick != 0) c.scheduled_tick += order->GetWaitTime();
|
||||
order = lod.v->orders->GetNext(order);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsIgnorableCallingAtOrder(order, calling_settings)) {
|
||||
if (via_state.HandleCallingPoint(d, order, c)) break;
|
||||
}
|
||||
|
||||
if (c.scheduled_tick != 0) c.scheduled_tick += order->GetWaitTime();
|
||||
|
||||
@ -851,60 +920,7 @@ DepartureList MakeDepartureList(DepartureOrderDestinationDetector source, const
|
||||
lod.arrival_history = std::move(new_history);
|
||||
}
|
||||
|
||||
std::vector<std::pair<StationID, uint>> possible_origins;
|
||||
|
||||
for (uint i = 0; i < (uint)lod.arrival_history.size(); i++) {
|
||||
const Order *o = lod.arrival_history[i];
|
||||
|
||||
if ((o->GetType() == OT_GOTO_STATION ||
|
||||
o->GetType() == OT_IMPLICIT) &&
|
||||
(o->GetNonStopType() & ONSF_NO_STOP_AT_DESTINATION_STATION) == 0) {
|
||||
if (source.StationMatches(o->GetDestination())) {
|
||||
/* Remove all possible origins */
|
||||
possible_origins.clear();
|
||||
} else {
|
||||
/* Remove all possible origins of this station */
|
||||
for (auto &item : possible_origins) {
|
||||
if (item.first == o->GetDestination()) {
|
||||
item.first = INVALID_STATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((o->GetLoadType() != OLFB_NO_LOAD ||
|
||||
calling_settings.show_all_stops) &&
|
||||
(o->GetType() == OT_GOTO_STATION ||
|
||||
o->GetType() == OT_IMPLICIT) &&
|
||||
!source.StationMatches(o->GetDestination()) &&
|
||||
(o->GetNonStopType() & ONSF_NO_STOP_AT_DESTINATION_STATION) == 0) {
|
||||
possible_origins.push_back({ o->GetDestination(), i });
|
||||
}
|
||||
}
|
||||
|
||||
const Order *origin = nullptr;
|
||||
uint origin_arrival_history_index = 0;
|
||||
for (const auto &item : possible_origins) {
|
||||
if (item.first != INVALID_STATION) {
|
||||
origin_arrival_history_index = item.second;
|
||||
origin = lod.arrival_history[item.second];
|
||||
break;
|
||||
}
|
||||
}
|
||||
possible_origins.clear();
|
||||
if (origin != nullptr) {
|
||||
for (uint i = origin_arrival_history_index + 1; i < (uint)lod.arrival_history.size(); i++) {
|
||||
const Order *o = lod.arrival_history[i];
|
||||
if (o->GetType() == OT_GOTO_STATION &&
|
||||
(o->GetLoadType() != OLFB_NO_LOAD ||
|
||||
calling_settings.show_all_stops) &&
|
||||
(o->GetNonStopType() & ONSF_NO_STOP_AT_DESTINATION_STATION) == 0) {
|
||||
d->calling_at.push_back(CallAt((StationID)o->GetDestination()));
|
||||
}
|
||||
}
|
||||
|
||||
d->terminus = CallAt((StationID)origin->GetDestination());
|
||||
|
||||
if (ProcessArrivalHistory(d, lod.arrival_history, source, calling_settings)) {
|
||||
bool duplicate = false;
|
||||
|
||||
if (_settings_client.gui.departure_merge_identical) {
|
||||
@ -1034,3 +1050,414 @@ Ticks GetDeparturesMaxTicksAhead()
|
||||
return _settings_client.gui.max_departure_time * DAY_TICKS * DayLengthFactor();
|
||||
}
|
||||
}
|
||||
|
||||
struct DepartureListScheduleModeSlotEvaluator {
|
||||
struct DispatchScheduleAnno {
|
||||
StateTicks original_start_tick;
|
||||
int32_t original_last_dispatch;
|
||||
uint repetition = 0;
|
||||
bool usable = false;
|
||||
};
|
||||
|
||||
DepartureList &result;
|
||||
const Vehicle *v;
|
||||
const Order *start_order;
|
||||
DispatchSchedule &ds;
|
||||
DispatchScheduleAnno &anno;
|
||||
const uint schedule_index;
|
||||
const DepartureOrderDestinationDetector &source;
|
||||
DepartureType type;
|
||||
DepartureCallingSettings calling_settings;
|
||||
std::vector<const Order *> &arrival_history;
|
||||
|
||||
StateTicks slot{};
|
||||
uint slot_index{};
|
||||
bool departure_dependant_condition_found = false;
|
||||
|
||||
void EvaluateSlots();
|
||||
|
||||
private:
|
||||
inline bool IsDepartureDependantConditionVariable(OrderConditionVariable ocv) const { return ocv == OCV_DISPATCH_SLOT || ocv == OCV_TIME_DATE; }
|
||||
|
||||
uint8_t EvaluateConditionalOrder(const Order *order, StateTicks eval_tick);
|
||||
void EvaluateFromSourceOrder(const Order *source_order, StateTicks departure_tick);
|
||||
void EvaluateSlotIndex(uint slot_index);
|
||||
};
|
||||
|
||||
uint8_t DepartureListScheduleModeSlotEvaluator::EvaluateConditionalOrder(const Order *order, StateTicks eval_tick) {
|
||||
if (order->GetConditionVariable() == OCV_TIME_DATE) {
|
||||
TraceRestrictTimeDateValueField field = static_cast<TraceRestrictTimeDateValueField>(order->GetConditionValue());
|
||||
if (field != TRTDVF_MINUTE && field != TRTDVF_HOUR && field != TRTDVF_HOUR_MINUTE) {
|
||||
/* No reasonable way to handle this with a minutes schedule, give up */
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (order->GetConditionVariable() == OCV_DISPATCH_SLOT) {
|
||||
if (GB(order->GetConditionValue(), ODCB_SRC_START, ODCB_SRC_COUNT) == ODCS_VEH) {
|
||||
if (order->GetConditionDispatchScheduleID() == this->schedule_index) {
|
||||
extern LastDispatchRecord MakeLastDispatchRecord(const DispatchSchedule &ds, StateTicks slot, int slot_index);
|
||||
extern bool EvaluateDispatchSlotConditionalOrderVehicleRecord(const Order *order, const LastDispatchRecord &record);
|
||||
return EvaluateDispatchSlotConditionalOrderVehicleRecord(order, MakeLastDispatchRecord(this->ds, this->slot, this->slot_index)) ? 1 : 2;
|
||||
} else {
|
||||
/* Testing a different schedule index, handle as if there is no record */
|
||||
return OrderConditionCompare(order->GetConditionComparator(), 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
extern bool EvaluateDispatchSlotConditionalOrder(const Order *order, const Vehicle *v, StateTicks state_ticks, bool *predicted);
|
||||
return EvaluateDispatchSlotConditionalOrder(order, this->v, eval_tick, nullptr) ? 1 : 2;
|
||||
} else {
|
||||
return GetNonScheduleDepartureConditionalOrderMode(order, this->v, eval_tick);
|
||||
}
|
||||
}
|
||||
|
||||
void DepartureListScheduleModeSlotEvaluator::EvaluateFromSourceOrder(const Order *source_order, StateTicks departure_tick)
|
||||
{
|
||||
Departure d{};
|
||||
d.scheduled_tick = departure_tick;
|
||||
d.lateness = 0;
|
||||
d.status = D_SCHEDULED;
|
||||
d.vehicle = this->v;
|
||||
d.type = this->type;
|
||||
d.order = source_order;
|
||||
d.scheduled_waiting_time = 0;
|
||||
|
||||
/* We'll be going through the order list later, so we need a separate variable for it. */
|
||||
const Order *order = source_order;
|
||||
|
||||
const uint order_iteration_limit = this->v->GetNumOrders();
|
||||
|
||||
if (type == D_DEPARTURE) {
|
||||
/* Computing departures: */
|
||||
DepartureViaTerminusState via_state{};
|
||||
|
||||
order = this->v->orders->GetNext(order);
|
||||
CallAt c = CallAt((StationID)order->GetDestination(), 0);
|
||||
for (uint i = order_iteration_limit; i > 0; --i) {
|
||||
/* If we reach the order at which the departure occurs again, then use the departure station as the terminus. */
|
||||
if (order == source_order) {
|
||||
/* If we're not calling anywhere, then skip this departure. */
|
||||
via_state.found_terminus = (d.calling_at.size() > 0);
|
||||
break;
|
||||
}
|
||||
|
||||
/* If the order is a conditional branch, handle it. */
|
||||
if (order->IsType(OT_CONDITIONAL)) {
|
||||
if (this->IsDepartureDependantConditionVariable(order->GetConditionVariable())) this->departure_dependant_condition_found = true;
|
||||
switch (this->EvaluateConditionalOrder(order, departure_tick)) {
|
||||
case 0: {
|
||||
/* Give up */
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
/* Take the branch */
|
||||
order = this->v->GetOrder(order->GetConditionSkipToOrder());
|
||||
if (order == nullptr) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
/* Do not take the branch */
|
||||
order = this->v->orders->GetNext(order);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (via_state.CheckOrder(this->v, &d, order, this->source, this->calling_settings)) break;
|
||||
|
||||
c.station = (StationID)order->GetDestination();
|
||||
|
||||
/* We're not interested in this order any further if we're not calling at it. */
|
||||
if (!IsIgnorableCallingAtOrder(order, this->calling_settings)) {
|
||||
if (via_state.HandleCallingPoint(&d, order, c)) break;
|
||||
}
|
||||
|
||||
if (order->IsScheduledDispatchOrder(true)) {
|
||||
if (d.calling_at.size() > 0) {
|
||||
via_state.found_terminus = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* Get the next order, which may be the vehicle's first order. */
|
||||
order = this->v->orders->GetNext(order);
|
||||
}
|
||||
|
||||
if (via_state.found_terminus) {
|
||||
/* Add the departure to the result list. */
|
||||
this->result.push_back(std::make_unique<Departure>(std::move(d)));
|
||||
}
|
||||
} else {
|
||||
/* Computing arrivals: */
|
||||
|
||||
if (ProcessArrivalHistory(&d, this->arrival_history, this->source, this->calling_settings)) {
|
||||
this->result.push_back(std::make_unique<Departure>(std::move(d)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DepartureListScheduleModeSlotEvaluator::EvaluateSlotIndex(uint slot_index)
|
||||
{
|
||||
this->slot_index = slot_index;
|
||||
this->slot = this->ds.GetScheduledDispatchStartTick() + this->ds.GetScheduledDispatch()[slot_index].offset;
|
||||
StateTicks departure_tick = this->slot;
|
||||
this->arrival_history.clear();
|
||||
|
||||
/* The original last dispatch time will be restored in MakeDepartureListScheduleMode */
|
||||
this->ds.SetScheduledDispatchLastDispatch(ds.GetScheduledDispatch()[slot_index].offset);
|
||||
auto guard = scope_guard([&]() {
|
||||
this->ds.SetScheduledDispatchLastDispatch(INVALID_SCHEDULED_DISPATCH_OFFSET);
|
||||
});
|
||||
|
||||
if (type == D_DEPARTURE && calling_settings.IsDeparture(this->start_order, this->source)) {
|
||||
this->EvaluateFromSourceOrder(this->start_order, departure_tick);
|
||||
return;
|
||||
}
|
||||
if (type == D_ARRIVAL) {
|
||||
this->arrival_history.push_back(this->start_order);
|
||||
}
|
||||
|
||||
const Order *order = this->v->orders->GetNext(this->start_order);
|
||||
bool require_travel_time = true;
|
||||
|
||||
/* Loop through the vehicle's orders until we've found a suitable order or we've determined that no such order exists. */
|
||||
/* We only need to consider each order at most once. */
|
||||
for (int i = this->v->GetNumOrders(); i > 0; --i) {
|
||||
departure_tick += order->GetTravelTime();
|
||||
|
||||
if (type == D_ARRIVAL && this->calling_settings.IsArrival(order, this->source)) {
|
||||
this->EvaluateFromSourceOrder(order, departure_tick);
|
||||
break;
|
||||
}
|
||||
|
||||
departure_tick += order->GetWaitTime();
|
||||
|
||||
if (order->IsScheduledDispatchOrder(true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == D_DEPARTURE && this->calling_settings.IsDeparture(order, this->source)) {
|
||||
this->EvaluateFromSourceOrder(order, departure_tick);
|
||||
break;
|
||||
}
|
||||
|
||||
/* If the order is a conditional branch, handle it. */
|
||||
if (order->IsType(OT_CONDITIONAL)) {
|
||||
if (this->IsDepartureDependantConditionVariable(order->GetConditionVariable())) this->departure_dependant_condition_found = true;
|
||||
switch (this->EvaluateConditionalOrder(order, departure_tick)) {
|
||||
case 0: {
|
||||
/* Give up */
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
/* Take the branch */
|
||||
order = v->GetOrder(order->GetConditionSkipToOrder());
|
||||
if (order == nullptr) {
|
||||
break;
|
||||
}
|
||||
|
||||
departure_tick -= order->GetTravelTime();
|
||||
require_travel_time = false;
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
/* Do not take the branch */
|
||||
departure_tick -= order->GetWaitTime(); /* Added previously above */
|
||||
order = v->orders->GetNext(order);
|
||||
require_travel_time = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* If an order has a 0 travel time, and it's not explictly set, then stop. */
|
||||
if (require_travel_time && order->GetTravelTime() == 0 && !order->IsTravelTimetabled() && !order->IsType(OT_IMPLICIT)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == D_ARRIVAL) {
|
||||
this->arrival_history.push_back(order);
|
||||
}
|
||||
|
||||
order = v->orders->GetNext(order);
|
||||
require_travel_time = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DepartureListScheduleModeSlotEvaluator::EvaluateSlots()
|
||||
{
|
||||
const size_t start_number_departures = this->result.size();
|
||||
this->departure_dependant_condition_found = false;
|
||||
this->EvaluateSlotIndex(0);
|
||||
const auto &slots = this->ds.GetScheduledDispatch();
|
||||
if (this->departure_dependant_condition_found) {
|
||||
/* Need to evaluate every slot individually */
|
||||
for (size_t i = 1; i < slots.size(); i++) {
|
||||
this->EvaluateSlotIndex(i);
|
||||
}
|
||||
|
||||
if (this->anno.repetition > 1) {
|
||||
const auto dispatch_start_tick = this->ds.GetScheduledDispatchStartTick();
|
||||
auto guard = scope_guard([&]() {
|
||||
this->ds.SetScheduledDispatchStartTick(dispatch_start_tick);
|
||||
});
|
||||
for (uint i = 1; i < this->anno.repetition; i++) {
|
||||
this->ds.SetScheduledDispatchStartTick(this->ds.GetScheduledDispatchStartTick() + this->ds.GetScheduledDispatchDuration());
|
||||
for (size_t j = 0; j < slots.size(); j++) {
|
||||
this->EvaluateSlotIndex(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Trivially repeat found departures */
|
||||
const size_t done_first_slot_departures = this->result.size();
|
||||
if (done_first_slot_departures == start_number_departures) return;
|
||||
const uint32_t first_offset = slots[0].offset;
|
||||
for (size_t i = 1; i < slots.size(); i++) {
|
||||
for (size_t j = start_number_departures; j != done_first_slot_departures; j++) {
|
||||
std::unique_ptr<Departure> d = std::make_unique<Departure>(*this->result[j]); // Clone departure
|
||||
d->scheduled_tick += slots[i].offset - first_offset;
|
||||
this->result.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
const size_t done_schedule_departures = this->result.size();
|
||||
for (uint i = 1; i < this->anno.repetition; i++) {
|
||||
for (size_t j = start_number_departures; j != done_schedule_departures; j++) {
|
||||
std::unique_ptr<Departure> d = std::make_unique<Departure>(*this->result[j]); // Clone departure
|
||||
d->scheduled_tick += this->ds.GetScheduledDispatchDuration() * i;
|
||||
this->result.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DepartureList MakeDepartureListScheduleMode(DepartureOrderDestinationDetector source, const std::vector<const Vehicle *> &vehicles, DepartureType type,
|
||||
DepartureCallingSettings calling_settings, const StateTicks start_tick, const StateTicks end_tick, const uint max_departure_slots_per_schedule)
|
||||
{
|
||||
const Ticks tick_duration = (end_tick - start_tick).AsTicks();
|
||||
|
||||
std::vector<std::unique_ptr<Departure>> result;
|
||||
std::vector<const Order *> arrival_history;
|
||||
|
||||
for (const Vehicle *veh : vehicles) {
|
||||
if (!HasBit(veh->vehicle_flags, VF_SCHEDULED_DISPATCH)) continue;
|
||||
|
||||
const Vehicle *v = nullptr;
|
||||
for (const Vehicle *u = veh->FirstShared(); u != nullptr; u = u->NextShared()) {
|
||||
if (IsVehicleUsableForDepartures(u, calling_settings)) {
|
||||
v = u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (v == nullptr) continue;
|
||||
|
||||
std::vector<DepartureListScheduleModeSlotEvaluator::DispatchScheduleAnno> schedule_anno;
|
||||
schedule_anno.resize(v->orders->GetScheduledDispatchScheduleCount());
|
||||
for (uint i = 0; i < v->orders->GetScheduledDispatchScheduleCount(); i++) {
|
||||
/* This is mutable so that parts can be backed up, modified and restored later */
|
||||
DispatchSchedule &ds = const_cast<Vehicle *>(v)->orders->GetDispatchScheduleByIndex(i);
|
||||
DepartureListScheduleModeSlotEvaluator::DispatchScheduleAnno &anno = schedule_anno[i];
|
||||
|
||||
anno.original_start_tick = ds.GetScheduledDispatchStartTick();
|
||||
anno.original_last_dispatch = ds.GetScheduledDispatchLastDispatch();
|
||||
|
||||
const uint32_t duration = ds.GetScheduledDispatchDuration();
|
||||
if (duration < _settings_time.ticks_per_minute || duration > (uint)tick_duration) continue; // Duration is obviously out of range
|
||||
if (tick_duration % duration != 0) continue; // Duration does not evenly fit into range
|
||||
const uint slot_count = (uint)ds.GetScheduledDispatch().size();
|
||||
if (slot_count == 0) continue; // No departure slots
|
||||
|
||||
anno.repetition = tick_duration / ds.GetScheduledDispatchDuration();
|
||||
|
||||
if (anno.repetition * slot_count > max_departure_slots_per_schedule) continue;
|
||||
|
||||
StateTicks dispatch_tick = ds.GetScheduledDispatchStartTick();
|
||||
if (dispatch_tick < start_tick) {
|
||||
dispatch_tick += CeilDivT<StateTicksDelta>(start_tick - dispatch_tick, duration).AsTicks() * duration;
|
||||
}
|
||||
if (dispatch_tick > start_tick) {
|
||||
StateTicksDelta delta = (dispatch_tick - start_tick);
|
||||
dispatch_tick -= (delta / duration).AsTicksT<uint>() * duration;
|
||||
}
|
||||
|
||||
ds.SetScheduledDispatchStartTick(dispatch_tick);
|
||||
ds.SetScheduledDispatchLastDispatch(INVALID_SCHEDULED_DISPATCH_OFFSET);
|
||||
anno.usable = true;
|
||||
}
|
||||
|
||||
auto guard = scope_guard([&]() {
|
||||
for (uint i = 0; i < v->orders->GetScheduledDispatchScheduleCount(); i++) {
|
||||
/* Restore backup */
|
||||
DispatchSchedule &ds = const_cast<Vehicle *>(v)->orders->GetDispatchScheduleByIndex(i);
|
||||
const DepartureListScheduleModeSlotEvaluator::DispatchScheduleAnno &anno = schedule_anno[i];
|
||||
ds.SetScheduledDispatchStartTick(anno.original_start_tick);
|
||||
ds.SetScheduledDispatchLastDispatch(anno.original_last_dispatch);
|
||||
}
|
||||
});
|
||||
|
||||
for (const Order *start_order : v->Orders()) {
|
||||
if (start_order->IsScheduledDispatchOrder(true)) {
|
||||
const uint schedule_index = start_order->GetDispatchScheduleIndex();
|
||||
DepartureListScheduleModeSlotEvaluator::DispatchScheduleAnno &anno = schedule_anno[schedule_index];
|
||||
if (!anno.usable) continue;
|
||||
|
||||
DispatchSchedule &ds = const_cast<Vehicle *>(v)->orders->GetDispatchScheduleByIndex(schedule_index);
|
||||
DepartureListScheduleModeSlotEvaluator evaluator{
|
||||
result, v, start_order, ds, anno, schedule_index, source, type, calling_settings, arrival_history
|
||||
};
|
||||
evaluator.EvaluateSlots();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<Departure> &d : result) {
|
||||
if (d->scheduled_tick < start_tick) {
|
||||
d->scheduled_tick += CeilDivT<StateTicksDelta>(start_tick - d->scheduled_tick, tick_duration).AsTicks() * tick_duration;
|
||||
}
|
||||
if (d->scheduled_tick > start_tick) {
|
||||
StateTicksDelta delta = (d->scheduled_tick - start_tick);
|
||||
d->scheduled_tick -= (delta / tick_duration).AsTicksT<uint>() * tick_duration;
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(result.begin(), result.end(), [](std::unique_ptr<Departure> &a, std::unique_ptr<Departure> &b) -> bool {
|
||||
return a->scheduled_tick < b->scheduled_tick;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute an up-to-date list of departures for a station.
|
||||
* @param source_mode the departure source mode to use
|
||||
* @param source the station/etc to compute the departures of
|
||||
* @param vehicles set of all the vehicles stopping at this station, of all vehicles types that we are interested in
|
||||
* @param type the type of departures to get (departures or arrivals)
|
||||
* @param calling_settings departure calling settings
|
||||
* @return a list of departures, which is empty if an error occurred
|
||||
*/
|
||||
DepartureList MakeDepartureList(DeparturesSourceMode source_mode, DepartureOrderDestinationDetector source, const std::vector<const Vehicle *> &vehicles,
|
||||
DepartureType type, DepartureCallingSettings calling_settings)
|
||||
{
|
||||
switch (source_mode) {
|
||||
case DSM_LIVE:
|
||||
return MakeDepartureListLiveMode(source, vehicles, type, calling_settings);
|
||||
|
||||
case DSM_SCHEDULE_24H: {
|
||||
if (!_settings_time.time_in_minutes) return {};
|
||||
TickMinutes start = _settings_time.NowInTickMinutes().ToSameDayClockTime(0, 0);
|
||||
StateTicks start_tick = _settings_time.FromTickMinutes(start);
|
||||
StateTicks end_tick = _settings_time.FromTickMinutes(start + (24 * 60));
|
||||
|
||||
/* Set maximum to 90 departures per hour per dispatch schedule, to prevent excessive numbers of departures */
|
||||
return MakeDepartureListScheduleMode(source, vehicles, type, calling_settings, start_tick, end_tick, 90 * 24);
|
||||
}
|
||||
|
||||
default:
|
||||
NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
DepartureList MakeDepartureList(DepartureOrderDestinationDetector source, const std::vector<const Vehicle *> &vehicles, DepartureType type, DepartureCallingSettings calling_settings);
|
||||
DepartureList MakeDepartureList(DeparturesSourceMode source_mode, DepartureOrderDestinationDetector source, const std::vector<const Vehicle *> &vehicles, DepartureType type, DepartureCallingSettings calling_settings);
|
||||
|
||||
Ticks GetDeparturesMaxTicksAhead();
|
||||
|
||||
|
@ -52,10 +52,10 @@ static constexpr NWidgetPart _nested_departures_list[] = {
|
||||
NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_DB_SCROLLBAR),
|
||||
EndContainer(),
|
||||
|
||||
NWidget(NWID_HORIZONTAL),
|
||||
NWidget(WWT_PANEL, COLOUR_GREY), SetMinimalSize(0, 12), SetResize(1, 0), SetFill(1, 1), EndContainer(),
|
||||
NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_DB_CARGO_MODE), SetFill(0, 1), SetDataTip(STR_JUST_STRING, STR_DEPARTURES_CARGO_MODE_TOOLTIP),
|
||||
NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_DB_DEPARTURE_MODE), SetFill(0, 1), SetDataTip(STR_JUST_STRING, STR_DEPARTURES_DEPARTURE_MODE_TOOLTIP),
|
||||
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
|
||||
NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_DB_CARGO_MODE), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_JUST_STRING, STR_DEPARTURES_CARGO_MODE_TOOLTIP),
|
||||
NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_DB_SOURCE_MODE), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_JUST_STRING, STR_DEPARTURES_SOURCE_MODE_TOOLTIP),
|
||||
NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_DB_DEPARTURE_MODE), SetFill(1, 1), SetResize(1, 0), SetDataTip(STR_JUST_STRING, STR_DEPARTURES_DEPARTURE_MODE_TOOLTIP),
|
||||
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_VIA), SetMinimalSize(11, 12), SetFill(0, 1), SetDataTip(STR_DEPARTURES_VIA_BUTTON, STR_DEPARTURES_VIA_TOOLTIP),
|
||||
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_TRAINS), SetMinimalSize(14, 12), SetFill(0, 1), SetDataTip(STR_TRAIN, STR_NULL),
|
||||
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_DB_SHOW_ROADVEHS), SetMinimalSize(14, 12), SetFill(0, 1), SetDataTip(STR_LORRY, STR_NULL),
|
||||
@ -121,6 +121,12 @@ enum DepartureSourceType : uint8_t {
|
||||
DST_DEPOT,
|
||||
};
|
||||
|
||||
|
||||
static const StringID _departure_source_mode_strings[DSM_END] = {
|
||||
STR_DEPARTURES_SOURCE_MODE_LIVE,
|
||||
STR_DEPARTURES_SOURCE_MODE_SCHEDULE_24_HOUR,
|
||||
};
|
||||
|
||||
struct DeparturesWindow : public Window {
|
||||
protected:
|
||||
DepartureSourceType source_type{}; ///< Source type.
|
||||
@ -135,6 +141,7 @@ protected:
|
||||
bool show_types[4]; ///< The vehicle types to show in the departure list.
|
||||
DeparturesCargoMode cargo_mode = DCF_ALL_CARGOES;
|
||||
DeparturesMode mode = DM_DEPARTURES;
|
||||
DeparturesSourceMode source_mode = DSM_LIVE;
|
||||
bool show_via = false;
|
||||
mutable bool scroll_refresh; ///< Whether the window should be refreshed when paused due to scrolling
|
||||
uint min_width = 400; ///< The minimum width of this window.
|
||||
@ -145,9 +152,10 @@ protected:
|
||||
int toc_width; ///< current width of company field
|
||||
std::array<uint32_t, 3> title_params{};///< title string parameters
|
||||
|
||||
virtual uint GetMinWidth() const;
|
||||
uint GetScrollbarCapacity() const;
|
||||
uint GetMinWidth() const;
|
||||
static void RecomputeDateWidth();
|
||||
virtual void DrawDeparturesListItems(const Rect &r) const;
|
||||
void DrawDeparturesListItems(const Rect &r) const;
|
||||
|
||||
void FillVehicleList()
|
||||
{
|
||||
@ -166,10 +174,12 @@ protected:
|
||||
}
|
||||
for (const Vehicle *veh : Vehicle::IterateTypeMaskFrontOnly(vt_mask)) {
|
||||
if (veh->IsPrimaryVehicle() && veh == veh->FirstShared()) {
|
||||
if (this->source_mode != DSM_LIVE && !HasBit(veh->vehicle_flags, VF_SCHEDULED_DISPATCH)) continue;
|
||||
for (const Order *order : veh->Orders()) {
|
||||
if (this->source.OrderMatches(order)) {
|
||||
if (this->source_mode != DSM_LIVE) this->vehicles.push_back(veh);
|
||||
for (const Vehicle *v = veh; v != nullptr; v = v->NextShared()) {
|
||||
this->vehicles.push_back(v);
|
||||
if (this->source_mode == DSM_LIVE) this->vehicles.push_back(v);
|
||||
|
||||
if (_settings_client.gui.departure_show_vehicle) {
|
||||
if (v->name.empty() && !(v->group_id != DEFAULT_GROUP && _settings_client.gui.vehicle_names != 0)) {
|
||||
@ -357,6 +367,11 @@ public:
|
||||
size.width = GetStringListWidth(_departure_mode_strings);
|
||||
size.width += padding.width;
|
||||
break;
|
||||
|
||||
case WID_DB_SOURCE_MODE:
|
||||
size.width = GetStringListWidth(_departure_source_mode_strings);
|
||||
size.width += padding.width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,6 +394,11 @@ public:
|
||||
SetDParam(0, _departure_mode_strings[this->mode]);
|
||||
break;
|
||||
}
|
||||
|
||||
case WID_DB_SOURCE_MODE: {
|
||||
SetDParam(0, _departure_source_mode_strings[this->source_mode]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -494,6 +514,13 @@ public:
|
||||
case WID_DB_DEPARTURE_MODE:
|
||||
ShowDropDownMenu(this, _departure_mode_strings, this->mode, WID_DB_DEPARTURE_MODE, 0, 0);
|
||||
break;
|
||||
|
||||
case WID_DB_SOURCE_MODE: {
|
||||
uint32_t disabled_mask = 0;
|
||||
if (!_settings_time.time_in_minutes) SetBit(disabled_mask, DSM_SCHEDULE_24H);
|
||||
ShowDropDownMenu(this, _departure_source_mode_strings, this->source_mode, WID_DB_SOURCE_MODE, disabled_mask, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,6 +550,20 @@ public:
|
||||
this->SetWidgetDirty(widget);
|
||||
break;
|
||||
}
|
||||
|
||||
case WID_DB_SOURCE_MODE: {
|
||||
if (this->source_mode != index) {
|
||||
this->source_mode = static_cast<DeparturesSourceMode>(index);
|
||||
if (!_settings_time.time_in_minutes && this->source_mode == DSM_SCHEDULE_24H) {
|
||||
this->source_mode = DSM_LIVE;
|
||||
}
|
||||
this->vehicles_invalid = true;
|
||||
this->calc_tick_countdown = 0;
|
||||
if (_pause_mode != PM_UNPAUSED) this->OnGameTick();
|
||||
}
|
||||
this->SetWidgetDirty(widget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -562,17 +603,19 @@ public:
|
||||
settings.show_freight = show_freight;
|
||||
|
||||
if (this->mode != DM_ARRIVALS) {
|
||||
this->departures = MakeDepartureList(list_source, this->vehicles, D_DEPARTURE, settings);
|
||||
this->departures = MakeDepartureList(this->source_mode, list_source, this->vehicles, D_DEPARTURE, settings);
|
||||
} else {
|
||||
this->departures.clear();
|
||||
}
|
||||
if (this->mode == DM_ARRIVALS || this->mode == DM_SEPARATE) {
|
||||
this->arrivals = MakeDepartureList(list_source, this->vehicles, D_ARRIVAL, settings);
|
||||
this->arrivals = MakeDepartureList(this->source_mode, list_source, this->vehicles, D_ARRIVAL, settings);
|
||||
} else {
|
||||
this->arrivals.clear();
|
||||
}
|
||||
this->departures_invalid = false;
|
||||
this->vscroll->SetCount(this->GetScrollbarCapacity());
|
||||
this->SetWidgetDirty(WID_DB_LIST);
|
||||
this->SetWidgetDirty(WID_DB_SCROLLBAR);
|
||||
}
|
||||
|
||||
uint new_width = this->GetMinWidth();
|
||||
@ -601,12 +644,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
virtual void OnPaint() override
|
||||
{
|
||||
this->vscroll->SetCount(std::min<uint>(_settings_client.gui.max_departures, (uint)this->departures.size() + (uint)this->arrivals.size()));
|
||||
this->DrawWidgets();
|
||||
}
|
||||
|
||||
virtual void DrawWidget(const Rect &r, WidgetID widget) const override
|
||||
{
|
||||
switch (widget) {
|
||||
@ -633,6 +670,10 @@ public:
|
||||
this->vehicles_invalid = true;
|
||||
this->departures_invalid = true;
|
||||
if (data > 0) {
|
||||
if (!_settings_time.time_in_minutes && this->source_mode == DSM_SCHEDULE_24H) {
|
||||
this->source_mode = DSM_LIVE;
|
||||
this->vehicles_invalid = true;
|
||||
}
|
||||
this->SetupValues();
|
||||
this->ReInit();
|
||||
if (_pause_mode != PM_UNPAUSED) this->OnGameTick();
|
||||
@ -694,6 +735,13 @@ void DeparturesWindow::RecomputeDateWidth()
|
||||
cached_date_width -= cached_date_arrow_width;
|
||||
}
|
||||
|
||||
uint DeparturesWindow::GetScrollbarCapacity() const
|
||||
{
|
||||
uint count = (uint)this->departures.size() + (uint)this->arrivals.size();
|
||||
if (this->source_mode == DSM_LIVE) count = std::min<uint>(_settings_client.gui.max_departures, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
static int PadWidth(int width)
|
||||
{
|
||||
if (width > 0) width += WidgetDimensions::scaled.hsep_wide;
|
||||
@ -734,11 +782,7 @@ void DeparturesWindow::DrawDeparturesListItems(const Rect &r) const
|
||||
const int text_right = right - (rtl ? text_offset : 0);
|
||||
|
||||
int y = r.top + 1;
|
||||
uint max_departures = std::min<uint>(this->vscroll->GetPosition() + this->vscroll->GetCapacity(), (uint)this->departures.size() + (uint)this->arrivals.size());
|
||||
|
||||
if (max_departures > _settings_client.gui.max_departures) {
|
||||
max_departures = _settings_client.gui.max_departures;
|
||||
}
|
||||
uint max_departures = std::min<uint>(this->vscroll->GetPosition() + this->vscroll->GetCapacity(), this->GetScrollbarCapacity());
|
||||
|
||||
const int small_font_size = _settings_client.gui.departure_larger_font ? GetCharacterHeight(FS_NORMAL) : GetCharacterHeight(FS_SMALL);
|
||||
|
||||
@ -828,7 +872,7 @@ void DeparturesWindow::DrawDeparturesListItems(const Rect &r) const
|
||||
if (this->mode == DM_COMBINED) {
|
||||
time_str = STR_DEPARTURES_TIME_BOTH;
|
||||
SetDParam(0, STR_JUST_TT_TIME_ABS);
|
||||
SetDParam(1, d->scheduled_tick - (d->scheduled_waiting_time > 0 ? d->scheduled_waiting_time : d->order->GetWaitTime()));
|
||||
SetDParam(1, d->scheduled_tick - d->EffectiveWaitingTime());
|
||||
SetDParam(2, STR_JUST_TT_TIME_ABS);
|
||||
SetDParam(3, d->scheduled_tick);
|
||||
} else {
|
||||
@ -981,6 +1025,9 @@ void DeparturesWindow::DrawDeparturesListItems(const Rect &r) const
|
||||
} else if (d->status == D_CANCELLED) {
|
||||
/* The vehicle has been cancelled. */
|
||||
DrawString(status_left, status_right, y + 1, STR_DEPARTURES_CANCELLED);
|
||||
} else if (d->status == D_SCHEDULED) {
|
||||
/* Display as scheduled. */
|
||||
DrawString(status_left, status_right, y + 1, STR_DEPARTURES_SCHEDULED);
|
||||
} else {
|
||||
if (d->lateness <= TimetableAbsoluteDisplayUnitSize() && d->scheduled_tick > now_date) {
|
||||
/* We have no evidence that the vehicle is late, so assume it is on time. */
|
||||
|
@ -21,8 +21,9 @@
|
||||
/** Whether or not a vehicle has arrived for a departure. */
|
||||
enum DepartureStatus : uint8_t {
|
||||
D_TRAVELLING = 0,
|
||||
D_ARRIVED = 1,
|
||||
D_CANCELLED = 2,
|
||||
D_ARRIVED,
|
||||
D_CANCELLED,
|
||||
D_SCHEDULED,
|
||||
};
|
||||
|
||||
/** The type of departures. */
|
||||
@ -31,6 +32,13 @@ enum DepartureType : uint8_t {
|
||||
D_ARRIVAL = 1,
|
||||
};
|
||||
|
||||
enum DeparturesSourceMode : uint8_t {
|
||||
DSM_LIVE,
|
||||
DSM_SCHEDULE_24H,
|
||||
|
||||
DSM_END
|
||||
};
|
||||
|
||||
struct CallAt {
|
||||
StationID station;
|
||||
StateTicks scheduled_tick;
|
||||
|
@ -1380,6 +1380,9 @@ STR_DEPARTURES_CALLING_AT_LIST_SMART_TERMINUS :{ORANGE}{RAW_ST
|
||||
STR_DEPARTURES_TINY :{TINY_FONT}{STRING2}
|
||||
STR_DEPARTURES_VIA_DESCRIPTOR :{STRING1}{STRING}
|
||||
STR_DEPARTURES_VIA_AND :{STRING} and {STRING}
|
||||
STR_DEPARTURES_SOURCE_MODE_TOOLTIP :{BLACK}Set whether to show live departures, or a 24 hour timetable of scheduled dispatch departures.
|
||||
STR_DEPARTURES_SOURCE_MODE_LIVE :Live
|
||||
STR_DEPARTURES_SOURCE_MODE_SCHEDULE_24_HOUR :24 hour schedule
|
||||
|
||||
STR_DEPARTURES_TYPE_TRAIN :{ORANGE}{TRAIN}
|
||||
STR_DEPARTURES_TYPE_TRAIN_SILVER :{SILVER}{TRAIN}
|
||||
@ -1402,6 +1405,7 @@ STR_DEPARTURES_ARRIVED :{GREEN}Arrived
|
||||
STR_DEPARTURES_DELAYED :{YELLOW}Delayed
|
||||
STR_DEPARTURES_EXPECTED :{YELLOW}Expt {STRING1}
|
||||
STR_DEPARTURES_CANCELLED :{RED}Cancelled
|
||||
STR_DEPARTURES_SCHEDULED :{GREEN}Scheduled
|
||||
|
||||
STR_CONFIG_SETTING_DEPARTUREBOARDS :{ORANGE}Departure boards
|
||||
STR_CONFIG_MAX_DEPARTURES :Show at most {STRING2} departure board entries for each station
|
||||
|
@ -17,6 +17,7 @@ enum DeparturesWindowWidgets {
|
||||
WID_DB_SCROLLBAR, ///< List scrollbar
|
||||
WID_DB_CARGO_MODE, ///< Cargo filter mode
|
||||
WID_DB_DEPARTURE_MODE, ///< Departure type mode
|
||||
WID_DB_SOURCE_MODE, ///< Departure source mode
|
||||
WID_DB_SHOW_VIA, ///< Toggle via button
|
||||
WID_DB_SHOW_TRAINS, ///< Toggle trains button
|
||||
WID_DB_SHOW_ROADVEHS, ///< Toggle road vehicles button
|
||||
|
Loading…
Reference in New Issue
Block a user