1
0
Fork 0

Feature #8095: Allow automatically separating vehicles in shared orders

pull/8342/head
James Ross 2020-11-17 15:32:07 +00:00
parent 661e0cd82d
commit 0f0dbc0b3b
15 changed files with 199 additions and 3 deletions

View File

@ -42,3 +42,12 @@ void BaseConsist::CopyConsistPropertiesFrom(const BaseConsist *src)
}
if (HasBit(src->vehicle_flags, VF_SERVINT_IS_CUSTOM)) SetBit(this->vehicle_flags, VF_SERVINT_IS_CUSTOM);
}
/**
* Resets all the data used for automatic separation
*/
void BaseConsist::ResetAutomaticSeparation()
{
this->first_order_last_departure = 0;
this->first_order_round_trip_time = 0;
}

View File

@ -22,6 +22,9 @@ struct BaseConsist {
TimerGameTick::Ticks lateness_counter; ///< How many ticks late (or early if negative) this vehicle is.
TimerGameTick::TickCounter timetable_start; ///< At what tick of TimerGameTick::counter the vehicle should start its timetable.
TimerGameTick::Ticks first_order_last_departure; ///< When the vehicle last left the first order.
TimerGameTick::Ticks first_order_round_trip_time; ///< How many ticks for a single circumnavigation of the orders.
uint16_t service_interval; ///< The interval for (automatic) servicing; either in days or %.
VehicleOrderID cur_real_order_index;///< The index to the current real (non-implicit) order
@ -32,6 +35,7 @@ struct BaseConsist {
virtual ~BaseConsist() = default;
void CopyConsistPropertiesFrom(const BaseConsist *src);
void ResetAutomaticSeparation();
};
#endif /* BASE_CONSIST_H */

View File

@ -239,6 +239,7 @@ enum Commands : uint16_t {
CMD_SKIP_TO_ORDER, ///< skip an order to the next of specific one
CMD_DELETE_ORDER, ///< delete an order
CMD_INSERT_ORDER, ///< insert a new order
CMD_ORDER_AUTOMATIC_SEPARATION, ///< set automatic separation
CMD_CHANGE_SERVICE_INT, ///< change the server interval of a vehicle

View File

@ -4485,6 +4485,9 @@ STR_ORDERS_DELETE_ALL_TOOLTIP :{BLACK}Delete a
STR_ORDERS_STOP_SHARING_BUTTON :{BLACK}Stop sharing
STR_ORDERS_STOP_SHARING_TOOLTIP :{BLACK}Stop sharing the order list. Ctrl+Click additionally deletes all orders for this vehicle
STR_ORDERS_AUTOMATIC_SEPARATION :{BLACK}Automatic separation
STR_ORDERS_AUTOMATIC_SEPARATION_TOOLTIP :{BLACK}Automatically separate all vehicles sharing this order
STR_ORDERS_GO_TO_BUTTON :{BLACK}Go To
STR_ORDER_GO_TO_NEAREST_DEPOT :Go to nearest depot
STR_ORDER_GO_TO_NEAREST_HANGAR :Go to nearest hangar

View File

@ -271,6 +271,8 @@ private:
TimerGameTick::Ticks timetable_duration; ///< NOSAVE: Total timetabled duration of the order list.
TimerGameTick::Ticks total_duration; ///< NOSAVE: Total (timetabled or not) duration of the order list.
bool automatic_separation; ///< Is automatic separation enabled?
public:
/** Default constructor producing an invalid order list. */
OrderList(VehicleOrderID num_orders = INVALID_VEH_ORDER_ID)
@ -392,6 +394,18 @@ public:
*/
void UpdateTotalDuration(TimerGameTick::Ticks delta) { this->total_duration += delta; }
/**
* Is this order list using automatic separation?
* @return whether automatic separation is enabled
*/
inline bool AutomaticSeparationIsEnabled() const { return this->automatic_separation; }
/**
* Enables or disables automatic separation for this order list
* @param enabled whether to enable (true) or disable (false) automatic separation
*/
void SetAutomaticSeparationIsEnabled(bool enabled) { this->automatic_separation = enabled; }
void FreeChain(bool keep_orderlist = false);
void DebugCheckSanity() const;

View File

@ -1466,6 +1466,36 @@ static bool CheckAircraftOrderDistance(const Aircraft *v_new, const Vehicle *v_o
return true;
}
/**
* Enable or disable automatic separation for a vehicle's order list
* @param flags operation to perform
* @param veh vehicle who's order list is being modified
* @param enabled value indicating whether to enable (true) or disable (false) automatic separation
* @return the cost of this operation or an error
*/
CommandCost CmdOrderAutomaticSeparation(DoCommandFlag flags, VehicleID veh, bool enabled)
{
Vehicle *vehicle = Vehicle::GetIfValid(veh);
if (vehicle == nullptr || !vehicle->IsPrimaryVehicle()) return CMD_ERROR;
CommandCost ret = CheckOwnership(vehicle->owner);
if (ret.Failed()) return ret;
if (flags & DC_EXEC) {
vehicle->SetAutomaticSeparationIsEnabled(enabled);
if (!vehicle->AutomaticSeparationIsEnabled()) {
Vehicle *v = vehicle->FirstShared();
while (v != nullptr) {
v->ResetAutomaticSeparation();
v = v->NextShared();
}
}
}
return CommandCost();
}
/**
* Clone/share/copy an order-list of another vehicle.
* @param flags operation to perform
@ -1835,6 +1865,8 @@ void DeleteVehicleOrders(Vehicle *v, bool keep_orderlist, bool reset_order_indic
if (!keep_orderlist) v->orders = nullptr;
}
v->ResetAutomaticSeparation();
if (reset_order_indices) {
v->cur_implicit_order_index = v->cur_real_order_index = 0;
if (v->current_order.IsType(OT_LOADING)) {

View File

@ -18,6 +18,7 @@ CommandCost CmdModifyOrder(DoCommandFlag flags, VehicleID veh, VehicleOrderID se
CommandCost CmdSkipToOrder(DoCommandFlag flags, VehicleID veh_id, VehicleOrderID sel_ord);
CommandCost CmdDeleteOrder(DoCommandFlag flags, VehicleID veh_id, VehicleOrderID sel_ord);
CommandCost CmdInsertOrder(DoCommandFlag flags, VehicleID veh, VehicleOrderID sel_ord, const Order &new_order);
CommandCost CmdOrderAutomaticSeparation(DoCommandFlag flags, VehicleID veh, bool enabled);
CommandCost CmdOrderRefit(DoCommandFlag flags, VehicleID veh, VehicleOrderID order_number, CargoID cargo);
CommandCost CmdCloneOrder(DoCommandFlag flags, CloneOptions action, VehicleID veh_dst, VehicleID veh_src);
CommandCost CmdMoveOrder(DoCommandFlag flags, VehicleID veh, VehicleOrderID moving_order, VehicleOrderID target_order);
@ -27,6 +28,7 @@ DEF_CMD_TRAIT(CMD_MODIFY_ORDER, CmdModifyOrder, CMD_LOCATION, CMDT_
DEF_CMD_TRAIT(CMD_SKIP_TO_ORDER, CmdSkipToOrder, CMD_LOCATION, CMDT_ROUTE_MANAGEMENT)
DEF_CMD_TRAIT(CMD_DELETE_ORDER, CmdDeleteOrder, CMD_LOCATION, CMDT_ROUTE_MANAGEMENT)
DEF_CMD_TRAIT(CMD_INSERT_ORDER, CmdInsertOrder, CMD_LOCATION, CMDT_ROUTE_MANAGEMENT)
DEF_CMD_TRAIT(CMD_ORDER_AUTOMATIC_SEPARATION, CmdOrderAutomaticSeparation, 0, CMDT_ROUTE_MANAGEMENT)
DEF_CMD_TRAIT(CMD_ORDER_REFIT, CmdOrderRefit, CMD_LOCATION, CMDT_ROUTE_MANAGEMENT)
DEF_CMD_TRAIT(CMD_CLONE_ORDER, CmdCloneOrder, CMD_LOCATION, CMDT_ROUTE_MANAGEMENT)
DEF_CMD_TRAIT(CMD_MOVE_ORDER, CmdMoveOrder, CMD_LOCATION, CMDT_ROUTE_MANAGEMENT)

View File

@ -490,9 +490,9 @@ enum {
* \section bottom-row Bottom row
* The second row (the bottom row) is for manipulating the list of orders:
* \verbatim
* +-----------------+-----------------+-----------------+
* | SKIP | DELETE | GOTO |
* +-----------------+-----------------+-----------------+
* +-----------------+-----------------+-----------------+-----------------+
* | SKIP | DELETE | AUTO SEPARATION | GOTO |
* +-----------------+-----------------+-----------------+-----------------+
* \endverbatim
*
* For vehicles of other companies, both button rows are not displayed.
@ -572,6 +572,16 @@ private:
return sel;
}
/**
* Handle the click on the automatic separation button
*/
void OrderClick_AutomaticSeparation()
{
if (Command<CMD_ORDER_AUTOMATIC_SEPARATION>::Post(this->vehicle->index, !this->vehicle->AutomaticSeparationIsEnabled())) {
this->UpdateButtonState();
}
}
/**
* Handle the click on the goto button.
*/
@ -941,6 +951,10 @@ public:
}
}
/* automatic separation */
this->SetWidgetDisabledState(WID_O_AUTOMATIC_SEPARATION, this->vehicle->GetNumOrders() == 0);
this->SetWidgetLoweredState(WID_O_AUTOMATIC_SEPARATION, this->vehicle->AutomaticSeparationIsEnabled());
/* First row. */
this->RaiseWidget(WID_O_FULL_LOAD);
this->RaiseWidget(WID_O_UNLOAD);
@ -1236,6 +1250,10 @@ public:
}
break;
case WID_O_AUTOMATIC_SEPARATION:
this->OrderClick_AutomaticSeparation();
break;
case WID_O_GOTO:
if (this->GetWidget<NWidgetLeaf>(widget)->ButtonHit(pt)) {
if (this->goto_type != OPOS_NONE) {
@ -1629,6 +1647,8 @@ static const NWidgetPart _nested_orders_train_widgets[] = {
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_O_STOP_SHARING), SetMinimalSize(124, 12), SetFill(1, 0),
SetDataTip(STR_ORDERS_STOP_SHARING_BUTTON, STR_ORDERS_STOP_SHARING_TOOLTIP), SetResize(1, 0),
EndContainer(),
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_O_AUTOMATIC_SEPARATION), SetMinimalSize(124, 12), SetFill(1, 0),
SetDataTip(STR_ORDERS_AUTOMATIC_SEPARATION, STR_ORDERS_AUTOMATIC_SEPARATION_TOOLTIP), SetResize(1, 0),
NWidget(NWID_BUTTON_DROPDOWN, COLOUR_GREY, WID_O_GOTO), SetMinimalSize(124, 12), SetFill(1, 0),
SetDataTip(STR_ORDERS_GO_TO_BUTTON, STR_ORDERS_GO_TO_TOOLTIP), SetResize(1, 0),
EndContainer(),
@ -1703,6 +1723,8 @@ static const NWidgetPart _nested_orders_widgets[] = {
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_O_STOP_SHARING), SetMinimalSize(124, 12), SetFill(1, 0),
SetDataTip(STR_ORDERS_STOP_SHARING_BUTTON, STR_ORDERS_STOP_SHARING_TOOLTIP), SetResize(1, 0),
EndContainer(),
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_O_AUTOMATIC_SEPARATION), SetMinimalSize(124, 12), SetFill(1, 0),
SetDataTip(STR_ORDERS_AUTOMATIC_SEPARATION, STR_ORDERS_AUTOMATIC_SEPARATION_TOOLTIP), SetResize(1, 0),
NWidget(NWID_BUTTON_DROPDOWN, COLOUR_GREY, WID_O_GOTO), SetMinimalSize(124, 12), SetFill(1, 0),
SetDataTip(STR_ORDERS_GO_TO_BUTTON, STR_ORDERS_GO_TO_TOOLTIP), SetResize(1, 0),
NWidget(WWT_RESIZEBOX, COLOUR_GREY),

View File

@ -202,6 +202,7 @@ SaveLoadTable GetOrderListDescription()
{
static const SaveLoad _orderlist_desc[] = {
SLE_REF(OrderList, first, REF_ORDER),
SLE_CONDVAR(OrderList, automatic_separation, SLE_BOOL, SLV_AUTOMATIC_SEPARATION, SL_MAX_VERSION),
};
return _orderlist_desc;

View File

@ -331,6 +331,7 @@ enum SaveLoadVersion : uint16_t {
SLV_CUSTOM_SUBSIDY_DURATION, ///< 292 PR#9081 Configurable subsidy duration.
SLV_SAVELOAD_LIST_LENGTH, ///< 293 PR#9374 Consistency in list length with SL_STRUCT / SL_STRUCTLIST / SL_DEQUE / SL_REFLIST.
SLV_RIFF_TO_ARRAY, ///< 294 PR#9375 Changed many CH_RIFF chunks to CH_ARRAY chunks.
SLV_AUTOMATIC_SEPARATION, ///< 295 PR#8342 Allow automatically separating vehicles in shared orders.
SLV_TABLE_CHUNKS, ///< 295 PR#9322 Introduction of CH_TABLE and CH_SPARSE_TABLE.
SLV_SCRIPT_INT64, ///< 296 PR#9415 SQInteger is 64bit but was saved as 32bit.

View File

@ -724,6 +724,9 @@ public:
SLE_CONDVAR(Vehicle, current_order_time, SLE_INT32, SLV_TIMETABLE_TICKS_TYPE, SL_MAX_VERSION),
SLE_CONDVAR(Vehicle, last_loading_tick, SLE_UINT64, SLV_LAST_LOADING_TICK, SL_MAX_VERSION),
SLE_CONDVAR(Vehicle, lateness_counter, SLE_INT32, SLV_67, SL_MAX_VERSION),
SLE_CONDVAR(Vehicle, first_order_last_departure, SLE_INT32, SLV_AUTOMATIC_SEPARATION, SL_MAX_VERSION),
SLE_CONDVAR(Vehicle, first_order_round_trip_time, SLE_INT32, SLV_AUTOMATIC_SEPARATION, SL_MAX_VERSION),
};
#if defined(_MSC_VER) && (_MSC_VER == 1915 || _MSC_VER == 1916)
return description;

View File

@ -1537,6 +1537,7 @@ void VehicleEnterDepot(Vehicle *v)
v->vehstatus |= VS_HIDDEN;
v->cur_speed = 0;
v->ResetAutomaticSeparation();
VehicleServiceInDepot(v);
@ -2338,6 +2339,10 @@ void Vehicle::HandleLoading(bool mode)
/* Not the first call for this tick, or still loading */
if (mode || !HasBit(this->vehicle_flags, VF_LOADING_FINISHED) || this->current_order_time < wait_time) return;
this->UpdateAutomaticSeparation();
if (this->IsWaitingForAutomaticSeparation()) return;
this->PlayLeaveStationSound();
this->LeaveStation();
@ -2360,6 +2365,96 @@ void Vehicle::HandleLoading(bool mode)
this->IncrementImplicitOrderIndex();
}
/**
* Checks whether a vehicle is waiting for automatic separation (if not,
* it is ready to depart)
*/
bool Vehicle::IsWaitingForAutomaticSeparation() const {
TimerGameTick::Ticks now = TimerGameCalendar::date.base() * Ticks::DAY_TICKS + TimerGameCalendar::date_fract;
return this->AutomaticSeparationIsEnabled() && this->first_order_last_departure > now;
};
/**
* If enabled, calculates the departure time for this vehicle based on the
* automatic separation feature.
*/
void Vehicle::UpdateAutomaticSeparation()
{
/* Check this feature is enabled on the vehicle's orders */
if (!this->AutomaticSeparationIsEnabled()) return;
/* Only perform the separation at the first manual order (saves on storage) */
VehicleOrderID first_manual_order = 0;
for (Order *o = this->GetFirstOrder(); o != nullptr && o->IsType(OT_IMPLICIT); o = o->next) {
++first_manual_order;
}
if (this->cur_implicit_order_index != first_manual_order) return;
/* A "last departure" >= now means we've already calculated the separation */
TimerGameTick::Ticks now = TimerGameCalendar::date.base() * Ticks::DAY_TICKS + TimerGameCalendar::date_fract;
if (this->first_order_last_departure >= now) return;
/* Calculate round trip time from last departure and now - automatic separation waiting time is not included */
if (this->first_order_last_departure > 0) {
this->first_order_round_trip_time = now - this->first_order_last_departure;
}
/* To work out the automatic separation waiting time we need to know:
* - When the last vehicle departed or will depart
* - Average time to perform the order list (as sum/count)
* - How many vehicles are currently operating the order list
* - How many vehicles are currently queuing for the first manual order
*/
TimerGameTick::Ticks last_departure = 0;
TimerGameTick::Ticks round_trip_sum = 0;
int round_trip_count = 0;
int vehicles = 0;
int vehicles_queuing = 0;
Vehicle *v = this->FirstShared();
while (v != nullptr) {
last_departure = std::max(last_departure, v->first_order_last_departure);
if (v->first_order_round_trip_time > 0) {
round_trip_sum += v->first_order_round_trip_time;
round_trip_count++;
}
/* A stopped vehicle is not included; it might be stopped by player or parked in a depot */
if (!(v->vehstatus & VS_STOPPED)) {
vehicles++;
/* Count vehicles queing for the first manual order but not currently in the station */
if (v != this && v->cur_speed == 0 && v->cur_implicit_order_index == first_manual_order && !v->current_order.IsType(OT_LOADING)) {
vehicles_queuing++;
}
}
v = v->NextShared();
}
/* Calculate the mean round trip time and separation. The time spent queuing for stations is included in vehicle
* round trip times.
*
* For a single shared order into a single station, this will increase and decrease the separation as needed.
* However, for multiple shared orders into the same station, each shared order can back up the others and all
* the routes will slowly increase their separation until every available vehicle is in the same queue.
*
* To counter this, we need to reduce the round trip time when vehicles are queuing. The scaling here reduces it
* by twice the proportion of queuing vehicles, e.g. if 1/N vehicles are queuing, the RTT is reduced by 2/N.
*
* This is based on the idea that if only M/N vehicles are progressing, the non-queuing RTT is approximately M/N
* of the measured RTT, because (N-M)/N of the RTT is spent in the queue, with the 'twice' coming from the need
* to over-compensate rather than aim exactly for the ideal (which is very approximate here). */
TimerGameTick::Ticks round_trip_time = round_trip_count > 0 ? round_trip_sum / round_trip_count : 0;
int vehicles_moving_ratio = std::max(1, vehicles - 2 * vehicles_queuing);
TimerGameTick::Ticks separation = std::max(1, vehicles > 0 ? round_trip_time * vehicles_moving_ratio / vehicles / vehicles : 1);
/* Finally we can calculate when this vehicle should depart; if that's in the past, it'll depart right now */
this->first_order_last_departure = std::max(last_departure + separation, now);
/* Debug logging can be quite spammy as it prints a line every time a vehicle departs the first manual order */
if (_debug_misc_level >= 4) {
SetDParam(0, this->index);
Debug(misc, 4, "Orders for {}: RTT = {} [{:.2f} days, {} veh], separation = {} [{:.2f} days, {} veh, {} queuing] / gap = {} [{:.2f} days], wait = {} [{:.2f} days]", GetString(STR_VEHICLE_NAME), round_trip_time, (float)round_trip_time / Ticks::DAY_TICKS, round_trip_count, separation, (float)separation / Ticks::DAY_TICKS, vehicles, vehicles_queuing, now - last_departure, (float)(now - last_departure) / Ticks::DAY_TICKS, this->first_order_last_departure - now, (float)(this->first_order_last_departure - now) / Ticks::DAY_TICKS);
}
}
/**
* Send this vehicle to the depot using the given command(s).
* @param flags the command flags (like execute and such).

View File

@ -812,6 +812,13 @@ public:
inline void SetServiceIntervalIsPercent(bool on) { SB(this->vehicle_flags, VF_SERVINT_IS_PERCENT, 1, on); }
inline bool AutomaticSeparationIsEnabled() const { return (this->orders == nullptr) ? false : this->orders->AutomaticSeparationIsEnabled(); }
inline void SetAutomaticSeparationIsEnabled(bool enabled) const { if (this->orders != nullptr) this->orders->SetAutomaticSeparationIsEnabled(enabled); }
bool IsWaitingForAutomaticSeparation() const;
void UpdateAutomaticSeparation();
private:
/**
* Advance cur_real_order_index to the next real order.

View File

@ -628,6 +628,7 @@ CommandCost CmdStartStopVehicle(DoCommandFlag flags, VehicleID veh_id, bool eval
if (v->IsStoppedInDepot() && (flags & DC_AUTOREPLACE) == 0) DeleteVehicleNews(veh_id, STR_NEWS_TRAIN_IS_WAITING + v->type);
v->vehstatus ^= VS_STOPPED;
if (v->vehstatus & VS_STOPPED) v->ResetAutomaticSeparation();
if (v->type != VEH_TRAIN) v->cur_speed = 0; // trains can stop 'slowly'
v->MarkDirty();
SetWindowWidgetDirty(WC_VEHICLE_VIEW, v->index, WID_VV_START_STOP);

View File

@ -20,6 +20,7 @@ enum OrderWidgets {
WID_O_DELETE, ///< Delete selected order.
WID_O_STOP_SHARING, ///< Stop sharing orders.
WID_O_NON_STOP, ///< Goto non-stop to destination.
WID_O_AUTOMATIC_SEPARATION, ///< Toggle automatic separation.
WID_O_GOTO, ///< Goto destination.
WID_O_FULL_LOAD, ///< Select full load.
WID_O_UNLOAD, ///< Select unload.