diff --git a/docs/landscape.html b/docs/landscape.html index 6c0f4cbce8..3f33126dd3 100644 --- a/docs/landscape.html +++ b/docs/landscape.html @@ -721,7 +721,8 @@ -
  • m3 bits 6..5 : free
  • +
  • m3 bit 6 : free
  • +
  • m3 bit 5 : The house is protected from the town upgrading it
  • m3 bits 4..0 : triggers activated (newhouses)
  • m4 : free
  • m5 : see m3 bit 7
  • diff --git a/docs/landscape_grid.html b/docs/landscape_grid.html index b2ee8293e2..495d6de112 100644 --- a/docs/landscape_grid.html +++ b/docs/landscape_grid.html @@ -156,7 +156,7 @@ the array so you can quickly see what is used and what is not. finished house XXXX XXXX XXXX XXXX XXXX XXXX - 1OOX XXXX + 1OXX XXXX OOOO OOOO XXXX XXXX XXXX XXXX @@ -165,7 +165,7 @@ the array so you can quickly see what is used and what is not. house under construction - OXOX XXXX + OXXX XXXX OOOX XXXX diff --git a/src/lang/english.txt b/src/lang/english.txt index 21388b667e..46e9ef9615 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2866,6 +2866,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_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_STATION_CLASS_DFLT :Default STR_STATION_CLASS_DFLT_STATION :Default station STR_STATION_CLASS_DFLT_ROADSTOP :Default road stop @@ -3142,6 +3147,8 @@ STR_LANG_AREA_INFORMATION_TRAM_TYPE :{BLACK}Tram typ STR_LANG_AREA_INFORMATION_RAIL_SPEED_LIMIT :{BLACK}Rail speed limit: {LTBLUE}{VELOCITY} STR_LANG_AREA_INFORMATION_ROAD_SPEED_LIMIT :{BLACK}Road speed limit: {LTBLUE}{VELOCITY} STR_LANG_AREA_INFORMATION_TRAM_SPEED_LIMIT :{BLACK}Tram speed limit: {LTBLUE}{VELOCITY} +STR_LAND_AREA_INFORMATION_TOWN_CAN_UPGRADE :{BLACK}Town can upgrade: {LTBLUE}Yes +STR_LAND_AREA_INFORMATION_TOWN_CANNOT_UPGRADE :{BLACK}Town can upgrade: {LTBLUE}No # Description of land area of different tiles STR_LAI_CLEAR_DESCRIPTION_ROCKS :Rocks diff --git a/src/misc_gui.cpp b/src/misc_gui.cpp index 5de355942c..61225a6934 100644 --- a/src/misc_gui.cpp +++ b/src/misc_gui.cpp @@ -168,6 +168,7 @@ public: td.road_speed = 0; td.tramtype = STR_NULL; td.tram_speed = 0; + td.town_can_upgrade = std::nullopt; td.grf = nullptr; @@ -300,6 +301,11 @@ public: this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_TRAM_SPEED_LIMIT)); } + /* Tile protection status */ + if (td.town_can_upgrade.has_value()) { + this->landinfo_data.push_back(GetString(td.town_can_upgrade.value() ? STR_LAND_AREA_INFORMATION_TOWN_CAN_UPGRADE : STR_LAND_AREA_INFORMATION_TOWN_CANNOT_UPGRADE)); + } + /* NewGRF name */ if (td.grf != nullptr) { SetDParamStr(0, td.grf); diff --git a/src/newgrf_house.cpp b/src/newgrf_house.cpp index 55fdba282c..2043599d01 100644 --- a/src/newgrf_house.cpp +++ b/src/newgrf_house.cpp @@ -622,7 +622,7 @@ bool CanDeleteHouse(TileIndex tile) uint16_t callback_res = GetHouseCallback(CBID_HOUSE_DENY_DESTRUCTION, 0, 0, GetHouseType(tile), Town::GetByTile(tile), tile); return (callback_res == CALLBACK_FAILED || !ConvertBooleanCallback(hs->grf_prop.grffile, CBID_HOUSE_DENY_DESTRUCTION, callback_res)); } else { - return !hs->extra_flags.Test(HouseExtraFlag::BuildingIsProtected); + return !IsHouseProtected(tile); } } diff --git a/src/saveload/afterload.cpp b/src/saveload/afterload.cpp index 4e75b995cd..0c54fca0e5 100644 --- a/src/saveload/afterload.cpp +++ b/src/saveload/afterload.cpp @@ -1548,6 +1548,16 @@ bool AfterLoadGame() } } + if (IsSavegameVersionBefore(SLV_PROTECT_PLACED_HOUSES)) { + for (auto t : Map::Iterate()) { + if (IsTileType(t, MP_HOUSE)) { + /* We now store house protection status in the map. Set this based on the house spec flags. */ + const HouseSpec *hs = HouseSpec::Get(GetHouseType(t)); + SetHouseProtected(t, hs->extra_flags.Test(HouseExtraFlag::BuildingIsProtected)); + } + } + } + /* Check and update house and town values */ UpdateHousesAndTowns(); diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index 04babb9c1d..246b598694 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -398,6 +398,7 @@ enum SaveLoadVersion : uint16_t { SLV_COMPANY_INAUGURATED_PERIOD_V2, ///< 349 PR#13448 Fix savegame storage for company inaugurated year in wallclock mode. SLV_ENCODED_STRING_FORMAT, ///< 350 PR#13499 Encoded String format changed. + SLV_PROTECT_PLACED_HOUSES, ///< 351 PR#13270 Houses individually placed by players can be protected from town/AI removal. SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/tile_cmd.h b/src/tile_cmd.h index 37c8b37ff7..8b2b206f8a 100644 --- a/src/tile_cmd.h +++ b/src/tile_cmd.h @@ -67,6 +67,7 @@ struct TileDesc { uint16_t road_speed; ///< Speed limit of road (bridges and track) StringID tramtype; ///< Type of tram on the tile. uint16_t tram_speed; ///< Speed limit of tram (bridges and track) + std::optional town_can_upgrade; ///< Whether the town can upgrade this house during town growth. }; /** diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp index 3fba7a80a0..e8da7ae878 100644 --- a/src/town_cmd.cpp +++ b/src/town_cmd.cpp @@ -872,6 +872,7 @@ static void GetTileDesc_Town(TileIndex tile, TileDesc *td) bool house_completed = IsHouseCompleted(tile); td->str = hs->building_name; + td->town_can_upgrade = !IsHouseProtected(tile); uint16_t callback_res = GetHouseCallback(CBID_HOUSE_CUSTOM_NAME, house_completed ? 1 : 0, 0, house, Town::GetByTile(tile), tile); if (callback_res != CALLBACK_FAILED && callback_res != 0x400) { @@ -2503,15 +2504,16 @@ HouseZonesBits GetTownRadiusGroup(const Town *t, TileIndex tile) * @param stage The current construction stage of the house. * @param type The type of house. * @param random_bits Random bits for newgrf houses to use. + * @param is_protected Whether the house is protected from the town upgrading it. * @pre The house can be built here. */ -static inline void ClearMakeHouseTile(TileIndex tile, Town *t, uint8_t counter, uint8_t stage, HouseID type, uint8_t random_bits) +static inline void ClearMakeHouseTile(TileIndex tile, Town *t, uint8_t counter, uint8_t stage, HouseID type, uint8_t random_bits, bool is_protected) { [[maybe_unused]] CommandCost cc = Command::Do(DC_EXEC | DC_AUTO | DC_NO_WATER, tile); assert(cc.Succeeded()); IncreaseBuildingCount(t, type); - MakeHouseTile(tile, t->index, counter, stage, type, random_bits); + MakeHouseTile(tile, t->index, counter, stage, type, random_bits, is_protected); if (HouseSpec::Get(type)->building_flags.Test(BuildingFlag::IsAnimated)) AddAnimatedTile(tile, false); MarkTileDirtyByTile(tile); @@ -2526,16 +2528,17 @@ static inline void ClearMakeHouseTile(TileIndex tile, Town *t, uint8_t counter, * @param stage The current construction stage. * @param The type of house. * @param random_bits Random bits for newgrf houses to use. + * @param is_protected Whether the house is protected from the town upgrading it. * @pre The house can be built here. */ -static void MakeTownHouse(TileIndex tile, Town *t, uint8_t counter, uint8_t stage, HouseID type, uint8_t random_bits) +static void MakeTownHouse(TileIndex tile, Town *t, uint8_t counter, uint8_t stage, HouseID type, uint8_t random_bits, bool is_protected) { BuildingFlags size = HouseSpec::Get(type)->building_flags; - ClearMakeHouseTile(tile, t, counter, stage, type, random_bits); - if (size.Any(BUILDING_2_TILES_Y)) ClearMakeHouseTile(tile + TileDiffXY(0, 1), t, counter, stage, ++type, random_bits); - if (size.Any(BUILDING_2_TILES_X)) ClearMakeHouseTile(tile + TileDiffXY(1, 0), t, counter, stage, ++type, random_bits); - if (size.Any(BUILDING_HAS_4_TILES)) ClearMakeHouseTile(tile + TileDiffXY(1, 1), t, counter, stage, ++type, random_bits); + ClearMakeHouseTile(tile, t, counter, stage, type, random_bits, is_protected); + if (size.Any(BUILDING_2_TILES_Y)) ClearMakeHouseTile(tile + TileDiffXY(0, 1), t, counter, stage, ++type, random_bits, is_protected); + if (size.Any(BUILDING_2_TILES_X)) ClearMakeHouseTile(tile + TileDiffXY(1, 0), t, counter, stage, ++type, random_bits, is_protected); + if (size.Any(BUILDING_HAS_4_TILES)) ClearMakeHouseTile(tile + TileDiffXY(1, 1), t, counter, stage, ++type, random_bits, is_protected); ForAllStationsAroundTiles(TileArea(tile, size.Any(BUILDING_2_TILES_X) ? 2 : 1, size.Any(BUILDING_2_TILES_Y) ? 2 : 1), [t](Station *st, TileIndex) { t->stations_near.insert(st); @@ -2732,8 +2735,9 @@ static bool CheckTownBuild2x2House(TileIndex *tile, Town *t, int maxz, bool nosl * @param house The @a HouseID of the house. * @param random_bits The random data to be associated with the house. * @param house_completed Should the house be placed already complete, instead of under construction? + * @param is_protected Whether the house is protected from the town upgrading it. */ -static void BuildTownHouse(Town *t, TileIndex tile, const HouseSpec *hs, HouseID house, uint8_t random_bits, bool house_completed) +static void BuildTownHouse(Town *t, TileIndex tile, const HouseSpec *hs, HouseID house, uint8_t random_bits, bool house_completed, bool is_protected) { /* build the house */ t->cache.num_houses++; @@ -2754,7 +2758,7 @@ static void BuildTownHouse(Town *t, TileIndex tile, const HouseSpec *hs, HouseID } } - MakeTownHouse(tile, t, construction_counter, construction_stage, house, random_bits); + MakeTownHouse(tile, t, construction_counter, construction_stage, house, random_bits, is_protected); UpdateTownRadius(t); UpdateTownGrowthRate(t); } @@ -2880,7 +2884,7 @@ static bool TryBuildTownHouse(Town *t, TileIndex tile) /* Special houses that there can be only one of. */ t->flags |= oneof; - BuildTownHouse(t, tile, hs, house, random_bits, false); + BuildTownHouse(t, tile, hs, house, random_bits, false, hs->extra_flags.Test(HouseExtraFlag::BuildingIsProtected)); return true; } @@ -2888,7 +2892,15 @@ static bool TryBuildTownHouse(Town *t, TileIndex tile) return false; } -CommandCost CmdPlaceHouse(DoCommandFlag flags, TileIndex tile, HouseID house) +/** + * Place an individual house. + * @param flags Type of operation. + * @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. + * @return Empty cost or an error. + */ +CommandCost CmdPlaceHouse(DoCommandFlag flags, TileIndex tile, HouseID house, bool is_protected) { if (_game_mode != GM_EDITOR && _settings_game.economy.place_houses == PH_FORBIDDEN) return CMD_ERROR; @@ -2932,7 +2944,7 @@ CommandCost CmdPlaceHouse(DoCommandFlag flags, TileIndex tile, HouseID house) if (flags & DC_EXEC) { bool house_completed = _settings_game.economy.place_houses == PH_ALLOWED_CONSTRUCTED; - BuildTownHouse(t, tile, hs, house, Random(), house_completed); + BuildTownHouse(t, tile, hs, house, Random(), house_completed, is_protected); } return CommandCost(); diff --git a/src/town_cmd.h b/src/town_cmd.h index 3c492eb434..4aa7eb3089 100644 --- a/src/town_cmd.h +++ b/src/town_cmd.h @@ -26,7 +26,7 @@ CommandCost CmdTownCargoGoal(DoCommandFlag flags, TownID town_id, TownAcceptance CommandCost CmdTownSetText(DoCommandFlag flags, TownID town_id, const std::string &text); CommandCost CmdExpandTown(DoCommandFlag flags, TownID town_id, uint32_t grow_amount); CommandCost CmdDeleteTown(DoCommandFlag flags, TownID town_id); -CommandCost CmdPlaceHouse(DoCommandFlag flags, TileIndex tile, HouseID house); +CommandCost CmdPlaceHouse(DoCommandFlag flags, TileIndex tile, HouseID house, bool house_protected); DEF_CMD_TRAIT(CMD_FOUND_TOWN, CmdFoundTown, CMD_DEITY | CMD_NO_TEST, CMDT_LANDSCAPE_CONSTRUCTION) // founding random town can fail only in exec run DEF_CMD_TRAIT(CMD_RENAME_TOWN, CmdRenameTown, CMD_DEITY | CMD_SERVER, CMDT_OTHER_MANAGEMENT) diff --git a/src/town_gui.cpp b/src/town_gui.cpp index 29c09e06b4..78eb185e5d 100644 --- a/src/town_gui.cpp +++ b/src/town_gui.cpp @@ -1627,6 +1627,7 @@ public: struct BuildHouseWindow : public PickerWindow { std::string house_info; + bool house_protected; BuildHouseWindow(WindowDesc &desc, Window *parent) : PickerWindow(desc, parent, 0, HousePickerCallbacks::instance) { @@ -1680,7 +1681,7 @@ struct BuildHouseWindow : public PickerWindow { /** * Get information string for a house. - * @param hs HosueSpec to get information string for. + * @param hs HouseSpec to get information string for. * @return Formatted string with information for house. */ static std::string GetHouseInformation(const HouseSpec *hs) @@ -1714,6 +1715,14 @@ struct BuildHouseWindow : public PickerWindow { return line.str(); } + void OnInit() override + { + this->SetWidgetLoweredState(WID_BH_PROTECT_OFF, !this->house_protected); + this->SetWidgetLoweredState(WID_BH_PROTECT_ON, this->house_protected); + + this->PickerWindow::OnInit(); + } + void DrawWidget(const Rect &r, WidgetID widget) const override { if (widget == WID_BH_INFO) { @@ -1723,22 +1732,52 @@ 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: + this->house_protected = (widget == WID_BH_PROTECT_ON); + this->SetWidgetLoweredState(WID_BH_PROTECT_OFF, !this->house_protected); + this->SetWidgetLoweredState(WID_BH_PROTECT_ON, this->house_protected); + + if (_settings_client.sound.click_beep) SndPlayFx(SND_15_BEEP); + this->SetDirty(); + break; + + default: + this->PickerWindow::OnClick(pt, widget, click_count); + break; + } + } + void OnInvalidateData(int data = 0, bool gui_scope = true) override { this->PickerWindow::OnInvalidateData(data, gui_scope); if (!gui_scope) return; + const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type); + if ((data & PickerWindow::PFI_POSITION) != 0) { - const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type); UpdateSelectSize(spec); this->house_info = GetHouseInformation(spec); } + + /* If house spec already has the protected flag, handle it automatically and disable the buttons. */ + bool hasflag = spec->extra_flags.Test(HouseExtraFlag::BuildingIsProtected); + if (hasflag) this->house_protected = true; + + this->SetWidgetLoweredState(WID_BH_PROTECT_OFF, !this->house_protected); + this->SetWidgetLoweredState(WID_BH_PROTECT_ON, this->house_protected); + + this->SetWidgetDisabledState(WID_BH_PROTECT_OFF, hasflag); + this->SetWidgetDisabledState(WID_BH_PROTECT_ON, 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()); + Command::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index(), this->house_protected); } IntervalTimer view_refresh_interval = {std::chrono::milliseconds(2500), [this](auto) { @@ -1768,8 +1807,14 @@ static constexpr NWidgetPart _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(), EndContainer(), EndContainer(), + EndContainer(), NWidgetFunction(MakePickerTypeWidgets), EndContainer(), diff --git a/src/town_map.h b/src/town_map.h index 19f3bc15cc..111897beb1 100644 --- a/src/town_map.h +++ b/src/town_map.h @@ -74,6 +74,28 @@ inline void SetHouseType(Tile t, HouseID house_id) SB(t.m8(), 0, 12, house_id); } +/** + * Check if the house is protected from removal by towns. + * @param t The tile. + * @return If the house is protected from the town upgrading it. + */ +inline bool IsHouseProtected(Tile t) +{ + assert(IsTileType(t, MP_HOUSE)); + return HasBit(t.m3(), 5); +} + +/** + * Set a house as protected from removal by towns. + * @param t The tile. + * @param house_protected Whether the house is protected from the town upgrading it. + */ +inline void SetHouseProtected(Tile t, bool house_protected) +{ + assert(IsTileType(t, MP_HOUSE)); + SB(t.m3(), 5, 1, house_protected ? 1 : 0); +} + /** * Check if the lift of this animated house has a destination * @param t the tile @@ -347,9 +369,10 @@ inline void DecHouseProcessingTime(Tile t) * @param stage of construction (used for drawing) * @param type of house. Index into house specs array * @param random_bits required for newgrf houses + * @param house_protected Whether the house is protected from the town upgrading it. * @pre IsTileType(t, MP_CLEAR) */ -inline void MakeHouseTile(Tile t, TownID tid, uint8_t counter, uint8_t stage, HouseID type, uint8_t random_bits) +inline void MakeHouseTile(Tile t, TownID tid, uint8_t counter, uint8_t stage, HouseID type, uint8_t random_bits, bool house_protected) { assert(IsTileType(t, MP_CLEAR)); @@ -360,6 +383,7 @@ inline void MakeHouseTile(Tile t, TownID tid, uint8_t counter, uint8_t stage, Ho SetHouseType(t, type); SetHouseCompleted(t, stage == TOWN_HOUSE_COMPLETED); t.m5() = IsHouseCompleted(t) ? 0 : (stage << 3 | counter); + SetHouseProtected(t, house_protected); SetAnimationFrame(t, 0); SetHouseProcessingTime(t, HouseSpec::Get(type)->processing_time); } diff --git a/src/widgets/town_widget.h b/src/widgets/town_widget.h index c321bb42ca..25446fc825 100644 --- a/src/widgets/town_widget.h +++ b/src/widgets/town_widget.h @@ -72,6 +72,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. }; #endif /* WIDGETS_TOWN_WIDGET_H */