diff --git a/docs/changelog.txt b/docs/changelog.txt index be0b51c69e..331604e716 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -74,6 +74,7 @@ Template for new versions: - `buildingplan`: fix roller material estimate asking for one chain per tile instead of one chain total - `buildingplan`: fix "Unlink all" only unlinking some mechanisms (or crashing) when freeing mechanisms from the building - `tiletypes`: remove plants (including trees) rooted on a tile that is painted into something that cannot host them, so cleared ground no longer regrows floating trees +- Fixed gem and glass windows being unable to be made part of a room; they are the only buildings nobles can demand that could not be, whether the window or the room is placed first - `timestream`: deal properly with units who have breathing difficulties - `stocks`: overlay now resets scroll position when collapsing categories so the item list is no longer left blank and unscrollable - Fixed persistent site data API (``dfhack.persistent.saveSiteData``/``getSiteData``) failing on newly reclaimed fortresses until the first save diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index ddedbbc1b6..357c744358 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -33,6 +33,7 @@ distribution. #include "TileTypes.h" #include "MiscUtils.h" #include "DataDefs.h" +#include "VTableInterpose.h" #include "modules/Buildings.h" #include "modules/Maps.h" @@ -61,6 +62,8 @@ distribution. #include "df/building_water_wheelst.h" #include "df/building_weaponst.h" #include "df/building_wellst.h" +#include "df/building_window_gemst.h" +#include "df/building_window_glassst.h" #include "df/building_workshopst.h" #include "df/buildingitemst.h" #include "df/buildings_other_id.h" @@ -134,13 +137,43 @@ static df::building_extents_type *getExtentTile(const df::building::T_room &room */ bool buildings_do_onupdate = false; +/* + * Mitigation for a DF bug: gem and glass windows are the only buildings that + * nobles can demand in their rooms whose masterablebuilding (canMakeRoom) + * vmethod returns false, so a window built inside an existing room is linked + * to it while a room designated over an existing window is not. Interpose the + * vmethod on the two concrete subclasses; the abstract building_windowst + * vtable cannot be hooked. + */ +struct window_gem_masterable_hook : df::building_window_gemst { + typedef df::building_window_gemst interpose_base; + DEFINE_VMETHOD_INTERPOSE(bool, canMakeRoom, ()) + { + return true; + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(window_gem_masterable_hook, canMakeRoom); + +struct window_glass_masterable_hook : df::building_window_glassst { + typedef df::building_window_glassst interpose_base; + DEFINE_VMETHOD_INTERPOSE(bool, canMakeRoom, ()) + { + return true; + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(window_glass_masterable_hook, canMakeRoom); + void buildings_onStateChange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: + INTERPOSE_HOOK(window_gem_masterable_hook, canMakeRoom).apply(); + INTERPOSE_HOOK(window_glass_masterable_hook, canMakeRoom).apply(); buildings_do_onupdate = true; break; case SC_MAP_UNLOADED: + INTERPOSE_HOOK(window_gem_masterable_hook, canMakeRoom).remove(); + INTERPOSE_HOOK(window_glass_masterable_hook, canMakeRoom).remove(); buildings_do_onupdate = false; break; default: diff --git a/test/modules/buildings_fortress.lua b/test/modules/buildings_fortress.lua new file mode 100644 index 0000000000..53b9026648 --- /dev/null +++ b/test/modules/buildings_fortress.lua @@ -0,0 +1,92 @@ +config.target = 'core' +config.mode = 'fortress' + +local function contains(vec, value) + for _,entry in ipairs(vec) do + if entry == value then return true end + end + return false +end + +local function construct_window_in_zone(window_type) + for _,zone in ipairs(df.global.world.buildings.other.ANY_ZONE) do + if zone.room.extents then + for y = zone.y1, zone.y2 do + for x = zone.x1, zone.x2 do + local pos = xyz2pos(x, y, zone.z) + if not dfhack.buildings.findAtTile(pos) then + local bld = dfhack.buildings.constructBuilding{ + pos=pos, + type=window_type, + filters=dfhack.buildings.getFiltersByType( + {}, window_type, -1, -1), + } + if bld then return bld, zone end + end + end + end + end + end +end + +local function expect_window_linked(window_type) + local bld, zone = construct_window_in_zone(window_type) + expect.ne(nil, bld, 'could not find a zone tile suitable for a window') + if not bld then return end + dfhack.with_finalize( + function() dfhack.buildings.deconstruct(bld) end, + function() + expect.true_(contains(bld.relations, zone)) + expect.true_(contains(zone.contained_buildings, bld)) + end) +end + +function test.constructed_gem_window_is_linked_to_zone() + expect_window_linked(df.building_type.WindowGem) +end + +function test.constructed_glass_window_is_linked_to_zone() + expect_window_linked(df.building_type.WindowGlass) +end + +local function expect_zoned_window_linked(window_type) + local bld, _ = construct_window_in_zone(window_type) + expect.ne(nil, bld, 'could not find a tile suitable for a window') + if not bld then return end + dfhack.with_finalize( + function() dfhack.buildings.deconstruct(bld) end, + function() + local pos = xyz2pos(bld.centerx, bld.centery, bld.z) + local extents = df.reinterpret_cast(df.building_extents_type, + df.new('uint8_t', 1)) + extents[0] = 1 + local zone, err = dfhack.buildings.constructBuilding{ + type=df.building_type.Civzone, + subtype=df.civzone_type.Bedroom, + abstract=true, + pos=pos, width=1, height=1, + fields={ + assigned_unit_id=-1, + room={x=pos.x, y=pos.y, width=1, height=1, + extents=extents}, + }, + } + expect.ne(nil, zone, 'could not place zone over window: ' .. + tostring(err)) + if not zone then return end + dfhack.with_finalize( + function() dfhack.buildings.deconstruct(zone) end, + function() + expect.true_(contains(bld.relations, zone)) + expect.true_(contains(zone.contained_buildings, bld)) + end) + end) +end + +function test.zoned_gem_window_is_linked_to_zone() + expect_zoned_window_linked(df.building_type.WindowGem) +end + +function test.zoned_glass_window_is_linked_to_zone() + expect_zoned_window_linked(df.building_type.WindowGlass) +end