-
+
Any files with the .lua extension placed into hack/scripts/*
are automatically used by the DFHack core as commands. The
matching command name consists of the name of the file sans
diff --git a/dfhack.init-example b/dfhack.init-example
index a9de146c2..afde13a88 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -16,6 +16,10 @@ keybinding add Ctrl-K autodump-destroy-item
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
+# gui/rename script
+keybinding add Ctrl-Shift-N gui/rename
+keybinding add Ctrl-Shift-P "gui/rename unit-profession"
+
##############################
# Generic adv mode bindings #
##############################
@@ -64,3 +68,9 @@ tweak stable-cursor
# stop military from considering training as 'patrol duty'
tweak patrol-duty
+
+# display creature weight in build plate menu as ??K, instead of (???df: Max
+tweak readable-build-plate
+
+# improve FPS by squashing endless item temperature update loops
+tweak stable-temp
diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp
index 341164441..fa2aacf78 100644
--- a/library/DataDefs.cpp
+++ b/library/DataDefs.cpp
@@ -374,7 +374,7 @@ void DFHack::bitfieldToString(std::vector *pvec, const void *p,
unsigned size, const bitfield_item_info *items)
{
for (unsigned i = 0; i < size; i++) {
- int value = getBitfieldField(p, i, std::min(1,items[i].size));
+ int value = getBitfieldField(p, i, std::max(1,items[i].size));
if (value) {
std::string name = format_key(items[i].name, i);
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 6dfb2f354..1dcb001f1 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -78,6 +78,7 @@ distribution.
#include "df/burrow.h"
#include "df/building_civzonest.h"
#include "df/region_map_entry.h"
+#include "df/flow_info.h"
#include
#include
@@ -727,6 +728,7 @@ static std::string getOSType()
}
static std::string getDFVersion() { return Core::getInstance().vinfo->getVersion(); }
+static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); }
static std::string getDFPath() { return Core::getInstance().p->getPath(); }
static std::string getHackPath() { return Core::getInstance().getHackPath(); }
@@ -738,6 +740,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = {
WRAP(getOSType),
WRAP(getDFVersion),
WRAP(getDFPath),
+ WRAP(getTickCount),
WRAP(getHackPath),
WRAP(isWorldLoaded),
WRAP(isMapLoaded),
@@ -756,7 +759,9 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, getSelectedUnit),
WRAPM(Gui, getSelectedItem),
WRAPM(Gui, showAnnouncement),
+ WRAPM(Gui, showZoomAnnouncement),
WRAPM(Gui, showPopupAnnouncement),
+ WRAPM(Gui, showAutoAnnouncement),
{ NULL, NULL }
};
@@ -912,9 +917,17 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPM(Maps, getGlobalInitFeature),
WRAPM(Maps, getLocalInitFeature),
WRAPM(Maps, canWalkBetween),
+ WRAPM(Maps, spawnFlow),
{ NULL, NULL }
};
+static int maps_isValidTilePos(lua_State *L)
+{
+ auto pos = CheckCoordXYZ(L, 1, true);
+ lua_pushboolean(L, Maps::isValidTilePos(pos));
+ return 1;
+}
+
static int maps_getTileBlock(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
@@ -936,6 +949,7 @@ static int maps_getTileBiomeRgn(lua_State *L)
}
static const luaL_Reg dfhack_maps_funcs[] = {
+ { "isValidTilePos", maps_isValidTilePos },
{ "getTileBlock", maps_getTileBlock },
{ "getRegionBiome", maps_getRegionBiome },
{ "getTileBiomeRgn", maps_getTileBiomeRgn },
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 7c2c8f8d6..a283d215c 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -1211,6 +1211,39 @@ static int dfhack_open_plugin(lua_State *L)
return 0;
}
+static int dfhack_curry_wrap(lua_State *L)
+{
+ int nargs = lua_gettop(L);
+ int ncurry = lua_tointeger(L, lua_upvalueindex(1));
+ int scount = nargs + ncurry;
+
+ luaL_checkstack(L, ncurry, "stack overflow in curry");
+
+ // Insert values in O(N+M) by first shifting the existing data
+ lua_settop(L, scount);
+ for (int i = 0; i < nargs; i++)
+ lua_copy(L, nargs-i, scount-i);
+ for (int i = 1; i <= ncurry; i++)
+ lua_copy(L, lua_upvalueindex(i+1), i);
+
+ lua_callk(L, scount-1, LUA_MULTRET, 0, lua_gettop);
+
+ return lua_gettop(L);
+}
+
+static int dfhack_curry(lua_State *L)
+{
+ luaL_checkany(L, 1);
+ if (lua_isnil(L, 1))
+ luaL_argerror(L, 1, "nil function in curry");
+ if (lua_gettop(L) == 1)
+ return 1;
+ lua_pushinteger(L, lua_gettop(L));
+ lua_insert(L, 1);
+ lua_pushcclosure(L, dfhack_curry_wrap, lua_gettop(L));
+ return 1;
+}
+
bool Lua::IsCoreContext(lua_State *state)
{
// This uses a private field of the lua state to
@@ -1234,6 +1267,7 @@ static const luaL_Reg dfhack_funcs[] = {
{ "call_with_finalizer", dfhack_call_with_finalizer },
{ "with_suspend", lua_dfhack_with_suspend },
{ "open_plugin", dfhack_open_plugin },
+ { "curry", dfhack_curry },
{ NULL, NULL }
};
@@ -1546,6 +1580,9 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_BASE_G_TOKEN);
lua_setfield(state, -2, "BASE_G");
+ lua_pushstring(state, DFHACK_VERSION);
+ lua_setfield(state, -2, "VERSION");
+
lua_pushboolean(state, IsCoreContext(state));
lua_setfield(state, -2, "is_core_context");
diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp
index e71977960..9f2689fa9 100644
--- a/library/LuaTypes.cpp
+++ b/library/LuaTypes.cpp
@@ -894,7 +894,7 @@ static int meta_bitfield_len(lua_State *state)
static void read_bitfield(lua_State *state, uint8_t *ptr, bitfield_identity *id, int idx)
{
- int size = id->getBits()[idx].size;
+ int size = std::max(1, id->getBits()[idx].size);
int value = getBitfieldField(ptr, idx, size);
if (size <= 1)
@@ -951,7 +951,7 @@ static int meta_bitfield_newindex(lua_State *state)
}
int idx = check_container_index(state, id->getNumBits(), 2, iidx, "write");
- int size = id->getBits()[idx].size;
+ int size = std::max(1, id->getBits()[idx].size);
if (lua_isboolean(state, 3) || lua_isnil(state, 3))
setBitfieldField(ptr, idx, size, lua_toboolean(state, 3));
diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp
index 5a97d9e00..3893cfc5f 100644
--- a/library/Process-darwin.cpp
+++ b/library/Process-darwin.cpp
@@ -27,6 +27,7 @@ distribution.
#include
#include
#include
+#include
#include
@@ -262,6 +263,13 @@ bool Process::getThreadIDs(vector & threads )
return true;
}
+uint32_t Process::getTickCount()
+{
+ struct timeval tp;
+ gettimeofday(&tp, NULL);
+ return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
string Process::getPath()
{
char path[1024];
diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp
index fe8647845..4a66470f9 100644
--- a/library/Process-linux.cpp
+++ b/library/Process-linux.cpp
@@ -27,6 +27,7 @@ distribution.
#include
#include
#include
+#include
#include
#include
@@ -192,6 +193,13 @@ bool Process::getThreadIDs(vector & threads )
return true;
}
+uint32_t Process::getTickCount()
+{
+ struct timeval tp;
+ gettimeofday(&tp, NULL);
+ return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
string Process::getPath()
{
const char * cwd_name = "/proc/self/cwd";
diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp
index 7eb6ff5f7..db58c4d33 100644
--- a/library/Process-windows.cpp
+++ b/library/Process-windows.cpp
@@ -410,6 +410,11 @@ string Process::doReadClassName (void * vptr)
return raw;
}
+uint32_t Process::getTickCount()
+{
+ return GetTickCount();
+}
+
string Process::getPath()
{
HMODULE hmod;
diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp
index 079890fe4..583ef5184 100644
--- a/library/VTableInterpose.cpp
+++ b/library/VTableInterpose.cpp
@@ -335,8 +335,14 @@ void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
}
}
-bool VMethodInterposeLinkBase::apply()
+bool VMethodInterposeLinkBase::apply(bool enable)
{
+ if (!enable)
+ {
+ remove();
+ return true;
+ }
+
if (is_applied())
return true;
if (!host->vtable_ptr)
diff --git a/library/include/BitArray.h b/library/include/BitArray.h
index fd9bd98fc..ff68ea1d1 100644
--- a/library/include/BitArray.h
+++ b/library/include/BitArray.h
@@ -64,7 +64,7 @@ namespace DFHack
if (newsize == size)
return;
uint8_t* mem = (uint8_t *) realloc(bits, newsize);
- if(!mem)
+ if(!mem && newsize != 0)
throw std::bad_alloc();
bits = mem;
if (newsize > size)
@@ -207,7 +207,7 @@ namespace DFHack
else
{
T* mem = (T*) realloc(m_data, sizeof(T)*new_size);
- if(!mem)
+ if(!mem && new_size != 0)
throw std::bad_alloc();
m_data = mem;
}
diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h
index 0f5fd9e7c..21dc68d1a 100644
--- a/library/include/DataIdentity.h
+++ b/library/include/DataIdentity.h
@@ -390,7 +390,7 @@ namespace df
}
virtual bool resize(void *ptr, int size) {
- ((container*)ptr)->resize(size);
+ ((container*)ptr)->resize(size*8);
return true;
}
diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h
index 0e5f618e2..a226018a6 100644
--- a/library/include/MemAccess.h
+++ b/library/include/MemAccess.h
@@ -281,6 +281,9 @@ namespace DFHack
/// get the DF Process FilePath
std::string getPath();
+ /// millisecond tick count, exactly as DF uses
+ uint32_t getTickCount();
+
/// modify permisions of memory range
bool setPermisions(const t_memrange & range,const t_memrange &trgrange);
diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h
index c9482f82c..7ba6b67aa 100644
--- a/library/include/VTableInterpose.h
+++ b/library/include/VTableInterpose.h
@@ -159,7 +159,7 @@ namespace DFHack
~VMethodInterposeLinkBase();
bool is_applied() { return applied; }
- bool apply();
+ bool apply(bool enable = true);
void remove();
};
diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h
index 58f222419..97e8bd422 100644
--- a/library/include/modules/Gui.h
+++ b/library/include/modules/Gui.h
@@ -32,6 +32,7 @@ distribution.
#include "DataDefs.h"
#include "df/init.h"
#include "df/ui.h"
+#include "df/announcement_type.h"
namespace df {
struct viewscreen;
@@ -92,14 +93,32 @@ namespace DFHack
// Show a plain announcement, or a titan-style popup message
DFHACK_EXPORT void showAnnouncement(std::string message, int color = 7, bool bright = true);
+ DFHACK_EXPORT void showZoomAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
DFHACK_EXPORT void showPopupAnnouncement(std::string message, int color = 7, bool bright = true);
+ // Show an announcement with effects determined by announcements.txt
+ DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
+
/*
* Cursor and window coords
*/
DFHACK_EXPORT df::coord getViewportPos();
DFHACK_EXPORT df::coord getCursorPos();
+ static const int AREA_MAP_WIDTH = 23;
+ static const int MENU_WIDTH = 30;
+
+ struct DwarfmodeDims {
+ int map_x1, map_x2, menu_x1, menu_x2, area_x1, area_x2;
+ int y1, y2;
+ bool menu_on, area_on, menu_forced;
+ };
+
+ DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims();
+
+ DFHACK_EXPORT void resetDwarfmodeView(bool pause = false);
+ DFHACK_EXPORT bool revealInDwarfmodeMap(df::coord pos, bool center = false);
+
DFHACK_EXPORT bool getViewCoords (int32_t &x, int32_t &y, int32_t &z);
DFHACK_EXPORT bool setViewCoords (const int32_t x, const int32_t y, const int32_t z);
diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h
index e6e9682eb..984cf16cf 100644
--- a/library/include/modules/Maps.h
+++ b/library/include/modules/Maps.h
@@ -50,6 +50,7 @@ distribution.
#include "df/tile_dig_designation.h"
#include "df/tile_traffic.h"
#include "df/feature_init.h"
+#include "df/flow_type.h"
/**
* \defgroup grp_maps Maps module and its types
@@ -232,6 +233,9 @@ extern DFHACK_EXPORT void getSize(uint32_t& x, uint32_t& y, uint32_t& z);
/// get the position of the map on world map
extern DFHACK_EXPORT void getPosition(int32_t& x, int32_t& y, int32_t& z);
+extern DFHACK_EXPORT bool isValidTilePos(int32_t x, int32_t y, int32_t z);
+inline bool isValidTilePos(df::coord pos) { return isValidTilePos(pos.x, pos.y, pos.z); }
+
/**
* Get the map block or NULL if block is not valid
*/
@@ -272,6 +276,8 @@ inline df::coord2d getTileBiomeRgn(df::coord pos) {
// Enables per-frame updates for liquid flow and/or temperature.
DFHACK_EXPORT void enableBlockUpdates(df::map_block *blk, bool flow = false, bool temperature = false);
+DFHACK_EXPORT df::flow_info *spawnFlow(df::coord pos, df::flow_type type, int mat_type = 0, int mat_index = -1, int density = 100);
+
/// sorts the block event vector into multiple vectors by type
/// mineral veins, what's under ice, blood smears and mud
extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block,
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index 2cbd019a6..a1e899761 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -46,6 +46,7 @@ end
-- Error handling
safecall = dfhack.safecall
+curry = dfhack.curry
function dfhack.pcall(f, ...)
return xpcall(f, dfhack.onerror, ...)
@@ -118,7 +119,12 @@ function defclass(class,parent)
if parent then
setmetatable(class, parent)
else
- rawset_default(class, { init_fields = rawset_default })
+ rawset_default(class, {
+ init_fields = rawset_default,
+ callback = function(self, name, ...)
+ return dfhack.curry(self[name], self, ...)
+ end
+ })
end
return class
end
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
index 9e189ea13..23904c14f 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -94,6 +94,9 @@ function Painter:isValidPos()
end
function Painter:viewport(x,y,w,h)
+ if type(x) == 'table' then
+ x,y,w,h = x.x1, x.y1, x.width, x.height
+ end
local x1,y1 = self.x1+x, self.y1+y
local x2,y2 = x1+w-1, y1+h-1
local vp = {
@@ -353,11 +356,16 @@ local function hint_coord(gap,hint)
end
end
+function FramedScreen:getWantedFrameSize()
+ return self.frame_width, self.frame_height
+end
+
function FramedScreen:updateFrameSize()
local sw, sh = dscreen.getWindowSize()
local iw, ih = sw-2, sh-2
- local width = math.min(self.frame_width or iw, iw)
- local height = math.min(self.frame_height or ih, ih)
+ local fw, fh = self:getWantedFrameSize()
+ local width = math.min(fw or iw, iw)
+ local height = math.min(fh or ih, ih)
local gw, gh = iw-width, ih-height
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
new file mode 100644
index 000000000..35720f871
--- /dev/null
+++ b/library/lua/gui/dialogs.lua
@@ -0,0 +1,178 @@
+-- Some simple dialog screens
+
+local _ENV = mkmodule('gui.dialogs')
+
+local gui = require('gui')
+local utils = require('utils')
+
+local dscreen = dfhack.screen
+
+MessageBox = defclass(MessageBox, gui.FramedScreen)
+
+MessageBox.focus_path = 'MessageBox'
+MessageBox.frame_style = gui.GREY_LINE_FRAME
+
+function MessageBox:init(info)
+ info = info or {}
+ self:init_fields{
+ text = info.text or {},
+ frame_title = info.title,
+ frame_width = info.frame_width,
+ on_accept = info.on_accept,
+ on_cancel = info.on_cancel,
+ on_close = info.on_close,
+ text_pen = info.text_pen
+ }
+ if type(self.text) == 'string' then
+ self.text = utils.split_string(self.text, "\n")
+ end
+ gui.FramedScreen.init(self, info)
+ return self
+end
+
+function MessageBox:getWantedFrameSize()
+ local text = self.text
+ local w = #(self.frame_title or '') + 4
+ w = math.max(w, 20)
+ w = math.max(self.frame_width or w, w)
+ for _, l in ipairs(text) do
+ w = math.max(w, #l)
+ end
+ local h = #text+1
+ if h > 1 then
+ h = h+1
+ end
+ return w+2, #text+2
+end
+
+function MessageBox:onRenderBody(dc)
+ if #self.text > 0 then
+ dc:newline(1):pen(self.text_pen or COLOR_GREY)
+ for _, l in ipairs(self.text or {}) do
+ dc:string(l):newline(1)
+ end
+ end
+
+ if self.on_accept then
+ local x,y = self.frame_rect.x1+1, self.frame_rect.y2+1
+ dscreen.paintString({fg=COLOR_LIGHTGREEN},x,y,'ESC')
+ dscreen.paintString({fg=COLOR_GREY},x+3,y,'/')
+ dscreen.paintString({fg=COLOR_LIGHTGREEN},x+4,y,'y')
+ end
+end
+
+function MessageBox:onDestroy()
+ if self.on_close then
+ self.on_close()
+ end
+end
+
+function MessageBox:onInput(keys)
+ if keys.MENU_CONFIRM then
+ self:dismiss()
+ if self.on_accept then
+ self.on_accept()
+ end
+ elseif keys.LEAVESCREEN or (keys.SELECT and not self.on_accept) then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ end
+end
+
+function showMessage(title, text, tcolor, on_close)
+ mkinstance(MessageBox):init{
+ text = text,
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ on_close = on_close
+ }:show()
+end
+
+function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
+ mkinstance(MessageBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ on_accept = on_accept,
+ on_cancel = on_cancel,
+ }:show()
+end
+
+InputBox = defclass(InputBox, MessageBox)
+
+InputBox.focus_path = 'InputBox'
+
+function InputBox:init(info)
+ info = info or {}
+ self:init_fields{
+ input = info.input or '',
+ input_pen = info.input_pen,
+ on_input = info.on_input,
+ }
+ MessageBox.init(self, info)
+ self.on_accept = nil
+ return self
+end
+
+function InputBox:getWantedFrameSize()
+ local mw, mh = MessageBox.getWantedFrameSize(self)
+ return mw, mh+2
+end
+
+function InputBox:onRenderBody(dc)
+ MessageBox.onRenderBody(self, dc)
+
+ dc:newline(1)
+ dc:pen(self.input_pen or COLOR_LIGHTCYAN)
+ dc:fill(dc.x1+1,dc.y,dc.x2-1,dc.y)
+
+ local cursor = '_'
+ if math.floor(dfhack.getTickCount()/300) % 2 == 0 then
+ cursor = ' '
+ end
+ local txt = self.input .. cursor
+ if #txt > dc.width-2 then
+ txt = string.sub(txt, #txt-dc.width+3)
+ -- Add prefix arrow
+ dc:advance(-1):char(27)
+ end
+ dc:string(txt)
+end
+
+function InputBox:onInput(keys)
+ if keys.SELECT then
+ self:dismiss()
+ if self.on_input then
+ self.on_input(self.input)
+ end
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ elseif keys._STRING then
+ if keys._STRING == 0 then
+ self.input = string.sub(self.input, 1, #self.input-1)
+ else
+ self.input = self.input .. string.char(keys._STRING)
+ end
+ end
+end
+
+function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
+ mkinstance(InputBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ input = input,
+ on_input = on_input,
+ on_cancel = on_cancel,
+ frame_width = min_width,
+ }:show()
+end
+
+
+return _ENV
diff --git a/library/lua/utils.lua b/library/lua/utils.lua
index 19a4e6f6a..9fa473ed8 100644
--- a/library/lua/utils.lua
+++ b/library/lua/utils.lua
@@ -381,6 +381,19 @@ function getBuildingCenter(building)
return xyz2pos(building.centerx, building.centery, building.z)
end
+function split_string(self, delimiter)
+ local result = { }
+ local from = 1
+ local delim_from, delim_to = string.find( self, delimiter, from )
+ while delim_from do
+ table.insert( result, string.sub( self, from , delim_from-1 ) )
+ from = delim_to + 1
+ delim_from, delim_to = string.find( self, delimiter, from )
+ end
+ table.insert( result, string.sub( self, from ) )
+ return result
+end
+
-- Ask a yes-no question
function prompt_yes_no(msg,default)
local prompt = msg
diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp
index 0f28860bf..1ea4bf687 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -43,6 +43,7 @@ using namespace DFHack;
#include "modules/Job.h"
#include "modules/Screen.h"
+#include "modules/Maps.h"
#include "DataDefs.h"
#include "df/world.h"
@@ -81,6 +82,8 @@ using namespace DFHack;
#include "df/graphic.h"
#include "df/layer_object_listst.h"
#include "df/assign_trade_status.h"
+#include "df/announcement_flags.h"
+#include "df/announcements.h"
using namespace df::enums;
using df::global::gview;
@@ -88,6 +91,9 @@ using df::global::init;
using df::global::gps;
using df::global::ui;
using df::global::world;
+using df::global::selection_rect;
+using df::global::ui_menu_width;
+using df::global::ui_area_map_width;
static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
{
@@ -921,8 +927,9 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet)
//
-void Gui::showAnnouncement(std::string message, int color, bool bright)
-{
+static void doShowAnnouncement(
+ df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
using df::global::world;
using df::global::cur_year;
using df::global::cur_year_tick;
@@ -948,6 +955,9 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
{
df::report *new_rep = new df::report();
+ new_rep->type = type;
+ new_rep->pos = pos;
+
new_rep->color = color;
new_rep->bright = bright;
new_rep->year = year;
@@ -969,7 +979,17 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
world->status.announcements.push_back(new_rep);
world->status.display_timer = 2000;
}
+}
+
+void Gui::showAnnouncement(std::string message, int color, bool bright)
+{
+ doShowAnnouncement(df::announcement_type(0), df::coord(), message, color, bright);
+}
+void Gui::showZoomAnnouncement(
+ df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
+ doShowAnnouncement(type, pos, message, color, bright);
}
void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
@@ -983,6 +1003,29 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
world->status.popups.push_back(popup);
}
+void Gui::showAutoAnnouncement(
+ df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
+ using df::global::announcements;
+
+ df::announcement_flags flags;
+ if (is_valid_enum_item(type) && announcements)
+ flags = announcements->flags[type];
+
+ doShowAnnouncement(type, pos, message, color, bright);
+
+ if (flags.bits.DO_MEGA || flags.bits.PAUSE || flags.bits.RECENTER)
+ {
+ resetDwarfmodeView(flags.bits.DO_MEGA || flags.bits.PAUSE);
+
+ if (flags.bits.RECENTER && pos.isValid())
+ revealInDwarfmodeMap(pos, true);
+ }
+
+ if (flags.bits.DO_MEGA)
+ showPopupAnnouncement(message, color, bright);
+}
+
df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
{
df::viewscreen * ws = &gview->view;
@@ -1015,6 +1058,110 @@ df::coord Gui::getCursorPos()
return df::coord(cursor->x, cursor->y, cursor->z);
}
+Gui::DwarfmodeDims Gui::getDwarfmodeViewDims()
+{
+ DwarfmodeDims dims;
+
+ auto ws = Screen::getWindowSize();
+ dims.y1 = 1;
+ dims.y2 = ws.y-2;
+ dims.map_x1 = 1;
+ dims.map_x2 = ws.x-2;
+ dims.area_x1 = dims.area_x2 = dims.menu_x1 = dims.menu_x2 = -1;
+ dims.menu_forced = false;
+
+ int menu_pos = (ui_menu_width ? *ui_menu_width : 2);
+ int area_pos = (ui_area_map_width ? *ui_area_map_width : 3);
+
+ if (ui && ui->main.mode && menu_pos >= area_pos)
+ {
+ dims.menu_forced = true;
+ menu_pos = area_pos-1;
+ }
+
+ dims.area_on = (area_pos < 3);
+ dims.menu_on = (menu_pos < area_pos);
+
+ if (dims.menu_on)
+ {
+ dims.menu_x2 = ws.x - 2;
+ dims.menu_x1 = dims.menu_x2 - Gui::MENU_WIDTH + 1;
+ if (menu_pos == 1)
+ dims.menu_x1 -= Gui::AREA_MAP_WIDTH + 1;
+ dims.map_x2 = dims.menu_x1 - 2;
+ }
+ if (dims.area_on)
+ {
+ dims.area_x2 = ws.x-2;
+ dims.area_x1 = dims.area_x2 - Gui::AREA_MAP_WIDTH + 1;
+ if (dims.menu_on)
+ dims.menu_x2 = dims.area_x1 - 2;
+ else
+ dims.map_x2 = dims.area_x1 - 2;
+ }
+
+ return dims;
+}
+
+void Gui::resetDwarfmodeView(bool pause)
+{
+ using df::global::cursor;
+
+ if (ui)
+ {
+ ui->follow_unit = -1;
+ ui->follow_item = -1;
+ ui->main.mode = ui_sidebar_mode::Default;
+ }
+
+ if (selection_rect)
+ {
+ selection_rect->start_x = -30000;
+ selection_rect->end_x = -30000;
+ }
+
+ if (cursor)
+ cursor->x = cursor->y = cursor->z = -30000;
+
+ if (pause && df::global::pause_state)
+ *df::global::pause_state = true;
+}
+
+bool Gui::revealInDwarfmodeMap(df::coord pos, bool center)
+{
+ using df::global::window_x;
+ using df::global::window_y;
+ using df::global::window_z;
+
+ if (!window_x || !window_y || !window_z || !world)
+ return false;
+ if (!Maps::isValidTilePos(pos))
+ return false;
+
+ auto dims = getDwarfmodeViewDims();
+ int w = dims.map_x2 - dims.map_x1 + 1;
+ int h = dims.y2 - dims.y1 + 1;
+
+ *window_z = pos.z;
+
+ if (center)
+ {
+ *window_x = pos.x - w/2;
+ *window_y = pos.y - h/2;
+ }
+ else
+ {
+ while (*window_x + w < pos.x+5) *window_x += 10;
+ while (*window_y + h < pos.y+5) *window_y += 10;
+ while (*window_x + 5 > pos.x) *window_x -= 10;
+ while (*window_y + 5 > pos.y) *window_y -= 10;
+ }
+
+ *window_x = std::max(0, std::min(*window_x, world->map.x_count-w));
+ *window_y = std::max(0, std::min(*window_y, world->map.y_count-h));
+ return true;
+}
+
bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z)
{
x = *df::global::window_x;
diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp
index 4107680b0..305f1296d 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -58,6 +58,7 @@ using namespace std;
#include "df/block_square_event_grassst.h"
#include "df/z_level_flags.h"
#include "df/region_map_entry.h"
+#include "df/flow_info.h"
using namespace DFHack;
using namespace df::enums;
@@ -138,13 +139,20 @@ df::map_block *Maps::getBlock (int32_t blockx, int32_t blocky, int32_t blockz)
return world->map.block_index[blockx][blocky][blockz];
}
-df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
+bool Maps::isValidTilePos(int32_t x, int32_t y, int32_t z)
{
if (!IsValid())
- return NULL;
+ return false;
if ((x < 0) || (y < 0) || (z < 0))
- return NULL;
+ return false;
if ((x >= world->map.x_count) || (y >= world->map.y_count) || (z >= world->map.z_count))
+ return false;
+ return true;
+}
+
+df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
+{
+ if (!isValidTilePos(x,y,z))
return NULL;
return world->map.block_index[x >> 4][y >> 4][z];
}
@@ -204,6 +212,26 @@ void Maps::enableBlockUpdates(df::map_block *blk, bool flow, bool temperature)
}
}
+df::flow_info *Maps::spawnFlow(df::coord pos, df::flow_type type, int mat_type, int mat_index, int density)
+{
+ using df::global::flows;
+
+ auto block = getTileBlock(pos);
+ if (!flows || !block)
+ return NULL;
+
+ auto flow = new df::flow_info();
+ flow->type = type;
+ flow->mat_type = mat_type;
+ flow->mat_index = mat_index;
+ flow->density = std::min(100, density);
+ flow->pos = pos;
+
+ block->flows.push_back(flow);
+ flows->push_back(flow);
+ return flow;
+}
+
df::feature_init *Maps::getGlobalInitFeature(int32_t index)
{
auto data = world->world_data;
diff --git a/library/modules/World.cpp b/library/modules/World.cpp
index 393e7cbfe..e14aa02a0 100644
--- a/library/modules/World.cpp
+++ b/library/modules/World.cpp
@@ -285,13 +285,13 @@ PersistentDataItem World::GetPersistentData(int entry_id)
PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
{
- *added = false;
+ if (added) *added = false;
PersistentDataItem rv = GetPersistentData(key);
if (!rv.isValid())
{
- *added = true;
+ if (added) *added = true;
rv = AddPersistentData(key);
}
diff --git a/library/xml b/library/xml
index 9b3ded158..df8178a98 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit 9b3ded15848e830784ef2dc4dea6093175669bc9
+Subproject commit df8178a989373ec7868d9195d82ae5f85145ef81
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index a2e520178..04da3e6c8 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -92,7 +92,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(seedwatch seedwatch.cpp)
DFHACK_PLUGIN(initflags initflags.cpp)
DFHACK_PLUGIN(stockpiles stockpiles.cpp)
- DFHACK_PLUGIN(rename rename.cpp PROTOBUFS rename)
+ DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(workflow workflow.cpp)
DFHACK_PLUGIN(showmood showmood.cpp)
diff --git a/plugins/devel/building_zsteam_engine.txt b/plugins/devel/building_steam_engine.txt
similarity index 57%
rename from plugins/devel/building_zsteam_engine.txt
rename to plugins/devel/building_steam_engine.txt
index f76b237d5..48657b0c1 100644
--- a/plugins/devel/building_zsteam_engine.txt
+++ b/plugins/devel/building_steam_engine.txt
@@ -1,4 +1,4 @@
-building_zsteam_engine
+building_steam_engine
[OBJECT:BUILDING]
@@ -15,33 +15,36 @@ building_zsteam_engine
[TILE:0:1:240:' ':254]
[TILE:0:2:' ':' ':128]
[TILE:0:3:246:' ':' ']
- [COLOR:0:1:MAT:0:0:0:7:0:0]
+ [COLOR:0:1:6:0:0:0:0:0:7:0:0]
[COLOR:0:2:0:0:0:0:0:0:7:0:0]
- [COLOR:0:3:6:0:0:0:0:0:0:0:0]
+ [COLOR:0:3:MAT:0:0:0:0:0:0]
[TILE:1:1:246:128:' ']
[TILE:1:2:' ':' ':254]
- [TILE:1:3:254:240:240]
- [COLOR:1:1:6:0:0:7:0:0:0:0:0]
+ [TILE:1:3:254:'/':240]
+ [COLOR:1:1:MAT:7:0:0:0:0:0]
[COLOR:1:2:0:0:0:0:0:0:7:0:0]
- [COLOR:1:3:7:0:0:MAT:MAT]
+ [COLOR:1:3:7:0:0:6:0:0:6:0:0]
[TILE:2:1:21:' ':128]
[TILE:2:2:128:' ':246]
[TILE:2:3:177:19:177]
[COLOR:2:1:6:0:0:0:0:0:7:0:0]
- [COLOR:2:2:7:0:0:0:0:0:6:0:0]
- [COLOR:2:3:7:0:0:MAT:7:0:0]
+ [COLOR:2:2:7:0:0:0:0:0:MAT]
+ [COLOR:2:3:7:0:0:6:0:0:7:0:0]
+ Tile 15 marks places where machines can connect.
+ Tile 19 marks the hearth (color changed to reflect power).
[TILE:3:1:15:246:15]
[TILE:3:2:'\':19:'/']
[TILE:3:3:7:' ':7]
- [COLOR:3:1:6:0:0:6:0:0:6:0:0]
- [COLOR:3:2:6:7:0:0:0:1:6:7:0]
+ Color 1:?:1 water indicator, 4:?:1 magma indicator:
+ [COLOR:3:1:7:0:0:MAT:7:0:0]
+ [COLOR:3:2:6:0:0:0:0:1:6:0:0]
[COLOR:3:3:1:7:1:0:0:0:4:7:1]
[BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:WEAPON:WEAPON_MACE:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:2:BLOCKS:NONE:NONE:NONE][BUILDMAT][FIRE_BUILD_SAFE]
+ [BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][FIRE_BUILD_SAFE]
[BUILDING_WORKSHOP:MAGMA_STEAM_ENGINE]
[NAME:Magma Steam Engine]
@@ -57,30 +60,33 @@ building_zsteam_engine
[TILE:0:1:240:' ':254]
[TILE:0:2:' ':' ':128]
[TILE:0:3:246:' ':' ']
- [COLOR:0:1:MAT:0:0:0:7:0:0]
+ [COLOR:0:1:6:0:0:0:0:0:7:0:0]
[COLOR:0:2:0:0:0:0:0:0:7:0:0]
- [COLOR:0:3:6:0:0:0:0:0:0:0:0]
+ [COLOR:0:3:MAT:0:0:0:0:0:0]
[TILE:1:1:246:128:' ']
[TILE:1:2:' ':' ':254]
- [TILE:1:3:254:240:240]
- [COLOR:1:1:6:0:0:7:0:0:0:0:0]
+ [TILE:1:3:254:'/':240]
+ [COLOR:1:1:MAT:7:0:0:0:0:0]
[COLOR:1:2:0:0:0:0:0:0:7:0:0]
- [COLOR:1:3:7:0:0:MAT:MAT]
+ [COLOR:1:3:7:0:0:6:0:0:6:0:0]
[TILE:2:1:21:' ':128]
[TILE:2:2:128:' ':246]
[TILE:2:3:177:19:177]
[COLOR:2:1:6:0:0:0:0:0:7:0:0]
- [COLOR:2:2:7:0:0:0:0:0:6:0:0]
- [COLOR:2:3:7:0:0:MAT:7:0:0]
+ [COLOR:2:2:7:0:0:0:0:0:MAT]
+ [COLOR:2:3:7:0:0:6:0:0:7:0:0]
+ Tile 15 marks places where machines can connect.
+ Tile 19 marks the hearth (color changed to reflect power).
[TILE:3:1:15:246:15]
[TILE:3:2:'\':19:'/']
[TILE:3:3:7:' ':7]
- [COLOR:3:1:6:0:0:6:0:0:6:0:0]
- [COLOR:3:2:6:7:0:0:0:1:6:7:0]
+ Color 1:?:1 water indicator, 4:?:1 magma indicator:
+ [COLOR:3:1:7:0:0:MAT:7:0:0]
+ [COLOR:3:2:6:0:0:0:0:1:6:0:0]
[COLOR:3:3:1:7:1:0:0:0:4:7:1]
[BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:WEAPON:WEAPON_MACE:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:2:BLOCKS:NONE:NONE:NONE][BUILDMAT][MAGMA_BUILD_SAFE]
+ [BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][MAGMA_BUILD_SAFE]
diff --git a/plugins/devel/item_trapcomp_steam_engine.txt b/plugins/devel/item_trapcomp_steam_engine.txt
new file mode 100644
index 000000000..c35f6ef45
--- /dev/null
+++ b/plugins/devel/item_trapcomp_steam_engine.txt
@@ -0,0 +1,12 @@
+item_trapcomp_steam_engine
+
+[OBJECT:ITEM]
+
+[ITEM_TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
+[NAME:piston:pistons]
+[ADJECTIVE:heavy]
+[SIZE:1800]
+[HITS:1]
+[MATERIAL_SIZE:6]
+[METAL]
+[ATTACK:BLUNT:40:200:bash:bashes:NO_SUB:2000]
diff --git a/plugins/devel/reaction_steam_engine.txt b/plugins/devel/reaction_steam_engine.txt
new file mode 100644
index 000000000..175ffdd50
--- /dev/null
+++ b/plugins/devel/reaction_steam_engine.txt
@@ -0,0 +1,14 @@
+reaction_steam_engine
+
+[OBJECT:REACTION]
+
+[REACTION:STOKE_BOILER]
+ [NAME:stoke the boiler]
+ [BUILDING:STEAM_ENGINE:CUSTOM_S]
+ [BUILDING:MAGMA_STEAM_ENGINE:CUSTOM_S]
+ [FUEL]
+ [SKILL:SMELT]
+ Dimension is the number of days it can produce 100 power * 100.
+ I.e. with 2000 it means energy of 1 job = 1 water wheel for 20 days.
+ [PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:2000]
+
diff --git a/plugins/devel/reaction_zsteam_engine.txt b/plugins/devel/reaction_zsteam_engine.txt
deleted file mode 100644
index b8267cf55..000000000
--- a/plugins/devel/reaction_zsteam_engine.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-reaction_other
-
-[OBJECT:REACTION]
-
-[REACTION:STOKE_BOILER]
- [NAME:stoke the boiler]
- [BUILDING:STEAM_ENGINE:CUSTOM_S]
- [BUILDING:MAGMA_STEAM_ENGINE:CUSTOM_S]
- [FUEL]
- [PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:333]
- [SKILL:SMELT]
-
diff --git a/plugins/devel/steam-engine.cpp b/plugins/devel/steam-engine.cpp
index 23af12177..cacfc6e16 100644
--- a/plugins/devel/steam-engine.cpp
+++ b/plugins/devel/steam-engine.cpp
@@ -4,6 +4,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -22,9 +24,92 @@
#include "df/world.h"
#include "df/buildings_other_id.h"
#include "df/machine.h"
+#include "df/job.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
#include "MiscUtils.h"
+/*
+ * This plugin implements a steam engine workshop. It activates
+ * if there are any workshops in the raws with STEAM_ENGINE in
+ * their token, and provides the necessary behavior.
+ *
+ * Construction:
+ *
+ * The workshop needs water as its input, which it takes via a
+ * passable floor tile below it, like usual magma workshops do.
+ * The magma version also needs magma.
+ *
+ * ISSUE: Since this building is a machine, and machine collapse
+ * code cannot be modified, it would collapse over true open space.
+ * As a loophole, down stair provides support to machines, while
+ * being passable, so use them.
+ *
+ * After constructing the building itself, machines can be connected
+ * to the edge tiles that look like gear boxes. Their exact position
+ * is extracted from the workshop raws.
+ *
+ * ISSUE: Like with collapse above, part of the code involved in
+ * machine connection cannot be modified. As a result, the workshop
+ * can only immediately connect to machine components built AFTER it.
+ * This also means that engines cannot be chained without intermediate
+ * short axles that can be built later.
+ *
+ * Operation:
+ *
+ * In order to operate the engine, queue the Stoke Boiler job.
+ * A furnace operator will come, possibly bringing a bar of fuel,
+ * and perform it. As a result, a "boiling water" item will appear
+ * in the 't' view of the workshop.
+ *
+ * Note: The completion of the job will actually consume one unit
+ * of appropriate liquids from below the workshop.
+ *
+ * Every such item gives 100 power, up to a limit of 300 for coal,
+ * and 500 for a magma engine. The building can host twice that
+ * amount of items to provide longer autonomous running. When the
+ * boiler gets filled to capacity, all queued jobs are suspended;
+ * once it drops back to 3+1 or 5+1 items, they are re-enabled.
+ *
+ * While the engine is providing power, steam is being consumed.
+ * The consumption speed includes a fixed 10% waste rate, and
+ * the remaining 90% are applied proportionally to the actual
+ * load in the machine. With the engine at nominal 300 power with
+ * 150 load in the system, it will consume steam for actual
+ * 300*(10% + 90%*150/300) = 165 power.
+ *
+ * Masterpiece mechanism and chain will decrease the mechanical
+ * power drawn by the engine itself from 10 to 5. Masterpiece
+ * barrel decreases waste rate by 4%. Masterpiece piston and pipe
+ * decrease it by further 4%, and also decrease the whole steam
+ * use rate by 10%.
+ *
+ * Explosions:
+ *
+ * The engine must be constructed using barrel, pipe and piston
+ * from fire-safe, or in the magma version magma-safe metals.
+ *
+ * During operation weak parts get gradually worn out, and
+ * eventually the engine explodes. It should also explode if
+ * toppled during operation by a building destroyer, or a
+ * tantruming dwarf.
+ *
+ * Save files:
+ *
+ * It should be safe to load and view fortresses using engines
+ * from a DF version without DFHack installed, except that in such
+ * case the engines won't work. However actually making modifications
+ * to them, or machines they connect to (including by pulling levers),
+ * can easily result in inconsistent state once this plugin is
+ * available again. The effects may be as weird as negative power
+ * being generated.
+ */
+
using std::vector;
using std::string;
using std::stack;
@@ -33,13 +118,23 @@ using namespace df::enums;
using df::global::gps;
using df::global::world;
+using df::global::ui;
using df::global::ui_build_selector;
DFHACK_PLUGIN("steam-engine");
+/*
+ * List of known steam engine workshop raws.
+ */
+
struct steam_engine_workshop {
int id;
df::building_def_workshopst *def;
+ // Cached properties
+ bool is_magma;
+ int max_power, max_capacity;
+ int wear_temp;
+ // Special tiles (relative position)
std::vector gear_tiles;
df::coord2d hearth_tile;
df::coord2d water_tile;
@@ -48,42 +143,268 @@ struct steam_engine_workshop {
std::vector engines;
+steam_engine_workshop *find_steam_engine(int id)
+{
+ for (size_t i = 0; i < engines.size(); i++)
+ if (engines[i].id == id)
+ return &engines[i];
+
+ return NULL;
+}
+
+/*
+ * Misc utilities.
+ */
+
+static const int hearth_colors[6][2] = {
+ { COLOR_BLACK, 1 },
+ { COLOR_BROWN, 0 },
+ { COLOR_RED, 0 },
+ { COLOR_RED, 1 },
+ { COLOR_BROWN, 1 },
+ { COLOR_GREY, 1 }
+};
+
+void enable_updates_at(df::coord pos, bool flow, bool temp)
+{
+ static const int delta[4][2] = { { -1, -1 }, { 1, -1 }, { -1, 1 }, { 1, 1 } };
+
+ for (int i = 0; i < 4; i++)
+ {
+ auto blk = Maps::getTileBlock(pos.x+delta[i][0], pos.y+delta[i][1], pos.z);
+ Maps::enableBlockUpdates(blk, flow, temp);
+ }
+}
+
+void decrement_flow(df::coord pos, int amount)
+{
+ auto pldes = Maps::getTileDesignation(pos);
+ if (!pldes) return;
+
+ int nsize = std::max(0, int(pldes->bits.flow_size - amount));
+ pldes->bits.flow_size = nsize;
+ pldes->bits.flow_forbid = (nsize > 3 || pldes->bits.liquid_type == tile_liquid::Magma);
+
+ enable_updates_at(pos, true, false);
+}
+
+void make_explosion(df::coord center, int power)
+{
+ static const int bias[9] = {
+ 60, 30, 60,
+ 30, 0, 30,
+ 60, 30, 60
+ };
+
+ int mat_type = builtin_mats::WATER, mat_index = -1;
+ int i = 0;
+
+ for (int dx = -1; dx <= 1; dx++)
+ {
+ for (int dy = -1; dy <= 1; dy++)
+ {
+ int size = power - bias[i++];
+ auto pos = center + df::coord(dx,dy,0);
+
+ if (size > 0)
+ Maps::spawnFlow(pos, flow_type::MaterialDust, mat_type, mat_index, size);
+ }
+ }
+
+ Gui::showAutoAnnouncement(
+ announcement_type::CAVE_COLLAPSE, center,
+ "A boiler has exploded!", COLOR_RED, true
+ );
+}
+
+static const int WEAR_TICKS = 806400;
+
+bool add_wear_nodestroy(df::item_actual *item, int rate)
+{
+ if (item->incWearTimer(rate))
+ {
+ while (item->wear_timer >= WEAR_TICKS)
+ {
+ item->wear_timer -= WEAR_TICKS;
+ item->wear++;
+ }
+ }
+
+ return item->wear > 3;
+}
+
+/*
+ * Hook for the liquid item. Implements a special 'boiling'
+ * matter state with a modified description and temperature
+ * locked at boiling-1.
+ */
+
+struct liquid_hook : df::item_liquid_miscst {
+ typedef df::item_liquid_miscst interpose_base;
+
+ static const uint32_t BOILING_FLAG = 0x80000000U;
+
+ DEFINE_VMETHOD_INTERPOSE(void, getItemDescription, (std::string *buf, int8_t mode))
+ {
+ if (mat_state.whole & BOILING_FLAG)
+ buf->append("boiling ");
+
+ INTERPOSE_NEXT(getItemDescription)(buf, mode);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t unk))
+ {
+ if (mat_state.whole & BOILING_FLAG)
+ temp = std::max(int(temp), getBoilingPoint()-1);
+
+ return INTERPOSE_NEXT(adjustTemperature)(temp, unk);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkTemperatureDamage, ())
+ {
+ if (mat_state.whole & BOILING_FLAG)
+ temperature = std::max(int(temperature), getBoilingPoint()-1);
+
+ return INTERPOSE_NEXT(checkTemperatureDamage)();
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, getItemDescription);
+IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, adjustTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, checkTemperatureDamage);
+
+/*
+ * Hook for the workshop itself. Implements core logic.
+ */
+
struct workshop_hook : df::building_workshopst {
typedef df::building_workshopst interpose_base;
+ // Engine detection
+
steam_engine_workshop *get_steam_engine()
{
if (type == workshop_type::Custom)
- for (size_t i = 0; i < engines.size(); i++)
- if (engines[i].id == custom_type)
- return &engines[i];
+ return find_steam_engine(custom_type);
return NULL;
}
+ inline bool is_fully_built()
+ {
+ return getBuildStage() >= getMaxBuildStage();
+ }
+
+ // Use high bits of flags to store current steam amount.
+ // This is necessary for consistency if items disappear unexpectedly.
+
int get_steam_amount()
{
- int cnt = 0;
+ return (flags.whole >> 28) & 15;
+ }
- for (size_t i = 0; i < contained_items.size(); i++)
+ void set_steam_amount(int count)
+ {
+ flags.whole = (flags.whole & 0x0FFFFFFFU) | uint32_t((count & 15) << 28);
+ }
+
+ // Find liquids to consume below the engine.
+
+ bool find_liquids(df::coord *pwater, df::coord *pmagma, bool is_magma, int min_level)
+ {
+ if (!is_magma)
+ pmagma = NULL;
+
+ for (int x = x1; x <= x2; x++)
{
- if (contained_items[i]->use_mode == 0 &&
- contained_items[i]->item->flags.bits.in_building)
- cnt++;
+ for (int y = y1; y <= y2; y++)
+ {
+ auto ptile = Maps::getTileType(x,y,z);
+ if (!ptile || !LowPassable(*ptile))
+ continue;
+
+ auto pltile = Maps::getTileType(x,y,z-1);
+ if (!pltile || !FlowPassable(*pltile))
+ continue;
+
+ auto pldes = Maps::getTileDesignation(x,y,z-1);
+ if (!pldes || pldes->bits.flow_size < min_level)
+ continue;
+
+ if (pldes->bits.liquid_type == tile_liquid::Magma)
+ {
+ if (pmagma)
+ *pmagma = df::coord(x,y,z-1);
+ if (pwater->isValid())
+ return true;
+ }
+ else
+ {
+ *pwater = df::coord(x,y,z-1);
+ if (!pmagma || pmagma->isValid())
+ return true;
+ }
+ }
}
- return cnt;
+ return false;
}
- int get_power_output(steam_engine_workshop *engine)
+ // Absorbs a water item produced by stoke reaction into the engine.
+
+ bool absorb_unit(steam_engine_workshop *engine, df::item_liquid_miscst *liquid)
{
- int maxv = engine->def->needs_magma ? 5 : 3;
- return std::min(get_steam_amount(), maxv)*100;
+ // Consume liquid inputs
+ df::coord water, magma;
+
+ if (!find_liquids(&water, &magma, engine->is_magma, 1))
+ {
+ // Destroy the item with enormous wear amount.
+ liquid->addWear(WEAR_TICKS*5, true, false);
+ return false;
+ }
+
+ decrement_flow(water, 1);
+ if (engine->is_magma)
+ decrement_flow(magma, 1);
+
+ // Update flags
+ liquid->flags.bits.in_building = true;
+ liquid->mat_state.whole |= liquid_hook::BOILING_FLAG;
+ liquid->temperature = liquid->getBoilingPoint()-1;
+ liquid->temperature_fraction = 0;
+
+ // This affects where the steam appears to come from
+ if (engine->hearth_tile.isValid())
+ liquid->pos = df::coord(x1+engine->hearth_tile.x, y1+engine->hearth_tile.y, z);
+
+ // Enable block temperature updates
+ enable_updates_at(liquid->pos, false, true);
+ return true;
}
- df::item_liquid_miscst *collect_steam()
+ bool boil_unit(df::item_liquid_miscst *liquid)
+ {
+ liquid->wear = 4;
+ liquid->flags.bits.in_building = false;
+ liquid->temperature = liquid->getBoilingPoint() + 10;
+
+ return liquid->checkMeltBoil();
+ }
+
+ void suspend_jobs(bool suspend)
+ {
+ for (size_t i = 0; i < jobs.size(); i++)
+ if (jobs[i]->job_type == job_type::CustomReaction)
+ jobs[i]->flags.bits.suspend = suspend;
+ }
+
+ // Scan contained items for boiled steam to absorb.
+
+ df::item_liquid_miscst *collect_steam(steam_engine_workshop *engine, int *count)
{
df::item_liquid_miscst *first = NULL;
+ *count = 0;
for (int i = contained_items.size()-1; i >= 0; i--)
{
@@ -98,19 +419,194 @@ struct workshop_hook : df::building_workshopst {
if (!liquid->flags.bits.in_building)
{
if (liquid->mat_type != builtin_mats::WATER ||
- liquid->dimension != 333 ||
+ liquid->age > 1 ||
liquid->wear != 0)
continue;
- liquid->flags.bits.in_building = true;
+ // This may destroy the item
+ if (!absorb_unit(engine, liquid))
+ continue;
}
- first = liquid;
+ if (*count < engine->max_capacity)
+ {
+ first = liquid;
+ ++*count;
+ }
+ else
+ {
+ // Overpressure valve
+ boil_unit(liquid);
+ suspend_jobs(true);
+ }
}
return first;
}
+ void random_boil()
+ {
+ int cnt = 0;
+
+ for (int i = contained_items.size()-1; i >= 0; i--)
+ {
+ auto item = contained_items[i];
+ if (item->use_mode != 0 || !item->item->flags.bits.in_building)
+ continue;
+
+ auto liquid = strict_virtual_cast(item->item);
+ if (!liquid)
+ continue;
+
+ if (cnt == 0 || rand() < RAND_MAX/2)
+ {
+ cnt++;
+ boil_unit(liquid);
+ }
+ }
+ }
+
+ int classify_component(df::building_actual::T_contained_items *item)
+ {
+ if (item->use_mode != 2 || item->item->isBuildMat())
+ return -1;
+
+ switch (item->item->getType())
+ {
+ case item_type::TRAPPARTS:
+ case item_type::CHAIN:
+ return 0;
+ case item_type::BARREL:
+ return 2;
+ default:
+ return 1;
+ }
+ }
+
+ bool check_component_wear(steam_engine_workshop *engine, int count, int power)
+ {
+ int coeffs[3] = { 0, power, count };
+
+ for (int i = contained_items.size()-1; i >= 0; i--)
+ {
+ int type = classify_component(contained_items[i]);
+ if (type < 0)
+ continue;
+
+ df::item *item = contained_items[i]->item;
+ int melt_temp = item->getMeltingPoint();
+ if (coeffs[type] == 0 || melt_temp >= engine->wear_temp)
+ continue;
+
+ // let 500 degree delta at 4 pressure work 1 season
+ float ticks = coeffs[type]*(engine->wear_temp - melt_temp)*3.0f/500.0f/4.0f;
+ if (item->addWear(int(8*(1 + ticks)), true, true))
+ return true;
+ }
+
+ return false;
+ }
+
+ float get_component_quality(int use_type)
+ {
+ float sum = 0, cnt = 0;
+
+ for (size_t i = 0; i < contained_items.size(); i++)
+ {
+ int type = classify_component(contained_items[i]);
+ if (type != use_type)
+ continue;
+
+ sum += contained_items[i]->item->getQuality();
+ cnt += 1;
+ }
+
+ return (cnt > 0 ? sum/cnt : 0);
+ }
+
+ int get_steam_use_rate(steam_engine_workshop *engine, int dimension, int power_level)
+ {
+ // total ticks to wear off completely
+ float ticks = WEAR_TICKS * 4.0f;
+ // dimension == days it lasts * 100
+ ticks /= 1200.0f * dimension / 100.0f;
+ // true power use
+ float power_rate = 1.0f;
+ // check the actual load
+ if (auto mptr = df::machine::find(machine.machine_id))
+ {
+ if (mptr->cur_power >= mptr->min_power)
+ power_rate = float(mptr->min_power) / mptr->cur_power;
+ else
+ power_rate = 0.0f;
+ }
+ // waste rate: 1-10% depending on piston assembly quality
+ float piston_qual = get_component_quality(1);
+ float waste = 0.1f - 0.016f * 0.5f * (piston_qual + get_component_quality(2));
+ float efficiency_coeff = 1.0f - 0.02f * piston_qual;
+ // apply rate and waste factor
+ ticks *= (waste + 0.9f*power_rate)*power_level*efficiency_coeff;
+ // end result
+ return std::max(1, int(ticks));
+ }
+
+ void update_under_construction(steam_engine_workshop *engine)
+ {
+ if (machine.machine_id != -1)
+ return;
+
+ int cur_count = 0;
+
+ if (auto first = collect_steam(engine, &cur_count))
+ {
+ if (add_wear_nodestroy(first, WEAR_TICKS*4/10))
+ {
+ boil_unit(first);
+ cur_count--;
+ }
+ }
+
+ set_steam_amount(cur_count);
+ }
+
+ void update_working(steam_engine_workshop *engine)
+ {
+ int old_count = get_steam_amount();
+ int old_power = std::min(engine->max_power, old_count);
+ int cur_count = 0;
+
+ if (auto first = collect_steam(engine, &cur_count))
+ {
+ int rate = get_steam_use_rate(engine, first->dimension, old_power);
+
+ if (add_wear_nodestroy(first, rate))
+ {
+ boil_unit(first);
+ cur_count--;
+ }
+
+ if (check_component_wear(engine, old_count, old_power))
+ return;
+ }
+
+ if (old_count < engine->max_capacity && cur_count == engine->max_capacity)
+ suspend_jobs(true);
+ else if (cur_count <= engine->max_power+1 && old_count > engine->max_power+1)
+ suspend_jobs(false);
+
+ set_steam_amount(cur_count);
+
+ int cur_power = std::min(engine->max_power, cur_count);
+ if (cur_power != old_power)
+ {
+ auto mptr = df::machine::find(machine.machine_id);
+ if (mptr)
+ mptr->cur_power += (cur_power - old_power)*100;
+ }
+ }
+
+ // Furnaces need architecture, and this is a workshop
+ // only because furnaces cannot connect to machines.
DEFINE_VMETHOD_INTERPOSE(bool, needsDesign, ())
{
if (get_steam_engine())
@@ -119,12 +615,13 @@ struct workshop_hook : df::building_workshopst {
return INTERPOSE_NEXT(needsDesign)();
}
+ // Machine interface
DEFINE_VMETHOD_INTERPOSE(void, getPowerInfo, (df::power_info *info))
{
if (auto engine = get_steam_engine())
{
- info->produced = get_power_output(engine);
- info->consumed = 10;
+ info->produced = std::min(engine->max_power, get_steam_amount())*100;
+ info->consumed = 10 - int(get_component_quality(0));
return;
}
@@ -178,6 +675,7 @@ struct workshop_hook : df::building_workshopst {
for (size_t i = 0; i < engine->gear_tiles.size(); i++)
{
+ // the original function connects to the center tile
centerx = x1 + engine->gear_tiles[i].x;
centery = y1 + engine->gear_tiles[i].y;
@@ -195,40 +693,106 @@ struct workshop_hook : df::building_workshopst {
return INTERPOSE_NEXT(canConnectToMachine)(info);
}
+ // Operation logic
+ DEFINE_VMETHOD_INTERPOSE(bool, isUnpowered, ())
+ {
+ if (auto engine = get_steam_engine())
+ {
+ df::coord water, magma;
+ return !find_liquids(&water, &magma, engine->is_magma, 3);
+ }
+
+ return INTERPOSE_NEXT(isUnpowered)();
+ }
+
DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
{
if (auto engine = get_steam_engine())
{
- int output = get_power_output(engine);
+ if (is_fully_built())
+ update_working(engine);
+ else
+ update_under_construction(engine);
+
+ if (flags.bits.almost_deleted)
+ return;
+ }
+
+ INTERPOSE_NEXT(updateAction)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk))
+ {
+ INTERPOSE_NEXT(drawBuilding)(db, unk);
+
+ if (auto engine = get_steam_engine())
+ {
+ if (!is_fully_built())
+ return;
- if (auto first = collect_steam())
+ // If machine is running, tweak gear assemblies
+ auto mptr = df::machine::find(machine.machine_id);
+ if (mptr && (mptr->visual_phase & 1) != 0)
{
- if (first->incWearTimer(output))
+ for (size_t i = 0; i < engine->gear_tiles.size(); i++)
{
- while (first->wear_timer >= 806400)
- {
- first->wear_timer -= 806400;
- first->wear++;
- }
+ auto pos = engine->gear_tiles[i];
+ db->tile[pos.x][pos.y] = 42;
+ }
+ }
- if (first->wear > 3)
- {
- first->flags.bits.in_building = 0;
- first->temperature = first->getBoilingPoint()+50;
- }
+ // Use the hearth color to display power level
+ if (engine->hearth_tile.isValid())
+ {
+ auto pos = engine->hearth_tile;
+ int power = std::min(engine->max_power, get_steam_amount());
+ db->fore[pos.x][pos.y] = hearth_colors[power][0];
+ db->bright[pos.x][pos.y] = hearth_colors[power][1];
+ }
+
+ // Set liquid indicator state
+ if (engine->water_tile.isValid() || engine->magma_tile.isValid())
+ {
+ df::coord water, magma;
+ find_liquids(&water, &magma, engine->is_magma, 3);
+ df::coord dwater, dmagma;
+ find_liquids(&dwater, &dmagma, engine->is_magma, 5);
+
+ if (engine->water_tile.isValid())
+ {
+ if (!water.isValid())
+ db->fore[engine->water_tile.x][engine->water_tile.y] = 0;
+ else if (!dwater.isValid())
+ db->bright[engine->water_tile.x][engine->water_tile.y] = 0;
+ }
+ if (engine->magma_tile.isValid() && engine->is_magma)
+ {
+ if (!magma.isValid())
+ db->fore[engine->magma_tile.x][engine->magma_tile.y] = 0;
+ else if (!dmagma.isValid())
+ db->bright[engine->magma_tile.x][engine->magma_tile.y] = 0;
}
}
+ }
+ }
- int new_out = get_power_output(engine);
- if (new_out != output)
+ DEFINE_VMETHOD_INTERPOSE(void, deconstructItems, (bool noscatter, bool lost))
+ {
+ if (get_steam_engine())
+ {
+ // Explode if any steam left
+ if (int amount = get_steam_amount())
{
- auto mptr = df::machine::find(machine.machine_id);
- if (mptr)
- mptr->cur_power += (new_out - output);
+ make_explosion(
+ df::coord((x1+x2)/2, (y1+y2)/2, z),
+ 40 + amount * 20
+ );
+
+ random_boil();
}
}
- INTERPOSE_NEXT(updateAction)();
+ INTERPOSE_NEXT(deconstructItems)(noscatter, lost);
}
};
@@ -239,9 +803,95 @@ IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isPowerSource);
IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, categorize);
IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, uncategorize);
IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, canConnectToMachine);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isUnpowered);
IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, updateAction);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, drawBuilding);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, deconstructItems);
+
+/*
+ * Hook for the dwarfmode screen. Tweaks the build menu
+ * behavior to suit the steam engine building more.
+ */
+
+struct dwarfmode_hook : df::viewscreen_dwarfmodest
+{
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ steam_engine_workshop *get_steam_engine()
+ {
+ if (ui->main.mode == ui_sidebar_mode::Build &&
+ ui_build_selector->stage == 1 &&
+ ui_build_selector->building_type == building_type::Workshop &&
+ ui_build_selector->building_subtype == workshop_type::Custom)
+ {
+ return find_steam_engine(ui_build_selector->custom_type);
+ }
+
+ return NULL;
+ }
+
+ void check_hanging_tiles(steam_engine_workshop *engine)
+ {
+ using df::global::cursor;
+
+ if (!engine) return;
-static void find_engines()
+ bool error = false;
+
+ int x1 = cursor->x - engine->def->workloc_x;
+ int y1 = cursor->y - engine->def->workloc_y;
+
+ for (int x = 0; x < engine->def->dim_x; x++)
+ {
+ for (int y = 0; y < engine->def->dim_y; y++)
+ {
+ if (ui_build_selector->tiles[x][y] >= 5)
+ continue;
+
+ auto ptile = Maps::getTileType(x1+x,y1+y,cursor->z);
+ if (ptile && !isOpenTerrain(*ptile))
+ continue;
+
+ ui_build_selector->tiles[x][y] = 6;
+ error = true;
+ }
+ }
+
+ if (error)
+ {
+ const char *msg = "Hanging - cover channels with down stairs.";
+ ui_build_selector->errors.push_back(new std::string(msg));
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input))
+ {
+ steam_engine_workshop *engine = get_steam_engine();
+
+ // Selector insists that workshops cannot be placed hanging
+ // unless they require magma, so pretend we always do.
+ if (engine)
+ engine->def->needs_magma = true;
+
+ INTERPOSE_NEXT(feed)(input);
+
+ // Restore the flag
+ if (engine)
+ engine->def->needs_magma = engine->is_magma;
+
+ // And now, check for open space. Since these workshops
+ // are machines, they will collapse over true open space.
+ check_hanging_tiles(get_steam_engine());
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(dwarfmode_hook, feed);
+
+/*
+ * Scan raws for matching workshop buildings.
+ */
+
+static bool find_engines()
{
engines.clear();
@@ -261,70 +911,78 @@ static void find_engines()
{
for (int y = 0; y < ws.def->dim_y; y++)
{
- if (ws.def->tile[bs][x][y] == 15)
+ switch (ws.def->tile[bs][x][y])
+ {
+ case 15:
ws.gear_tiles.push_back(df::coord2d(x,y));
+ break;
+ case 19:
+ ws.hearth_tile = df::coord2d(x,y);
+ break;
+ }
if (ws.def->tile_color[2][bs][x][y])
{
switch (ws.def->tile_color[0][bs][x][y])
{
- case 0:
- ws.hearth_tile = df::coord2d(x,y);
- break;
case 1:
ws.water_tile = df::coord2d(x,y);
break;
case 4:
ws.magma_tile = df::coord2d(x,y);
break;
- default:
- break;
}
}
}
}
- engines.push_back(ws);
+ ws.is_magma = ws.def->needs_magma;
+ ws.max_power = ws.is_magma ? 5 : 3;
+ ws.max_capacity = ws.is_magma ? 10 : 6;
+ ws.wear_temp = ws.is_magma ? 12000 : 11000;
+
+ if (!ws.gear_tiles.empty())
+ engines.push_back(ws);
}
-}
-static void enable_hooks()
-{
- INTERPOSE_HOOK(workshop_hook, needsDesign).apply();
- INTERPOSE_HOOK(workshop_hook, getPowerInfo).apply();
- INTERPOSE_HOOK(workshop_hook, getMachineInfo).apply();
- INTERPOSE_HOOK(workshop_hook, isPowerSource).apply();
- INTERPOSE_HOOK(workshop_hook, categorize).apply();
- INTERPOSE_HOOK(workshop_hook, uncategorize).apply();
- INTERPOSE_HOOK(workshop_hook, canConnectToMachine).apply();
- INTERPOSE_HOOK(workshop_hook, updateAction).apply();
+ return !engines.empty();
}
-static void disable_hooks()
+static void enable_hooks(bool enable)
{
- INTERPOSE_HOOK(workshop_hook, needsDesign).remove();
- INTERPOSE_HOOK(workshop_hook, getPowerInfo).remove();
- INTERPOSE_HOOK(workshop_hook, getMachineInfo).remove();
- INTERPOSE_HOOK(workshop_hook, isPowerSource).remove();
- INTERPOSE_HOOK(workshop_hook, categorize).remove();
- INTERPOSE_HOOK(workshop_hook, uncategorize).remove();
- INTERPOSE_HOOK(workshop_hook, canConnectToMachine).remove();
- INTERPOSE_HOOK(workshop_hook, updateAction).remove();
+ INTERPOSE_HOOK(liquid_hook, getItemDescription).apply(enable);
+ INTERPOSE_HOOK(liquid_hook, adjustTemperature).apply(enable);
+ INTERPOSE_HOOK(liquid_hook, checkTemperatureDamage).apply(enable);
+
+ INTERPOSE_HOOK(workshop_hook, needsDesign).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, getPowerInfo).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, getMachineInfo).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, isPowerSource).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, categorize).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, uncategorize).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, canConnectToMachine).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, isUnpowered).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, updateAction).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, drawBuilding).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, deconstructItems).apply(enable);
+
+ INTERPOSE_HOOK(dwarfmode_hook, feed).apply(enable);
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
- find_engines();
- if (!engines.empty())
+ if (find_engines())
{
out.print("Detected steam engine workshops - enabling plugin.\n");
- enable_hooks();
+ enable_hooks(true);
}
+ else
+ enable_hooks(false);
break;
case SC_MAP_UNLOADED:
- disable_hooks();
+ enable_hooks(false);
engines.clear();
break;
default:
@@ -344,6 +1002,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
}
- int x = 1;
+ int x = 2;
OutputString(10, x, gps->dimy - 3, "Enter"); // SELECT key
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, ");
@@ -760,7 +758,7 @@ void viewscreen_unitlaborsst::render()
OutputString(10, x, gps->dimy - 3, "c"); // UNITJOB_ZOOM_CRE key
OutputString(15, x, gps->dimy - 3, ": Zoom-Cre");
- x = 1;
+ x = 2;
OutputString(10, x, gps->dimy - 2, "Esc"); // LEAVESCREEN key
OutputString(15, x, gps->dimy - 2, ": Done, ");
@@ -803,23 +801,35 @@ struct unitlist_hook : df::viewscreen_unitlistst
}
INTERPOSE_NEXT(feed)(input);
}
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ())
+ {
+ INTERPOSE_NEXT(render)();
+
+ if (units[page].size())
+ {
+ int x = 2;
+ OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
+ OutputString(15, x, gps->dimy - 2, ": Manage labors");
+ }
+ }
};
IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, render);
+
+DFHACK_PLUGIN("manipulator");
DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands)
{
- if (gps)
- {
- if (!INTERPOSE_HOOK(unitlist_hook, feed).apply())
- out.printerr("Could not interpose viewscreen_unitlistst::feed\n");
- }
-
+ if (!gps || !INTERPOSE_HOOK(unitlist_hook, feed).apply() || !INTERPOSE_HOOK(unitlist_hook, render).apply())
+ out.printerr("Could not insert Dwarf Manipulator hooks!\n");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
INTERPOSE_HOOK(unitlist_hook, feed).remove();
+ INTERPOSE_HOOK(unitlist_hook, render).remove();
return CR_OK;
}
diff --git a/plugins/proto/rename.proto b/plugins/proto/rename.proto
index aa1e95f48..810091fc7 100644
--- a/plugins/proto/rename.proto
+++ b/plugins/proto/rename.proto
@@ -17,3 +17,10 @@ message RenameUnitIn {
optional string nickname = 2;
optional string profession = 3;
}
+
+// RPC RenameBuilding : RenameBuildingIn -> EmptyMessage
+message RenameBuildingIn {
+ required int32 building_id = 1;
+
+ optional string name = 2;
+}
diff --git a/plugins/rename.cpp b/plugins/rename.cpp
index 1871d0f73..99dc6848a 100644
--- a/plugins/rename.cpp
+++ b/plugins/rename.cpp
@@ -3,11 +3,15 @@
#include "Export.h"
#include "PluginManager.h"
+#include
+#include
+
#include "modules/Gui.h"
#include "modules/Translation.h"
#include "modules/Units.h"
+#include "modules/World.h"
-#include "DataDefs.h"
+#include
#include "df/ui.h"
#include "df/world.h"
#include "df/squad.h"
@@ -18,6 +22,11 @@
#include "df/historical_figure_info.h"
#include "df/assumed_identity.h"
#include "df/language_name.h"
+#include "df/building_stockpilest.h"
+#include "df/building_workshopst.h"
+#include "df/building_furnacest.h"
+#include "df/building_trapst.h"
+#include "df/building_siegeenginest.h"
#include "RemoteServer.h"
#include "rename.pb.h"
@@ -36,6 +45,8 @@ using namespace dfproto;
using df::global::ui;
using df::global::world;
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event);
+
static command_result rename(color_ostream &out, vector & parameters);
DFHACK_PLUGIN("rename");
@@ -51,8 +62,32 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector clear(); \
+ *buf += name; \
+ *buf += " ("; \
+ if (tag) *buf += (const char*)tag; \
+ else { std::string tmp; INTERPOSE_NEXT(getName)(&tmp); *buf += tmp; } \
+ *buf += ")"; \
+ return; \
+ } \
+ else \
+ INTERPOSE_NEXT(getName)(buf); \
+ } \
+ }; \
+ IMPLEMENT_VMETHOD_INTERPOSE(cname##_hook, getName);
+KNOWN_BUILDINGS
+#undef BUILDING
+
+static char getBuildingCode(df::building *bld)
+{
+ CHECK_NULL_POINTER(bld);
+
+#define BUILDING(code, cname, tag) \
+ if (strict_virtual_cast(bld)) return code;
+KNOWN_BUILDINGS
+#undef BUILDING
+
+ return 0;
+}
+
+static bool enable_building_rename(char code, bool enable)
+{
+ switch (code) {
+#define BUILDING(code, cname, tag) \
+ case code: return INTERPOSE_HOOK(cname##_hook, getName).apply(enable);
+KNOWN_BUILDINGS
+#undef BUILDING
+ default:
+ return false;
+ }
+}
+
+static void disable_building_rename()
+{
+#define BUILDING(code, cname, tag) \
+ INTERPOSE_HOOK(cname##_hook, getName).remove();
+KNOWN_BUILDINGS
+#undef BUILDING
+}
+
+static bool is_enabled_building(char code)
+{
+ switch (code) {
+#define BUILDING(code, cname, tag) \
+ case code: return INTERPOSE_HOOK(cname##_hook, getName).is_applied();
+KNOWN_BUILDINGS
+#undef BUILDING
+ default:
+ return false;
+ }
+}
+
+static void init_buildings(bool enable)
+{
+ disable_building_rename();
+
+ if (enable)
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("rename/building_types");
+
+ if (entry.isValid())
+ {
+ std::string val = entry.val();
+ for (size_t i = 0; i < val.size(); i++)
+ enable_building_rename(val[i], true);
+ }
+ }
+}
+
+static bool canRenameBuilding(df::building *bld)
+{
+ return getBuildingCode(bld) != 0;
+}
+
+static bool isRenamingBuilding(df::building *bld)
+{
+ return is_enabled_building(getBuildingCode(bld));
+}
+
+static bool renameBuilding(df::building *bld, std::string name)
+{
+ char code = getBuildingCode(bld);
+ if (code == 0 && !name.empty())
+ return false;
+
+ if (!name.empty() && !is_enabled_building(code))
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("rename/building_types", NULL);
+ if (!entry.isValid())
+ return false;
+
+ if (!enable_building_rename(code, true))
+ return false;
+
+ entry.val().push_back(code);
+ }
+
+ bld->name = name;
+ return true;
+}
+
static df::squad *getSquadByIndex(unsigned idx)
{
auto entity = df::historical_entity::find(ui->group_id);
@@ -101,14 +263,37 @@ static command_result RenameUnit(color_ostream &stream, const RenameUnitIn *in)
return CR_OK;
}
+static command_result RenameBuilding(color_ostream &stream, const RenameBuildingIn *in)
+{
+ auto building = df::building::find(in->building_id());
+ if (!building)
+ return CR_NOT_FOUND;
+
+ if (in->has_name())
+ {
+ if (!renameBuilding(building, in->name()))
+ return CR_FAILURE;
+ }
+
+ return CR_OK;
+}
+
DFhackCExport RPCService *plugin_rpcconnect(color_ostream &)
{
RPCService *svc = new RPCService();
svc->addFunction("RenameSquad", RenameSquad);
svc->addFunction("RenameUnit", RenameUnit);
+ svc->addFunction("RenameBuilding", RenameBuilding);
return svc;
}
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(canRenameBuilding),
+ DFHACK_LUA_FUNCTION(isRenamingBuilding),
+ DFHACK_LUA_FUNCTION(renameBuilding),
+ DFHACK_LUA_END
+};
+
static command_result rename(color_ostream &out, vector ¶meters)
{
CoreSuspender suspend;
@@ -167,6 +352,20 @@ static command_result rename(color_ostream &out, vector ¶meters)
unit->custom_profession = parameters[1];
}
+ else if (cmd == "building")
+ {
+ if (parameters.size() != 2)
+ return CR_WRONG_USAGE;
+
+ if (ui->main.mode != ui_sidebar_mode::QueryBuilding)
+ return CR_WRONG_USAGE;
+
+ if (!renameBuilding(world->selected_building, parameters[1]))
+ {
+ out.printerr("This type of building is not supported.\n");
+ return CR_FAILURE;
+ }
+ }
else
{
if (!parameters.empty() && cmd != "?")
diff --git a/plugins/tweak.cpp b/plugins/tweak.cpp
index fa99f39e5..fbea30231 100644
--- a/plugins/tweak.cpp
+++ b/plugins/tweak.cpp
@@ -32,6 +32,8 @@
#include "df/squad_order_trainst.h"
#include "df/ui_build_selector.h"
#include "df/building_trapst.h"
+#include "df/item_actual.h"
+#include "df/contaminant.h"
#include
@@ -85,6 +87,8 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector building_subtype == trap_type::PressurePlate &&
ui_build_selector->plate_info.flags.bits.units)
{
- auto wsize = Screen::getWindowSize();
- int x = wsize.x - MENU_WIDTH - 1;
- if (*ui_menu_width == 1 || *ui_area_map_width == 2)
- x -= AREA_MAP_WIDTH + 1;
+ auto dims = Gui::getDwarfmodeViewDims();
+ int x = dims.menu_x1;
Screen::Pen pen(' ',COLOR_WHITE);
@@ -248,6 +247,52 @@ struct readable_build_plate_hook : df::viewscreen_dwarfmodest
IMPLEMENT_VMETHOD_INTERPOSE(readable_build_plate_hook, render);
+struct stable_temp_hook : df::item_actual {
+ typedef df::item_actual interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult))
+ {
+ if (temperature != temp)
+ {
+ // Bug 6012 is caused by fixed-point precision mismatch jitter
+ // when an item is being pushed by two sources at N and N+1.
+ // This check suppresses it altogether.
+ if (temp == temperature+1 ||
+ (temp == temperature-1 && temperature_fraction == 0))
+ temp = temperature;
+ // When SPEC_HEAT is NONE, the original function seems to not
+ // change the temperature, yet return true, which is silly.
+ else if (getSpecHeat() == 60001)
+ temp = temperature;
+ }
+
+ return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, updateContaminants, ())
+ {
+ if (contaminants)
+ {
+ // Force 1-degree difference in contaminant temperature to 0
+ for (size_t i = 0; i < contaminants->size(); i++)
+ {
+ auto obj = (*contaminants)[i];
+
+ if (abs(obj->temperature - temperature) == 1)
+ {
+ obj->temperature = temperature;
+ obj->temperature_fraction = temperature_fraction;
+ }
+ }
+ }
+
+ return INTERPOSE_NEXT(updateContaminants)();
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, adjustTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, updateContaminants);
+
static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector ¶meters)
{
if (vector_get(parameters, 1) == "disable")
@@ -380,6 +425,11 @@ static command_result tweak(color_ostream &out, vector ¶meters)
enable_hook(out, INTERPOSE_HOOK(readable_build_plate_hook, render), parameters);
}
+ else if (cmd == "stable-temp")
+ {
+ enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, adjustTemperature), parameters);
+ enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, updateContaminants), parameters);
+ }
else
return CR_WRONG_USAGE;
diff --git a/scripts/devel/lsmem.lua b/scripts/devel/lsmem.lua
new file mode 100644
index 000000000..75586324d
--- /dev/null
+++ b/scripts/devel/lsmem.lua
@@ -0,0 +1,14 @@
+-- Prints memory ranges of the process.
+
+for _,v in ipairs(dfhack.internal.getMemRanges()) do
+ local access = { '-', '-', '-', 'p' }
+ if v.read then access[1] = 'r' end
+ if v.write then access[2] = 'w' end
+ if v.execute then access[3] = 'x' end
+ if not v.valid then
+ access[4] = '?'
+ elseif v.shared then
+ access[4] = 's'
+ end
+ print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name))
+end
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index 6b4b4042b..4468e1dcb 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
end
end
-if dfhack.gui.getCurFocus() ~= 'dwarfmode/QueryBuilding/Some' then
+if not string.find(dfhack.gui.getCurFocus(), 'dwarfmode/QueryBuilding/Some') then
qerror("This script requires the main dwarfmode view in 'q' mode")
end
diff --git a/scripts/gui/rename.lua b/scripts/gui/rename.lua
new file mode 100644
index 000000000..a457a0bfd
--- /dev/null
+++ b/scripts/gui/rename.lua
@@ -0,0 +1,63 @@
+-- Rename various objects via gui.
+
+local gui = require 'gui'
+local dlg = require 'gui.dialogs'
+local plugin = require 'plugins.rename'
+
+local mode = ...
+local focus = dfhack.gui.getCurFocus()
+
+local function verify_mode(expected)
+ if mode ~= nil and mode ~= expected then
+ qerror('Invalid UI state for mode '..mode)
+ end
+end
+
+if string.match(focus, '^dwarfmode/QueryBuilding/Some') then
+ verify_mode('building')
+
+ local building = df.global.world.selected_building
+ if plugin.canRenameBuilding(building) then
+ dlg.showInputPrompt(
+ 'Rename Building',
+ 'Enter a new name for the building:', COLOR_GREEN,
+ building.name,
+ curry(plugin.renameBuilding, building)
+ )
+ else
+ dlg.showMessage(
+ 'Rename Building',
+ 'Cannot rename this type of building.', COLOR_LIGHTRED
+ )
+ end
+elseif dfhack.gui.getSelectedUnit(true) then
+ local unit = dfhack.gui.getSelectedUnit(true)
+
+ if mode == 'unit-profession' then
+ dlg.showInputPrompt(
+ 'Rename Unit',
+ 'Enter a new profession for the unit:', COLOR_GREEN,
+ unit.custom_profession,
+ function(newval)
+ unit.custom_profession = newval
+ end
+ )
+ else
+ verify_mode('unit')
+
+ local vname = dfhack.units.getVisibleName(unit)
+ local vnick = ''
+ if vname and vname.has_name then
+ vnick = vname.nickname
+ end
+
+ dlg.showInputPrompt(
+ 'Rename Unit',
+ 'Enter a new nickname for the unit:', COLOR_GREEN,
+ vnick,
+ curry(dfhack.units.setNickname, unit)
+ )
+ end
+elseif mode then
+ verify_mode(nil)
+end