From c39d3f6053eaf68ac33731ccd56d81ca192e0faf Mon Sep 17 00:00:00 2001 From: Stanislav Shamrai Date: Sun, 9 Jun 2024 15:53:15 +0300 Subject: [PATCH] Feature: Add the ability to clone an area with structures --- media/baseset/openttd.grf | Bin 553738 -> 554830 bytes media/baseset/openttd.grf.hash | 2 +- media/baseset/openttd/clone_area.png | Bin 0 -> 11195 bytes media/baseset/openttd/openttdgui.nfo | 6 +- src/CMakeLists.txt | 2 + src/clone_area_cmd.cpp | 533 +++++++++++++++++++++++++++ src/clone_area_cmd.h | 21 ++ src/command.cpp | 1 + src/command_type.h | 2 + src/lang/english.txt | 4 + src/network/network_command.cpp | 2 + src/script/api/script_story_page.hpp | 2 + src/story_base.h | 2 + src/story_gui.cpp | 2 + src/table/sprites.h | 7 +- src/terraform_gui.cpp | 44 +++ src/viewport_type.h | 2 + src/widgets/terraform_widget.h | 2 + 18 files changed, 631 insertions(+), 3 deletions(-) create mode 100644 media/baseset/openttd/clone_area.png create mode 100644 src/clone_area_cmd.cpp create mode 100644 src/clone_area_cmd.h diff --git a/media/baseset/openttd.grf b/media/baseset/openttd.grf index 7f4f6cbc499876021243cd11e7bc33fac4132c52..7bbaf47b8b177c6566a4bad5c2bb5ee94d28186d 100644 GIT binary patch delta 14444 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 z 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 delta 12975 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 RX8HllWl9tS-xfF6{{Sj2=Boez 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 0000000000000000000000000000000000000000..d5ee73bb1d905821d03d40ce5d9ca92ba8b5988a GIT binary patch literal 11195 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 IfqUHl0b(cXbpQYW literal 0 HcmV?d00001 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, };