diff --git a/src/command.cpp b/src/command.cpp index b4355e826f..3bf3f31503 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -53,6 +53,7 @@ CommandProc CmdBuildBridge; CommandProcEx CmdBuildRailStation; CommandProc CmdRemoveFromRailStation; CommandProc CmdConvertRail; +CommandProc CmdConvertRailTrack; CommandProc CmdBuildSingleSignal; CommandProc CmdRemoveSingleSignal; @@ -334,6 +335,7 @@ static const Command _command_proc_table[] = { DEF_CMD(CmdBuildTunnel, CMD_DEITY | CMD_AUTO, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_BUILD_TUNNEL DEF_CMD(CmdRemoveFromRailStation, 0, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_REMOVE_FROM_RAIL_STATION DEF_CMD(CmdConvertRail, 0, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_CONVERT_RAIL + DEF_CMD(CmdConvertRailTrack, 0, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_CONVERT_RAIL_TRACK DEF_CMD(CmdBuildRailWaypoint, 0, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_BUILD_RAIL_WAYPOINT DEF_CMD(CmdBuildRoadWaypoint, 0, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_BUILD_ROAD_WAYPOINT DEF_CMD(CmdRenameWaypoint, 0, CMDT_OTHER_MANAGEMENT ), // CMD_RENAME_WAYPOINT diff --git a/src/command_type.h b/src/command_type.h index 4badfd358f..307350e227 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -296,6 +296,7 @@ enum Commands { CMD_REMOVE_FROM_RAIL_STATION, ///< remove a (rectangle of) tiles from a rail station CMD_CONVERT_RAIL, ///< convert a rail type + CMD_CONVERT_RAIL_TRACK, ///< convert a rail type (track) CMD_BUILD_RAIL_WAYPOINT, ///< build a waypoint CMD_BUILD_ROAD_WAYPOINT, ///< build a road waypoint diff --git a/src/lang/extra/english.txt b/src/lang/extra/english.txt index d483e20477..9f8275ff5c 100644 --- a/src/lang/extra/english.txt +++ b/src/lang/extra/english.txt @@ -2068,3 +2068,5 @@ STR_STATION_VIEW_RENAME_TOOLTIP_EXTRA :{BLACK}{STRING} STR_ERROR_CAN_T_EXCHANGE_STATION_NAMES :{WHITE}Can't exchange station names... STR_ERROR_STATIONS_NOT_IN_SAME_TOWN :{WHITE}... stations are not in the same town STR_ERROR_STATION_ATTACHED_TO_INDUSTRY :{WHITE}... station is attached to an industry + +STR_RAIL_TOOLBAR_TOOLTIP_CONVERT_RAIL_EXTRA :{STRING}{}{BLACK}Ctrl+Click to convert track pieces instead of whole tiles. diff --git a/src/rail_cmd.cpp b/src/rail_cmd.cpp index 8d2604914f..6aef0b62d1 100644 --- a/src/rail_cmd.cpp +++ b/src/rail_cmd.cpp @@ -2323,6 +2323,36 @@ static Vehicle *UpdateTrainPowerProc(Vehicle *v, void *data) return nullptr; } +struct UpdateTrainPowerProcData { + TrainList *train_list; + TrackBits track_bits; +}; + +/** Update power of train under which is the railtype being converted */ +static Vehicle *UpdateTrainPowerProcAcrossTunnelBridge(Vehicle *v, void *data) +{ + UpdateTrainPowerProcData *utpp_data = static_cast(data); + + TrackBits vehicle_track = Train::From(v)->track; + if (!(vehicle_track & TRACK_BIT_WORMHOLE) && !(utpp_data->track_bits & vehicle_track)) return nullptr; + + include(*(utpp_data->train_list), Train::From(v)->First()); + + return nullptr; +} + +/** Update power of train under which is the railtype being converted */ +static Vehicle *UpdateTrainPowerProcOnTrackBits(Vehicle *v, void *data) +{ + UpdateTrainPowerProcData *utpp_data = static_cast(data); + + if (!(utpp_data->track_bits & Train::From(v)->track)) return nullptr; + + include(*(utpp_data->train_list), Train::From(v)->First()); + + return nullptr; +} + struct EnsureNoIncompatibleRailtypeTrainOnGroundData { int z; RailType type; @@ -2659,6 +2689,320 @@ CommandCost CmdConvertRail(TileIndex tile, DoCommandFlag flags, uint32 p1, uint3 return found_convertible_track ? cost : error; } +/** + * Convert rail on a stretch of track. + * @param tile start tile of drag + * @param flags operation to perform + * @param p1 end tile of drag + * @param p2 various bitstuffed elements + * - p2 = (bit 0-5) - railroad type normal/maglev (0 = normal, 1 = mono, 2 = maglev) + * - p2 = (bit 6-8) - track-orientation, valid values: 0-5 (Track enum) + * @param text unused + * @return the cost of this operation or an error + */ +CommandCost CmdConvertRailTrack(TileIndex tile, DoCommandFlag flags, uint32 p1, uint32 p2, const char *text) +{ + const RailType totype = Extract(p2); + const TileIndex end_tile = p1; + + if (!ValParamRailtype(totype)) return CMD_ERROR; + if (end_tile >= MapSize()) return CMD_ERROR; + + const Track start_track = Extract(p2); + Trackdir trackdir = TrackToTrackdir(start_track); + + CommandCost ret = ValidateAutoDrag(&trackdir, tile, end_tile); + if (ret.Failed()) return ret; + + TrainList affected_trains; + + CommandCost cost(EXPENSES_CONSTRUCTION); + CommandCost error = CommandCost(STR_ERROR_NO_SUITABLE_RAILROAD_TRACK); // by default, there is no track to convert. + bool found_convertible_track = false; // whether we actually did convert some track (see bug #7633) + + std::vector exclude_tiles; + + auto advance_tile = [&]() -> bool { + if (tile == end_tile) return false; + + tile += ToTileIndexDiff(_trackdelta[trackdir]); + + /* toggle railbit for the non-diagonal tracks */ + if (!IsDiagonalTrackdir(trackdir)) ToggleBit(trackdir, 0); + return true; + }; + do { + if (std::find(exclude_tiles.begin(), exclude_tiles.end(), tile) != exclude_tiles.end()) continue; + + const Track track = TrackdirToTrack(trackdir); + const TileType tt = GetTileType(tile); + + TrackBits all_track_bits = TRACK_BIT_NONE; + + /* Check if our track piece matches any track on tile */ + switch (tt) { + case MP_RAILWAY: + if (IsPlainRail(tile)) { + if (!HasTrack(tile, track)) continue; + all_track_bits = GetTrackBits(tile); + } else if (IsRailDepot(tile)) { + if (GetRailDepotTrack(tile) != track) continue; + all_track_bits = TrackToTrackBits(track); + } else { + continue; + } + break; + case MP_STATION: + if (!HasStationRail(tile) || GetRailStationTrack(tile) != track) continue; + all_track_bits = GetRailStationTrackBits(tile); + break; + case MP_ROAD: + if (!IsLevelCrossing(tile) || GetCrossingRailTrack(tile) != track) continue; + if (RailNoLevelCrossings(totype)) { + error.MakeError(STR_ERROR_CROSSING_DISALLOWED_RAIL); + continue; + } + all_track_bits = GetCrossingRailBits(tile); + break; + case MP_TUNNELBRIDGE: + if (GetTunnelBridgeTransportType(tile) != TRANSPORT_RAIL || !HasBit(GetTunnelBridgeTrackBits(tile), track)) continue; + all_track_bits = GetTunnelBridgeTrackBits(tile); + break; + default: continue; + } + + /* Original railtype we are converting from */ + const RailType type = GetRailTypeByTrack(tile, track); + + /* Converting to the same type or converting 'hidden' elrail -> rail */ + if (type == totype || (_settings_game.vehicle.disable_elrails && totype == RAILTYPE_RAIL && type == RAILTYPE_ELECTRIC)) continue; + + /* Trying to convert other's rail */ + CommandCost ret = CheckTileOwnership(tile); + if (ret.Failed()) { + error = ret; + continue; + } + + /* Track bits on the tile to convert */ + const TrackBits track_bits = (all_track_bits == TRACK_BIT_HORZ || all_track_bits == TRACK_BIT_VERT) ? TrackToTrackBits(track) : all_track_bits; + + std::vector vehicles_affected; + + auto find_train_reservations = [&vehicles_affected, &totype, &flags](TileIndex tile, TrackBits reserved) -> CommandCost { + if (!(flags & DC_EXEC) && _settings_game.vehicle.train_braking_model != TBM_REALISTIC) { + /* Nothing to do */ + return CommandCost(); + } + Track track; + while ((track = RemoveFirstTrack(&reserved)) != INVALID_TRACK) { + Train *v = GetTrainForReservation(tile, track); + bool check_train = false; + if (v != nullptr && !HasPowerOnRail(v->railtype, totype)) { + check_train = true; + } else if (v != nullptr && _settings_game.vehicle.train_braking_model == TBM_REALISTIC) { + RailType original = GetRailTypeByTrack(tile, track); + if ((uint)(GetRailTypeInfo(original)->max_speed - 1) > (uint)(GetRailTypeInfo(totype)->max_speed - 1)) { + check_train = true; + } + } + if (check_train) { + CommandCost ret = CheckTrainReservationPreventsTrackModification(v); + if (ret.Failed()) return ret; + + /* No power on new rail type, reroute. */ + if (flags & DC_EXEC) { + FreeTrainTrackReservation(v); + vehicles_affected.push_back(v); + } + } + } + return CommandCost(); + }; + + auto yapf_notify_track_change = [](TileIndex tile, TrackBits tracks) { + while (tracks != TRACK_BIT_NONE) { + YapfNotifyTrackLayoutChange(tile, RemoveFirstTrack(&tracks)); + } + }; + + /* Vehicle on the tile when not converting Rail <-> ElRail + * Tunnels and bridges have special check later */ + if (tt != MP_TUNNELBRIDGE) { + if (!IsCompatibleRail(type, totype)) { + CommandCost ret = IsPlainRailTile(tile) ? EnsureNoIncompatibleRailtypeTrainOnTrackBits(tile, track_bits, totype) : EnsureNoIncompatibleRailtypeTrainOnGround(tile, totype); + if (ret.Failed()) { + error = ret; + continue; + } + } + CommandCost ret = find_train_reservations(tile, GetReservedTrackbits(tile) & track_bits); + if (ret.Failed()) return ret; + if (flags & DC_EXEC) { // we can safely convert, too + /* Update the company infrastructure counters. */ + if (!IsRailStationTile(tile) || !IsStationTileBlocked(tile)) { + Company *c = Company::Get(GetTileOwner(tile)); + uint num_pieces = IsLevelCrossingTile(tile) ? LEVELCROSSING_TRACKBIT_FACTOR : 1; + if (IsPlainRailTile(tile)) { + num_pieces = CountBits(track_bits); + if (TracksOverlap(track_bits)) num_pieces *= num_pieces; + } + c->infrastructure.rail[type] -= num_pieces; + c->infrastructure.rail[totype] += num_pieces; + DirtyCompanyInfrastructureWindows(c->index); + } + + if (track_bits != all_track_bits) { + /* only partially converting the tile */ + if (track_bits & TRACK_BIT_RT_1) { + SetRailType(tile, totype); + } else { + SetSecondaryRailType(tile, totype); + } + } else { + SetRailType(tile, totype); + if (IsPlainRailTile(tile)) SetSecondaryRailType(tile, totype); + } + + MarkTileDirtyByTile(tile); + /* update power of train on this tile */ + UpdateTrainPowerProcData data; + data.train_list = &affected_trains; + data.track_bits = track_bits; + FindVehicleOnPos(tile, VEH_TRAIN, &data, &UpdateTrainPowerProcOnTrackBits); + } + } + + switch (tt) { + case MP_RAILWAY: + switch (GetRailTileType(tile)) { + case RAIL_TILE_DEPOT: + if (flags & DC_EXEC) { + /* notify YAPF about the track layout change */ + YapfNotifyTrackLayoutChange(tile, GetRailDepotTrack(tile)); + + /* Update build vehicle window related to this depot */ + InvalidateWindowData(WC_VEHICLE_DEPOT, tile); + InvalidateWindowData(WC_BUILD_VEHICLE, tile); + } + found_convertible_track = true; + cost.AddCost(RailConvertCost(type, totype)); + break; + + default: // RAIL_TILE_NORMAL, RAIL_TILE_SIGNALS + if (flags & DC_EXEC) { + /* notify YAPF about the track layout change */ + yapf_notify_track_change(tile, track_bits); + } + found_convertible_track = true; + cost.AddCost(RailConvertCost(type, totype) * CountBits(track_bits)); + break; + } + break; + + case MP_TUNNELBRIDGE: { + TileIndex endtile = GetOtherTunnelBridgeEnd(tile); + + const bool across = (GetAcrossTunnelBridgeTrackBits(tile) & track_bits) != TRACK_BIT_NONE; + if (across) exclude_tiles.push_back(endtile); + + /* When not converting rail <-> el. rail, any vehicle cannot be in tunnel/bridge */ + if (!IsCompatibleRail(type, totype)) { + CommandCost ret; + if (across) { + ret = TunnelBridgeIsFree(tile, endtile, nullptr, TBIFM_PRIMARY_ONLY); + } else { + ret = EnsureNoIncompatibleRailtypeTrainOnTrackBits(tile, track_bits, totype); + } + if (ret.Failed()) { + error = ret; + continue; + } + } + + found_convertible_track = true; + + if (across) { + uint num_primary_pieces = GetTunnelBridgeLength(tile, endtile) + CountBits(GetPrimaryTunnelBridgeTrackBits(tile)) + CountBits(GetPrimaryTunnelBridgeTrackBits(endtile)); + cost.AddCost(num_primary_pieces * RailConvertCost(type, totype)); + } else { + cost.AddCost(RailConvertCost(type, totype)); + } + + CommandCost ret = find_train_reservations(tile, GetTunnelBridgeReservationTrackBits(tile) & track_bits); + if (ret.Failed()) return ret; + if (across) { + ret = find_train_reservations(endtile, GetTunnelBridgeReservationTrackBits(endtile) & GetPrimaryTunnelBridgeTrackBits(endtile)); + if (ret.Failed()) return ret; + } + if (across && (uint)(GetRailTypeInfo(type)->max_speed - 1) > (uint)(GetRailTypeInfo(totype)->max_speed - 1)) { + ret = CheckTrainInTunnelBridgePreventsTrackModification(tile, endtile); + if (ret.Failed()) return ret; + } + + if (flags & DC_EXEC) { + SubtractRailTunnelBridgeInfrastructure(tile, endtile); + + if (across) { + SetRailType(tile, totype); + SetRailType(endtile, totype); + } else { + SetSecondaryRailType(tile, totype); + } + + UpdateTrainPowerProcData data; + data.train_list = &affected_trains; + data.track_bits = track_bits; + if (across) { + FindVehicleOnPos(tile, VEH_TRAIN, &data, &UpdateTrainPowerProcAcrossTunnelBridge); + data.track_bits = GetPrimaryTunnelBridgeTrackBits(endtile); + FindVehicleOnPos(endtile, VEH_TRAIN, &data, &UpdateTrainPowerProcAcrossTunnelBridge); + } else { + FindVehicleOnPos(tile, VEH_TRAIN, &data, &UpdateTrainPowerProcOnTrackBits); + } + + /* notify YAPF about the track layout change */ + yapf_notify_track_change(tile, track_bits); + if (across) yapf_notify_track_change(endtile, GetPrimaryTunnelBridgeTrackBits(endtile)); + + if (IsBridge(tile)) { + MarkBridgeDirty(tile); + } else { + MarkTileDirtyByTile(tile); + MarkTileDirtyByTile(endtile); + } + + AddRailTunnelBridgeInfrastructure(tile, endtile); + DirtyCompanyInfrastructureWindows(Company::Get(GetTileOwner(tile))->index); + } + break; + } + + default: // MP_STATION, MP_ROAD + if (flags & DC_EXEC) { + YapfNotifyTrackLayoutChange(tile, track); + } + + found_convertible_track = true; + cost.AddCost(RailConvertCost(type, totype)); + break; + } + + for (uint i = 0; i < vehicles_affected.size(); ++i) { + TryPathReserve(vehicles_affected[i], true); + } + } while (advance_tile()); + + if (flags & DC_EXEC) { + /* Railtype changed, update trains as when entering different track */ + for (Train *v : affected_trains) { + v->ConsistChanged(CCF_TRACK); + } + } + + return found_convertible_track ? cost : error; +} + static CommandCost RemoveTrainDepot(TileIndex tile, DoCommandFlag flags) { if (_current_company != OWNER_WATER) { diff --git a/src/rail_gui.cpp b/src/rail_gui.cpp index e590cd9632..1ce01014c5 100644 --- a/src/rail_gui.cpp +++ b/src/rail_gui.cpp @@ -726,10 +726,12 @@ struct BuildRailToolbarWindow : Window { BuildRailClick_Remove(this); break; - case WID_RAT_CONVERT_RAIL: - HandlePlacePushButton(this, WID_RAT_CONVERT_RAIL, GetRailTypeInfo(_cur_railtype)->cursor.convert, HT_RECT | HT_DIAGONAL); + case WID_RAT_CONVERT_RAIL: { + bool active = HandlePlacePushButton(this, WID_RAT_CONVERT_RAIL, GetRailTypeInfo(_cur_railtype)->cursor.convert, _ctrl_pressed ? HT_RAIL : HT_RECT | HT_DIAGONAL); + if (active && _ctrl_pressed) _thd.square_palette = SPR_ZONING_INNER_HIGHLIGHT_GREEN; this->last_user_action = widget; break; + } default: NOT_REACHED(); } @@ -737,6 +739,14 @@ struct BuildRailToolbarWindow : Window { if (_ctrl_pressed) RailToolbar_CtrlChanged(this); } + virtual void OnHover(Point pt, int widget) override + { + if (widget == WID_RAT_CONVERT_RAIL) { + uint64 args[] = { STR_RAIL_TOOLBAR_TOOLTIP_CONVERT_RAIL }; + GuiShowTooltips(this, STR_RAIL_TOOLBAR_TOOLTIP_CONVERT_RAIL_EXTRA, lengthof(args), args, TCC_HOVER); + } + } + EventState OnHotkey(int hotkey) override { MarkTileDirtyByTile(TileVirtXY(_thd.pos.x, _thd.pos.y)); // redraw tile selection @@ -806,7 +816,11 @@ struct BuildRailToolbarWindow : Window { break; case WID_RAT_CONVERT_RAIL: - VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_CONVERT_RAIL); + if (_thd.place_mode & HT_RAIL) { + VpStartPlaceSizing(tile, VPM_RAILDIRS, DDSP_CONVERT_RAIL_TRACK); + } else { + VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_CONVERT_RAIL); + } break; default: NOT_REACHED(); @@ -847,6 +861,14 @@ struct BuildRailToolbarWindow : Window { DoCommandP(end_tile, start_tile, _cur_railtype | (_ctrl_pressed ? (1 << 6) : 0), CMD_CONVERT_RAIL | CMD_MSG(STR_ERROR_CAN_T_CONVERT_RAIL), CcPlaySound_CONSTRUCTION_RAIL); break; + case DDSP_CONVERT_RAIL_TRACK: { + Track track = (Track)(_thd.drawstyle & HT_DIR_MASK); // 0..5 + TileIndex start_tile = TileVirtXY(_thd.selstart.x, _thd.selstart.y); + TileIndex end_tile = TileVirtXY(_thd.selend.x, _thd.selend.y); + DoCommandP((_thd.drawstyle & HT_RAIL) ? end_tile : start_tile, end_tile, _cur_railtype | (track << 6), CMD_CONVERT_RAIL_TRACK | CMD_MSG(STR_ERROR_CAN_T_CONVERT_RAIL), CcPlaySound_CONSTRUCTION_RAIL); + break; + } + case DDSP_REMOVE_STATION: case DDSP_BUILD_STATION: if (this->IsWidgetLowered(WID_RAT_BUILD_STATION)) { @@ -986,7 +1008,7 @@ static const NWidgetPart _nested_build_rail_widgets[] = { NWidget(WWT_IMGBTN, COLOUR_DARK_GREEN, WID_RAT_REMOVE), SetFill(0, 1), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_REMOVE, STR_RAIL_TOOLBAR_TOOLTIP_TOGGLE_BUILD_REMOVE_FOR), NWidget(WWT_IMGBTN, COLOUR_DARK_GREEN, WID_RAT_CONVERT_RAIL), - SetFill(0, 1), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_CONVERT_RAIL, STR_RAIL_TOOLBAR_TOOLTIP_CONVERT_RAIL), + SetFill(0, 1), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_CONVERT_RAIL, 0), EndContainer(), }; diff --git a/src/vehicle.cpp b/src/vehicle.cpp index 5410af5918..bfe770f614 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -632,6 +632,7 @@ static Vehicle *GetVehicleTunnelBridgeProc(Vehicle *v, void *data) TrackBits vehicle_track = Train::From(v)->track; if (!(vehicle_track & TRACK_BIT_WORMHOLE)) { if (info->mode == TBIFM_ACROSS_ONLY && !(GetAcrossBridgePossibleTrackBits(info->t) & vehicle_track)) return nullptr; + if (info->mode == TBIFM_PRIMARY_ONLY && !(GetPrimaryTunnelBridgeTrackBits(info->t) & vehicle_track)) return nullptr; } } @@ -643,7 +644,7 @@ static Vehicle *GetVehicleTunnelBridgeProc(Vehicle *v, void *data) * @param tile first end * @param endtile second end * @param ignore Ignore this vehicle when searching - * @param mode Whether to only find vehicles which are passing across the bridge/tunnel or on connecting bridge head track pieces + * @param mode Whether to only find vehicles which are passing across the bridge/tunnel or on connecting bridge head track pieces, or only on primary track type pieces * @return Succeeded command (if tunnel/bridge is free) or failed command (if a vehicle is using the tunnel/bridge). */ CommandCost TunnelBridgeIsFree(TileIndex tile, TileIndex endtile, const Vehicle *ignore, TunnelBridgeIsFreeMode mode) diff --git a/src/vehicle_func.h b/src/vehicle_func.h index ce88600116..2f8812d0e0 100644 --- a/src/vehicle_func.h +++ b/src/vehicle_func.h @@ -136,6 +136,7 @@ void ShowNewGrfVehicleError(EngineID engine, StringID part1, StringID part2, GRF enum TunnelBridgeIsFreeMode { TBIFM_ALL, TBIFM_ACROSS_ONLY, + TBIFM_PRIMARY_ONLY, }; CommandCost TunnelBridgeIsFree(TileIndex tile, TileIndex endtile, const Vehicle *ignore = nullptr, TunnelBridgeIsFreeMode mode = TBIFM_ALL); Train *GetTrainClosestToTunnelBridgeEnd(TileIndex tile, TileIndex other_tile); diff --git a/src/viewport_type.h b/src/viewport_type.h index da6cd0ec35..b824784a45 100644 --- a/src/viewport_type.h +++ b/src/viewport_type.h @@ -186,6 +186,7 @@ enum ViewportDragDropSelectionProcess { DDSP_BUILD_STATION, ///< Station placement DDSP_REMOVE_STATION, ///< Station removal DDSP_CONVERT_RAIL, ///< Rail conversion + DDSP_CONVERT_RAIL_TRACK, ///< Rail conversion (track) /* Road specific actions */ DDSP_PLACE_ROAD_X_DIR, ///< Road placement (X axis)