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,
};