diff --git a/media/baseset/openttd.grf b/media/baseset/openttd.grf index 7f4f6cbc49..7bbaf47b8b 100644 Binary files a/media/baseset/openttd.grf and b/media/baseset/openttd.grf differ diff --git a/media/baseset/openttd.grf.hash b/media/baseset/openttd.grf.hash index 25a50247f5..8e66597bb5 100644 --- a/media/baseset/openttd.grf.hash +++ b/media/baseset/openttd.grf.hash @@ -1 +1 @@ -4f03553f614a06d86dc06376db3353c7 +24445cdf8f7f93cfe1fa864252bda7b7 diff --git a/media/baseset/openttd/clone_area.png b/media/baseset/openttd/clone_area.png new file mode 100644 index 0000000000..d5ee73bb1d Binary files /dev/null and b/media/baseset/openttd/clone_area.png differ diff --git a/media/baseset/openttd/openttdgui.nfo b/media/baseset/openttd/openttdgui.nfo index 2fd5a5bb4c..e3f5003897 100644 --- a/media/baseset/openttd/openttdgui.nfo +++ b/media/baseset/openttd/openttdgui.nfo @@ -4,7 +4,7 @@ // See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . // -1 * 0 0C "OpenTTD GUI graphics" - -1 * 3 05 15 \b 191 // OPENTTD_SPRITE_COUNT + -1 * 3 05 15 \b 195 // OPENTTD_SPRITE_COUNT -1 sprites/openttdgui.png 8bpp 66 8 64 31 -31 7 normal -1 sprites/openttdgui.png 8bpp 146 8 64 31 -31 7 normal -1 sprites/openttdgui.png 8bpp 226 8 64 31 -31 7 normal @@ -196,3 +196,7 @@ -1 sprites/openttdgui.png 8bpp 567 440 12 10 0 0 normal -1 sprites/openttdgui.png 8bpp 581 440 10 10 0 0 normal -1 sprites/openttdgui.png 8bpp 593 440 10 10 0 0 normal + -1 sprites/clone_area.png 8bpp 3 9 20 20 0 0 normal + -1 sprites/clone_area.png 8bpp 35 9 20 20 0 0 normal + -1 sprites/clone_area.png 8bpp 67 9 32 32 0 0 normal + -1 sprites/clone_area.png 8bpp 115 9 32 32 0 0 normal diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f7847ff8a..4b0cf719a6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -94,6 +94,8 @@ add_files( clear_cmd.cpp clear_func.h clear_map.h + clone_area_cmd.cpp + clone_area_cmd.h command.cpp command_func.h command_type.h diff --git a/src/clone_area_cmd.cpp b/src/clone_area_cmd.cpp new file mode 100644 index 0000000000..46834cf65b --- /dev/null +++ b/src/clone_area_cmd.cpp @@ -0,0 +1,533 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file clone_area_cmd.cpp Commands related to clone area. */ + +#include "stdafx.h" +#include "command_func.h" +#include "viewport_func.h" +#include "company_base.h" +#include "clone_area_cmd.h" +#include "newgrf_station.h" +#include "safeguards.h" +#include "road.h" +#include "road_internal.h" +#include "depot_base.h" +#include "tunnelbridge_map.h" +#include "sound_func.h" +#include "rail_cmd.h" +#include "tunnelbridge_cmd.h" +#include "station_cmd.h" +#include "terraform_cmd.h" + +TileIndex selected_tile; +TileIndex selected_start_tile; +bool selected_diagonal; + +/** + * Rotate by an angle using the formula for rotating a point on a plane. + * + * @param point Rotating point. + * @param angle Rotate an angle. + */ +TileIndexDiffC Rotate(TileIndexDiffC point, DiagDirDiff angle) { + switch (angle) { + case DIAGDIRDIFF_90LEFT: return {int16_t(-point.y), point.x}; + case DIAGDIRDIFF_REVERSE: return {int16_t(-point.x), int16_t(-point.y)}; + case DIAGDIRDIFF_90RIGHT: return {point.y, int16_t(-point.x)}; + default: return point; + } +} + +/** + * Adjusting the position of the rotated point, since the tile has four corners. + * + * @param rotated Rotated point setting. + * @param angle Angle of rotation. + */ +TileIndexDiffC FixAfterRotate(TileIndexDiffC rotated, DiagDirDiff angle) { + switch (angle) { + case DIAGDIRDIFF_90LEFT: + rotated.x -= 1; + break; + case DIAGDIRDIFF_REVERSE: + rotated.x -= 1; + rotated.y -= 1; + break; + case DIAGDIRDIFF_90RIGHT: + rotated.y -= 1; + break; + default: + return rotated; + } + return rotated; +} + +/** + * Rotate Track by an angle using the formula for rotating a point on a plane. + * + * @param track Rotating track. + * @param angle Rotate an angle. + */ +Track Rotate(Track track, DiagDirDiff angle) { + switch (angle) { + case DIAGDIRDIFF_90LEFT: + if (track == TRACK_X || track == TRACK_Y) { + return TrackToOppositeTrack(track); + } + switch (track) { + case TRACK_UPPER: return TRACK_LEFT; + case TRACK_LOWER: return TRACK_RIGHT; + case TRACK_LEFT: return TRACK_LOWER; + case TRACK_RIGHT: return TRACK_UPPER; + default: break; + } + break; + case DIAGDIRDIFF_REVERSE: + if (track != TRACK_X && track != TRACK_Y) { + return TrackToOppositeTrack(track); + } + return track; + break; + case DIAGDIRDIFF_90RIGHT: + if (track == TRACK_X || track == TRACK_Y) { + return TrackToOppositeTrack(track); + } + switch (track) { + case TRACK_UPPER: return TRACK_RIGHT; + case TRACK_LOWER: return TRACK_LEFT; + case TRACK_LEFT: return TRACK_UPPER; + case TRACK_RIGHT: return TRACK_LOWER; + default: break; + } + break; + default: + return track; + } + return track; +} + +/** + * Rotate DiagDirection by an angle using the formula for rotating a point on a plane. + * + * @param dir Rotating DiagDirection. + * @param angle Rotate an angle. + */ +DiagDirection Rotate(DiagDirection dir, DiagDirDiff angle) { + switch (angle) { + case DIAGDIRDIFF_90LEFT: + dir = (DiagDirection)(dir - 1); + if (dir == INVALID_DIAGDIR) { + dir = DIAGDIR_NW; + } + break; + case DIAGDIRDIFF_REVERSE: + dir = (DiagDirection)(dir ^ 2); + break; + case DIAGDIRDIFF_90RIGHT: + dir = (DiagDirection)(dir + 1); + if (dir == DIAGDIR_END) { + dir = DIAGDIR_NE; + } + break; + default: + return dir; + } + return dir; +} + +/** + * Rotate Axis by an angle using the formula for rotating a point on a plane. + * + * @param axis Rotating axis. + * @param angle Rotate an angle. + */ +Axis Rotate(Axis axis, DiagDirDiff angle) { + if (angle == DIAGDIRDIFF_90LEFT || angle == DIAGDIRDIFF_90RIGHT) { + return (Axis)(axis ^ 1); + } + return axis; +} + +/** + * Finding an angle using the formula for the angle between two straight lines + * + * @param first First line vector. + * @param second Second line vector. + */ +DiagDirDiff AngleBetweenTwoLines(TileIndexDiffC first, TileIndexDiffC second) { + if (second.x == 0 && second.y == 0) { + return DIAGDIRDIFF_SAME; + } + + first.x = first.x > 0 ? 1 : -1; + first.y = first.y > 0 ? 1 : -1; + second.x = second.x > 0 ? 1 : -1; + second.y = second.y > 0 ? 1 : -1; + + int16_t value = first.x * second.x + first.y * second.y; + if (value > 0) { + return DIAGDIRDIFF_SAME; + } else if (value < 0) { + return DIAGDIRDIFF_REVERSE; + } + TileIndexDiffC third = Rotate(first, DIAGDIRDIFF_90LEFT); + + value = third.x * second.x + third.y * second.y; + if (value > 0) { + return DIAGDIRDIFF_90LEFT; + } + return DIAGDIRDIFF_90RIGHT; +} + +/** + * Command callback. If the region cannot be inserted, an error message will be displayed. + * @param result Result of the command. + * @param tile Tile where the industry is placed. + */ +void CcCloneArea(Commands, const CommandCost &result, Money, TileIndex tile) +{ + if (result.Succeeded()) { + if (_settings_client.sound.confirm) SndPlayTileFx(SND_1F_CONSTRUCTION_OTHER, tile); + } else { + SetRedErrorSquare(tile); + } +} + +/** + * Mark the selected area on the map to copy + * @param flags for this command type + * @param tile end tile of area-drag + * @param start_tile start tile of area drag + * @param diagonal Whether to use the Orthogonal (false) or Diagonal (true) iterator. + * @return the cost of this operation or an error + */ +std::tuple CmdCloneAreaCopy([[maybe_unused]] DoCommandFlag flags, TileIndex tile, TileIndex start_tile, bool diagonal) +{ + if (start_tile >= Map::Size()) return { CMD_ERROR, 0, INVALID_TILE }; + + selected_tile = tile; + selected_start_tile = start_tile; + selected_diagonal = diagonal; + + CommandCost cost(EXPENSES_CONSTRUCTION); + return { cost, 0, tile }; +} + +/** + * Paste the selected area on the map + * @param flags for this command type + * @param tile end tile of area-drag + * @param start_tile start tile of area drag + * @param diagonal Whether to use the Orthogonal (false) or Diagonal (true) iterator. + * @return the cost of this operation or an error + */ +std::tuple CmdCloneAreaPaste(DoCommandFlag flags, TileIndex tile, TileIndex start_tile, bool diagonal) +{ + if (start_tile >= Map::Size()) return { CMD_ERROR, 0, INVALID_TILE }; + + Money money = GetAvailableMoneyForCommand(); + CommandCost cost(EXPENSES_CONSTRUCTION); + CommandCost last_error(STR_ERROR_ALREADY_BUILT); + bool had_success = false; + bool terraform_problem = false; + TileIndex terraform_problem_tile = INVALID_TILE; + CommandCost terraform_error(STR_ERROR_ALREADY_BUILT); + + const Company *c = Company::GetIfValid(_current_company); + int limit = (c == nullptr ? INT32_MAX : GB(c->terraform_limit, 16, 16)); + if (limit == 0) return { CommandCost(STR_ERROR_TERRAFORM_LIMIT_REACHED), 0, INVALID_TILE }; + + TileIndexDiffC origin_direction = TileIndexToTileIndexDiffC(selected_start_tile, selected_tile); + TileIndexDiffC dest_direction = TileIndexToTileIndexDiffC(start_tile, tile); + DiagDirDiff angle = AngleBetweenTwoLines(origin_direction, dest_direction); + + int16_t position_selected_tile_x = TileX(selected_start_tile); + int16_t position_selected_tile_y = TileY(selected_start_tile); + int16_t dest_position_x = TileX(start_tile); + int16_t dest_position_y = TileY(start_tile); + uint origin_main_height = TileHeight(selected_start_tile); + uint dest_main_height = TileHeight(start_tile); + uint difference_height = dest_main_height - origin_main_height; + + TileIndexDiffC dest_point; + + TileIndex error_tile = INVALID_TILE; + std::unique_ptr iter = TileIterator::Create(selected_start_tile, selected_tile, selected_diagonal); + for (; *iter != INVALID_TILE; ++(*iter)) { + TileIndex iter_tile = *iter; + + dest_point.x = TileX(iter_tile) - position_selected_tile_x; + dest_point.y = TileY(iter_tile) - position_selected_tile_y; + dest_point = Rotate(dest_point, angle); + dest_point.x += dest_position_x; + dest_point.y += dest_position_y; + + TileIndex dest_tile = TileXY(dest_point.x, dest_point.y); + + uint origin_height = TileHeight(iter_tile) + difference_height; + uint dest_height = TileHeight(dest_tile); + + while (dest_height != origin_height) { + CommandCost ret; + std::tie(ret, std::ignore, error_tile) = Command::Do(flags & ~DC_EXEC, dest_tile, SLOPE_N, dest_height <= origin_height); + if (ret.Failed()) { + last_error = ret; + + if (!terraform_problem) { + terraform_problem_tile = error_tile; + terraform_error = last_error; + terraform_problem = true; + } + + /* Did we reach the limit? */ + if (ret.GetErrorMessage() == STR_ERROR_TERRAFORM_LIMIT_REACHED) limit = 0; + break; + } + + if (flags & DC_EXEC) { + money -= ret.GetCost(); + if (money < 0) { + return { cost, ret.GetCost(), error_tile }; + } + Command::Do(flags, dest_tile, SLOPE_N, dest_height <= origin_height); + } else { + /* When we're at the terraform limit we better bail (unneeded) testing as well. + * This will probably cause the terraforming cost to be underestimated, but only + * when it's near the terraforming limit. Even then, the estimation is + * completely off due to it basically counting terraforming double, so it being + * cut off earlier might even give a better estimate in some cases. */ + if (--limit <= 0) { + had_success = true; + break; + } + } + + cost.AddCost(ret); + dest_height += (dest_height > origin_height) ? -1 : 1; + had_success = true; + } + if (limit <= 0) break; + } + + if (terraform_problem) { + return { terraform_error, 0, terraform_problem_tile }; + } + + CommandCost ret; + std::tie(ret, std::ignore, error_tile) = CmdCloneAreaPasteProperty(flags, tile, start_tile, diagonal); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + } + if (flags & DC_EXEC) { + money -= ret.GetCost(); + if (money < 0) { + return { cost, ret.GetCost(), error_tile }; + } + } + + CommandCost cc_ret = had_success ? cost : last_error; + return { cc_ret, 0, cc_ret.Succeeded() ? tile : error_tile }; +} + + +/** + * Insert structures into the selected area on the map. + * @param flags operation to perform + * @param tile end tile of rail conversion drag + * @param area_start start tile of drag + * @param diagonal build diagonally or not. + * @return the cost of this operation or an error + */ +std::tuple CmdCloneAreaPasteProperty(DoCommandFlag flags, TileIndex tile, TileIndex area_start, [[maybe_unused]] bool diagonal) +{ + if (area_start >= Map::Size()) return { CMD_ERROR, 0, INVALID_TILE }; + + CommandCost cost(EXPENSES_CONSTRUCTION); + CommandCost last_error(INVALID_STRING_ID); + bool had_success = false; + bool auto_remove_signals = true; + bool signals_copy = true; + CommandCost error = CommandCost(STR_ERROR_CAN_T_BUILD_HERE); + + TileIndexDiffC origin_direction = TileIndexToTileIndexDiffC(selected_start_tile, selected_tile); + TileIndexDiffC dest_direction = TileIndexToTileIndexDiffC(area_start, tile); + + DiagDirDiff angle = AngleBetweenTwoLines(origin_direction, dest_direction); + uint position_selected_tile_x = TileX(selected_start_tile); + uint position_selected_tile_y = TileY(selected_start_tile); + int16_t dest_position_x = TileX(area_start); + int16_t dest_position_y = TileY(area_start); + + TileIndexDiffC dest_point; + TileIndex iter_tile; + TileIndex error_tile = INVALID_TILE; + + uint x_max = std::max(position_selected_tile_x, TileX(selected_tile)) - 1; + uint y_max = std::max(position_selected_tile_y, TileY(selected_tile)) - 1; + + std::map station_map; + std::unique_ptr iter = TileIterator::Create(selected_start_tile, selected_tile, selected_diagonal); + for (; (iter_tile = *iter) != INVALID_TILE; ++(*iter)) { + if (TileX(iter_tile) > x_max || TileY(iter_tile) > y_max) { + continue; + } + + TileType iter_tile_type = GetTileType(iter_tile); + switch (iter_tile_type) { + case MP_RAILWAY: + break; + case MP_STATION: + if (!HasStationRail(iter_tile)) continue; + break; + case MP_ROAD: + if (!IsLevelCrossing(iter_tile)) continue; + break; + case MP_TUNNELBRIDGE: + if (GetTunnelBridgeTransportType(iter_tile) != TRANSPORT_RAIL) continue; + break; + default: continue; + } + + dest_point.x = TileX(iter_tile) - position_selected_tile_x; + dest_point.y = TileY(iter_tile) - position_selected_tile_y; + dest_point = Rotate(dest_point, angle); + dest_point = FixAfterRotate(dest_point, angle); + dest_point.x += dest_position_x; + dest_point.y += dest_position_y; + TileIndex dest_tile = TileXY(dest_point.x, dest_point.y); + + CommandCost ret = CheckTileOwnership(iter_tile); + if (ret.Failed()) { + error = ret; + continue; + } + + RailType railtype2; + DiagDirection entrance_dir; + if (iter_tile_type == MP_RAILWAY) { + switch (GetRailTileType(iter_tile)) { + case RAIL_TILE_DEPOT: + railtype2 = GetRailType(iter_tile); + entrance_dir = GetRailDepotDirection(iter_tile); + entrance_dir = Rotate(entrance_dir, angle); + ret = Command::Do(flags, dest_tile, railtype2, entrance_dir); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + cost.AddCost(ret); + } + break; + default: // RAIL_TILE_NORMAL, RAIL_TILE_SIGNALS + RailType rail_type = GetRailType(iter_tile); + TrackBits trackBits = GetTrackBits(iter_tile); + Track track; + while ((track = RemoveFirstTrack(&trackBits)) != INVALID_TRACK) { + Track track_dest = Rotate(track, angle); + ret = Command::Do(flags, dest_tile, rail_type, track_dest, auto_remove_signals); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + cost.AddCost(ret); + } + + if (signals_copy) { + if (HasSignalOnTrack(iter_tile, track)) { + SignalType sigtype = GetSignalType(iter_tile, track); + SignalVariant sigvar = GetSignalVariant(iter_tile, track); + ret = Command::Do(flags, dest_tile, track_dest, sigtype, sigvar, false, false, false, SIGTYPE_BLOCK, SIGTYPE_BLOCK, 0, 0); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + cost.AddCost(ret); + } + } + } + } + break; + } + } + + if (iter_tile_type == MP_TUNNELBRIDGE) { + if (IsTunnel(iter_tile)) { + RailType rail_type = GetRailType(iter_tile); + ret = Command::Do(flags, dest_tile, TRANSPORT_RAIL, rail_type); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + cost.AddCost(ret); + } + } else { + RailType rail_type = GetRailType(iter_tile); + BridgeType type = GetBridgeType(iter_tile); + TileIndex endIterTile = GetOtherTunnelBridgeEnd(iter_tile); + + TileIndexDiffC end_dest_point; + end_dest_point.x = TileX(endIterTile) - position_selected_tile_x; + end_dest_point.y = TileY(endIterTile) - position_selected_tile_y; + end_dest_point = Rotate(end_dest_point, angle); + end_dest_point = FixAfterRotate(end_dest_point, angle); + end_dest_point.x += dest_position_x; + end_dest_point.y += dest_position_y; + TileIndex end_dest_tile = TileXY(end_dest_point.x, end_dest_point.y); + + ret = Command::Do(flags, end_dest_tile, dest_tile, TRANSPORT_RAIL, type, rail_type); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + cost.AddCost(ret); + } + } + } + + if (iter_tile_type == MP_STATION) { + if (HasStationRail(iter_tile)) { + RailType rail_type = GetRailType(iter_tile); + StationID origin_station_id = GetStationIndex(iter_tile); + StationID dest_station_id; + if (station_map.contains(origin_station_id)) { + dest_station_id = station_map[origin_station_id]; + } else { + dest_station_id = NEW_STATION; + } + Axis origin_axis = GetRailStationAxis(iter_tile); + Axis dest_axis = Rotate(origin_axis, angle); + ret = Command::Do(flags, dest_tile, rail_type, dest_axis, 1, 1, STAT_CLASS_DFLT, STATION_RAIL, dest_station_id, false); + if (ret.Failed()) { + last_error = ret; + } else { + had_success = true; + cost.AddCost(ret); + if (flags & DC_EXEC) { + if (dest_station_id == NEW_STATION) { + dest_station_id = GetStationIndex(dest_tile); + station_map[origin_station_id] = dest_station_id; + } + StationGfx station_gfx = GetStationGfx(iter_tile); + if (origin_axis != dest_axis) { + ToggleBit(station_gfx, 0); + } + if (angle == DIAGDIRDIFF_90RIGHT || angle == DIAGDIRDIFF_REVERSE) { + ToggleBit(station_gfx, 1); + } + SetStationGfx(dest_tile, station_gfx); + } + } + } + } + } + + CommandCost cc_ret = had_success ? cost : last_error; + return { cc_ret, 0, cc_ret.Succeeded() ? tile : error_tile }; +} diff --git a/src/clone_area_cmd.h b/src/clone_area_cmd.h new file mode 100644 index 0000000000..e92c7e8e08 --- /dev/null +++ b/src/clone_area_cmd.h @@ -0,0 +1,21 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file clone_area_cmd.h Command definitions related to cloning area. */ + +#ifndef CLONE_AREA_CMD_H +#define CLONE_AREA_CMD_H +std::tuple CmdCloneAreaCopy(DoCommandFlag flags, TileIndex tile, TileIndex start_tile, bool diagonal); +std::tuple CmdCloneAreaPaste(DoCommandFlag flags, TileIndex tile, TileIndex start_tile, bool diagonal); +std::tuple CmdCloneAreaPasteProperty(DoCommandFlag flags, TileIndex tile, TileIndex area_start, bool diagonal); + +DEF_CMD_TRAIT(CMD_CLONE_AREA_COPY, CmdCloneAreaCopy, CMD_NO_TEST, CMDT_LANDSCAPE_CONSTRUCTION) +DEF_CMD_TRAIT(CMD_CLONE_AREA_PASTE, CmdCloneAreaPaste, CMD_ALL_TILES | CMD_AUTO | CMD_NO_TEST, CMDT_LANDSCAPE_CONSTRUCTION) + +void CcCloneArea(Commands cmd, const CommandCost &result, Money, TileIndex tile); + +#endif /* CLONE_AREA_CMD_H */ diff --git a/src/command.cpp b/src/command.cpp index b975c51fbb..04d1c2427b 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -58,6 +58,7 @@ #include "waypoint_cmd.h" #include "misc/endian_buffer.hpp" #include "string_func.h" +#include "clone_area_cmd.h" #include "table/strings.h" diff --git a/src/command_type.h b/src/command_type.h index 2d7fc86672..5f8656627f 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -317,6 +317,8 @@ enum Commands : uint16_t { CMD_STORY_PAGE_BUTTON, ///< selection via story page button CMD_LEVEL_LAND, ///< level land + CMD_CLONE_AREA_COPY, ///< Clone land + CMD_CLONE_AREA_PASTE, ///< Clone land CMD_BUILD_LOCK, ///< build a lock diff --git a/src/lang/english.txt b/src/lang/english.txt index cbb10cf08b..fa0328be0d 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2966,6 +2966,8 @@ STR_LANDSCAPING_TOOLTIP_LOWER_A_CORNER_OF_LAND :{BLACK}Lower a STR_LANDSCAPING_TOOLTIP_RAISE_A_CORNER_OF_LAND :{BLACK}Raise a corner of land. Click+Drag to raise the first selected corner and level the selected area to the new corner height. Ctrl+Click+Drag to select the area diagonally. Also press Shift to show cost estimate only STR_LANDSCAPING_LEVEL_LAND_TOOLTIP :{BLACK}Level an area of land to the height of the first selected corner. Ctrl+Click+Drag to select the area diagonally. Also press Shift to show cost estimate only STR_LANDSCAPING_TOOLTIP_PURCHASE_LAND :{BLACK}Purchase land for future use. Ctrl+Click+Drag to select the area diagonally. Also press Shift to show cost estimate only +STR_LANDSCAPING_TOOLTIP_CLONE_AREA_COPY :{BLACK}Copy Area +STR_LANDSCAPING_TOOLTIP_CLONE_AREA_PASTE :{BLACK}Paste Area # Object construction window STR_OBJECT_BUILD_CAPTION :{WHITE}Object Selection @@ -4971,6 +4973,8 @@ STR_ERROR_TREE_PLANT_LIMIT_REACHED :{WHITE}... tree STR_ERROR_NAME_MUST_BE_UNIQUE :{WHITE}Name must be unique STR_ERROR_GENERIC_OBJECT_IN_THE_WAY :{WHITE}{1:STRING} in the way STR_ERROR_NOT_ALLOWED_WHILE_PAUSED :{WHITE}Not allowed while paused +STR_ERROR_CAN_T_CLONE_AREA_COPY :{WHITE}Can't Copy this area... +STR_ERROR_CAN_T_CLONE_AREA_PASTE :{WHITE}Can't Paste this area... # Local authority errors STR_ERROR_LOCAL_AUTHORITY_REFUSES_TO_ALLOW_THIS :{WHITE}{TOWN} local authority refuses to allow this diff --git a/src/network/network_command.cpp b/src/network/network_command.cpp index 51c64e261e..7704d132a3 100644 --- a/src/network/network_command.cpp +++ b/src/network/network_command.cpp @@ -41,6 +41,7 @@ #include "../story_cmd.h" #include "../subsidy_cmd.h" #include "../terraform_cmd.h" +#include "../clone_area_cmd.h" #include "../timetable_cmd.h" #include "../town_cmd.h" #include "../train_cmd.h" @@ -74,6 +75,7 @@ static constexpr auto _callback_tuple = std::make_tuple( &CcPlaySound_CONSTRUCTION_RAIL, &CcStation, &CcTerraform, + &CcCloneArea, &CcAI, &CcCloneVehicle, &CcCreateGroup, diff --git a/src/script/api/script_story_page.hpp b/src/script/api/script_story_page.hpp index 65ea06f29b..dc35b76afd 100644 --- a/src/script/api/script_story_page.hpp +++ b/src/script/api/script_story_page.hpp @@ -140,6 +140,8 @@ public: SPBC_RAISELAND = ::SPBC_RAISELAND, SPBC_PICKSTATION = ::SPBC_PICKSTATION, SPBC_BUILDSIGNALS = ::SPBC_BUILDSIGNALS, + SPBC_CLONE_AREA_COPY = ::SPBC_CLONE_AREA_COPY, + SPBC_CLONE_AREA_PASTE = ::SPBC_CLONE_AREA_PASTE, }; /** diff --git a/src/story_base.h b/src/story_base.h index 6978ad3a42..e40d5146be 100644 --- a/src/story_base.h +++ b/src/story_base.h @@ -98,6 +98,8 @@ enum StoryPageButtonCursor : uint8_t { SPBC_CLONE_ROADVEH, SPBC_CLONE_SHIP, SPBC_CLONE_AIRPLANE, + SPBC_CLONE_AREA_COPY, + SPBC_CLONE_AREA_PASTE, SPBC_DEMOLISH, SPBC_LOWERLAND, SPBC_RAISELAND, diff --git a/src/story_gui.cpp b/src/story_gui.cpp index 838bee7f35..43efda2219 100644 --- a/src/story_gui.cpp +++ b/src/story_gui.cpp @@ -1033,6 +1033,8 @@ static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor) case SPBC_CLONE_ROADVEH: return SPR_CURSOR_CLONE_ROADVEH; case SPBC_CLONE_SHIP: return SPR_CURSOR_CLONE_SHIP; case SPBC_CLONE_AIRPLANE: return SPR_CURSOR_CLONE_AIRPLANE; + case SPBC_CLONE_AREA_COPY: return SPR_CURSOR_CLONE_AREA_COPY; + case SPBC_CLONE_AREA_PASTE: return SPR_CURSOR_CLONE_AREA_PASTE; case SPBC_DEMOLISH: return ANIMCURSOR_DEMOLISH; case SPBC_LOWERLAND: return ANIMCURSOR_LOWERLAND; case SPBC_RAISELAND: return ANIMCURSOR_RAISELAND; diff --git a/src/table/sprites.h b/src/table/sprites.h index ea522724b5..fbd3341469 100644 --- a/src/table/sprites.h +++ b/src/table/sprites.h @@ -54,7 +54,7 @@ static const SpriteID SPR_LARGE_SMALL_WINDOW = 682; /** Extra graphic spritenumbers */ static const SpriteID SPR_OPENTTD_BASE = 4896; -static const uint16_t OPENTTD_SPRITE_COUNT = 191; +static const uint16_t OPENTTD_SPRITE_COUNT = 195; /* Halftile-selection sprites */ static const SpriteID SPR_HALFTILE_SELECTION_FLAT = SPR_OPENTTD_BASE; @@ -174,6 +174,11 @@ static const SpriteID SPR_PLAYER_HOST = SPR_OPENTTD_BASE + 190; static const SpriteID SPR_IMG_CARGOFLOW = SPR_OPENTTD_BASE + 174; +static const SpriteID SPR_IMG_CLONE_AREA_COPY = SPR_OPENTTD_BASE + 191; +static const SpriteID SPR_IMG_CLONE_AREA_PASTE = SPR_OPENTTD_BASE + 192; +static const CursorID SPR_CURSOR_CLONE_AREA_COPY = SPR_OPENTTD_BASE + 193; +static const CursorID SPR_CURSOR_CLONE_AREA_PASTE = SPR_OPENTTD_BASE + 194; + static const SpriteID SPR_SIGNALS_BASE = SPR_OPENTTD_BASE + OPENTTD_SPRITE_COUNT; static const uint16_t PRESIGNAL_SPRITE_COUNT = 48; static const uint16_t PRESIGNAL_AND_SEMAPHORE_SPRITE_COUNT = 112; diff --git a/src/terraform_gui.cpp b/src/terraform_gui.cpp index df1d442742..48f7dd1be7 100644 --- a/src/terraform_gui.cpp +++ b/src/terraform_gui.cpp @@ -38,6 +38,7 @@ #include "landscape_cmd.h" #include "terraform_cmd.h" #include "object_cmd.h" +#include "clone_area_cmd.h" #include "widgets/terraform_widget.h" @@ -131,6 +132,12 @@ bool GUIPlaceProcDragXY(ViewportDragDropSelectionProcess proc, TileIndex start_t case DDSP_LEVEL_AREA: Command::Post(STR_ERROR_CAN_T_LEVEL_LAND_HERE, CcTerraform, end_tile, start_tile, _ctrl_pressed, LM_LEVEL); break; + case DDSP_CLONE_AREA_COPY: + Command::Post(STR_ERROR_CAN_T_CLONE_AREA_COPY, CcCloneArea, end_tile, start_tile, _ctrl_pressed); + break; + case DDSP_CLONE_AREA_PASTE: + Command::Post(STR_ERROR_CAN_T_CLONE_AREA_PASTE, CcCloneArea, end_tile, start_tile, _ctrl_pressed); + break; case DDSP_CREATE_ROCKS: GenerateRockyArea(end_tile, start_tile); break; @@ -162,6 +169,7 @@ struct TerraformToolbarWindow : Window { /* This is needed as we like to have the tree available on OnInit. */ this->CreateNestedTree(); this->FinishInitNested(window_number); + this->DisableWidget(WID_TT_CLONE_AREA_PASTE); this->last_user_action = INVALID_WID_TT; } @@ -196,6 +204,16 @@ struct TerraformToolbarWindow : Window { this->last_user_action = widget; break; + case WID_TT_CLONE_AREA_COPY: // Copy area button + HandlePlacePushButton(this, WID_TT_CLONE_AREA_COPY, SPR_CURSOR_CLONE_AREA_COPY, HT_POINT | HT_DIAGONAL); + this->last_user_action = widget; + break; + + case WID_TT_CLONE_AREA_PASTE: // Paste area button + HandlePlacePushButton(this, WID_TT_CLONE_AREA_PASTE, SPR_CURSOR_CLONE_AREA_PASTE, HT_POINT | HT_DIAGONAL); + this->last_user_action = widget; + break; + case WID_TT_DEMOLISH: // Demolish aka dynamite button HandlePlacePushButton(this, WID_TT_DEMOLISH, ANIMCURSOR_DEMOLISH, HT_RECT | HT_DIAGONAL); this->last_user_action = widget; @@ -238,6 +256,14 @@ struct TerraformToolbarWindow : Window { VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_LEVEL_AREA); break; + case WID_TT_CLONE_AREA_COPY: // Clone area button + VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_CLONE_AREA_COPY); + break; + + case WID_TT_CLONE_AREA_PASTE: // Clone area button + VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_CLONE_AREA_PASTE); + break; + case WID_TT_DEMOLISH: // Demolish aka dynamite button PlaceProc_DemolishArea(tile); break; @@ -275,6 +301,11 @@ struct TerraformToolbarWindow : Window { case DDSP_RAISE_AND_LEVEL_AREA: case DDSP_LOWER_AND_LEVEL_AREA: case DDSP_LEVEL_AREA: + case DDSP_CLONE_AREA_PASTE: + GUIPlaceProcDragXY(select_proc, start_tile, end_tile); + break; + case DDSP_CLONE_AREA_COPY: + this->CloneAreaPasteWidgetEnable(true); GUIPlaceProcDragXY(select_proc, start_tile, end_tile); break; case DDSP_BUILD_OBJECT: @@ -296,6 +327,13 @@ struct TerraformToolbarWindow : Window { this->RaiseButtons(); } + void CloneAreaPasteWidgetEnable(bool value) + { + this->SetWidgetDisabledState(WID_TT_CLONE_AREA_PASTE, !value); + this->RaiseWidget(WID_TT_CLONE_AREA_PASTE); + this->SetWidgetDirty(WID_TT_CLONE_AREA_PASTE); + } + /** * Handler for global hotkeys of the TerraformToolbarWindow. * @param hotkey Hotkey @@ -345,6 +383,12 @@ static constexpr NWidgetPart _nested_terraform_widgets[] = { SetFill(0, 1), SetDataTip(SPR_IMG_PLANTTREES, STR_SCENEDIT_TOOLBAR_PLANT_TREES), NWidget(WWT_IMGBTN, COLOUR_DARK_GREEN, WID_TT_PLACE_SIGN), SetMinimalSize(22, 22), SetFill(0, 1), SetDataTip(SPR_IMG_SIGN, STR_SCENEDIT_TOOLBAR_PLACE_SIGN), + + NWidget(WWT_IMGBTN, COLOUR_DARK_GREEN, WID_TT_CLONE_AREA_COPY), SetMinimalSize(22, 22), + SetFill(0, 1), SetDataTip(SPR_IMG_CLONE_AREA_COPY, STR_LANDSCAPING_TOOLTIP_CLONE_AREA_COPY), + NWidget(WWT_IMGBTN, COLOUR_DARK_GREEN, WID_TT_CLONE_AREA_PASTE), SetMinimalSize(22, 22), + SetFill(0, 1), SetDataTip(SPR_IMG_CLONE_AREA_PASTE, STR_LANDSCAPING_TOOLTIP_CLONE_AREA_PASTE), + NWidget(NWID_SELECTION, INVALID_COLOUR, WID_TT_SHOW_PLACE_OBJECT), NWidget(WWT_PUSHIMGBTN, COLOUR_DARK_GREEN, WID_TT_PLACE_OBJECT), SetMinimalSize(22, 22), SetFill(0, 1), SetDataTip(SPR_IMG_TRANSMITTER, STR_SCENEDIT_TOOLBAR_PLACE_OBJECT), diff --git a/src/viewport_type.h b/src/viewport_type.h index 4a433387dd..aee3de2b13 100644 --- a/src/viewport_type.h +++ b/src/viewport_type.h @@ -120,6 +120,8 @@ enum ViewportDragDropSelectionProcess { DDSP_PLANT_TREES, ///< Plant trees DDSP_BUILD_BRIDGE, ///< Bridge placement DDSP_BUILD_OBJECT, ///< Build an object + DDSP_CLONE_AREA_COPY, ///< Copy area + DDSP_CLONE_AREA_PASTE, ///< Paste area /* Rail specific actions */ DDSP_PLACE_RAIL, ///< Rail placement diff --git a/src/widgets/terraform_widget.h b/src/widgets/terraform_widget.h index 6b5796f9b5..d9ce84e47a 100644 --- a/src/widgets/terraform_widget.h +++ b/src/widgets/terraform_widget.h @@ -22,6 +22,8 @@ enum TerraformToolbarWidgets : WidgetID { WID_TT_PLANT_TREES, ///< Plant trees button (note: opens separate window, no place-push-button). WID_TT_PLACE_SIGN, ///< Place sign button. WID_TT_PLACE_OBJECT, ///< Place object button. + WID_TT_CLONE_AREA_COPY, ///< Copy area button. + WID_TT_CLONE_AREA_PASTE, ///< Paste area button. INVALID_WID_TT = -1, };