From 09a8b1cf5c0cd71690f1fd430c09f58169488b72 Mon Sep 17 00:00:00 2001 From: Tyler Trahan Date: Thu, 8 Jan 2026 15:43:50 -0500 Subject: [PATCH] Feature: Allow placing an area of 1x1 houses (#14708) --- src/command_type.h | 1 + src/company_base.h | 2 +- src/town_cmd.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++++ src/town_cmd.h | 3 +++ src/town_gui.cpp | 23 +++++++++++++++++++- src/viewport_type.h | 1 + 6 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/command_type.h b/src/command_type.h index d77274cfeb..29dc9b8850 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -299,6 +299,7 @@ enum Commands : uint8_t { CMD_EXPAND_TOWN, ///< expand a town CMD_DELETE_TOWN, ///< delete a town CMD_PLACE_HOUSE, ///< place a house + CMD_PLACE_HOUSE_AREA, ///< place an area of houses CMD_ORDER_REFIT, ///< change the refit information of an order (for "goto depot" ) CMD_CLONE_ORDER, ///< clone (and share) an order diff --git a/src/company_base.h b/src/company_base.h index 91b1dbf5b5..7d6cf2f03f 100644 --- a/src/company_base.h +++ b/src/company_base.h @@ -105,7 +105,7 @@ struct CompanyProperties { uint32_t terraform_limit = 0; ///< Amount of tileheights we can (still) terraform (times 65536). uint32_t clear_limit = 0; ///< Amount of tiles we can (still) clear (times 65536). uint32_t tree_limit = 0; ///< Amount of trees we can (still) plant (times 65536). - uint32_t build_object_limit = 0; ///< Amount of tiles we can (still) build objects on (times 65536). Also applies to buying land. + uint32_t build_object_limit = 0; ///< Amount of tiles we can (still) build objects on (times 65536). Also applies to buying land and placing houses. /** * If \c true, the company is (also) controlled by the computer (a NoAI program). diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp index 6b2d12817d..fd4f03ac39 100644 --- a/src/town_cmd.cpp +++ b/src/town_cmd.cpp @@ -3000,6 +3000,59 @@ CommandCost CmdPlaceHouse(DoCommandFlags flags, TileIndex tile, HouseID house, b return CommandCost(); } +/** + * Construct multiple houses in an area + * @param flags Type of operation. + * @param tile End tile of area dragging. + * @param start_tile Start tile of area dragging. + * @param HouseID The HouseID of the house spec. + * @param is_protected Whether the house is protected from the town upgrading it. + * @param replace Whether we can replace existing houses. + * @param diagonal Whether to use the Diagonal or Orthogonal tile iterator. + * @return Empty cost or an error. + */ +CommandCost CmdPlaceHouseArea(DoCommandFlags flags, TileIndex tile, TileIndex start_tile, HouseID house, bool is_protected, bool replace, bool diagonal) +{ + if (start_tile >= Map::Size()) return CMD_ERROR; + + if (_game_mode != GM_EDITOR && _settings_game.economy.place_houses == PH_FORBIDDEN) return CMD_ERROR; + + if (Town::GetNumItems() == 0) return CommandCost(STR_ERROR_MUST_FOUND_TOWN_FIRST); + + if (static_cast(house) >= HouseSpec::Specs().size()) return CMD_ERROR; + const HouseSpec *hs = HouseSpec::Get(house); + if (!hs->enabled) return CMD_ERROR; + + /* Only allow placing an area of 1x1 houses. */ + if (!hs->building_flags.Test(BuildingFlag::Size1x1)) return CMD_ERROR; + + /* Use the built object limit to rate limit house placement. */ + const Company *c = Company::GetIfValid(_current_company); + int limit = (c == nullptr ? INT32_MAX : GB(c->build_object_limit, 16, 16)); + + CommandCost last_error = CMD_ERROR; + bool had_success = false; + + std::unique_ptr iter = TileIterator::Create(tile, start_tile, diagonal); + for (; *iter != INVALID_TILE; ++(*iter)) { + TileIndex t = *iter; + CommandCost ret = Command::Do(DoCommandFlags{flags}.Reset(DoCommandFlag::Execute), t, house, is_protected, replace); + + /* If we've reached the limit, stop building (or testing). */ + if (c != nullptr && limit-- <= 0) break; + + if (ret.Failed()) { + last_error = std::move(ret); + continue; + } + + if (flags.Test(DoCommandFlag::Execute)) Command::Do(flags, t, house, is_protected, replace); + had_success = true; + } + + return had_success ? CommandCost{} : last_error; +} + /** * Update data structures when a house is removed * @param tile Tile of the house diff --git a/src/town_cmd.h b/src/town_cmd.h index 168999560a..f990f8385b 100644 --- a/src/town_cmd.h +++ b/src/town_cmd.h @@ -28,6 +28,7 @@ CommandCost CmdTownSetText(DoCommandFlags flags, TownID town_id, const EncodedSt CommandCost CmdExpandTown(DoCommandFlags flags, TownID town_id, uint32_t grow_amount, TownExpandModes modes); CommandCost CmdDeleteTown(DoCommandFlags flags, TownID town_id); CommandCost CmdPlaceHouse(DoCommandFlags flags, TileIndex tile, HouseID house, bool house_protected, bool replace); +CommandCost CmdPlaceHouseArea(DoCommandFlags flags, TileIndex tile, TileIndex start_tile, HouseID house, bool is_protected, bool replace, bool diagonal); DEF_CMD_TRAIT(CMD_FOUND_TOWN, CmdFoundTown, CommandFlags({CommandFlag::Deity, CommandFlag::NoTest}), CommandType::LandscapeConstruction) // founding random town can fail only in exec run DEF_CMD_TRAIT(CMD_RENAME_TOWN, CmdRenameTown, CommandFlags({CommandFlag::Deity, CommandFlag::Server}), CommandType::OtherManagement) @@ -39,6 +40,8 @@ DEF_CMD_TRAIT(CMD_TOWN_SET_TEXT, CmdTownSetText, CommandFlags({CommandFlag DEF_CMD_TRAIT(CMD_EXPAND_TOWN, CmdExpandTown, CommandFlags({CommandFlag::Deity}), CommandType::LandscapeConstruction) DEF_CMD_TRAIT(CMD_DELETE_TOWN, CmdDeleteTown, CommandFlags({CommandFlag::Offline}), CommandType::LandscapeConstruction) DEF_CMD_TRAIT(CMD_PLACE_HOUSE, CmdPlaceHouse, CommandFlags({CommandFlag::Deity}), CommandType::OtherManagement) +DEF_CMD_TRAIT(CMD_PLACE_HOUSE_AREA, CmdPlaceHouseArea, CommandFlags({ CommandFlag::Deity }), CommandType::OtherManagement) + CommandCallback CcFoundTown; void CcFoundRandomTown(Commands cmd, const CommandCost &result, Money, TownID town_id); diff --git a/src/town_gui.cpp b/src/town_gui.cpp index 8011b6334d..70a1e42e9a 100644 --- a/src/town_gui.cpp +++ b/src/town_gui.cpp @@ -1798,7 +1798,28 @@ struct BuildHouseWindow : public PickerWindow { void OnPlaceObject([[maybe_unused]] Point pt, TileIndex tile) override { const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type); - Command::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index(), BuildHouseWindow::house_protected, BuildHouseWindow::replace); + + if (spec->building_flags.Test(BuildingFlag::Size1x1)) { + VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_PLACE_HOUSE); + } else { + Command::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index(), BuildHouseWindow::house_protected, BuildHouseWindow::replace); + } + } + + void OnPlaceDrag(ViewportPlaceMethod select_method, [[maybe_unused]] ViewportDragDropSelectionProcess select_proc, [[maybe_unused]] Point pt) override + { + VpSelectTilesWithMethod(pt.x, pt.y, select_method); + } + + void OnPlaceMouseUp([[maybe_unused]] ViewportPlaceMethod select_method, [[maybe_unused]] ViewportDragDropSelectionProcess select_proc, [[maybe_unused]] Point pt, TileIndex start_tile, TileIndex end_tile) override + { + if (pt.x == -1) return; + + assert(select_proc == DDSP_PLACE_HOUSE); + + const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type); + Command::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, + end_tile, start_tile, spec->Index(), BuildHouseWindow::house_protected, BuildHouseWindow::replace, _ctrl_pressed); } const IntervalTimer view_refresh_interval = {std::chrono::milliseconds(2500), [this](auto) { diff --git a/src/viewport_type.h b/src/viewport_type.h index 61c76c78c6..6c77802ae2 100644 --- a/src/viewport_type.h +++ b/src/viewport_type.h @@ -124,6 +124,7 @@ enum ViewportDragDropSelectionProcess : uint8_t { DDSP_PLANT_TREES, ///< Plant trees DDSP_BUILD_BRIDGE, ///< Bridge placement DDSP_BUILD_OBJECT, ///< Build an object + DDSP_PLACE_HOUSE, ///< Place a house /* Rail specific actions */ DDSP_PLACE_RAIL, ///< Rail placement