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 */