Feature: Prevent towns from upgrading individually-placed houses (#13270)

This commit is contained in:
Tyler Trahan 2025-02-09 15:37:06 -05:00 committed by GitHub
parent 1ed685b5c1
commit 521b860394
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 130 additions and 21 deletions

View File

@ -721,7 +721,8 @@
</ul>
</li>
</ul>
<li>m3 bits 6..5 : free</li>
<li>m3 bit 6 : free</li>
<li>m3 bit 5 : The house is protected from the town upgrading it</li>
<li>m3 bits 4..0 : triggers activated <a href="#newhouses">(newhouses)</a></li>
<li>m4 : free</li>
<li>m5 : see m3 bit 7</li>

View File

@ -156,7 +156,7 @@ the array so you can quickly see what is used and what is not.
<td class="caption">finished house</td>
<td class="bits" rowspan=2><span class="used" title="House random bits">XXXX XXXX</span></td>
<td class="bits" rowspan=2><span class="pool" title="Town index on pool">XXXX XXXX XXXX XXXX</span></td>
<td class="bits"><span class="used" title="House is complete/in construction (see m5)">1</span><span class="free">OO</span><span class="usable" title="Activated triggers (bits 2..4 don't have a meaning)">X XX</span><span class="used" title="Activated triggers (bits 2..4 don't have a meaning)">XX</span></td>
<td class="bits"><span class="used" title="House is complete/in construction (see m5)">1</span><span class="free">O</span><span class="used" title="The house is protected from the town upgrading it.">X</span><span class="usable" title="Activated triggers (bits 2..4 don't have a meaning)">X XX</span><span class="used" title="Activated triggers (bits 2..4 don't have a meaning)">XX</span></td>
<td class="bits" rowspan=2><span class="free">OOOO OOOO</span></td>
<td class="bits"><span class="used" title="Age in years, clamped at 255">XXXX XXXX</span></td>
<td class="bits" rowspan=2><span class="abuse" title="Newhouses activated: periodic processing time remaining; if not, lift position for houses 04 and 05">XXXX XX</span><span class="used" title="Animated tile state">XX</span></td>
@ -165,7 +165,7 @@ the array so you can quickly see what is used and what is not.
</tr>
<tr>
<td class="caption">house under construction</td>
<td class="bits"><span class="used" title="House is complete/in construction (see m5)">O</span><span class="used" title="House type (m4 + m3[6])">X</span><span class="free">O</span><span class="usable" title="Activated triggers (bits 2..4 don't have a meaning)">X XX</span><span class="used" title="Activated triggers (bits 2..4 don't have a meaning)">XX</span></td>
<td class="bits"><span class="used" title="House is complete/in construction (see m5)">O</span><span class="used" title="House type (m4 + m3[6])">X</span><span class="used" title="The house is protected from the town upgrading it.">X</span><span class="usable" title="Activated triggers (bits 2..4 don't have a meaning)">X XX</span><span class="used" title="Activated triggers (bits 2..4 don't have a meaning)">XX</span></td>
<td class="bits"><span class="free">OOO</span><span class="used" title="Construction stage">X X</span><span class="used" title="Construction counter">XXX</span></td>
</tr>
<tr>

View File

@ -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

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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
};

View File

@ -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<bool> town_can_upgrade; ///< Whether the town can upgrade this house during town growth.
};
/**

View File

@ -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<CMD_LANDSCAPE_CLEAR>::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();

View File

@ -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)

View File

@ -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<CMD_PLACE_HOUSE>::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index());
Command<CMD_PLACE_HOUSE>::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index(), this->house_protected);
}
IntervalTimer<TimerWindow> 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(),

View File

@ -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);
}

View File

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