diff --git a/src/base_consist.cpp b/src/base_consist.cpp index ad9d476b66..ff48bbced0 100644 --- a/src/base_consist.cpp +++ b/src/base_consist.cpp @@ -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; +} diff --git a/src/base_consist.h b/src/base_consist.h index 6189604537..de8ce676c6 100644 --- a/src/base_consist.h +++ b/src/base_consist.h @@ -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 */ diff --git a/src/command_type.h b/src/command_type.h index 6ec5748c12..ede9a9a0dd 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -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 diff --git a/src/lang/english.txt b/src/lang/english.txt index b9f21666b1..ccdbc5f649 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -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 diff --git a/src/order_base.h b/src/order_base.h index 577dbe4c47..d55bc5372b 100644 --- a/src/order_base.h +++ b/src/order_base.h @@ -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; diff --git a/src/order_cmd.cpp b/src/order_cmd.cpp index 2650843c1f..0621ec41df 100644 --- a/src/order_cmd.cpp +++ b/src/order_cmd.cpp @@ -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)) { diff --git a/src/order_cmd.h b/src/order_cmd.h index b0ab888d94..539832fa60 100644 --- a/src/order_cmd.h +++ b/src/order_cmd.h @@ -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) diff --git a/src/order_gui.cpp b/src/order_gui.cpp index aca85ecd4d..50c1ec5109 100644 --- a/src/order_gui.cpp +++ b/src/order_gui.cpp @@ -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::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(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), diff --git a/src/saveload/order_sl.cpp b/src/saveload/order_sl.cpp index cf6849b20d..fc1ca85917 100644 --- a/src/saveload/order_sl.cpp +++ b/src/saveload/order_sl.cpp @@ -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; diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index 23531fa7ae..88660b335c 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -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. diff --git a/src/saveload/vehicle_sl.cpp b/src/saveload/vehicle_sl.cpp index 8d482a1b89..8d9609a974 100644 --- a/src/saveload/vehicle_sl.cpp +++ b/src/saveload/vehicle_sl.cpp @@ -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; diff --git a/src/vehicle.cpp b/src/vehicle.cpp index 386b956b4f..f377efab87 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -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). diff --git a/src/vehicle_base.h b/src/vehicle_base.h index cd0ac70aae..c083db7933 100644 --- a/src/vehicle_base.h +++ b/src/vehicle_base.h @@ -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. diff --git a/src/vehicle_cmd.cpp b/src/vehicle_cmd.cpp index 549bd189c4..78b75f11a9 100644 --- a/src/vehicle_cmd.cpp +++ b/src/vehicle_cmd.cpp @@ -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); diff --git a/src/widgets/order_widget.h b/src/widgets/order_widget.h index 82ca472e20..881703d605 100644 --- a/src/widgets/order_widget.h +++ b/src/widgets/order_widget.h @@ -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.