diff --git a/src/command.cpp b/src/command.cpp index 0ed55b8eed..8149cd9188 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -195,6 +195,7 @@ CommandProc CmdMoveOrder; CommandProc CmdChangeTimetable; CommandProc CmdSetVehicleOnTime; CommandProc CmdAutofillTimetable; +CommandProc CmdAutomateTimetable; CommandProc CmdSetTimetableStart; CommandProc CmdOpenCloseAirport; @@ -359,6 +360,7 @@ static const Command _command_proc_table[] = { DEF_CMD(CmdChangeTimetable, 0, CMDT_ROUTE_MANAGEMENT ), // CMD_CHANGE_TIMETABLE DEF_CMD(CmdSetVehicleOnTime, 0, CMDT_ROUTE_MANAGEMENT ), // CMD_SET_VEHICLE_ON_TIME DEF_CMD(CmdAutofillTimetable, 0, CMDT_ROUTE_MANAGEMENT ), // CMD_AUTOFILL_TIMETABLE + DEF_CMD(CmdAutomateTimetable, 0, CMDT_ROUTE_MANAGEMENT ), // CMD_AUTOMATE_TIMETABLE] DEF_CMD(CmdSetTimetableStart, 0, CMDT_ROUTE_MANAGEMENT ), // CMD_SET_TIMETABLE_START DEF_CMD(CmdOpenCloseAirport, 0, CMDT_ROUTE_MANAGEMENT ), // CMD_OPEN_CLOSE_AIRPORT diff --git a/src/command_type.h b/src/command_type.h index 845b89d0b5..30e48435f2 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -326,6 +326,7 @@ enum Commands { CMD_CHANGE_TIMETABLE, ///< change the timetable for a vehicle CMD_SET_VEHICLE_ON_TIME, ///< set the vehicle on time feature (timetable) CMD_AUTOFILL_TIMETABLE, ///< autofill the timetable + CMD_AUTOMATE_TIMETABLE, ///< automate the timetable CMD_SET_TIMETABLE_START, ///< set the date that a timetable should start CMD_OPEN_CLOSE_AIRPORT, ///< open/close an airport to incoming aircraft diff --git a/src/lang/english.txt b/src/lang/english.txt index e6cdf4c41d..15daa89471 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -1401,6 +1401,8 @@ STR_CONFIG_SETTING_ADVANCED_VEHICLE_LISTS :Use groups in v STR_CONFIG_SETTING_ADVANCED_VEHICLE_LISTS_HELPTEXT :Enable usage of the advanced vehicle lists for grouping vehicles STR_CONFIG_SETTING_LOADING_INDICATORS :Use loading indicators: {STRING2} STR_CONFIG_SETTING_LOADING_INDICATORS_HELPTEXT :Select whether loading indicators are displayed above loading or unloading vehicles +STR_CONFIG_SETTING_TIMETABLE_AUTOMATED :Automatically manage timetables: {STRING2} +STR_CONFIG_SETTING_TIMETABLE_AUTOMATED_HELPTEXT :Whether to enable automatic timetables STR_CONFIG_SETTING_TIMETABLE_IN_TICKS :Show timetable in ticks rather than days: {STRING2} STR_CONFIG_SETTING_TIMETABLE_IN_TICKS_HELPTEXT :Show travel times in time tables in game ticks instead of days STR_CONFIG_SETTING_TIME_IN_MINUTES :Show time in minutes rather than days: {STRING2} @@ -1417,6 +1419,8 @@ STR_CONFIG_SETTING_DATE_WITH_TIME_YM :Month and year STR_CONFIG_SETTING_DATE_WITH_TIME_YMD :Full date STR_CONFIG_SETTING_TIMETABLE_START_TEXT_ENTRY :Enter timetable start times as text (requires time to be in minutes): {STRING2} STR_CONFIG_SETTING_TIMETABLE_START_TEXT_ENTRY_HELPTEXT :Select whether timetable start times may be entered as text if time is being shown in minutes +STR_CONFIG_SETTING_TIMETABLE_SEPARATION :Use timetable to ensure vehicle separation: {STRING2} +STR_CONFIG_SETTING_TIMETABLE_SEPARATION_HELPTEXT :Select whether to ensure separation of vehicles when using automatic timetables STR_CONFIG_SETTING_TIMETABLE_SHOW_ARRIVAL_DEPARTURE :Show arrival and departure in timetables: {STRING2} STR_CONFIG_SETTING_TIMETABLE_SHOW_ARRIVAL_DEPARTURE_HELPTEXT :Display anticipated arrival and departure times in timetables STR_CONFIG_SETTING_QUICKGOTO :Quick creation of vehicle orders: {STRING2} @@ -4288,6 +4292,9 @@ STR_TIMETABLE_RESET_LATENESS_TOOLTIP :{BLACK}Reset th STR_TIMETABLE_AUTOFILL :{BLACK}Autofill STR_TIMETABLE_AUTOFILL_TOOLTIP :{BLACK}Fill the timetable automatically with the values from the next journey (Ctrl+Click to try to keep waiting times) +STR_TIMETABLE_AUTOMATE :{BLACK}Automate +STR_TIMETABLE_AUTOMATE_TOOLTIP :{BLACK}Manage the timetables automatically by updating the values for each journey + STR_TIMETABLE_EXPECTED :{BLACK}Expected STR_TIMETABLE_SCHEDULED :{BLACK}Scheduled STR_TIMETABLE_EXPECTED_TOOLTIP :{BLACK}Switch between expected and scheduled diff --git a/src/order_cmd.cpp b/src/order_cmd.cpp index 2dd2bc427b..0a30fccb71 100644 --- a/src/order_cmd.cpp +++ b/src/order_cmd.cpp @@ -1166,6 +1166,9 @@ CommandCost CmdSkipToOrder(TileIndex tile, DoCommandFlag flags, uint32 p1, uint3 v->UpdateRealOrderIndex(); InvalidateVehicleOrder(v, VIWD_MODIFY_ORDERS); + + if (_settings_game.order.timetable_separation) v->ClearSeparation(); + if (_settings_game.order.timetable_separation) ClrBit(v->vehicle_flags, VF_TIMETABLE_STARTED); } /* We have an aircraft/ship, they have a mini-schedule, so update them all */ @@ -1623,9 +1626,19 @@ CommandCost CmdCloneOrder(TileIndex tile, DoCommandFlag flags, uint32 p1, uint32 /* Link this vehicle in the shared-list */ dst->AddToShared(src); + + /* Set automation bit if target has it. */ + if (HasBit(src->vehicle_flags, VF_AUTOMATE_TIMETABLE)) { + SetBit(dst->vehicle_flags, VF_AUTOMATE_TIMETABLE); + } + + if (_settings_game.order.timetable_separation) dst->ClearSeparation(); + if (_settings_game.order.timetable_separation) ClrBit(dst->vehicle_flags, VF_TIMETABLE_STARTED); + InvalidateVehicleOrder(dst, VIWD_REMOVE_ALL_ORDERS); InvalidateVehicleOrder(src, VIWD_MODIFY_ORDERS); + InvalidateWindowClassesData(GetWindowClassForVehicleType(dst->type), 0); } break; diff --git a/src/saveload/extended_ver_sl.cpp b/src/saveload/extended_ver_sl.cpp index 00303e1e13..475d204528 100644 --- a/src/saveload/extended_ver_sl.cpp +++ b/src/saveload/extended_ver_sl.cpp @@ -55,6 +55,7 @@ const SlxiSubChunkInfo _sl_xv_sub_chunk_infos[] = { { XSLFI_SIG_TUNNEL_BRIDGE, XSCF_NULL, 1, 1, "signal_tunnel_bridge", NULL, NULL, NULL }, { XLSFI_IMPROVED_BREAKDOWNS, XSCF_NULL, 1, 1, "improved_breakdowns", NULL, NULL, NULL }, { XSLFI_TT_WAIT_IN_DEPOT, XSCF_NULL, 1, 1, "tt_wait_in_depot", NULL, NULL, NULL }, + { XSLFI_AUTO_TIMETABLE, XSCF_NULL, 1, 1, "auto_timetables", NULL, NULL, NULL }, { XSLFI_NULL, XSCF_NULL, 0, 0, NULL, NULL, NULL, NULL },// This is the end marker }; diff --git a/src/saveload/extended_ver_sl.h b/src/saveload/extended_ver_sl.h index 97a3aef69c..c488f7d6d5 100644 --- a/src/saveload/extended_ver_sl.h +++ b/src/saveload/extended_ver_sl.h @@ -30,6 +30,7 @@ enum SlXvFeatureIndex { XSLFI_SIG_TUNNEL_BRIDGE, ///< Signals on tunnels and bridges XLSFI_IMPROVED_BREAKDOWNS, ///< Improved breakdowns patch XSLFI_TT_WAIT_IN_DEPOT, ///< Timetabling waiting time in depot patch + XSLFI_AUTO_TIMETABLE, ///< Auto timetables and separation patch XSLFI_SIZE, ///< Total count of features, including null feature }; diff --git a/src/saveload/vehicle_sl.cpp b/src/saveload/vehicle_sl.cpp index da89ae9c21..48ffbc5a4d 100644 --- a/src/saveload/vehicle_sl.cpp +++ b/src/saveload/vehicle_sl.cpp @@ -696,6 +696,9 @@ const SaveLoad *GetVehicleDescription(VehicleType vt) SLE_CONDVAR(Vehicle, random_bits, SLE_UINT8, 2, SL_MAX_VERSION), SLE_CONDVAR(Vehicle, waiting_triggers, SLE_UINT8, 2, SL_MAX_VERSION), + SLE_CONDREF_X(Vehicle, ahead_separation, REF_VEHICLE, 0, SL_MAX_VERSION, SlXvFeatureTest(XSLFTO_AND, XSLFI_AUTO_TIMETABLE)), + SLE_CONDREF_X(Vehicle, behind_separation, REF_VEHICLE, 0, SL_MAX_VERSION, SlXvFeatureTest(XSLFTO_AND, XSLFI_AUTO_TIMETABLE)), + SLE_CONDREF(Vehicle, next_shared, REF_VEHICLE, 2, SL_MAX_VERSION), SLE_CONDNULL(2, 2, 68), SLE_CONDNULL(4, 69, 100), @@ -703,6 +706,7 @@ const SaveLoad *GetVehicleDescription(VehicleType vt) SLE_CONDVAR(Vehicle, group_id, SLE_UINT16, 60, SL_MAX_VERSION), SLE_CONDVAR(Vehicle, current_order_time, SLE_UINT32, 67, SL_MAX_VERSION), + SLE_CONDVAR_X(Vehicle, current_loading_time, SLE_UINT32, 0, SL_MAX_VERSION, SlXvFeatureTest(XSLFTO_AND, XSLFI_AUTO_TIMETABLE)), SLE_CONDVAR(Vehicle, lateness_counter, SLE_INT32, 67, SL_MAX_VERSION), SLE_CONDNULL(10, 2, 143), // old reserved space diff --git a/src/settings_gui.cpp b/src/settings_gui.cpp index 88e56bae43..8cb429305e 100644 --- a/src/settings_gui.cpp +++ b/src/settings_gui.cpp @@ -1656,6 +1656,8 @@ static SettingsContainer &GetSettingsTree() vehicles->Add(new SettingEntry("order.no_servicing_if_no_breakdowns")); vehicles->Add(new SettingEntry("order.serviceathelipad")); + vehicles->Add(new SettingEntry("order.timetable_automated")); + vehicles->Add(new SettingEntry("order.timetable_separation")); vehicles->Add(new SettingEntry("vehicle.adjacent_crossings")); } diff --git a/src/settings_type.h b/src/settings_type.h index bf1e4946d9..18a8855bf3 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -461,6 +461,8 @@ struct OrderSettings { bool gradual_loading; ///< load vehicles gradually bool selectgoods; ///< only send the goods to station if a train has been there bool no_servicing_if_no_breakdowns; ///< don't send vehicles to depot when breakdowns are disabled + bool timetable_automated; ///< whether to automatically manage timetables + bool timetable_separation; ///< whether to perform automatic separation based on timetable bool serviceathelipad; ///< service helicopters at helipads automatically (no need to send to depot) }; diff --git a/src/table/settings.ini b/src/table/settings.ini index 6caf215d25..532d062608 100644 --- a/src/table/settings.ini +++ b/src/table/settings.ini @@ -331,6 +331,26 @@ min = 0 max = 3 cat = SC_BASIC +[SDT_BOOL] +base = GameSettings +var = order.timetable_automated +def = true +str = STR_CONFIG_SETTING_TIMETABLE_AUTOMATED +strhelp = STR_CONFIG_SETTING_TIMETABLE_AUTOMATED_HELPTEXT +cat = SC_EXPERT +extver = SlXvFeatureTest(XSLFTO_AND, XSLFI_AUTO_TIMETABLE) +patxname = ""auto_timetables.order.timetable_automated"" + +[SDT_BOOL] +base = GameSettings +var = order.timetable_separation +def = true +str = STR_CONFIG_SETTING_TIMETABLE_SEPARATION +strhelp = STR_CONFIG_SETTING_TIMETABLE_SEPARATION_HELPTEXT +cat = SC_EXPERT +extver = SlXvFeatureTest(XSLFTO_AND, XSLFI_AUTO_TIMETABLE) +patxname = ""auto_timetables.order.timetable_separation"" + ; There are only 21 predefined town_name values (0-20), but you can have more with newgrf action F so allow ; these bigger values (21-255). Invalid values will fallback to english on use and (undefined string) in GUI. [SDT_OMANY] diff --git a/src/timetable_cmd.cpp b/src/timetable_cmd.cpp index 4f5735c964..3d2f258d94 100644 --- a/src/timetable_cmd.cpp +++ b/src/timetable_cmd.cpp @@ -15,6 +15,7 @@ #include "date_func.h" #include "window_func.h" #include "vehicle_base.h" +#include "settings_type.h" #include "cmd_helper.h" #include "core/sort_func.hpp" #include "settings_type.h" @@ -380,6 +381,167 @@ CommandCost CmdAutofillTimetable(TileIndex tile, DoCommandFlag flags, uint32 p1, return CommandCost(); } +/** +* Start or stop automatic management of timetables. + * @param tile Not used. + * @param flags Operation to perform. + * @param p1 Vehicle index. + * @param p2 Various bitstuffed elements + * - p2 = (bit 0) - Set to 1 to enable, 0 to disable automation. + * - p2 = (bit 1) - Ctrl was pressed. Used when disabling to keep times. + * @param text unused + * @return the cost of this operation or an error + */ + +CommandCost CmdAutomateTimetable(TileIndex index, DoCommandFlag flags, uint32 p1, uint32 p2, const char *text) +{ + if (!_settings_game.order.timetable_automated) return CMD_ERROR; + + VehicleID veh = GB(p1, 0, 16); + + Vehicle *v = Vehicle::GetIfValid(veh); + if (v == NULL || !v->IsPrimaryVehicle()) return CMD_ERROR; + + CommandCost ret = CheckOwnership(v->owner); + if (ret.Failed()) return ret; + + if (flags & DC_EXEC) { + for (Vehicle *v2 = v->FirstShared(); v2 != NULL; v2 = v2->NextShared()) { + if (HasBit(p2, 0)) { + /* Automated timetable. Set flags and clear current times. */ + SetBit(v2->vehicle_flags, VF_AUTOMATE_TIMETABLE); + ClrBit(v2->vehicle_flags, VF_AUTOFILL_TIMETABLE); + ClrBit(v2->vehicle_flags, VF_AUTOFILL_PRES_WAIT_TIME); + ClrBit(v2->vehicle_flags, VF_TIMETABLE_STARTED); + v2->timetable_start = 0; + v2->lateness_counter = 0; + v2->current_loading_time = 0; + v2->ClearSeparation(); + } else { + /* De-automate timetable. Clear flags. */ + ClrBit(v2->vehicle_flags, VF_AUTOMATE_TIMETABLE); + ClrBit(v2->vehicle_flags, VF_AUTOFILL_TIMETABLE); + ClrBit(v2->vehicle_flags, VF_AUTOFILL_PRES_WAIT_TIME); + v2->ClearSeparation(); + if (!HasBit(p2, 1)) { + /* Ctrl wasn't pressed, so clear all timetabled times. */ + SetBit(v2->vehicle_flags, VF_TIMETABLE_STARTED); + v2->timetable_start = 0; + v2->lateness_counter = 0; + v2->current_loading_time = 0; + OrderList *orders = v2->orders.list; + if (orders != NULL) { + for (int i = 0; i < orders->GetNumOrders(); i++) { + ChangeTimetable(v2, i, 0, MTF_WAIT_TIME, true); + ChangeTimetable(v2, i, 0, MTF_TRAVEL_TIME, true); + } + } + } + } + SetWindowDirty(WC_VEHICLE_TIMETABLE, v2->index); + } + } + + return CommandCost(); +} + +int TimeToFinishOrder(Vehicle *v, int n) +{ + int left; + Order *order = v->GetOrder(n); + int wait_time = order->GetWaitTime(); + int travel_time = order->GetTravelTime(); + assert(order != NULL); + if ((v->cur_real_order_index == n) && (v->last_station_visited == order->GetDestination())) { + if (wait_time == 0) return -1; + if (v->current_loading_time > 0) { + left = wait_time - v->current_order_time; + } else { + left = wait_time; + } + if (left < 0) left = 0; + } else { + left = travel_time; + if (v->cur_real_order_index == n) left -= v->current_order_time; + if (travel_time == 0 || wait_time == 0) return -1; + if (left < 0) left = 0; + left +=wait_time; + } + return left; +} + +int SeparationBetween(Vehicle *v1, Vehicle *v2) +{ + if (v1 == v2) return -1; + int separation = 0; + int time; + int n = v1->cur_real_order_index; + while (n != v2->cur_real_order_index) { + time = TimeToFinishOrder(v1, n); + if (time == -1) return -1; + separation += time; + n++; + if (n >= v1->GetNumOrders()) n = 0; + } + int time1 = TimeToFinishOrder(v1, n); + int time2 = TimeToFinishOrder(v2, n); + if (time1 == -1 || time2 == -1) return -1; + time = time1 - time2; + if (time < 0) { + for (n = 0; n < v1->GetNumOrders(); n++) { + Order *order = v1->GetOrder(n); + int wait_time = order->GetWaitTime(); + int travel_time = order->GetTravelTime(); + if (travel_time == 0 || wait_time == 0) return -1; + time += travel_time + wait_time; + } + } + separation += time; + assert(separation >= 0); + if (separation == 0) return -1; + return separation; +} + +void UpdateSeparationOrder(Vehicle *v_start) +{ + /* First check if we have a vehicle ahead, and if not search for one. */ + if (v_start->AheadSeparation() == NULL) { + v_start->InitSeparation(); + } + if (v_start->AheadSeparation() == NULL) { + return; + } + /* Switch positions if necessary. */ + int swaps = 0; + bool done = false; + while (!done) { + done = true; + int min_sep = SeparationBetween(v_start, v_start->AheadSeparation()); + Vehicle *v = v_start; + do { + if (v != v_start) { + int tmp_sep = SeparationBetween(v_start, v); + if (tmp_sep < min_sep && tmp_sep != -1) { + swaps++; + if (swaps >= 50) { + return; + } + done = false; + v_start->ClearSeparation(); + v_start->AddToSeparationBehind(v); + break; + } + } + int separation_ahead = SeparationBetween(v, v->AheadSeparation()); + int separation_behind = SeparationBetween(v->BehindSeparation(), v); + v->lateness_counter = (separation_ahead - separation_behind) / 2; + if (separation_ahead == -1 || separation_behind == -1) v->lateness_counter = 0; + v = v->AheadSeparation(); + } while (v != v_start); + } +} + + /** * Update the timetable for the vehicle. * @param v The vehicle to update the timetable for. @@ -387,9 +549,12 @@ CommandCost CmdAutofillTimetable(TileIndex tile, DoCommandFlag flags, uint32 p1, */ void UpdateVehicleTimetable(Vehicle *v, bool travelling) { + if (!travelling) v->current_loading_time++; // +1 because this time is one tick behind uint time_taken = v->current_order_time; + uint time_loading = v->current_loading_time; v->current_order_time = 0; + v->current_loading_time = 0; if (v->current_order.IsType(OT_IMPLICIT)) return; // no timetabling of auto orders @@ -403,6 +568,18 @@ void UpdateVehicleTimetable(Vehicle *v, bool travelling) bool just_started = false; + /* Start automated timetables at first opportunity */ + if (!HasBit(v->vehicle_flags, VF_TIMETABLE_STARTED) && HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)) { + if (_settings_game.order.timetable_separation) v->ClearSeparation(); + SetBit(v->vehicle_flags, VF_TIMETABLE_STARTED); + v->lateness_counter = 0; + if (_settings_game.order.timetable_separation) UpdateSeparationOrder(v); + for (v = v->FirstShared(); v != NULL; v = v->NextShared()) { + SetWindowDirty(WC_VEHICLE_TIMETABLE, v->index); + } + return; + } + /* This vehicle is arriving at the first destination in the timetable. */ if (v->cur_real_order_index == first_manual_order && travelling) { /* If the start date hasn't been set, or it was set automatically when @@ -472,12 +649,60 @@ void UpdateVehicleTimetable(Vehicle *v, bool travelling) uint timetabled = travelling ? real_current_order->GetTimetabledTravel() : real_current_order->GetTimetabledWait(); + /* Update the timetable to gradually shift order times towards the actual travel times. */ + if (timetabled != 0 && HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)) { + int32 new_time; + if (travelling) { + new_time = time_taken; + } else { + new_time = time_loading; + } + + /* Check for too large a difference from expected time, and if so don't average. */ + if (!(new_time > (int32)timetabled * 2 || new_time < (int32)timetabled / 2)) { + int arrival_error = timetabled - new_time; + /* Compute running average, with sign conversion to avoid negative overflow. */ + new_time = ((int32)timetabled * 4 + new_time + 2) / 5; + /* Use arrival_error to finetune order ticks. */ + if (arrival_error < 0) new_time++; + if (arrival_error > 0) new_time--; + } else if (new_time > (int32)timetabled * 10 && travelling) { + /* Possible jam, clear time and restart timetable for all vehicles. + * Otherwise we risk trains blocking 1-lane stations for long times. */ + ChangeTimetable(v, v->cur_real_order_index, 0, travelling ? MTF_TRAVEL_TIME : MTF_WAIT_TIME, true); + for (Vehicle *v2 = v->FirstShared(); v2 != NULL; v2 = v2->NextShared()) { + if (_settings_game.order.timetable_separation) v2->ClearSeparation(); + ClrBit(v2->vehicle_flags, VF_TIMETABLE_STARTED); + SetWindowDirty(WC_VEHICLE_TIMETABLE, v2->index); + } + return; + } + + if (new_time < 1) new_time = 1; + if (new_time != (int32)timetabled) + ChangeTimetable(v, v->cur_real_order_index, new_time, travelling ? MTF_TRAVEL_TIME : MTF_WAIT_TIME, true); + } else if (timetabled == 0 && HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)) { + /* Add times for orders that are not yet timetabled, even while not autofilling */ + if (travelling) + ChangeTimetable(v, v->cur_real_order_index, time_taken, travelling ? MTF_TRAVEL_TIME : MTF_WAIT_TIME, true); + else + ChangeTimetable(v, v->cur_real_order_index, time_loading, travelling ? MTF_TRAVEL_TIME : MTF_WAIT_TIME, true); + } + /* Vehicles will wait at stations if they arrive early even if they are not * timetabled to wait there, so make sure the lateness counter is updated * when this happens. */ if (timetabled == 0 && (travelling || v->lateness_counter >= 0)) return; - v->lateness_counter -= (timetabled - time_taken); + if (_settings_game.order.timetable_separation && HasBit(v->vehicle_flags, VF_TIMETABLE_STARTED)) { + v->current_order_time = time_taken; + v->current_loading_time = time_loading; + UpdateSeparationOrder(v); + v->current_order_time = 0; + v->current_loading_time = 0; + } else { + v->lateness_counter -= (timetabled - time_taken); + } /* When we are more late than this timetabled bit takes we (somewhat expensively) * check how many ticks the (fully filled) timetable has. If a timetable cycle is diff --git a/src/timetable_gui.cpp b/src/timetable_gui.cpp index 34ba261db2..c266134ab9 100644 --- a/src/timetable_gui.cpp +++ b/src/timetable_gui.cpp @@ -336,6 +336,7 @@ struct TimetableWindow : Window { this->SetWidgetDisabledState(WID_VT_START_DATE, v->orders.list == NULL); this->SetWidgetDisabledState(WID_VT_RESET_LATENESS, v->orders.list == NULL); this->SetWidgetDisabledState(WID_VT_AUTOFILL, v->orders.list == NULL); + this->EnableWidget(WID_VT_AUTOMATE); } else { this->DisableWidget(WID_VT_START_DATE); this->DisableWidget(WID_VT_CHANGE_TIME); @@ -344,10 +345,17 @@ struct TimetableWindow : Window { this->DisableWidget(WID_VT_CLEAR_SPEED); this->DisableWidget(WID_VT_RESET_LATENESS); this->DisableWidget(WID_VT_AUTOFILL); + this->DisableWidget(WID_VT_AUTOMATE); this->DisableWidget(WID_VT_SHARED_ORDER_LIST); } this->SetWidgetLoweredState(WID_VT_AUTOFILL, HasBit(v->vehicle_flags, VF_AUTOFILL_TIMETABLE)); + this->SetWidgetLoweredState(WID_VT_AUTOMATE, HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)); + this->SetWidgetDisabledState(WID_VT_START_DATE, _settings_game.order.timetable_separation); + this->SetWidgetDisabledState(WID_VT_AUTOMATE, !_settings_game.order.timetable_automated); + this->SetWidgetDisabledState(WID_VT_CHANGE_TIME, HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)); + this->SetWidgetDisabledState(WID_VT_AUTOFILL, HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)); + this->SetWidgetDisabledState(WID_VT_CLEAR_TIME, HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)); this->DrawWidgets(); } @@ -630,6 +638,14 @@ struct TimetableWindow : Window { break; } + case WID_VT_AUTOMATE: { + uint32 p2 = 0; + if (!HasBit(v->vehicle_flags, VF_AUTOMATE_TIMETABLE)) SetBit(p2, 0); + if (!_ctrl_pressed) SetBit(p2, 1); + DoCommandP(0, v->index, p2, CMD_AUTOMATE_TIMETABLE | CMD_MSG(STR_ERROR_CAN_T_TIMETABLE_VEHICLE)); + break; + } + case WID_VT_EXPECTED: this->show_expected = !this->show_expected; break; @@ -697,6 +713,7 @@ struct TimetableWindow : Window { void UpdateSelectionStates() { this->GetWidget(WID_VT_ARRIVAL_DEPARTURE_SELECTION)->SetDisplayedPlane(_settings_client.gui.timetable_arrival_departure ? 0 : SZSP_NONE); + // this->GetWidget(TTV_AUTO_SELECTION)->SetDisplayedPlane(!_settings_game.order.timetable_automated ? 0 : 1); this->GetWidget(WID_VT_EXPECTED_SELECTION)->SetDisplayedPlane(_settings_client.gui.timetable_arrival_departure ? 0 : 1); } }; @@ -734,6 +751,7 @@ static const NWidgetPart _nested_timetable_widgets[] = { EndContainer(), NWidget(NWID_VERTICAL, NC_EQUALSIZE), NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_VT_AUTOFILL), SetResize(1, 0), SetFill(1, 1), SetDataTip(STR_TIMETABLE_AUTOFILL, STR_TIMETABLE_AUTOFILL_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_VT_AUTOMATE), SetResize(1, 0), SetFill(1, 1), SetDataTip(STR_TIMETABLE_AUTOMATE, STR_TIMETABLE_AUTOMATE_TOOLTIP), NWidget(NWID_SELECTION, INVALID_COLOUR, WID_VT_EXPECTED_SELECTION), NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_VT_EXPECTED), SetResize(1, 0), SetFill(1, 1), SetDataTip(STR_BLACK_STRING, STR_TIMETABLE_EXPECTED_TOOLTIP), NWidget(WWT_PANEL, COLOUR_GREY), SetResize(1, 0), SetFill(1, 1), EndContainer(), @@ -742,6 +760,7 @@ static const NWidgetPart _nested_timetable_widgets[] = { EndContainer(), NWidget(NWID_VERTICAL, NC_EQUALSIZE), NWidget(WWT_PUSHIMGBTN, COLOUR_GREY, WID_VT_SHARED_ORDER_LIST), SetFill(0, 1), SetDataTip(SPR_SHARED_ORDERS_ICON, STR_ORDERS_VEH_WITH_SHARED_ORDERS_LIST_TOOLTIP), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(1, 0), SetFill(1, 1), EndContainer(), NWidget(WWT_RESIZEBOX, COLOUR_GREY), SetFill(0, 1), EndContainer(), EndContainer(), diff --git a/src/vehicle.cpp b/src/vehicle.cpp index a00577fb8b..5854c04094 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -229,6 +229,11 @@ uint Vehicle::Crash(bool flooded) v->MarkAllViewportsDirty(); } + if (_settings_game.order.timetable_separation) { + this->ClearSeparation(); + ClrBit(this->vehicle_flags, VF_TIMETABLE_STARTED); + } + /* Dirty some windows */ InvalidateWindowClassesData(GetWindowClassForVehicleType(this->type), 0); SetWindowWidgetDirty(WC_VEHICLE_VIEW, this->index, WID_VV_START_STOP); @@ -2373,6 +2378,11 @@ void Vehicle::HandleLoading(bool mode) case OT_LOADING: { uint wait_time = max(this->current_order.GetTimetabledWait() - this->lateness_counter, 0); + /* Save time just loading took since that is what goes into the timetable */ + if (!HasBit(this->vehicle_flags, VF_LOADING_FINISHED)) { + this->current_loading_time = this->current_order_time; + } + /* 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; @@ -2477,6 +2487,10 @@ CommandCost Vehicle::SendToDepot(DoCommandFlag flags, DepotCommand command) if (flags & DC_EXEC) { if (!(this->current_order.GetDepotOrderType() & ODTFB_BREAKDOWN)) this->current_order.SetDepotOrderType(ODTF_MANUAL); this->current_order.SetDepotActionType(halt_in_depot ? ODATF_SERVICE_ONLY : ODATFB_HALT); + if (_settings_game.order.timetable_separation) { + this->ClearSeparation(); + ClrBit(this->vehicle_flags, VF_TIMETABLE_STARTED); + } SetWindowWidgetDirty(WC_VEHICLE_VIEW, this->index, WID_VV_START_STOP); } return CommandCost(); @@ -2497,6 +2511,11 @@ CommandCost Vehicle::SendToDepot(DoCommandFlag flags, DepotCommand command) if (this->current_order.GetDepotOrderType() & ODTFB_BREAKDOWN) { this->current_order.SetDepotActionType(this->current_order.GetDepotActionType() == ODATFB_HALT ? ODATF_SERVICE_ONLY : ODATFB_HALT); } else { + if (_settings_game.order.timetable_separation) { + this->ClearSeparation(); + ClrBit(this->vehicle_flags, VF_TIMETABLE_STARTED); + } + this->current_order.MakeDummy(); SetWindowWidgetDirty(WC_VEHICLE_VIEW, this->index, WID_VV_START_STOP); } @@ -2841,6 +2860,55 @@ void Vehicle::SetNext(Vehicle *next) } } +void Vehicle::ClearSeparation() +{ + if (this->ahead_separation == NULL && this->behind_separation == NULL) return; + + assert(this->ahead_separation != NULL); + assert(this->behind_separation != NULL); + + this->ahead_separation->behind_separation = this->behind_separation; + this->behind_separation->ahead_separation = this->ahead_separation; + + this->ahead_separation = NULL; + this->behind_separation = NULL; + + SetWindowDirty(WC_VEHICLE_TIMETABLE, this->index); +} + +void Vehicle::InitSeparation() +{ + assert(this->ahead_separation == NULL && this->behind_separation == NULL); + Vehicle *best_match = this; + int lowest_separation; + for (Vehicle *v_other = this->FirstShared(); v_other != NULL; v_other = v_other->NextShared()) { + if ((HasBit(v_other->vehicle_flags, VF_TIMETABLE_STARTED)) && v_other != this) { + if (best_match == this) { + best_match = v_other; + lowest_separation = 0; // TODO call SeparationBetween() here + } else { + int temp_sep = 0; // TODO call SeparationBetween() here + if (temp_sep < lowest_separation && temp_sep != -1) { + best_match = v_other; + lowest_separation = temp_sep; + } + } + } + } + this->AddToSeparationBehind(best_match); +} + +void Vehicle::AddToSeparationBehind(Vehicle *v_other) +{ + if (v_other->ahead_separation == NULL) v_other->ahead_separation = v_other; + if (v_other->behind_separation == NULL) v_other->behind_separation = v_other; + + this->ahead_separation = v_other; + v_other->behind_separation->ahead_separation = this; + this->behind_separation = v_other->behind_separation; + v_other->behind_separation = this; +} + /** * Adds this vehicle to a shared vehicle chain. * @param shared_chain a vehicle of the chain with shared vehicles. @@ -2898,6 +2966,9 @@ void Vehicle::RemoveFromShared() this->next_shared = NULL; this->previous_shared = NULL; + + if (_settings_game.order.timetable_separation) this->ClearSeparation(); + if (_settings_game.order.timetable_separation) ClrBit(this->vehicle_flags, VF_TIMETABLE_STARTED); } void VehiclesYearlyLoop() diff --git a/src/vehicle_base.h b/src/vehicle_base.h index f89234f254..756d635af4 100644 --- a/src/vehicle_base.h +++ b/src/vehicle_base.h @@ -51,6 +51,11 @@ enum VehicleFlags { VF_PATHFINDER_LOST, ///< Vehicle's pathfinder is lost. VF_SERVINT_IS_CUSTOM, ///< Service interval is custom. VF_SERVINT_IS_PERCENT, ///< Service interval is percent. + + // Additional flags not in trunk are added at the end to avoid clashing with any new + // flags which get added in future trunk, and to avoid re-ordering flags which are in trunk already, + // as this breaks savegame compatibility. + VF_AUTOMATE_TIMETABLE = 15, ///< Whether the vehicle should manage the timetable automatically. }; /** Bit numbers used to indicate which of the #NewGRFCache values are valid. */ @@ -164,6 +169,9 @@ private: Vehicle *next_shared; ///< pointer to the next vehicle that shares the order Vehicle *previous_shared; ///< NOSAVE: pointer to the previous vehicle in the shared order chain + Vehicle *ahead_separation; + Vehicle *behind_separation; + public: friend const SaveLoad *GetVehicleDescription(VehicleType vt); ///< So we can use private/protected variables in the saveload code friend void FixOldVehicles(); @@ -185,6 +193,13 @@ public: CargoPayment *cargo_payment; ///< The cargo payment we're currently in + + /* Used for timetabling. */ + uint32 current_order_time; ///< How many ticks have passed since this order started. + uint32 current_loading_time; ///< How long loading took. Less than current_order_time if vehicle is early. + int32 lateness_counter; ///< How many ticks late (or early if negative) this vehicle is. + Date timetable_start; ///< When the vehicle is supposed to start the timetable. + Rect coord; ///< NOSAVE: Graphical bounding box of the vehicle, i.e. what to redraw on moves. Vehicle *hash_viewport_next; ///< NOSAVE: Next vehicle in the visual location hash. @@ -601,6 +616,37 @@ public: */ inline Order *GetFirstOrder() const { return (this->orders.list == NULL) ? NULL : this->orders.list->GetFirstOrder(); } + /** + * Get the vehicle ahead on track. + * @return the vehicle ahead on track or NULL when there isn't one. + */ + inline Vehicle *AheadSeparation() const { return this->ahead_separation; } + + /** + * Get the vehicle behind on track. + * @return the vehicle behind on track or NULL when there isn't one. + */ + inline Vehicle *BehindSeparation() const { return this->behind_separation; } + + /** + * Clears a vehicle's separation status, removing it from any chain. + */ + void ClearSeparation(); + + /** + * Adds this vehicle to a shared vehicle separation chain. + * @param v_other a vehicle of the separation chain + * @pre !this->IsOrderListShared() + */ + void InitSeparation(); + + /** + * Adds this vehicle behind another in a separation chain. + * @param v_other a vehicle of the separation chain. + * @pre !this->IsOrderListShared() + */ + void AddToSeparationBehind(Vehicle *v_other); + void AddToShared(Vehicle *shared_chain); void RemoveFromShared(); @@ -668,6 +714,17 @@ public: this->profit_this_year = src->profit_this_year; this->profit_last_year = src->profit_last_year; + + this->current_order_time = src->current_order_time; + this->current_loading_time = src->current_loading_time; + this->lateness_counter = src->lateness_counter; + this->timetable_start = src->timetable_start; + + if (HasBit(src->vehicle_flags, VF_TIMETABLE_STARTED)) SetBit(this->vehicle_flags, VF_TIMETABLE_STARTED); + if (HasBit(src->vehicle_flags, VF_AUTOFILL_TIMETABLE)) SetBit(this->vehicle_flags, VF_AUTOFILL_TIMETABLE); + if (HasBit(src->vehicle_flags, VF_AUTOFILL_PRES_WAIT_TIME)) SetBit(this->vehicle_flags, VF_AUTOFILL_PRES_WAIT_TIME); + + this->service_interval = src->service_interval; } diff --git a/src/vehicle_cmd.cpp b/src/vehicle_cmd.cpp index 72cff271a4..93250da44f 100644 --- a/src/vehicle_cmd.cpp +++ b/src/vehicle_cmd.cpp @@ -569,6 +569,9 @@ CommandCost CmdStartStopVehicle(TileIndex tile, DoCommandFlag flags, uint32 p1, if (flags & DC_EXEC) { if (v->IsStoppedInDepot() && (flags & DC_AUTOREPLACE) == 0) DeleteVehicleNews(p1, STR_NEWS_TRAIN_IS_WAITING + v->type); + if (_settings_game.order.timetable_separation) v->ClearSeparation(); + if (_settings_game.order.timetable_separation) ClrBit(v->vehicle_flags, VF_TIMETABLE_STARTED); + v->vehstatus ^= VS_STOPPED; if (v->type != VEH_TRAIN) v->cur_speed = 0; // trains can stop 'slowly' v->MarkDirty(); diff --git a/src/widgets/timetable_widget.h b/src/widgets/timetable_widget.h index 09beb61672..d21e3c2b7e 100644 --- a/src/widgets/timetable_widget.h +++ b/src/widgets/timetable_widget.h @@ -25,9 +25,11 @@ enum VehicleTimetableWidgets { WID_VT_CLEAR_TIME, ///< Clear time button. WID_VT_RESET_LATENESS, ///< Reset lateness button. WID_VT_AUTOFILL, ///< Autofill button. + WID_VT_AUTOMATE, ///< Automate button. WID_VT_EXPECTED, ///< Toggle between expected and scheduled arrivals. WID_VT_SHARED_ORDER_LIST, ///< Show the shared order list. WID_VT_ARRIVAL_DEPARTURE_SELECTION, ///< Disable/hide the arrival departure panel. + WID_VT_AUTO_SELECTION, ///< Disable/hide the automate button. WID_VT_EXPECTED_SELECTION, ///< Disable/hide the expected selection button. WID_VT_CHANGE_SPEED, ///< Change speed limit button. WID_VT_CLEAR_SPEED, ///< Clear speed limit button.