diff --git a/src/depot.cpp b/src/depot.cpp index dfcdc5c1fd..542abc5f62 100644 --- a/src/depot.cpp +++ b/src/depot.cpp @@ -104,6 +104,7 @@ void Depot::AfterAddRemove(TileArea ta, bool adding) this->RescanDepotTiles(); assert(!this->depot_tiles.empty()); this->xy = this->depot_tiles[0]; + InvalidateWindowData(WC_SELECT_DEPOT, this->veh_type); } else { delete this; } diff --git a/src/depot_cmd.cpp b/src/depot_cmd.cpp index 5b29591616..32cd01558b 100644 --- a/src/depot_cmd.cpp +++ b/src/depot_cmd.cpp @@ -75,3 +75,69 @@ CommandCost CmdRenameDepot(DoCommandFlag flags, DepotID depot_id, const std::str } return CommandCost(); } + +/** + * Look for or check depot to join to, building a new one if necessary. + * @param ta The area of the new depot. + * @param veh_type The vehicle type of the new depot. + * @param join_to DepotID of the depot to join to. + * If INVALID_DEPOT, look whether it is possible to join to an existing depot. + * If NEW_DEPOT, directly create a new depot. + * @param depot The pointer to the depot. + * @param adjacent Whether adjacent depots are allowed + * @return command cost with the error or 'okay' + */ +CommandCost FindJoiningDepot(TileArea ta, VehicleType veh_type, DepotID &join_to, Depot *&depot, bool adjacent, DoCommandFlag flags) +{ + /* Look for a joining depot if needed. */ + if (join_to == INVALID_DEPOT) { + assert(depot == nullptr); + DepotID closest_depot = INVALID_DEPOT; + + TileArea check_area(ta); + check_area.Expand(1); + + /* Check around to see if there's any depot there. */ + for (TileIndex tile_cur : check_area) { + if (IsValidTile(tile_cur) && IsDepotTile(tile_cur)) { + Depot *d = Depot::GetByTile(tile_cur); + assert(d != nullptr); + if (d->veh_type != veh_type) continue; + if (d->owner != _current_company) continue; + + if (closest_depot == INVALID_DEPOT) { + closest_depot = d->index; + } else if (closest_depot != d->index) { + if (!adjacent) return_cmd_error(STR_ERROR_ADJOINS_MORE_THAN_ONE_EXISTING_DEPOT); + } + } + } + + if (closest_depot != INVALID_DEPOT) { + assert(Depot::IsValidID(closest_depot)); + depot = Depot::Get(closest_depot); + } + + join_to = depot == nullptr ? NEW_DEPOT : depot->index; + } + + /* At this point, join_to is NEW_DEPOT or a valid DepotID. */ + + if (join_to == NEW_DEPOT) { + /* New depot needed. */ + if (!Depot::CanAllocateItem()) return CMD_ERROR; + if (flags & DC_EXEC) { + depot = new Depot(ta.tile, veh_type, _current_company); + depot->build_date = TimerGameCalendar::date; + } + } else { + /* Joining depots. */ + assert(Depot::IsValidID(join_to)); + depot = Depot::Get(join_to); + assert(depot->owner == _current_company); + assert(depot->veh_type == veh_type); + return depot->BeforeAddTiles(ta); + } + + return CommandCost(); +} diff --git a/src/depot_func.h b/src/depot_func.h index 7b3b6a9eeb..d4d5ab69aa 100644 --- a/src/depot_func.h +++ b/src/depot_func.h @@ -10,8 +10,10 @@ #ifndef DEPOT_FUNC_H #define DEPOT_FUNC_H +#include "depot_type.h" #include "vehicle_type.h" #include "slope_func.h" +#include "command_type.h" void ShowDepotWindow(DepotID depot_id); void InitDepotWindowBlockSizes(); @@ -33,4 +35,10 @@ inline bool CanBuildDepotByTileh(DiagDirection direction, Slope tileh) return IsSteepSlope(tileh) ? (tileh & entrance_corners) == entrance_corners : (tileh & entrance_corners) != 0; } +struct Depot; +CommandCost FindJoiningDepot(TileArea ta, VehicleType veh_type, DepotID &join_to, Depot *&depot, bool adjacent, DoCommandFlag flags); + +using DepotPickerCmdProc = std::function; +void ShowSelectDepotIfNeeded(TileArea ta, DepotPickerCmdProc proc, VehicleType veh_type); + #endif /* DEPOT_FUNC_H */ diff --git a/src/depot_gui.cpp b/src/depot_gui.cpp index a33da66450..9055a39705 100644 --- a/src/depot_gui.cpp +++ b/src/depot_gui.cpp @@ -33,6 +33,7 @@ #include "train_cmd.h" #include "vehicle_cmd.h" #include "core/geometry_func.hpp" +#include "depot_func.h" #include "widgets/depot_widget.h" @@ -1167,3 +1168,243 @@ void DeleteDepotHighlightOfVehicle(const Vehicle *v) if (w->sel == v->index) ResetObjectToPlace(); } } + +static std::vector _depots_nearby_list; + +/** Structure with user-data for AddNearbyDepot. */ +struct AddNearbyDepotData { + TileArea search_area; ///< Search area. + VehicleType type; ///< Vehicle type of the searched depots. +}; + +/** + * Add depot on this tile to _depots_nearby_list if it's fully within the + * depot spread. + * @param tile Tile just being checked + * @param user_data Pointer to TileArea context + */ +static bool AddNearbyDepot(TileIndex tile, void *user_data) +{ + AddNearbyDepotData *andd = (AddNearbyDepotData *)user_data; + + /* Check if own depot and if we stay within station spread */ + if (!IsDepotTile(tile)) return false; + Depot *dep = Depot::GetByTile(tile); + if (dep->owner != _local_company || dep->veh_type != andd->type || + (find(_depots_nearby_list.begin(), _depots_nearby_list.end(), dep->index) != _depots_nearby_list.end())) { + return false; + } + + CommandCost cost = dep->BeforeAddTiles(andd->search_area); + if (cost.Succeeded()) { + _depots_nearby_list.push_back(dep->index); + } + + return false; +} + +/** + * Circulate around the to-be-built depot to find depots we could join. + * Make sure that only depots are returned where joining wouldn't exceed + * depot spread and are our own depot. + * @param ta Base tile area of the to-be-built depot + * @param veh_type Vehicle type depots to look for + * @param distant_join Search for adjacent depots (false) or depots fully + * within depot spread + */ +static const Depot *FindDepotsNearby(TileArea ta, VehicleType veh_type, bool distant_join) +{ + _depots_nearby_list.clear(); + _depots_nearby_list.push_back(NEW_DEPOT); + + /* Check the inside, to return, if we sit on another big depot */ + Depot *depot; + for (TileIndex t : ta) { + if (!IsDepotTile(t)) continue; + depot = Depot::GetByTile(t); + if (depot->veh_type == veh_type && depot->owner == _current_company) return depot; + } + + /* Only search tiles where we have a chance to stay within the depot spread. + * The complete check needs to be done in the callback as we don't know the + * extent of the found depot, yet. */ + if (distant_join && std::min(ta.w, ta.h) >= _settings_game.depot.depot_spread) return nullptr; + uint max_dist = distant_join ? _settings_game.depot.depot_spread - std::min(ta.w, ta.h) : 1; + + AddNearbyDepotData andd; + andd.search_area = ta; + andd.type = veh_type; + + TileIndex tile = TileAddByDir(andd.search_area.tile, DIR_N); + CircularTileSearch(&tile, max_dist, ta.w, ta.h, AddNearbyDepot, &andd); + + return nullptr; +} + +/** + * Check whether we need to show the depot selection window. + * @param ta Tile area of the to-be-built depot. + * @param proc The procedure for the depot picker. + * @param veh_type the vehicle type of the depot. + * @return whether we need to show the depot selection window. + */ +static bool DepotJoinerNeeded(TileArea ta, VehicleType veh_type) +{ + /* If a window is already opened and we didn't ctrl-click, + * return true (i.e. just flash the old window) */ + Window *selection_window = FindWindowById(WC_SELECT_DEPOT, veh_type); + if (selection_window != nullptr) { + /* Abort current distant-join and start new one */ + selection_window->Close(); + UpdateTileSelection(); + } + + /* Only show the popup if we press ctrl. */ + if (!_ctrl_pressed) return false; + + /* Test for adjacent depot or depot below selection. + * If adjacent-stations is disabled and we are building next to a depot, do not show the selection window. + * but join the other depot immediately. */ + return FindDepotsNearby(ta, veh_type, false) == nullptr; +} + +/** + * Window for selecting depots to (distant) join to. + */ +struct SelectDepotWindow : Window { + DepotPickerCmdProc select_depot_proc; ///< The procedure params + TileArea area; ///< Location of new depot + Scrollbar *vscroll; ///< Vertical scrollbar for the window + + SelectDepotWindow(WindowDesc &desc, TileArea ta, DepotPickerCmdProc& proc, VehicleType veh_type) : + Window(desc), + select_depot_proc(std::move(proc)), + area(ta) + { + this->CreateNestedTree(); + this->vscroll = this->GetScrollbar(WID_JD_SCROLLBAR); + this->FinishInitNested(veh_type); + this->OnInvalidateData(0); + } + + void UpdateWidgetSize(int widget, Dimension &size, const Dimension &padding, [[maybe_unused]] Dimension &fill, Dimension &resize) override + { + if (widget != WID_JD_PANEL) return; + + resize.height = GetCharacterHeight(FS_NORMAL); + size.height = 5 * resize.height + padding.height; + + /* Determine the widest string. */ + Dimension d = GetStringBoundingBox(STR_JOIN_DEPOT_CREATE_SPLITTED_DEPOT); + for (const auto &depot : _depots_nearby_list) { + if (depot == NEW_DEPOT) continue; + const Depot *dep = Depot::Get(depot); + SetDParam(0, this->window_number); + SetDParam(1, dep->index); + d = maxdim(d, GetStringBoundingBox(STR_DEPOT_LIST_DEPOT)); + } + + d.height = 5 * resize.height; + d.width += padding.width; + d.height += padding.height; + size = d; + } + + void DrawWidget(const Rect &r, int widget) const override + { + if (widget != WID_JD_PANEL) return; + + Rect tr = r.Shrink(WidgetDimensions::scaled.framerect); + + auto [first, last] = this->vscroll->GetVisibleRangeIterators(_depots_nearby_list); + for (auto it = first; it != last; ++it, tr.top += this->resize.step_height) { + if (*it == NEW_DEPOT) { + DrawString(tr, STR_JOIN_DEPOT_CREATE_SPLITTED_DEPOT); + } else { + SetDParam(0, this->window_number); + SetDParam(1, *it); + [[maybe_unused]] Depot *depot = Depot::GetIfValid(*it); + assert(depot != nullptr); + DrawString(tr, STR_DEPOT_LIST_DEPOT); + } + } + } + + void OnClick(Point pt, int widget, [[maybe_unused]] int click_count) override + { + if (widget != WID_JD_PANEL) return; + + auto it = this->vscroll->GetScrolledItemFromWidget(_depots_nearby_list, pt.y, this, WID_JD_PANEL, WidgetDimensions::scaled.framerect.top); + if (it == _depots_nearby_list.end()) return; + + /* Execute stored Command */ + this->select_depot_proc(*it); + + InvalidateWindowData(WC_SELECT_DEPOT, window_number); + this->Close(); + } + + void OnRealtimeTick([[maybe_unused]] uint delta_ms) override + { + if (_thd.dirty & 2) { + _thd.dirty &= ~2; + this->SetDirty(); + } + } + + void OnResize() override + { + this->vscroll->SetCapacityFromWidget(this, WID_JD_PANEL, WidgetDimensions::scaled.framerect.Vertical()); + } + + /** + * Some data on this window has become invalid. + * @param data Information about the changed data. + * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details. + */ + void OnInvalidateData([[maybe_unused]] int data = 0, bool gui_scope = true) override + { + if (!gui_scope) return; + FindDepotsNearby(this->area, (VehicleType)this->window_number, true); + this->vscroll->SetCount((uint)_depots_nearby_list.size()); + this->SetDirty(); + } +}; + +static const NWidgetPart _nested_select_depot_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN), + NWidget(WWT_CAPTION, COLOUR_DARK_GREEN, WID_JD_CAPTION), SetDataTip(STR_JOIN_DEPOT_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_DEFSIZEBOX, COLOUR_DARK_GREEN), + EndContainer(), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PANEL, COLOUR_DARK_GREEN, WID_JD_PANEL), SetResize(1, 0), SetScrollbar(WID_JD_SCROLLBAR), EndContainer(), + NWidget(NWID_VERTICAL), + NWidget(NWID_VSCROLLBAR, COLOUR_DARK_GREEN, WID_JD_SCROLLBAR), + NWidget(WWT_RESIZEBOX, COLOUR_DARK_GREEN), + EndContainer(), + EndContainer(), +}; + +static WindowDesc _select_depot_desc( + WDP_AUTO, "build_depot_join", 200, 180, + WC_SELECT_DEPOT, WC_NONE, + WDF_CONSTRUCTION, + _nested_select_depot_widgets +); + +/** + * Show the depot selection window when needed. If not, build the depot. + * @param ta Area to build the depot in. + * @param proc Details of the procedure for the depot picker. + * @param veh_type Vehicle type of the depot to be built. + */ +void ShowSelectDepotIfNeeded(TileArea ta, DepotPickerCmdProc proc, VehicleType veh_type) +{ + if (DepotJoinerNeeded(ta, veh_type)) { + if (!_settings_client.gui.persistent_buildingtools) ResetObjectToPlace(); + new SelectDepotWindow(_select_depot_desc, ta, proc, veh_type); + } else { + proc(INVALID_DEPOT); + } +} diff --git a/src/depot_type.h b/src/depot_type.h index 4eaf93473c..a03a33b4f8 100644 --- a/src/depot_type.h +++ b/src/depot_type.h @@ -14,6 +14,7 @@ typedef uint16_t DepotID; ///< Type for the unique identifier of depots. struct Depot; static const DepotID INVALID_DEPOT = UINT16_MAX; +static const DepotID NEW_DEPOT = INVALID_DEPOT - 1; static const uint MAX_LENGTH_DEPOT_NAME_CHARS = 32; ///< The maximum length of a depot name in characters including '\0' diff --git a/src/dock_gui.cpp b/src/dock_gui.cpp index ffbf5aa63e..ccf35c3e44 100644 --- a/src/dock_gui.cpp +++ b/src/dock_gui.cpp @@ -275,6 +275,7 @@ struct BuildDocksToolbarWindow : Window { CloseWindowById(WC_BUILD_STATION, TRANSPORT_WATER); CloseWindowById(WC_BUILD_DEPOT, TRANSPORT_WATER); CloseWindowById(WC_SELECT_STATION, 0); + CloseWindowById(WC_SELECT_DEPOT, VEH_SHIP); CloseWindowByClass(WC_BUILD_BRIDGE); } @@ -529,6 +530,12 @@ public: UpdateDocksDirection(); } + void Close([[maybe_unused]] int data = 0) override + { + CloseWindowById(WC_SELECT_DEPOT, VEH_SHIP); + this->PickerWindowBase::Close(); + } + void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override { switch (widget) { @@ -569,6 +576,7 @@ public: switch (widget) { case WID_BDD_X: case WID_BDD_Y: + CloseWindowById(WC_SELECT_DEPOT, VEH_SHIP); this->RaiseWidget(WID_BDD_X + _ship_depot_direction); _ship_depot_direction = (widget == WID_BDD_X ? AXIS_X : AXIS_Y); this->LowerWidget(WID_BDD_X + _ship_depot_direction); diff --git a/src/lang/english.txt b/src/lang/english.txt index fd806beb75..d3d14db942 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2769,6 +2769,11 @@ STR_JOIN_STATION_CREATE_SPLITTED_STATION :{YELLOW}Build a STR_JOIN_WAYPOINT_CAPTION :{WHITE}Join waypoint STR_JOIN_WAYPOINT_CREATE_SPLITTED_WAYPOINT :{YELLOW}Build a separate waypoint +# Join depot window +STR_JOIN_DEPOT_CAPTION :{WHITE}Join depot +STR_JOIN_DEPOT_CREATE_SPLITTED_DEPOT :{YELLOW}Build a separate depot +STR_DEPOT_LIST_DEPOT :{YELLOW}{DEPOT} + # Generic toolbar STR_TOOLBAR_DISABLED_NO_VEHICLE_AVAILABLE :{BLACK}Disabled as currently no vehicles are available for this infrastructure @@ -5120,6 +5125,7 @@ STR_ERROR_CAN_T_BUILD_ROAD_DEPOT :{WHITE}Can't bu STR_ERROR_CAN_T_BUILD_TRAM_DEPOT :{WHITE}Can't build tram vehicle depot here... STR_ERROR_CAN_T_BUILD_SHIP_DEPOT :{WHITE}Can't build ship depot here... +STR_ERROR_ADJOINS_MORE_THAN_ONE_EXISTING_DEPOT :{WHITE}... adjoins more than one existing depot STR_ERROR_CAN_T_RENAME_DEPOT :{WHITE}Can't rename depot... STR_ERROR_TRAIN_MUST_BE_STOPPED_INSIDE_DEPOT :{WHITE}... must be stopped inside a depot diff --git a/src/rail_gui.cpp b/src/rail_gui.cpp index 06819e4c3d..016ad51c59 100644 --- a/src/rail_gui.cpp +++ b/src/rail_gui.cpp @@ -804,6 +804,7 @@ struct BuildRailToolbarWindow : Window { CloseWindowById(WC_BUILD_DEPOT, TRANSPORT_RAIL); CloseWindowById(WC_BUILD_WAYPOINT, TRANSPORT_RAIL); CloseWindowById(WC_SELECT_STATION, 0); + CloseWindowById(WC_SELECT_DEPOT, VEH_TRAIN); CloseWindowByClass(WC_BUILD_BRIDGE); } @@ -1705,6 +1706,12 @@ struct BuildRailDepotWindow : public PickerWindowBase { this->LowerWidget(WID_BRAD_DEPOT_NE + _build_depot_direction); } + void Close([[maybe_unused]] int data = 0) override + { + CloseWindowById(WC_SELECT_DEPOT, VEH_TRAIN); + this->PickerWindowBase::Close(); + } + void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override { if (!IsInsideMM(widget, WID_BRAD_DEPOT_NE, WID_BRAD_DEPOT_NW + 1)) return; @@ -1734,6 +1741,7 @@ struct BuildRailDepotWindow : public PickerWindowBase { case WID_BRAD_DEPOT_SE: case WID_BRAD_DEPOT_SW: case WID_BRAD_DEPOT_NW: + CloseWindowById(WC_SELECT_DEPOT, VEH_TRAIN); this->RaiseWidget(WID_BRAD_DEPOT_NE + _build_depot_direction); _build_depot_direction = (DiagDirection)(widget - WID_BRAD_DEPOT_NE); this->LowerWidget(WID_BRAD_DEPOT_NE + _build_depot_direction); diff --git a/src/road_gui.cpp b/src/road_gui.cpp index 27066a2144..3f9b9251cf 100644 --- a/src/road_gui.cpp +++ b/src/road_gui.cpp @@ -687,6 +687,7 @@ struct BuildRoadToolbarWindow : Window { CloseWindowById(WC_BUILD_DEPOT, TRANSPORT_ROAD); CloseWindowById(WC_BUILD_WAYPOINT, TRANSPORT_ROAD); CloseWindowById(WC_SELECT_STATION, 0); + CloseWindowById(WC_SELECT_DEPOT, VEH_ROAD); CloseWindowByClass(WC_BUILD_BRIDGE); } @@ -1106,6 +1107,12 @@ struct BuildRoadDepotWindow : public PickerWindowBase { this->FinishInitNested(TRANSPORT_ROAD); } + void Close([[maybe_unused]] int data = 0) override + { + CloseWindowById(WC_SELECT_DEPOT, VEH_ROAD); + this->PickerWindowBase::Close(); + } + void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override { if (!IsInsideMM(widget, WID_BROD_DEPOT_NE, WID_BROD_DEPOT_NW + 1)) return; @@ -1135,6 +1142,7 @@ struct BuildRoadDepotWindow : public PickerWindowBase { case WID_BROD_DEPOT_NE: case WID_BROD_DEPOT_SW: case WID_BROD_DEPOT_SE: + CloseWindowById(WC_SELECT_DEPOT, VEH_ROAD); this->RaiseWidget(WID_BROD_DEPOT_NE + _road_depot_orientation); _road_depot_orientation = (DiagDirection)(widget - WID_BROD_DEPOT_NE); this->LowerWidget(WID_BROD_DEPOT_NE + _road_depot_orientation); diff --git a/src/table/settings/game_settings.ini b/src/table/settings/game_settings.ini index 6426080857..286c517fee 100644 --- a/src/table/settings/game_settings.ini +++ b/src/table/settings/game_settings.ini @@ -151,6 +151,7 @@ from = SLV_DEPOT_SPREAD def = true str = STR_CONFIG_SETTING_DISTANT_JOIN_DEPOTS strhelp = STR_CONFIG_SETTING_DISTANT_JOIN_DEPOTS_HELPTEXT +post_cb = [](auto) { CloseWindowByClass(WC_SELECT_DEPOT); } [SDT_VAR] var = depot.depot_spread @@ -162,6 +163,7 @@ max = 64 str = STR_CONFIG_SETTING_DEPOT_SPREAD strhelp = STR_CONFIG_SETTING_DEPOT_SPREAD_HELPTEXT strval = STR_CONFIG_SETTING_TILE_LENGTH +post_cb = [](auto) { CloseWindowByClass(WC_SELECT_DEPOT); } cat = SC_BASIC [SDT_OMANY] diff --git a/src/widgets/depot_widget.h b/src/widgets/depot_widget.h index f94f1263d2..5c98203a03 100644 --- a/src/widgets/depot_widget.h +++ b/src/widgets/depot_widget.h @@ -32,4 +32,11 @@ enum DepotWidgets : WidgetID { WID_D_START_ALL, ///< Start all button. }; +/** Widgets of the #SelectDepotWindow class. */ +enum JoinDepotWidgets { + WID_JD_CAPTION, // Caption of the window. + WID_JD_PANEL, // Main panel. + WID_JD_SCROLLBAR, // Scrollbar of the panel. +}; + #endif /* WIDGETS_DEPOT_WIDGET_H */ diff --git a/src/window_type.h b/src/window_type.h index 4437d26b3f..5ec2d3c332 100644 --- a/src/window_type.h +++ b/src/window_type.h @@ -241,6 +241,12 @@ enum WindowClass { */ WC_SELECT_STATION, + /** + * Select depot (when joining depots); %Window numbers: + * - #Vehicle type = #JoinDepotWidgets + */ + WC_SELECT_DEPOT, + /** * News window; %Window numbers: * - 0 = #NewsWidgets