From 2ea9f058f55c6cdff21eb691b046ad1069ea224a Mon Sep 17 00:00:00 2001 From: Tyler Trahan Date: Sun, 7 Dec 2025 12:05:47 -0500 Subject: [PATCH] Feature: House placer mode to replace existing houses (#14469) --- gen_commands.py | 2 +- src/citymania/generated/cm_gen_commands.cpp | 35 +++++++++++-- src/citymania/generated/cm_gen_commands.hpp | 47 +++++++++++++++++- src/lang/english.txt | 7 +-- src/town_cmd.cpp | 55 ++++++++++++--------- src/town_cmd.h | 2 +- src/town_gui.cpp | 34 +++++++------ src/widgets/town_widget.h | 4 +- 8 files changed, 134 insertions(+), 52 deletions(-) diff --git a/gen_commands.py b/gen_commands.py index 88875b8685..89450743b0 100644 --- a/gen_commands.py +++ b/gen_commands.py @@ -5,7 +5,7 @@ from pathlib import Path from pprint import pprint RX_COMMAND = re.compile(r'(?PCommandCost|std::tuple]*>) (?PCmd\w*)\((?P[^)]*)\);') -RX_DEF_TRAIT = re.compile(r'DEF_CMD_TRAIT\((?P\w+),\s+(?P\w+),\s+(?P[^,()]+(?:\([^)]+\))?),\s+(?P\w+)\)') +RX_DEF_TRAIT = re.compile(r'DEF_CMD_TRAIT\((?P\w+),\s+(?P\w+),\s+(?P[^,()]+(?:\([^)]+\))?),\s+(?P[\w:]+)\)') RX_ARG = re.compile(r'(?P(:?const |)[\w:]* &?)(?P\w*)') RX_CALLBACK = re.compile(r'void\s+(?PCc\w+)\(Commands') RX_CALLBACK_REF = re.compile(r'CommandCallback(?:Data|)\s+(?PCc\w+);') diff --git a/src/citymania/generated/cm_gen_commands.cpp b/src/citymania/generated/cm_gen_commands.cpp index 9b5232d5f1..826068553d 100644 --- a/src/citymania/generated/cm_gen_commands.cpp +++ b/src/citymania/generated/cm_gen_commands.cpp @@ -22,6 +22,8 @@ static constexpr auto _callback_tuple = std::make_tuple( (::CommandCallback *)nullptr, // Make sure this is actually a pointer-to-function. &CcBuildDocks, &CcPlaySound_CONSTRUCTION_WATER, + &CcMoveWaypointName, + &CcMoveStationName, &CcRoadDepot, &CcRoadStop, &CcPlaySound_CONSTRUCTION_OTHER, @@ -242,6 +244,15 @@ CommandCost RenameWaypoint::_do(DoCommandFlags flags) { return (::Command::Do(flags, waypoint_id, text)); } +Commands MoveWaypointName::get_command() { return CMD_MOVE_WAYPOINT_NAME; } +static constexpr auto _MoveWaypointName_dispatch = MakeDispatchTable(); +bool MoveWaypointName::_post(::CommandCallback *callback) { + return _MoveWaypointName_dispatch[FindCallbackIndex(callback)](this->error, this->waypoint_id, this->tile); +} +CommandCost MoveWaypointName::_do(DoCommandFlags flags) { + return std::get<0>(::Command::Do(flags, waypoint_id, tile)); +} + Commands BuildAirport::get_command() { return CMD_BUILD_AIRPORT; } static constexpr auto _BuildAirport_dispatch = MakeDispatchTable(); bool BuildAirport::_post(::CommandCallback *callback) { @@ -305,6 +316,15 @@ CommandCost RenameStation::_do(DoCommandFlags flags) { return (::Command::Do(flags, station_id, text)); } +Commands MoveStationName::get_command() { return CMD_MOVE_STATION_NAME; } +static constexpr auto _MoveStationName_dispatch = MakeDispatchTable(); +bool MoveStationName::_post(::CommandCallback *callback) { + return _MoveStationName_dispatch[FindCallbackIndex(callback)](this->error, this->station_id, this->tile); +} +CommandCost MoveStationName::_do(DoCommandFlags flags) { + return std::get<0>(::Command::Do(flags, station_id, tile)); +} + Commands OpenCloseAirport::get_command() { return CMD_OPEN_CLOSE_AIRPORT; } static constexpr auto _OpenCloseAirport_dispatch = MakeDispatchTable(); bool OpenCloseAirport::_post(::CommandCallback *callback) { @@ -692,6 +712,15 @@ CommandCost RenameSign::_do(DoCommandFlags flags) { return (::Command::Do(flags, sign_id, text)); } +Commands MoveSign::get_command() { return CMD_MOVE_SIGN; } +static constexpr auto _MoveSign_dispatch = MakeDispatchTable(); +bool MoveSign::_post(::CommandCallback *callback) { + return _MoveSign_dispatch[FindCallbackIndex(callback)](this->error, this->sign_id, this->tile); +} +CommandCost MoveSign::_do(DoCommandFlags flags) { + return (::Command::Do(flags, sign_id, tile)); +} + Commands BuildVehicle::get_command() { return CMD_BUILD_VEHICLE; } static constexpr auto _BuildVehicle_dispatch = MakeDispatchTable(); bool BuildVehicle::_post(::CommandCallback *callback) { @@ -1071,12 +1100,12 @@ CommandCost DeleteTown::_do(DoCommandFlags flags) { } Commands PlaceHouse::get_command() { return CMD_PLACE_HOUSE; } -static constexpr auto _PlaceHouse_dispatch = MakeDispatchTable(); +static constexpr auto _PlaceHouse_dispatch = MakeDispatchTable(); bool PlaceHouse::_post(::CommandCallback *callback) { - return _PlaceHouse_dispatch[FindCallbackIndex(callback)](this->error, this->tile, this->house, this->house_protected); + return _PlaceHouse_dispatch[FindCallbackIndex(callback)](this->error, this->tile, this->house, this->house_protected, this->replace); } CommandCost PlaceHouse::_do(DoCommandFlags flags) { - return (::Command::Do(flags, tile, house, house_protected)); + return (::Command::Do(flags, tile, house, house_protected, replace)); } Commands TurnRoadVeh::get_command() { return CMD_TURN_ROADVEH; } diff --git a/src/citymania/generated/cm_gen_commands.hpp b/src/citymania/generated/cm_gen_commands.hpp index adcbe0a86d..6a86c3e2f4 100644 --- a/src/citymania/generated/cm_gen_commands.hpp +++ b/src/citymania/generated/cm_gen_commands.hpp @@ -271,6 +271,20 @@ public: Commands get_command() override; }; +class MoveWaypointName: public Command { +public: + StationID waypoint_id; + TileIndex tile; + + MoveWaypointName(StationID waypoint_id, TileIndex tile) + :waypoint_id{waypoint_id}, tile{tile} {} + ~MoveWaypointName() override {} + + bool _post(::CommandCallback * callback) override; + CommandCost _do(DoCommandFlags flags) override; + Commands get_command() override; +}; + class BuildAirport: public StationBuildCommand { public: TileIndex tile; @@ -385,6 +399,20 @@ public: Commands get_command() override; }; +class MoveStationName: public Command { +public: + StationID station_id; + TileIndex tile; + + MoveStationName(StationID station_id, TileIndex tile) + :station_id{station_id}, tile{tile} {} + ~MoveStationName() override {} + + bool _post(::CommandCallback * callback) override; + CommandCost _do(DoCommandFlags flags) override; + Commands get_command() override; +}; + class OpenCloseAirport: public Command { public: StationID station_id; @@ -1036,6 +1064,20 @@ public: Commands get_command() override; }; +class MoveSign: public Command { +public: + SignID sign_id; + TileIndex tile; + + MoveSign(SignID sign_id, TileIndex tile) + :sign_id{sign_id}, tile{tile} {} + ~MoveSign() override {} + + bool _post(::CommandCallback * callback) override; + CommandCost _do(DoCommandFlags flags) override; + Commands get_command() override; +}; + class BuildVehicle: public Command { public: TileIndex tile; @@ -1688,9 +1730,10 @@ public: TileIndex tile; HouseID house; bool house_protected; + bool replace; - PlaceHouse(TileIndex tile, HouseID house, bool house_protected) - :tile{tile}, house{house}, house_protected{house_protected} {} + PlaceHouse(TileIndex tile, HouseID house, bool house_protected, bool replace) + :tile{tile}, house{house}, house_protected{house_protected}, replace{replace} {} ~PlaceHouse() override {} bool _post(::CommandCallback * callback) override; diff --git a/src/lang/english.txt b/src/lang/english.txt index 1b84639285..aae2ec7e92 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2883,10 +2883,11 @@ STR_HOUSE_PICKER_CLASS_ZONE3 :Outer Suburbs STR_HOUSE_PICKER_CLASS_ZONE4 :Inner Suburbs STR_HOUSE_PICKER_CLASS_ZONE5 :Town centre -STR_HOUSE_PICKER_PROTECT_TITLE :Prevent upgrades +STR_HOUSE_PICKER_PROTECT :Prevent upgrades STR_HOUSE_PICKER_PROTECT_TOOLTIP :Choose whether this house will be protected from replacement as the town grows -STR_HOUSE_PICKER_PROTECT_OFF :Off -STR_HOUSE_PICKER_PROTECT_ON :On + +STR_HOUSE_PICKER_REPLACE :Replace existing +STR_HOUSE_PICKER_REPLACE_TOOLTIP :Choose whether to automatically remove an existing house on the tile where this house is placed STR_STATION_CLASS_DFLT :Default STR_STATION_CLASS_DFLT_STATION :Default station diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp index 12f58b47c1..f5c2a18724 100644 --- a/src/town_cmd.cpp +++ b/src/town_cmd.cpp @@ -3254,9 +3254,10 @@ static bool TryBuildTownHouse(Town *t, TileIndex tile, TownExpandModes modes, bo * @param tile Tile on which to place the house. * @param HouseID The HouseID of the house spec. * @param is_protected Whether the house is protected from the town upgrading it. + * @param replace Whether to automatically demolish an existing house on this tile, if present. * @return Empty cost or an error. */ -CommandCost CmdPlaceHouse(DoCommandFlags flags, TileIndex tile, HouseID house, bool is_protected) +CommandCost CmdPlaceHouse(DoCommandFlags flags, TileIndex tile, HouseID house, bool is_protected, bool replace) { if (_game_mode != GM_EDITOR && _settings_game.economy.place_houses == PH_FORBIDDEN) return CMD_ERROR; @@ -3266,39 +3267,45 @@ CommandCost CmdPlaceHouse(DoCommandFlags flags, TileIndex tile, HouseID house, b const HouseSpec *hs = HouseSpec::Get(house); if (!hs->enabled) return CMD_ERROR; - Town *t = ClosestTownFromTile(tile, UINT_MAX); - - /* cannot build on these slopes... */ - Slope slope = GetTileSlope(tile); - if (IsSteepSlope(slope)) return CommandCost(STR_ERROR_LAND_SLOPED_IN_WRONG_DIRECTION); - - /* building under a bridge? */ - if (IsBridgeAbove(tile)) return CommandCost(STR_ERROR_MUST_DEMOLISH_BRIDGE_FIRST); - - /* can we clear the land? */ - CommandCost cost = Command::Do({DoCommandFlag::Auto, DoCommandFlag::NoWater}, tile); - if (!cost.Succeeded()) return cost; - int maxz = GetTileMaxZ(tile); - /* Make sure there is no slope? */ - bool noslope = hs->building_flags.Test(BuildingFlag::NotSloped); - if (noslope && slope != SLOPE_FLAT) return CommandCost(STR_ERROR_FLAT_LAND_REQUIRED); - - TileArea ta = tile; + /* Check each tile of a multi-tile house. */ + TileArea ta(tile, 1, 1); if (hs->building_flags.Test(BuildingFlag::Size2x2)) ta.Add(TileAddXY(tile, 1, 1)); if (hs->building_flags.Test(BuildingFlag::Size2x1)) ta.Add(TileAddByDiagDir(tile, DIAGDIR_SW)); if (hs->building_flags.Test(BuildingFlag::Size1x2)) ta.Add(TileAddByDiagDir(tile, DIAGDIR_SE)); - /* Check additional tiles covered by this house. */ - for (const TileIndex &subtile : ta) { - cost = Command::Do({DoCommandFlag::Auto, DoCommandFlag::NoWater}, subtile); - if (!cost.Succeeded()) return cost; + for (const TileIndex subtile : ta) { + /* Houses cannot be built on steep slopes. */ + Slope slope = GetTileSlope(subtile); + if (IsSteepSlope(slope)) return CommandCost(STR_ERROR_LAND_SLOPED_IN_WRONG_DIRECTION); - if (!CheckBuildHouseSameZ(subtile, maxz, noslope)) return CommandCost(STR_ERROR_LAND_SLOPED_IN_WRONG_DIRECTION); + /* Houses cannot be built under bridges. */ + if (IsBridgeAbove(subtile)) return CommandCost(STR_ERROR_MUST_DEMOLISH_BRIDGE_FIRST); + + /* Make sure there is no slope? */ + bool noslope = hs->building_flags.Test(BuildingFlag::NotSloped); + if (noslope && slope != SLOPE_FLAT) return CommandCost(STR_ERROR_FLAT_LAND_REQUIRED); + + /* All tiles of a multi-tile house must have the same z-level. */ + if (GetTileMaxZ(subtile) != maxz) return CommandCost(STR_ERROR_LAND_SLOPED_IN_WRONG_DIRECTION); + + /* We might be replacing an existing house, otherwise check if we can clear land. */ + if (!(replace && GetTileType(subtile) == MP_HOUSE)) { + CommandCost cost = Command::Do({DoCommandFlag::Auto, DoCommandFlag::NoWater}, subtile); + if (!cost.Succeeded()) return cost; + } } if (flags.Test(DoCommandFlag::Execute)) { + /* If replacing, clear any existing houses first. */ + if (replace) { + for (const TileIndex &subtile : ta) { + if (GetTileType(subtile) == MP_HOUSE) ClearTownHouse(Town::GetByTile(subtile), subtile); + } + } + + Town *t = ClosestTownFromTile(tile, UINT_MAX); bool house_completed = _settings_game.economy.place_houses == PH_ALLOWED_CONSTRUCTED; BuildTownHouse(t, tile, hs, house, Random(), house_completed, is_protected, false); } diff --git a/src/town_cmd.h b/src/town_cmd.h index 1403d6aa53..168999560a 100644 --- a/src/town_cmd.h +++ b/src/town_cmd.h @@ -27,7 +27,7 @@ CommandCost CmdTownCargoGoal(DoCommandFlags flags, TownID town_id, TownAcceptanc CommandCost CmdTownSetText(DoCommandFlags flags, TownID town_id, const EncodedString &text); 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); +CommandCost CmdPlaceHouse(DoCommandFlags flags, TileIndex tile, HouseID house, bool house_protected, bool replace); 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) diff --git a/src/town_gui.cpp b/src/town_gui.cpp index b3e5a77932..8db7915e81 100644 --- a/src/town_gui.cpp +++ b/src/town_gui.cpp @@ -1744,6 +1744,7 @@ static CargoTypes GetProducedCargoOfHouse(const HouseSpec *hs) struct BuildHouseWindow : public PickerWindow { std::string house_info{}; static inline bool house_protected; + static inline bool replace; BuildHouseWindow(WindowDesc &desc, Window *parent) : PickerWindow(desc, parent, 0, HousePickerCallbacks::instance) { @@ -1850,11 +1851,17 @@ struct BuildHouseWindow : public PickerWindow { void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override { switch (widget) { - case WID_BH_PROTECT_OFF: - case WID_BH_PROTECT_ON: - BuildHouseWindow::house_protected = (widget == WID_BH_PROTECT_ON); - this->SetWidgetLoweredState(WID_BH_PROTECT_OFF, !BuildHouseWindow::house_protected); - this->SetWidgetLoweredState(WID_BH_PROTECT_ON, BuildHouseWindow::house_protected); + case WID_BH_PROTECT_TOGGLE: + BuildHouseWindow::house_protected = !BuildHouseWindow::house_protected; + this->SetWidgetLoweredState(WID_BH_PROTECT_TOGGLE, BuildHouseWindow::house_protected); + + SndClickBeep(); + this->SetDirty(); + break; + + case WID_BH_REPLACE_TOGGLE: + BuildHouseWindow::replace = !BuildHouseWindow::replace; + this->SetWidgetLoweredState(WID_BH_REPLACE_TOGGLE, BuildHouseWindow::replace); SndClickBeep(); this->SetDirty(); @@ -1883,17 +1890,16 @@ struct BuildHouseWindow : public PickerWindow { bool hasflag = spec->extra_flags.Test(HouseExtraFlag::BuildingIsProtected); if (hasflag) BuildHouseWindow::house_protected = true; - this->SetWidgetLoweredState(WID_BH_PROTECT_OFF, !BuildHouseWindow::house_protected); - this->SetWidgetLoweredState(WID_BH_PROTECT_ON, BuildHouseWindow::house_protected); + this->SetWidgetLoweredState(WID_BH_PROTECT_TOGGLE, BuildHouseWindow::house_protected); + this->SetWidgetLoweredState(WID_BH_REPLACE_TOGGLE, BuildHouseWindow::replace); - this->SetWidgetDisabledState(WID_BH_PROTECT_OFF, hasflag); - this->SetWidgetDisabledState(WID_BH_PROTECT_ON, hasflag); + this->SetWidgetDisabledState(WID_BH_PROTECT_TOGGLE, hasflag); } 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); + Command::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index(), BuildHouseWindow::house_protected, BuildHouseWindow::replace); } const IntervalTimer view_refresh_interval = {std::chrono::milliseconds(2500), [this](auto) { @@ -1923,14 +1929,10 @@ static constexpr std::initializer_list _nested_build_house_widgets NWidget(WWT_PANEL, COLOUR_DARK_GREEN), NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_picker, 0), SetPadding(WidgetDimensions::unscaled.picker), NWidget(WWT_EMPTY, INVALID_COLOUR, WID_BH_INFO), SetFill(1, 1), SetMinimalTextLines(10, 0), - NWidget(WWT_LABEL, INVALID_COLOUR), SetStringTip(STR_HOUSE_PICKER_PROTECT_TITLE, STR_NULL), SetFill(1, 0), - NWidget(NWID_HORIZONTAL), SetPIPRatio(1, 0, 1), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_BH_PROTECT_OFF), SetMinimalSize(60, 12), SetStringTip(STR_HOUSE_PICKER_PROTECT_OFF, STR_HOUSE_PICKER_PROTECT_TOOLTIP), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_BH_PROTECT_ON), SetMinimalSize(60, 12), SetStringTip(STR_HOUSE_PICKER_PROTECT_ON, STR_HOUSE_PICKER_PROTECT_TOOLTIP), - EndContainer(), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_BH_PROTECT_TOGGLE), SetMinimalSize(60, 12), SetStringTip(STR_HOUSE_PICKER_PROTECT, STR_HOUSE_PICKER_PROTECT_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_BH_REPLACE_TOGGLE), SetMinimalSize(60, 12), SetStringTip(STR_HOUSE_PICKER_REPLACE, STR_HOUSE_PICKER_REPLACE_TOOLTIP), EndContainer(), EndContainer(), - EndContainer(), NWidgetFunction(MakePickerTypeWidgets), EndContainer(), diff --git a/src/widgets/town_widget.h b/src/widgets/town_widget.h index d8a26ca539..61fac612ed 100644 --- a/src/widgets/town_widget.h +++ b/src/widgets/town_widget.h @@ -80,8 +80,8 @@ enum TownFoundingWidgets : WidgetID { /** Widgets of the #BuildHouseWindow class. */ enum BuildHouseWidgets : WidgetID { WID_BH_INFO, ///< Information panel of selected house. - WID_BH_PROTECT_OFF, ///< Button to protect the next house built. - WID_BH_PROTECT_ON, ///< Button to not protect the next house built. + WID_BH_PROTECT_TOGGLE, ///< Button to toggle protecting the next house built. + WID_BH_REPLACE_TOGGLE, ///< Button to toggle replacing existing houses. }; enum CBTownWidgets {