diff --git a/LUA_API.rst b/LUA_API.rst
index 49e205235..a7dab21b0 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -880,6 +880,15 @@ Units module
Retrieves the profession name for the given race/caste using raws.
+* ``dfhack.units.getProfessionColor(unit[,ignore_noble])``
+
+ Retrieves the color associated with the profession, using noble assignments
+ or raws. The ``ignore_noble`` boolean disables the use of noble positions.
+
+* ``dfhack.units.getCasteProfessionColor(race,caste,prof_id)``
+
+ Retrieves the profession color for the given race/caste using raws.
+
Items module
------------
@@ -1032,6 +1041,11 @@ Burrows module
Buildings module
----------------
+* ``dfhack.buildings.setOwner(item,unit)``
+
+ Replaces the owner of the building. If unit is *nil*, removes ownership.
+ Returns *false* in case of error.
+
* ``dfhack.buildings.getSize(building)``
Returns *width, height, centerx, centery*.
diff --git a/Lua API.html b/Lua API.html
index 15f1d89cb..b9f09cf96 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -1113,6 +1113,13 @@ or raws. The ignore_noble boolean disables the
dfhack.units.getCasteProfessionName(race,caste,prof_id[,plural])
Retrieves the profession name for the given race/caste using raws.
+dfhack.units.getProfessionColor(unit[,ignore_noble])
+Retrieves the color associated with the profession, using noble assignments
+or raws. The ignore_noble boolean disables the use of noble positions.
+
+dfhack.units.getCasteProfessionColor(race,caste,prof_id)
+Retrieves the profession color for the given race/caste using raws.
+
@@ -1237,6 +1244,10 @@ burrows, or the presence of invaders.
+dfhack.buildings.setOwner(item,unit)
+Replaces the owner of the building. If unit is nil, removes ownership.
+Returns false in case of error.
+
dfhack.buildings.getSize(building)
Returns width, height, centerx, centery.
diff --git a/dfhack.init-example b/dfhack.init-example
index f5f40196c..c9408e375 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -45,3 +45,6 @@ keybinding add Shift-G "job-material GLASS_GREEN"
# browse linked mechanisms
keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms
+
+# browse rooms of same owner
+keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 35cf1435b..00d4c517d 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -815,6 +815,8 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getAge),
WRAPM(Units, getProfessionName),
WRAPM(Units, getCasteProfessionName),
+ WRAPM(Units, getProfessionColor),
+ WRAPM(Units, getCasteProfessionColor),
{ NULL, NULL }
};
@@ -985,6 +987,7 @@ static bool buildings_containsTile(df::building *bld, int x, int y, bool room) {
}
static const LuaWrapper::FunctionReg dfhack_buildings_module[] = {
+ WRAPM(Buildings, setOwner),
WRAPM(Buildings, allocInstance),
WRAPM(Buildings, checkFreeTiles),
WRAPM(Buildings, countExtentTiles),
diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h
index 6e0a22052..639df6865 100644
--- a/library/include/modules/Buildings.h
+++ b/library/include/modules/Buildings.h
@@ -92,6 +92,11 @@ DFHACK_EXPORT bool Read (const uint32_t index, t_building & building);
*/
DFHACK_EXPORT bool ReadCustomWorkshopTypes(std::map & btypes);
+/**
+ * Sets the owner unit for the building.
+ */
+DFHACK_EXPORT bool setOwner(df::building *building, df::unit *owner);
+
/**
* Find the building located at the specified tile.
* Does not work on civzones.
diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp
index 8ec60e55b..d1aed8979 100644
--- a/library/modules/Buildings.cpp
+++ b/library/modules/Buildings.cpp
@@ -49,6 +49,7 @@ using namespace DFHack;
#include "df/ui_look_list.h"
#include "df/d_init.h"
#include "df/item.h"
+#include "df/unit.h"
#include "df/job.h"
#include "df/job_item.h"
#include "df/general_ref_building_holderst.h"
@@ -177,6 +178,44 @@ bool Buildings::ReadCustomWorkshopTypes(map & btypes)
return true;
}
+bool Buildings::setOwner(df::building *bld, df::unit *unit)
+{
+ CHECK_NULL_POINTER(bld);
+
+ if (!bld->is_room)
+ return false;
+ if (bld->owner == unit)
+ return true;
+
+ if (bld->owner)
+ {
+ auto &blist = bld->owner->owned_buildings;
+ vector_erase_at(blist, linear_index(blist, bld));
+
+ if (auto spouse = df::unit::find(bld->owner->relations.spouse_id))
+ {
+ auto &blist = spouse->owned_buildings;
+ vector_erase_at(blist, linear_index(blist, bld));
+ }
+ }
+
+ bld->owner = unit;
+
+ if (unit)
+ {
+ unit->owned_buildings.push_back(bld);
+
+ if (auto spouse = df::unit::find(unit->relations.spouse_id))
+ {
+ auto &blist = spouse->owned_buildings;
+ if (bld->canUseSpouseRoom() && linear_index(blist, bld) < 0)
+ blist.push_back(bld);
+ }
+ }
+
+ return true;
+}
+
df::building *Buildings::findAtTile(df::coord pos)
{
auto occ = Maps::getTileOccupancy(pos);
diff --git a/scripts/gui/room-list.lua b/scripts/gui/room-list.lua
new file mode 100644
index 000000000..a4507466f
--- /dev/null
+++ b/scripts/gui/room-list.lua
@@ -0,0 +1,246 @@
+-- Browses rooms owned by a unit.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+
+local room_type_table = {
+ [df.building_bedst] = { token = 'bed', qidx = 2, tile = 233 },
+ [df.building_tablest] = { token = 'table', qidx = 3, tile = 209 },
+ [df.building_chairst] = { token = 'chair', qidx = 4, tile = 210 },
+ [df.building_coffinst] = { token = 'coffin', qidx = 5, tile = 48 },
+}
+
+local room_quality_table = {
+ { 1, 'Meager Quarters', 'Meager Dining Room', 'Meager Office', 'Grave' },
+ { 100, 'Modest Quarters', 'Modest Dining Room', 'Modest Office', "Servant's Burial Chamber" },
+ { 250, 'Quarters', 'Dining Room', 'Office', 'Burial Chamber' },
+ { 500, 'Decent Quarters', 'Decent Dining Room', 'Decent Office', 'Tomb' },
+ { 1000, 'Fine Quarters', 'Fine Dining Room', 'Splendid Office', 'Fine Tomb' },
+ { 1500, 'Great Bedroom', 'Great Dining Room', 'Throne Room', 'Mausoleum' },
+ { 2500, 'Grand Bedroom', 'Grand Dining Room', 'Opulent Throne Room', 'Grand Mausoleum' },
+ { 10000, 'Royal Bedroom', 'Royal Dining Room', 'Royal Throne Room', 'Royal Mausoleum' }
+}
+
+function getRoomName(building, unit)
+ local info = room_type_table[building._type]
+ if not info or not building.is_room then
+ return utils.getBuildingName(building)
+ end
+
+ local quality = building:getRoomValue(unit)
+ local row = room_quality_table[1]
+ for _,v in ipairs(room_quality_table) do
+ if v[1] <= quality then
+ row = v
+ else
+ break
+ end
+ end
+ return row[info.qidx]
+end
+
+function makeRoomEntry(bld, unit, is_spouse)
+ local info = room_type_table[bld._type] or {}
+
+ return {
+ obj = bld,
+ token = info.token or '?',
+ tile = info.tile or '?',
+ caption = getRoomName(bld, unit),
+ can_use = (not is_spouse or bld:canUseSpouseRoom()),
+ owner = unit
+ }
+end
+
+function listRooms(unit, spouse)
+ local rv = {}
+ for _,v in pairs(unit.owned_buildings) do
+ if v.owner == unit then
+ rv[#rv+1] = makeRoomEntry(v, unit, spouse)
+ end
+ end
+ return rv
+end
+
+function concat_lists(...)
+ local rv = {}
+ for i = 1,select('#',...) do
+ local v = select(i,...)
+ if v then
+ for _,x in ipairs(v) do rv[#rv+1] = x end
+ end
+ end
+ return rv
+end
+
+RoomList = defclass(RoomList, guidm.MenuOverlay)
+
+RoomList.focus_path = 'room-list'
+
+function RoomList:init(unit)
+ local base_bld = df.global.world.selected_building
+
+ self:init_fields{
+ unit = unit, base_building = base_bld,
+ items = {}, selected = 1,
+ own_rooms = {}, spouse_rooms = {}
+ }
+ guidm.MenuOverlay.init(self)
+
+ self.old_viewport = self:getViewport()
+ self.old_cursor = guidm.getCursorPos()
+
+ if unit then
+ self.own_rooms = listRooms(unit)
+ self.spouse = df.unit.find(unit.relations.spouse_id)
+ if self.spouse then
+ self.spouse_rooms = listRooms(self.spouse, unit)
+ end
+ self.items = concat_lists(self.own_rooms, self.spouse_rooms)
+ end
+
+ if base_bld then
+ for i,v in ipairs(self.items) do
+ if v.obj == base_bld then
+ self.selected = i
+ v.tile = 26
+ goto found
+ end
+ end
+ self.base_item = makeRoomEntry(base_bld, unit)
+ self.base_item.owner = unit
+ self.base_item.old_owner = base_bld.owner
+ self.base_item.tile = 26
+ self.items = concat_lists({self.base_item}, self.items)
+ ::found::
+ end
+
+ return self
+end
+
+local sex_char = { [0] = 12, [1] = 11 }
+
+function drawUnitName(dc, unit)
+ dc:pen(COLOR_GREY)
+ if unit then
+ local color = dfhack.units.getProfessionColor(unit)
+ dc:char(sex_char[unit.sex] or '?'):advance(1):pen(color)
+
+ local vname = dfhack.units.getVisibleName(unit)
+ if vname and vname.has_name then
+ dc:string(dfhack.TranslateName(vname)..', ')
+ end
+ dc:string(dfhack.units.getProfessionName(unit))
+ else
+ dc:string("No Owner Assigned")
+ end
+end
+
+function drawRoomEntry(dc, entry, selected)
+ local color = COLOR_GREEN
+ if not entry.can_use then
+ color = COLOR_RED
+ elseif entry.obj.owner ~= entry.owner or not entry.owner then
+ color = COLOR_CYAN
+ end
+ dc:pen{fg = color, bold = (selected == entry)}
+ dc:char(entry.tile):advance(1):string(entry.caption)
+end
+
+function can_modify(sel_item)
+ return sel_item and sel_item.owner
+ and sel_item.can_use and not sel_item.owner.flags1.dead
+end
+
+function RoomList:onRenderBody(dc)
+ local sel_item = self.items[self.selected]
+
+ dc:clear():seek(1,1)
+ drawUnitName(dc, self.unit)
+
+ if self.base_item then
+ dc:newline():newline(2)
+ drawRoomEntry(dc, self.base_item, sel_item)
+ end
+ if #self.own_rooms > 0 then
+ dc:newline()
+ for _,v in ipairs(self.own_rooms) do
+ dc:newline(2)
+ drawRoomEntry(dc, v, sel_item)
+ end
+ end
+ if #self.spouse_rooms > 0 then
+ dc:newline():newline(1)
+ drawUnitName(dc, self.spouse)
+
+ dc:newline()
+ for _,v in ipairs(self.spouse_rooms) do
+ dc:newline(2)
+ drawRoomEntry(dc, v, sel_item)
+ end
+ end
+ if self.unit and #self.own_rooms == 0 and #self.spouse_rooms == 0 then
+ dc:newline():newline(2):string("No already assigned rooms.", COLOR_LIGHTRED)
+ end
+
+ dc:newline():newline(1):pen(COLOR_WHITE)
+ dc:string("Esc", COLOR_LIGHTGREEN):string(": Back")
+
+ if can_modify(sel_item) then
+ dc:string(", "):string("Enter", COLOR_LIGHTGREEN)
+ if sel_item.obj.owner == sel_item.owner then
+ dc:string(": Unassign")
+ else
+ dc:string(": Assign")
+ end
+ end
+end
+
+function RoomList:changeSelected(delta)
+ if #self.items <= 1 then return end
+ self.selected = 1 + (self.selected + delta - 1) % #self.items
+ self:selectBuilding(self.items[self.selected].obj)
+end
+
+function RoomList:onInput(keys)
+ local sel_item = self.items[self.selected]
+
+ if keys.SECONDSCROLL_UP then
+ self:changeSelected(-1)
+ elseif keys.SECONDSCROLL_DOWN then
+ self:changeSelected(1)
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+
+ if self.base_building then
+ if not sel_item or self.base_building ~= sel_item.obj then
+ self:selectBuilding(self.base_building, self.old_cursor, self.old_view)
+ end
+ if self.unit and self.base_building.owner == self.unit then
+ df.global.ui_building_in_assign = false
+ end
+ end
+ elseif keys.SELECT then
+ if can_modify(sel_item) then
+ local owner = sel_item.owner
+ if sel_item.obj.owner == owner then
+ owner = sel_item.old_owner
+ end
+ dfhack.buildings.setOwner(sel_item.obj, owner)
+ end
+ elseif self:simulateViewScroll(keys) then
+ return
+ end
+end
+
+local focus = dfhack.gui.getCurFocus()
+if focus == 'dwarfmode/QueryBuilding/Some' then
+ local base = df.global.world.selected_building
+ mkinstance(RoomList):init(base.owner):show()
+elseif focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
+ local unit = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
+ mkinstance(RoomList):init(unit):show()
+else
+ qerror("This script requires the main dwarfmode view in 'q' mode")
+end