From 6bd67cc055a3e2ec9a7562536d8c51c11a2789d2 Mon Sep 17 00:00:00 2001
From: jj
Date: Thu, 14 Jun 2012 14:25:16 +0200
Subject: [PATCH 01/40] ruby: try to fix msvc build (use fastcall for thiscall
fptr)
---
plugins/ruby/ruby.cpp | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp
index 49119c9aa..0176e1eef 100644
--- a/plugins/ruby/ruby.cpp
+++ b/plugins/ruby/ruby.cpp
@@ -763,13 +763,18 @@ static VALUE rb_dfmemory_bitarray_set(VALUE self, VALUE addr, VALUE idx, VALUE v
static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE a1, VALUE a2, VALUE a3)
{
#ifdef WIN32
- __thiscall
-#endif
+ int (__fastcall *fptr)(char **me, int dummy_edx, int, int, int, int);
+#else
int (*fptr)(char **me, int, int, int, int);
+#endif
char **that = (char**)rb_num2ulong(cppobj);
int ret;
fptr = (decltype(fptr))*(void**)(*that + rb_num2ulong(cppvoff));
- ret = fptr(that, rb_num2ulong(a0), rb_num2ulong(a1), rb_num2ulong(a2), rb_num2ulong(a3));
+ ret = fptr(that,
+#ifdef WIN32
+ 0,
+#endif
+ rb_num2ulong(a0), rb_num2ulong(a1), rb_num2ulong(a2), rb_num2ulong(a3));
return rb_int2inum(ret);
}
From cfdf941c52beeca5ba06e6b626b21959781e2a21 Mon Sep 17 00:00:00 2001
From: jj
Date: Thu, 14 Jun 2012 22:35:59 +0200
Subject: [PATCH 02/40] ruby: fix windows build warnings/load ruby.rb
---
plugins/ruby/ruby.cpp | 4 +++-
plugins/ruby/ruby.rb | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp
index 0176e1eef..06f033d2f 100644
--- a/plugins/ruby/ruby.cpp
+++ b/plugins/ruby/ruby.cpp
@@ -483,6 +483,7 @@ static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr)
static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr)
{
rb_raise(*rb_eRuntimeError, "not implemented");
+ return Qnil;
}
static VALUE rb_dfget_global_address(VALUE self, VALUE name)
@@ -841,7 +842,8 @@ static void ruby_bind_dfhack(void) {
// load the default ruby-level definitions
int state=0;
- rb_load_protect(rb_str_new2("./hack/ruby.rb"), Qfalse, &state);
+ // windows cmake installs files in df/, linux installs files in df/hack/
+ rb_eval_string_protect("File.exist?('hack/ruby.rb') ? load('hack/ruby.rb') : load('ruby.rb')", &state);
if (state)
dump_rb_error();
}
diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb
index fd597e43f..9c11144c0 100644
--- a/plugins/ruby/ruby.rb
+++ b/plugins/ruby/ruby.rb
@@ -1048,7 +1048,7 @@ end
end
# load autogen'd file
-require 'hack/ruby-autogen'
+File.exist?('hack/ruby-autogen.rb') ? require('hack/ruby-autogen') : require('ruby-autogen')
# load optional user-specified startup file
load 'ruby_custom.rb' if File.exist?('ruby_custom.rb')
From 0bdae68294857c3ba745f91b559d9485b7366774 Mon Sep 17 00:00:00 2001
From: jj
Date: Fri, 15 Jun 2012 21:02:04 +0200
Subject: [PATCH 03/40] ruby: fix codegen enum base-types
---
plugins/ruby/codegen.pl | 2 ++
1 file changed, 2 insertions(+)
diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl
index 5cdeeedd9..45fcd8fa4 100755
--- a/plugins/ruby/codegen.pl
+++ b/plugins/ruby/codegen.pl
@@ -687,6 +687,7 @@ sub render_item_number {
my $initvalue = $item->getAttribute('init-value');
my $typename = $item->getAttribute('type-name');
undef $typename if ($meta and $meta eq 'bitfield-type');
+ my $g = $global_types{$typename} if ($typename);
$typename = rb_ucase($typename) if $typename;
$typename = $classname if (!$typename and $subtype and $subtype eq 'enum'); # compound enum
@@ -695,6 +696,7 @@ sub render_item_number {
$initvalue ||= 'nil' if $typename;
$subtype = $item->getAttribute('base-type') if (!$subtype or $subtype eq 'bitfield' or $subtype eq 'enum');
+ $subtype = $g->getAttribute('base-type') if ($g);
$subtype = 'int32_t' if (!$subtype);
if ($subtype eq 'int64_t') {
From bd5b675fa5ce1c8561b9d670180ae80ef4a2a2b1 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Mon, 18 Jun 2012 21:11:54 +0400
Subject: [PATCH 04/40] Add ui_menu_width finder, tweak instructions, and add a
case for win exe.
---
library/lua/memscan.lua | 3 +-
scripts/devel/find-offsets.lua | 118 ++++++++++++++++++---------------
2 files changed, 68 insertions(+), 53 deletions(-)
diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua
index 95b9197b1..4bd01c8f7 100644
--- a/library/lua/memscan.lua
+++ b/library/lua/memscan.lua
@@ -208,7 +208,8 @@ local function find_data_segment()
end
elseif mem.read and mem.write
and (string.match(mem.name,'/dwarfort%.exe$')
- or string.match(mem.name,'/Dwarf_Fortress$'))
+ or string.match(mem.name,'/Dwarf_Fortress$')
+ or string.match(mem.name,'Dwarf Fortress%.exe'))
then
data_start = mem.start_addr
data_end = mem.end_addr
diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua
index 6b3c31576..fddfbc948 100644
--- a/scripts/devel/find-offsets.lua
+++ b/scripts/devel/find-offsets.lua
@@ -20,10 +20,14 @@ MAKE IT RUN CORRECTLY if any data structures
changed, thus possibly leading to CRASHES AND/OR
PERMANENT SAVE CORRUPTION.
-This script should be initially started immediately
-after loading the game, WITHOUT first loading a world.
-It expects vanilla game configuration, without any
-custom tilesets or init file changes.
+Finding the first few globals requires this script to be
+started immediately after loading the game, WITHOUT
+first loading a world.
+
+The script expects vanilla game configuration, without
+any custom tilesets or init file changes. Never unpause
+the game unless instructed. When done, quit the game
+without saving using 'die'.
]]
if not utils.prompt_yes_no('Proceed?') then
@@ -140,8 +144,6 @@ local function find_cursor()
return false
end
-exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' })
-
--
-- Announcements
--
@@ -158,8 +160,6 @@ local function find_announcements()
dfhack.printerr('Could not find announcements.')
end
-exec_finder(find_announcements, 'announcements')
-
--
-- d_init
--
@@ -198,8 +198,6 @@ local function find_d_init()
dfhack.printerr('Could not find d_init')
end
-exec_finder(find_d_init, 'd_init')
-
--
-- gview
--
@@ -220,8 +218,6 @@ local function find_gview()
dfhack.printerr('Could not find gview')
end
-exec_finder(find_gview, 'gview')
-
--
-- World
--
@@ -257,8 +253,6 @@ menu, and select different types as instructed below:]],
validate_offset('world', is_valid_world, addr, df.world, 'selected_stockpile_type')
end
-exec_finder(find_world, 'world')
-
--
-- UI
--
@@ -291,8 +285,6 @@ menu, and switch modes as instructed below:]],
validate_offset('ui', is_valid_ui, addr, df.ui, 'main', 'mode')
end
-exec_finder(find_ui, 'ui')
-
--
-- ui_sidebar_menus
--
@@ -319,9 +311,9 @@ end
local function find_ui_sidebar_menus()
local addr = searcher:find_menu_cursor([[
-Searching for ui_sidebar_menus. Please open the add job
-ui of Mason, Craftsdwarfs, or Carpenters workshop, and
-select entries in the list:]],
+Searching for ui_sidebar_menus. Please switch to 'q' mode,
+select a Mason, Craftsdwarfs, or Carpenters workshop, open
+the Add Job menu, and move the cursor within:]],
'int32_t',
{ 0, 1, 2, 3, 4, 5, 6 },
ordinal_names
@@ -330,8 +322,6 @@ select entries in the list:]],
addr, df.ui_sidebar_menus, 'workshop_job', 'cursor')
end
-exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
-
--
-- ui_build_selector
--
@@ -366,7 +356,25 @@ number, so when it shows "Min (5000df", it means 50000:]],
addr, df.ui_build_selector, 'plate_info', 'unit_min')
end
-exec_finder(find_ui_build_selector, 'ui_build_selector')
+--
+-- ui_menu_width
+--
+
+local function find_ui_menu_width()
+ local addr = searcher:find_menu_cursor([[
+Searching for ui_menu_width. Please exit to the main
+dwarfmode menu, then use Tab to do as instructed below:]],
+ 'int8_t',
+ { 2, 3, 1 },
+ { [2] = 'switch to the most usual [mapmap][menu] layout',
+ [3] = 'hide the menu completely',
+ [1] = 'switch to the default [map][menu][map] layout' }
+ )
+ ms.found_offset('ui_menu_width', addr)
+
+ -- NOTE: Assume that the vars are adjacent, as always
+ ms.found_offset('ui_area_map_width', addr+1)
+end
--
-- ui_selected_unit
@@ -395,8 +403,6 @@ into the prompts below:]],
ms.found_offset('ui_selected_unit', addr)
end
-exec_finder(find_ui_selected_unit, 'ui_selected_unit')
-
--
-- ui_unit_view_mode
--
@@ -412,8 +418,6 @@ with 'v', switch the pages as requested:]],
ms.found_offset('ui_unit_view_mode', addr)
end
-exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode')
-
--
-- ui_look_cursor
--
@@ -434,8 +438,6 @@ and select list entries as instructed:]],
ms.found_offset('ui_look_cursor', addr)
end
-exec_finder(find_ui_look_cursor, 'ui_look_cursor')
-
--
-- ui_building_item_cursor
--
@@ -456,8 +458,6 @@ with many contained items, and select as instructed:]],
ms.found_offset('ui_building_item_cursor', addr)
end
-exec_finder(find_ui_building_item_cursor, 'ui_building_item_cursor')
-
--
-- ui_workshop_in_add
--
@@ -468,7 +468,7 @@ Searching for ui_workshop_in_add. Please activate the 'q'
mode, find a workshop without jobs (or delete jobs),
and do as instructed below.
-NOTE: After first 3 steps resize the game window.]],
+NOTE: If not done after first 3-4 steps, resize the game window.]],
'int8_t',
{ 1, 0 },
{ [1] = 'enter the add job menu',
@@ -477,8 +477,6 @@ NOTE: After first 3 steps resize the game window.]],
ms.found_offset('ui_workshop_in_add', addr)
end
-exec_finder(find_ui_workshop_in_add, 'ui_workshop_in_add')
-
--
-- ui_workshop_job_cursor
--
@@ -498,8 +496,6 @@ mode, find a workshop with many jobs, and select as instructed:]],
ms.found_offset('ui_workshop_job_cursor', addr)
end
-exec_finder(find_ui_workshop_job_cursor, 'ui_workshop_job_cursor')
-
--
-- ui_building_in_assign
--
@@ -510,7 +506,7 @@ Searching for ui_building_in_assign. Please activate
the 'q' mode, select a room building (e.g. a bedroom)
and do as instructed below.
-NOTE: After first 3 steps resize the game window.]],
+NOTE: If not done after first 3-4 steps, resize the game window.]],
'int8_t',
{ 1, 0 },
{ [1] = 'enter the Assign owner menu',
@@ -519,8 +515,6 @@ NOTE: After first 3 steps resize the game window.]],
ms.found_offset('ui_building_in_assign', addr)
end
-exec_finder(find_ui_building_in_assign, 'ui_building_in_assign')
-
--
-- ui_building_in_resize
--
@@ -531,7 +525,7 @@ Searching for ui_building_in_resize. Please activate
the 'q' mode, select a room building (e.g. a bedroom)
and do as instructed below.
-NOTE: After first 3 steps resize the game window.]],
+NOTE: If not done after first 3-4 steps, resize the game window.]],
'int8_t',
{ 1, 0 },
{ [1] = 'enter the Resize room mode',
@@ -540,9 +534,6 @@ NOTE: After first 3 steps resize the game window.]],
ms.found_offset('ui_building_in_resize', addr)
end
-exec_finder(find_ui_building_in_resize, 'ui_building_in_resize')
-
-
--
-- window_x
--
@@ -557,8 +548,6 @@ scroll to the LEFT edge, then do as instructed:]],
ms.found_offset('window_x', addr)
end
-exec_finder(find_window_x, 'window_x')
-
--
-- window_y
--
@@ -573,8 +562,6 @@ scroll to the TOP edge, then do as instructed:]],
ms.found_offset('window_y', addr)
end
-exec_finder(find_window_y, 'window_y')
-
--
-- window_z
--
@@ -582,20 +569,47 @@ exec_finder(find_window_y, 'window_y')
local function find_window_z()
local addr = searcher:find_counter([[
Searching for window_z. Please exit to main dwarfmode menu,
-scroll to ground level, then do as instructed below.
+scroll to a Z level near surface, then do as instructed below.
-NOTE: After first 3 steps resize the game window.]],
+NOTE: If not done after first 3-4 steps, resize the game window.]],
'int32_t', -1,
"Please press '>' to scroll one Z level down."
)
ms.found_offset('window_z', addr)
end
-exec_finder(find_window_z, 'window_z')
-
--
--- THE END
+-- MAIN FLOW
--
-print('Done.')
+print('\nInitial globals (need title screen):\n')
+
+exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' })
+exec_finder(find_announcements, 'announcements')
+exec_finder(find_d_init, 'd_init')
+exec_finder(find_gview, 'gview')
+
+print('\nCompound globals (need loaded world):\n')
+
+exec_finder(find_world, 'world')
+exec_finder(find_ui, 'ui')
+exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
+exec_finder(find_ui_build_selector, 'ui_build_selector')
+
+print('\nPrimitive globals:\n')
+
+exec_finder(find_ui_menu_width, { 'ui_menu_width', 'ui_area_map_width' })
+exec_finder(find_ui_selected_unit, 'ui_selected_unit')
+exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode')
+exec_finder(find_ui_look_cursor, 'ui_look_cursor')
+exec_finder(find_ui_building_item_cursor, 'ui_building_item_cursor')
+exec_finder(find_ui_workshop_in_add, 'ui_workshop_in_add')
+exec_finder(find_ui_workshop_job_cursor, 'ui_workshop_job_cursor')
+exec_finder(find_ui_building_in_assign, 'ui_building_in_assign')
+exec_finder(find_ui_building_in_resize, 'ui_building_in_resize')
+exec_finder(find_window_x, 'window_x')
+exec_finder(find_window_y, 'window_y')
+exec_finder(find_window_z, 'window_z')
+
+print('\nDone. Now add newly-found globals to symbols.xml.')
searcher:reset()
From 50dff568994a79feaa465b03da9e251ef87c0798 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Tue, 19 Jun 2012 18:41:18 +0400
Subject: [PATCH 05/40] Rewrite getMemRanges for windows to get rid of a number
of problems.
- Properly handle copy-on-write permission modes.
- Merge ranges with the same properties for us.
- Don't skip non-private areas.
- Use the mapped filename as name, so that it works for all ranges.
---
library/Process-windows.cpp | 136 ++++++++++++++++++++++++++++++------
1 file changed, 113 insertions(+), 23 deletions(-)
diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp
index 944a773c6..7eb6ff5f7 100644
--- a/library/Process-windows.cpp
+++ b/library/Process-windows.cpp
@@ -233,19 +233,55 @@ struct HeapBlock
ULONG reserved;
};
*/
-// FIXME: NEEDS TESTING!
-// FIXME: i noticed that if you enumerate it twice, second time it returns wrong .text region size
+
+static void GetDosNames(std::map &table)
+{
+ // Partially based on example from msdn:
+ // Translate path with device name to drive letters.
+ TCHAR szTemp[512];
+ szTemp[0] = '\0';
+
+ if (GetLogicalDriveStrings(sizeof(szTemp)-1, szTemp))
+ {
+ TCHAR szName[MAX_PATH];
+ TCHAR szDrive[3] = " :";
+ BOOL bFound = FALSE;
+ TCHAR* p = szTemp;
+
+ do
+ {
+ // Copy the drive letter to the template string
+ *szDrive = *p;
+
+ // Look up each device name
+ if (QueryDosDevice(szDrive, szName, MAX_PATH))
+ table[szName] = szDrive;
+
+ // Go to the next NULL character.
+ while (*p++);
+ } while (*p); // end of string
+ }
+}
+
void Process::getMemRanges( vector & ranges )
{
MEMORY_BASIC_INFORMATION MBI;
//map heaps;
uint64_t movingStart = 0;
+ PVOID LastAllocationBase = 0;
map nameMap;
+ map dosDrives;
// get page size
SYSTEM_INFO si;
GetSystemInfo(&si);
uint64_t PageSize = si.dwPageSize;
+
+ // get dos drive names
+ GetDosNames(dosDrives);
+
+ ranges.clear();
+
// enumerate heaps
// HeapNodes(d->my_pid, heaps);
// go through all the VM regions, convert them to our internal format
@@ -254,52 +290,106 @@ void Process::getMemRanges( vector & ranges )
movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize);
if(movingStart % PageSize != 0)
movingStart = (movingStart / PageSize + 1) * PageSize;
- // skip empty regions and regions we share with other processes (DLLs)
- if( !(MBI.State & MEM_COMMIT) /*|| !(MBI.Type & MEM_PRIVATE)*/ )
+
+ // Skip unallocated address space
+ if (MBI.State & MEM_FREE)
continue;
+
+ // Find range and permissions
t_memrange temp;
+ memset(&temp, 0, sizeof(temp));
+
temp.start = (char *) MBI.BaseAddress;
temp.end = ((char *)MBI.BaseAddress + (uint64_t)MBI.RegionSize);
- temp.read = MBI.Protect & PAGE_EXECUTE_READ || MBI.Protect & PAGE_EXECUTE_READWRITE || MBI.Protect & PAGE_READONLY || MBI.Protect & PAGE_READWRITE;
- temp.write = MBI.Protect & PAGE_EXECUTE_READWRITE || MBI.Protect & PAGE_READWRITE;
- temp.execute = MBI.Protect & PAGE_EXECUTE_READ || MBI.Protect & PAGE_EXECUTE_READWRITE || MBI.Protect & PAGE_EXECUTE;
- temp.valid = true;
- if(!GetModuleBaseName(d->my_handle, (HMODULE) temp.start, temp.name, 1024))
+ temp.valid = true;
+
+ if (!(MBI.State & MEM_COMMIT))
+ temp.valid = false; // reserved address space
+ else if (MBI.Protect & PAGE_EXECUTE)
+ temp.execute = true;
+ else if (MBI.Protect & PAGE_EXECUTE_READ)
+ temp.execute = temp.read = true;
+ else if (MBI.Protect & PAGE_EXECUTE_READWRITE)
+ temp.execute = temp.read = temp.write = true;
+ else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY)
+ temp.execute = temp.read = temp.write = true;
+ else if (MBI.Protect & PAGE_READONLY)
+ temp.read = true;
+ else if (MBI.Protect & PAGE_READWRITE)
+ temp.read = temp.write = true;
+ else if (MBI.Protect & PAGE_WRITECOPY)
+ temp.read = temp.write = true;
+
+ // Merge areas with the same properties
+ if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase)
{
- if(nameMap.count((char *)temp.start))
+ auto &last = ranges.back();
+
+ if (last.end == temp.start &&
+ last.valid == temp.valid && last.execute == temp.execute &&
+ last.read == temp.read && last.write == temp.write)
{
- // potential buffer overflow...
- strcpy(temp.name, nameMap[(char *)temp.start].c_str());
+ last.end = temp.end;
+ continue;
}
- else
+ }
+
+#if 1
+ // Find the mapped file name
+ if (GetMappedFileName(d->my_handle, temp.start, temp.name, 1024))
+ {
+ int vsize = strlen(temp.name);
+
+ // Translate NT name to DOS name
+ for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it)
{
- // filter away shared segments without a name.
- if( !(MBI.Type & MEM_PRIVATE) )
+ int ksize = it->first.size();
+ if (strncmp(temp.name, it->first.data(), ksize) != 0)
continue;
- else
- temp.name[0]=0;
+
+ memcpy(temp.name, it->second.data(), it->second.size());
+ memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize);
+ break;
}
}
else
+ temp.name[0] = 0;
+#else
+ // Find the executable name
+ char *base = (char*)MBI.AllocationBase;
+
+ if(nameMap.count(base))
+ {
+ strncpy(temp.name, nameMap[base].c_str(), 1023);
+ }
+ else if(GetModuleBaseName(d->my_handle, (HMODULE)base, temp.name, 1024))
{
+ std::string nm(temp.name);
+
+ nameMap[base] = nm;
+
// this is our executable! (could be generalized to pull segments from libs, but whatever)
- if(d->base == temp.start)
+ if(d->base == base)
{
for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++)
{
- char sectionName[9];
+ /*char sectionName[9];
memcpy(sectionName,d->sections[i].Name,8);
sectionName[8] = 0;
string nm;
nm.append(temp.name);
nm.append(" : ");
- nm.append(sectionName);
- nameMap[(char *)temp.start + d->sections[i].VirtualAddress] = nm;
+ nm.append(sectionName);*/
+ nameMap[base + d->sections[i].VirtualAddress] = nm;
}
}
- else
- continue;
}
+ else
+ temp.name[0] = 0;
+#endif
+
+ // Push the entry
+ LastAllocationBase = MBI.AllocationBase;
ranges.push_back(temp);
}
}
From 50bd758876adb8a39c54f107af9a5bd2274a3638 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Tue, 19 Jun 2012 18:48:22 +0400
Subject: [PATCH 06/40] Replace dfhack.internal.getBase with getRebaseDelta.
Also, when printing found offsets, subtract the delta.
---
LUA_API.rst | 4 ++--
library/LuaApi.cpp | 5 +++--
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/LUA_API.rst b/LUA_API.rst
index 1723711d3..9515690eb 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -1172,9 +1172,9 @@ and are only documented here for completeness:
Returns the pre-extracted vtable address ``name``, or *nil*.
-* ``dfhack.internal.getBase()``
+* ``dfhack.internal.getRebaseDelta()``
- Returns the base address of the process.
+ Returns the ASLR rebase offset of the DF executable.
* ``dfhack.internal.getMemRanges()``
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 631b3c499..3693070d0 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -1036,10 +1036,10 @@ static void *checkaddr(lua_State *L, int idx, bool allow_null = false)
return rv;
}
-static uint32_t getBase() { return Core::getInstance().p->getBase(); }
+static uint32_t getRebaseDelta() { return Core::getInstance().vinfo->getRebaseDelta(); }
static const LuaWrapper::FunctionReg dfhack_internal_module[] = {
- WRAP(getBase),
+ WRAP(getRebaseDelta),
{ NULL, NULL }
};
@@ -1074,6 +1074,7 @@ static int internal_setAddress(lua_State *L)
}
// Print via printerr, so that it is definitely logged to stderr.log.
+ addr -= Core::getInstance().vinfo->getRebaseDelta();
std::string msg = stl_sprintf("", name.c_str(), addr);
dfhack_printerr(L, msg);
From e687a07f2e463472ad4609371f7b914b5de649a6 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Tue, 19 Jun 2012 21:02:27 +0400
Subject: [PATCH 07/40] Fix getRebaseDelta: should be signed int.
---
Lua API.html | 4 ++--
library/LuaApi.cpp | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Lua API.html b/Lua API.html
index f1bdd17d2..84d13e2f0 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -1329,8 +1329,8 @@ global environment, persistent between calls to the script.
dfhack.internal.getVTable(name)
Returns the pre-extracted vtable address name, or nil.
-dfhack.internal.getBase()
-Returns the base address of the process.
+dfhack.internal.getRebaseDelta()
+Returns the ASLR rebase offset of the DF executable.
dfhack.internal.getMemRanges()
Returns a sequence of tables describing virtual memory ranges of the process.
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 3693070d0..092404e33 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -1036,7 +1036,7 @@ static void *checkaddr(lua_State *L, int idx, bool allow_null = false)
return rv;
}
-static uint32_t getRebaseDelta() { return Core::getInstance().vinfo->getRebaseDelta(); }
+static int getRebaseDelta() { return Core::getInstance().vinfo->getRebaseDelta(); }
static const LuaWrapper::FunctionReg dfhack_internal_module[] = {
WRAP(getRebaseDelta),
From f989ef213e69fc2806ac01fa8f98ad88e7e1fcde Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Tue, 19 Jun 2012 22:43:31 +0400
Subject: [PATCH 08/40] Add 5 more finders to the script; these use relative
location heuristics.
---
scripts/devel/find-offsets.lua | 159 ++++++++++++++++++++++++++++++++-
1 file changed, 158 insertions(+), 1 deletion(-)
diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua
index fddfbc948..ef9c98299 100644
--- a/scripts/devel/find-offsets.lua
+++ b/scripts/devel/find-offsets.lua
@@ -5,6 +5,8 @@ local ms = require 'memscan'
local is_known = dfhack.internal.getAddress
+local os_type = dfhack.getOSType()
+
local force_scan = {}
for _,v in ipairs({...}) do
force_scan[v] = true
@@ -56,6 +58,34 @@ local function validate_offset(name,validator,addr,tname,...)
ms.found_offset(name,obj)
end
+local function zoomed_searcher(startn, end_or_sz)
+ if force_scan.nozoom then
+ return nil
+ end
+ local sv = is_known(startn)
+ if not sv then
+ return nil
+ end
+ local ev
+ if type(end_or_sz) == 'number' then
+ ev = sv + end_or_sz
+ if end_or_sz < 0 then
+ sv, ev = ev, sv
+ end
+ else
+ ev = is_known(end_or_sz)
+ if not ev then
+ return nil
+ end
+ end
+ sv = sv - (sv % 4)
+ ev = ev + 3
+ ev = ev - (ev % 4)
+ if data:contains_range(sv, ev-sv) then
+ return ms.DiffSearcher.new(ms.MemoryArea.new(sv,ev))
+ end
+end
+
local function exec_finder(finder, names)
if type(names) ~= 'table' then
names = { names }
@@ -80,7 +110,8 @@ end
local ordinal_names = {
[0] = '1st entry',
- [1] = '2nd entry'
+ [1] = '2nd entry',
+ [2] = '3rd entry'
}
setmetatable(ordinal_names, {
__index = function(self,idx) return (idx+1)..'th entry' end
@@ -578,6 +609,124 @@ NOTE: If not done after first 3-4 steps, resize the game window.]],
ms.found_offset('window_z', addr)
end
+--
+-- cur_year
+--
+
+local function find_cur_year()
+ local zone
+ if os_type == 'windows' then
+ zone = zoomed_searcher('formation_next_id', 32)
+ elseif os_type == 'darwin' then
+ zone = zoomed_searcher('cursor', -32)
+ elseif os_type == 'linux' then
+ zone = zoomed_searcher('ui_building_assign_type', -512)
+ end
+ if not zone then
+ dfhack.printerr('Cannot search for cur_year - prerequisites missing.')
+ return
+ end
+
+ local yvalue = utils.prompt_input('Please enter current in-game year: ', utils.check_number)
+ local idx, addr = zone.area.int32_t:find_one{yvalue}
+ if idx then
+ ms.found_offset('cur_year', addr)
+ return
+ end
+
+ dfhack.printerr('Could not find cur_year')
+end
+
+--
+-- cur_year_tick
+--
+
+local function find_cur_year_tick()
+ local zone
+ if os_type == 'windows' then
+ zone = zoomed_searcher('artifact_next_id', -32)
+ else
+ zone = zoomed_searcher('cur_year', 128)
+ end
+ if not zone then
+ dfhack.printerr('Cannot search for cur_year_tick - prerequisites missing.')
+ return
+ end
+
+ local addr = zone:find_counter([[
+Searching for cur_year_tick. Please exit to main dwarfmode
+menu, then do as instructed below:]],
+ 'int32_t', 1,
+ "Please press '.' to step the game one frame."
+ )
+ ms.found_offset('cur_year_tick', addr)
+end
+
+--
+-- process_jobs
+--
+
+local function get_process_zone()
+ if os_type == 'windows' then
+ return zoomed_searcher('ui_workshop_job_cursor', 'ui_building_in_resize')
+ else
+ return zoomed_searcher('cur_year', 'cur_year_tick')
+ end
+end
+
+local function find_process_jobs()
+ local zone = get_process_zone() or searcher
+
+ local addr = zone:find_menu_cursor([[
+Searching for process_jobs. Please do as instructed below:]],
+ 'int8_t',
+ { 1, 0 },
+ { [1] = 'designate a building to be constructed, e.g a bed',
+ [0] = 'step or unpause the game to reset the flag' }
+ )
+ ms.found_offset('process_jobs', addr)
+end
+
+--
+-- process_dig
+--
+
+local function find_process_dig()
+ local zone = get_process_zone() or searcher
+
+ local addr = zone:find_menu_cursor([[
+Searching for process_dig. Please do as instructed below:]],
+ 'int8_t',
+ { 1, 0 },
+ { [1] = 'designate a tile to be mined out',
+ [0] = 'step or unpause the game to reset the flag' }
+ )
+ ms.found_offset('process_dig', addr)
+end
+
+--
+-- pause_state
+--
+
+local function find_pause_state()
+ local zone
+ if os_type == 'linux' then
+ zone = zoomed_searcher('ui_look_cursor', 32)
+ elseif os_type == 'windows' then
+ zone = zoomed_searcher('ui_workshop_job_cursor', 64)
+ end
+ zone = zone or searcher
+
+ local addr = zone:find_menu_cursor([[
+Searching for pause_state. Please do as instructed below:]],
+ 'int8_t',
+ { 1, 0 },
+ { [1] = 'PAUSE the game',
+ [0] = 'UNPAUSE the game' }
+ )
+ ms.found_offset('pause_state', addr)
+end
+
--
-- MAIN FLOW
--
@@ -611,5 +760,13 @@ exec_finder(find_window_x, 'window_x')
exec_finder(find_window_y, 'window_y')
exec_finder(find_window_z, 'window_z')
+print('\nUnpausing globals:\n')
+
+exec_finder(find_cur_year, 'cur_year')
+exec_finder(find_cur_year_tick, 'cur_year_tick')
+exec_finder(find_process_jobs, 'process_jobs')
+exec_finder(find_process_dig, 'process_dig')
+exec_finder(find_pause_state, 'pause_state')
+
print('\nDone. Now add newly-found globals to symbols.xml.')
searcher:reset()
From ed4acbdedbb0ddecac5b8b666ef159289697ec93 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Wed, 20 Jun 2012 10:12:26 +0400
Subject: [PATCH 09/40] Add a searcher for current_weather, using a prepared
save.
---
library/lua/memscan.lua | 2 +-
scripts/devel/find-offsets.lua | 49 +++++++++++++++++++++--
scripts/devel/prepare-save.lua | 71 ++++++++++++++++++++++++++++++++++
3 files changed, 117 insertions(+), 5 deletions(-)
create mode 100644 scripts/devel/prepare-save.lua
diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua
index 4bd01c8f7..65b02194c 100644
--- a/library/lua/memscan.lua
+++ b/library/lua/memscan.lua
@@ -168,7 +168,7 @@ function MemoryArea:__tostring()
return string.format('', self.start_addr, self.end_addr)
end
function MemoryArea:contains_range(start,size)
- return start >= self.start_addr and (start+size) <= self.end_addr
+ return size >= 0 and start >= self.start_addr and (start+size) <= self.end_addr
end
function MemoryArea:contains_obj(obj,count)
local size, base = df.sizeof(obj)
diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua
index ef9c98299..391390f14 100644
--- a/scripts/devel/find-offsets.lua
+++ b/scripts/devel/find-offsets.lua
@@ -24,7 +24,10 @@ PERMANENT SAVE CORRUPTION.
Finding the first few globals requires this script to be
started immediately after loading the game, WITHOUT
-first loading a world.
+first loading a world. The rest expect a loaded save,
+not a fresh embark. Finding current_weather requires
+a special save previously processed with devel/prepare-save
+on a DF version with working dfhack.
The script expects vanilla game configuration, without
any custom tilesets or init file changes. Never unpause
@@ -387,6 +390,43 @@ number, so when it shows "Min (5000df", it means 50000:]],
addr, df.ui_build_selector, 'plate_info', 'unit_min')
end
+--
+-- current_weather
+--
+
+local function find_current_weather()
+ print('\nPlease load the save previously processed with prepare-save.')
+ if not utils.prompt_yes_no('Proceed?', true) then
+ return
+ end
+
+ local zone
+ if os_type == 'windows' then
+ zone = zoomed_searcher('crime_next_id', 512)
+ elseif os_type == 'darwin' then
+ zone = zoomed_searcher('cursor', -64)
+ elseif os_type == 'linux' then
+ zone = zoomed_searcher('ui_building_assign_type', -512)
+ end
+ zone = zone or searcher
+
+ local wbytes = {
+ 2, 1, 0, 2, 0,
+ 1, 2, 1, 0, 0,
+ 2, 0, 2, 1, 2,
+ 1, 2, 0, 1, 1,
+ 2, 0, 1, 0, 2
+ }
+
+ local idx, addr = zone.area.int8_t:find_one(wbytes)
+ if idx then
+ ms.found_offset('current_weather', addr)
+ return
+ end
+
+ dfhack.printerr('Could not find current_weather - must be a wrong save.')
+end
+
--
-- ui_menu_width
--
@@ -669,7 +709,7 @@ end
local function get_process_zone()
if os_type == 'windows' then
return zoomed_searcher('ui_workshop_job_cursor', 'ui_building_in_resize')
- else
+ elseif os_type == 'linux' or os_type == 'darwin' then
return zoomed_searcher('cur_year', 'cur_year_tick')
end
end
@@ -710,10 +750,10 @@ end
local function find_pause_state()
local zone
- if os_type == 'linux' then
+ if os_type == 'linux' or os_type == 'darwin' then
zone = zoomed_searcher('ui_look_cursor', 32)
elseif os_type == 'windows' then
- zone = zoomed_searcher('ui_workshop_job_cursor', 64)
+ zone = zoomed_searcher('ui_workshop_job_cursor', 80)
end
zone = zone or searcher
@@ -747,6 +787,7 @@ exec_finder(find_ui_build_selector, 'ui_build_selector')
print('\nPrimitive globals:\n')
+exec_finder(find_current_weather, 'current_weather')
exec_finder(find_ui_menu_width, { 'ui_menu_width', 'ui_area_map_width' })
exec_finder(find_ui_selected_unit, 'ui_selected_unit')
exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode')
diff --git a/scripts/devel/prepare-save.lua b/scripts/devel/prepare-save.lua
new file mode 100644
index 000000000..781e3b892
--- /dev/null
+++ b/scripts/devel/prepare-save.lua
@@ -0,0 +1,71 @@
+-- Prepare the current save for use with devel/find-offsets.
+
+df.global.pause_state = true
+
+--[[print('Placing anchor...')
+
+do
+ local wp = df.global.ui.waypoints
+
+ for _,pt in ipairs(wp.points) do
+ if pt.name == 'dfhack_anchor' then
+ print('Already placed.')
+ goto found
+ end
+ end
+
+ local x,y,z = pos2xyz(df.global.cursor)
+
+ if not x then
+ error("Place cursor at your preferred anchor point.")
+ end
+
+ local id = wp.next_point_id
+ wp.next_point_id = id + 1
+
+ wp.points:insert('#',{
+ new = true, id = id, name = 'dfhack_anchor',
+ comment=(x..','..y..','..z),
+ tile = string.byte('!'), fg_color = COLOR_LIGHTRED, bg_color = COLOR_BLUE,
+ pos = xyz2pos(x,y,z)
+ })
+
+::found::
+end]]
+
+print('Nicknaming units...')
+
+for i,unit in ipairs(df.global.world.units.active) do
+ dfhack.units.setNickname(unit, i..':'..unit.id)
+end
+
+print('Setting weather...')
+
+local wbytes = {
+ 2, 1, 0, 2, 0,
+ 1, 2, 1, 0, 0,
+ 2, 0, 2, 1, 2,
+ 1, 2, 0, 1, 1,
+ 2, 0, 1, 0, 2
+}
+
+for i=0,4 do
+ for j = 0,4 do
+ df.global.current_weather[i][j] = (wbytes[i*5+j+1] or 2)
+ end
+end
+
+local yearstr = df.global.cur_year..','..df.global.cur_year_tick
+
+print('Cur year and tick: '..yearstr)
+
+dfhack.persistent.save{
+ key='prepare-save/cur_year',
+ value=yearstr,
+ ints={df.global.cur_year, df.global.cur_year_tick}
+}
+
+-- Save
+
+dfhack.run_script('quicksave')
+
From f207714d4225e729fa43d04048d595ad6954521d Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Thu, 21 Jun 2012 21:08:36 +0400
Subject: [PATCH 10/40] Add finders for enabler, gps and init.
---
library/lua/memscan.lua | 29 +++++++-
scripts/devel/find-offsets.lua | 125 +++++++++++++++++++++++++++++++++
2 files changed, 153 insertions(+), 1 deletion(-)
diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua
index 65b02194c..4cf8d41c8 100644
--- a/library/lua/memscan.lua
+++ b/library/lua/memscan.lua
@@ -154,7 +154,8 @@ function MemoryArea.new(astart, aend)
int16_t = CheckedArray.new('int16_t',astart,aend),
uint16_t = CheckedArray.new('uint16_t',astart,aend),
int32_t = CheckedArray.new('int32_t',astart,aend),
- uint32_t = CheckedArray.new('uint32_t',astart,aend)
+ uint32_t = CheckedArray.new('uint32_t',astart,aend),
+ float = CheckedArray.new('float',astart,aend)
}
setmetatable(obj, MemoryArea)
return obj
@@ -453,4 +454,30 @@ function DiffSearcher:find_counter(prompt,data_type,delta,action_prompt)
)
end
+-- Screen size
+
+function get_screen_size()
+ -- Use already known globals
+ if dfhack.internal.getAddress('init') then
+ local d = df.global.init.display
+ return d.grid_x, d.grid_y
+ end
+ if dfhack.internal.getAddress('gps') then
+ local g = df.global.gps
+ return g.dimx, g.dimy
+ end
+
+ -- Parse stdout.log for resize notifications
+ io.stdout:flush()
+
+ local w,h = 80,25
+ for line in io.lines('stdout.log') do
+ local cw, ch = string.match(line, '^Resizing grid to (%d+)x(%d+)$')
+ if cw and ch then
+ w, h = tonumber(cw), tonumber(ch)
+ end
+ end
+ return w,h
+end
+
return _ENV
diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua
index 391390f14..bddd29dfe 100644
--- a/scripts/devel/find-offsets.lua
+++ b/scripts/devel/find-offsets.lua
@@ -252,6 +252,83 @@ local function find_gview()
dfhack.printerr('Could not find gview')
end
+--
+-- enabler
+--
+
+local function is_valid_enabler(e)
+ if not ms.is_valid_vector(e.textures.raws, 4)
+ or not ms.is_valid_vector(e.text_system, 4)
+ then
+ dfhack.printerr('Vector layout check failed.')
+ return false
+ end
+
+ return true
+end
+
+local function find_enabler()
+ -- Data from data/init/colors.txt
+ local colors = {
+ 0, 0, 0, 0, 0, 128, 0, 128, 0,
+ 0, 128, 128, 128, 0, 0, 128, 0, 128,
+ 128, 128, 0, 192, 192, 192, 128, 128, 128,
+ 0, 0, 255, 0, 255, 0, 0, 255, 255,
+ 255, 0, 0, 255, 0, 255, 255, 255, 0,
+ 255, 255, 255
+ }
+
+ for i = 1,#colors do colors[i] = colors[i]/255 end
+
+ local idx, addr = data.float:find_one(colors)
+ if idx then
+ validate_offset('enabler', is_valid_enabler, addr, df.enabler, 'ccolor')
+ return
+ end
+
+ dfhack.printerr('Could not find enabler')
+end
+
+--
+-- gps
+--
+
+local function is_valid_gps(g)
+ if g.clipx[0] < 0 or g.clipx[0] > g.clipx[1] or g.clipx[1] >= g.dimx then
+ dfhack.printerr('Invalid clipx: ', g.clipx[0], g.clipx[1], g.dimx)
+ end
+ if g.clipy[0] < 0 or g.clipy[0] > g.clipy[1] or g.clipy[1] >= g.dimy then
+ dfhack.printerr('Invalid clipy: ', g.clipy[0], g.clipy[1], g.dimy)
+ end
+
+ return true
+end
+
+local function find_gps()
+ print('\nPlease ensure the mouse cursor is not over the game window.')
+ if not utils.prompt_yes_no('Proceed?', true) then
+ return
+ end
+
+ local zone
+ if os_type == 'windows' or os_type == 'linux' then
+ zone = zoomed_searcher('cursor', 0x1000)
+ elseif os_type == 'darwin' then
+ zone = zoomed_searcher('enabler', 0x1000)
+ end
+ zone = zone or searcher
+
+ local w,h = ms.get_screen_size()
+
+ local idx, addr = zone.area.int32_t:find_one{w, h, -1, -1}
+ if idx then
+ validate_offset('gps', is_valid_gps, addr, df.graphic, 'dimx')
+ return
+ end
+
+ dfhack.printerr('Could not find gps')
+end
+
--
-- World
--
@@ -390,6 +467,51 @@ number, so when it shows "Min (5000df", it means 50000:]],
addr, df.ui_build_selector, 'plate_info', 'unit_min')
end
+--
+-- init
+--
+
+local function is_valid_init(i)
+ -- derived from curses_*.png image sizes presumably
+ if i.font.small_font_dispx ~= 8 or i.font.small_font_dispy ~= 12 or
+ i.font.large_font_dispx ~= 10 or i.font.large_font_dispy ~= 12 then
+ print('Unexpected font sizes: ',
+ i.font.small_font_dispx, i.font.small_font_dispy,
+ i.font.large_font_dispx, i.font.large_font_dispy)
+ if not utils.prompt_yes_no('Ignore?') then
+ return false
+ end
+ end
+
+ return true
+end
+
+local function find_init()
+ local zone
+ if os_type == 'windows' then
+ zone = zoomed_searcher('ui_build_selector', 0x3000)
+ elseif os_type == 'linux' or os_type == 'darwin' then
+ zone = zoomed_searcher('d_init', -0x2000)
+ end
+ zone = zone or searcher
+
+ local idx, addr = zone.area.int32_t:find_one{250, 150, 15, 0}
+ if idx then
+ validate_offset('init', is_valid_init, addr, df.init, 'input', 'hold_time')
+ return
+ end
+
+ local w,h = ms.get_screen_size()
+
+ local idx, addr = zone.area.int32_t:find_one{w, h}
+ if idx then
+ validate_offset('init', is_valid_init, addr, df.init, 'display', 'grid_x')
+ return
+ end
+
+ dfhack.printerr('Could not find init')
+end
+
--
-- current_weather
--
@@ -777,6 +899,8 @@ exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' })
exec_finder(find_announcements, 'announcements')
exec_finder(find_d_init, 'd_init')
exec_finder(find_gview, 'gview')
+exec_finder(find_enabler, 'enabler')
+exec_finder(find_gps, 'gps')
print('\nCompound globals (need loaded world):\n')
@@ -784,6 +908,7 @@ exec_finder(find_world, 'world')
exec_finder(find_ui, 'ui')
exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
exec_finder(find_ui_build_selector, 'ui_build_selector')
+exec_finder(find_init, 'init')
print('\nPrimitive globals:\n')
From 752da9ced5ce2df8cc9638cbf75a769325540e31 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Thu, 21 Jun 2012 21:26:25 +0400
Subject: [PATCH 11/40] Move formatting newly-found globals for symbols.xml to
lua code.
---
LUA_API.rst | 1 +
Lua API.html | 3 ++-
library/LuaApi.cpp | 6 +++---
library/LuaTools.cpp | 5 ++++-
library/include/ColorText.h | 2 ++
library/lua/memscan.lua | 10 ++++++++++
6 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/LUA_API.rst b/LUA_API.rst
index 9515690eb..5136bba57 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -451,6 +451,7 @@ Currently it defines the following features:
* ``dfhack.color([color])``
Sets the current output color. If color is *nil* or *-1*, resets to default.
+ Returns the previous color value.
* ``dfhack.is_interactive()``
diff --git a/Lua API.html b/Lua API.html
index 84d13e2f0..1c4dc4059 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -734,7 +734,8 @@ works with DFHack output infrastructure.
Same as println; intended for errors. Uses red color and logs to stderr.log.
dfhack.color([color])
-Sets the current output color. If color is nil or -1, resets to default.
+Sets the current output color. If color is nil or -1, resets to default.
+Returns the previous color value.
dfhack.is_interactive()
Checks if the thread can access the interactive console and returns true or false.
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 092404e33..b0a085eca 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -1074,9 +1074,9 @@ static int internal_setAddress(lua_State *L)
}
// Print via printerr, so that it is definitely logged to stderr.log.
- addr -= Core::getInstance().vinfo->getRebaseDelta();
- std::string msg = stl_sprintf("", name.c_str(), addr);
- dfhack_printerr(L, msg);
+ uint32_t iaddr = addr - Core::getInstance().vinfo->getRebaseDelta();
+ fprintf(stderr, "Setting global '%s' to %x (%x)\n", name.c_str(), addr, iaddr);
+ fflush(stderr);
return 1;
}
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 752c341b2..48244dedf 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -256,8 +256,11 @@ static int lua_dfhack_color(lua_State *S)
luaL_argerror(S, 1, "invalid color value");
color_ostream *out = Lua::GetOutput(S);
- if (out)
+ if (out) {
+ lua_pushinteger(S, (int)out->color());
out->color(color_ostream::color_value(cv));
+ return 1;
+ }
return 0;
}
diff --git a/library/include/ColorText.h b/library/include/ColorText.h
index 105832efd..0cc286dcf 100644
--- a/library/include/ColorText.h
+++ b/library/include/ColorText.h
@@ -111,6 +111,8 @@ namespace DFHack
void printerr(const char *format, ...);
void vprinterr(const char *format, va_list args);
+ /// Get color
+ color_value color() { return cur_color; }
/// Set color (ANSI color number)
void color(color_value c);
/// Reset color to default
diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua
index 4cf8d41c8..92a3e3e80 100644
--- a/library/lua/memscan.lua
+++ b/library/lua/memscan.lua
@@ -252,6 +252,16 @@ function found_offset(name,val)
end
else
dfhack.internal.setAddress(name, val)
+
+ local ival = val - dfhack.internal.getRebaseDelta()
+ local entry = string.format("\n", name, ival)
+
+ local ccolor = dfhack.color(COLOR_LIGHTGREEN)
+ dfhack.print(entry)
+ dfhack.color(ccolor)
+
+ io.stdout:write(entry)
+ io.stdout:flush()
end
end
From 65e82f7c12f95e461363e15c781c3fd4c5d241d3 Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Fri, 22 Jun 2012 16:36:50 +0400
Subject: [PATCH 12/40] Support controllable error presentation verbosity in
lua code.
Use qerror to squash stack traces and location prefix.
---
LUA_API.rst | 88 +++++++++++++-----
Lua API.html | 162 ++++++++++++++++++++++-----------
library/LuaTools.cpp | 78 ++++++++++++++--
library/lua/dfhack.lua | 4 +
library/lua/memscan.lua | 2 +-
library/lua/utils.lua | 2 +-
scripts/devel/find-offsets.lua | 6 +-
scripts/fix/item-occupancy.lua | 3 +-
scripts/quicksave.lua | 3 +-
9 files changed, 259 insertions(+), 89 deletions(-)
diff --git a/LUA_API.rst b/LUA_API.rst
index 5136bba57..e8c413fe7 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -426,13 +426,17 @@ not destroy any objects allocated in this way, so the user
should be prepared to catch the error and do the necessary
cleanup.
-================
-DFHack utilities
-================
+==========
+DFHack API
+==========
DFHack utility functions are placed in the ``dfhack`` global tree.
-Currently it defines the following features:
+Native utilities
+================
+
+Input & Output
+--------------
* ``dfhack.print(args...)``
@@ -474,23 +478,9 @@ Currently it defines the following features:
If the interactive console is not accessible, returns *nil, error*.
-* ``dfhack.pcall(f[,args...])``
-
- Invokes f via xpcall, using an error function that attaches
- a stack trace to the error. The same function is used by SafeCall
- in C++, and dfhack.safecall.
-
- The returned error is a table with separate ``message`` and
- ``stacktrace`` string fields; it implements ``__tostring``.
-
-* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])``
-
- Just like pcall, but also prints the error using printerr before
- returning. Intended as a convenience function.
-
-* ``dfhack.saferesume(coroutine[,args...])``
- Compares to coroutine.resume like dfhack.safecall vs pcall.
+Miscellaneous
+-------------
* ``dfhack.run_script(name[,args...])``
@@ -511,6 +501,36 @@ Currently it defines the following features:
to group operations together in one big critical section. A plugin
can choose to run all lua code inside a C++-side suspend lock.
+
+Exception handling
+------------------
+
+* ``dfhack.error(msg[,level[,verbose]])``
+
+ Throws a dfhack exception object with location and stack trace.
+ The verbose parameter controls whether the trace is printed by default.
+
+* ``qerror(msg[,level])``
+
+ Calls ``dfhack.error()`` with ``verbose`` being *false*. Intended to
+ be used for user-caused errors in scripts, where stack traces are not
+ desirable.
+
+* ``dfhack.pcall(f[,args...])``
+
+ Invokes f via xpcall, using an error function that attaches
+ a stack trace to the error. The same function is used by SafeCall
+ in C++, and dfhack.safecall.
+
+* ``safecall(f[,args...])``, ``dfhack.safecall(f[,args...])``
+
+ Just like pcall, but also prints the error using printerr before
+ returning. Intended as a convenience function.
+
+* ``dfhack.saferesume(coroutine[,args...])``
+
+ Compares to coroutine.resume like dfhack.safecall vs pcall.
+
* ``dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])``
Invokes ``fn`` with ``args``, and after it returns or throws an
@@ -535,9 +555,33 @@ Currently it defines the following features:
Calls ``fn(obj,args...)``, then finalizes with ``obj:delete()``.
+* ``dfhack.exception``
+
+ Metatable of error objects used by dfhack. The objects have the
+ following properties:
+
+ ``err.where``
+ The location prefix string, or *nil*.
+ ``err.message``
+ The base message string.
+ ``err.stacktrace``
+ The stack trace string, or *nil*.
+ ``err.cause``
+ A different exception object, or *nil*.
+ ``err.thread``
+ The coroutine that has thrown the exception.
+ ``err.verbose``
+ Boolean, or *nil*; specifies if where and stacktrace should be printed.
+ ``tostring(err)``, or ``err:tostring([verbose])``
+ Converts the exception to string.
+
+* ``dfhack.exception.verbose``
+
+ The default value of the ``verbose`` argument of ``err:tostring()``.
+
Persistent configuration storage
-================================
+--------------------------------
This api is intended for storing configuration options in the world itself.
It probably should be restricted to data that is world-dependent.
@@ -579,7 +623,7 @@ functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.
Material info lookup
-====================
+--------------------
A material info record has fields:
diff --git a/Lua API.html b/Lua API.html
index 1c4dc4059..47cf08ab6 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -333,30 +333,36 @@ ul.auto-toc {
Recursive table assignment
-DFHack utilities
-- Persistent configuration storage
-- Material info lookup
-- C++ function wrappers
-- Gui module
-- Job module
-- Units module
-- Items module
-- Maps module
-- Burrows module
-- Buildings module
-- Constructions module
-- Internal API
+- DFHack API
+- Native utilities
-- Core interpreter context
@@ -717,10 +723,13 @@ should be prepared to catch the error and do the necessary
cleanup.
-
-
+
+
DFHack utility functions are placed in the dfhack global tree.
-
Currently it defines the following features:
+
+
+
+
+
+
dfhack.run_script(name[,args...])
Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
The name argument should be the name stem, as would be used on the command line.
@@ -782,6 +782,32 @@ the lock. It is safe to nest suspends.
to group operations together in one big critical section. A plugin
can choose to run all lua code inside a C++-side suspend lock.
+
+
+
+
+
+dfhack.error(msg[,level[,verbose]])
+Throws a dfhack exception object with location and stack trace.
+The verbose parameter controls whether the trace is printed by default.
+
+qerror(msg[,level])
+Calls dfhack.error() with verbose being false. Intended to
+be used for user-caused errors in scripts, where stack traces are not
+desirable.
+
+dfhack.pcall(f[,args...])
+Invokes f via xpcall, using an error function that attaches
+a stack trace to the error. The same function is used by SafeCall
+in C++, and dfhack.safecall.
+
+safecall(f[,args...]), dfhack.safecall(f[,args...])
+Just like pcall, but also prints the error using printerr before
+returning. Intended as a convenience function.
+
+dfhack.saferesume(coroutine[,args...])
+Compares to coroutine.resume like dfhack.safecall vs pcall.
+
dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])
Invokes fn with args, and after it returns or throws an
error calls cleanup_fn with cleanup_args. Any return values from
@@ -801,9 +827,40 @@ Implemented using call_with_final
dfhack.with_temp_object(obj,fn[,args...])
Calls fn(obj,args...), then finalizes with obj:delete().
+dfhack.exception
+Metatable of error objects used by dfhack. The objects have the
+following properties:
+
+- err.where
+The location prefix string, or nil.
+
+- err.message
+The base message string.
+
+- err.stacktrace
+The stack trace string, or nil.
+
+- err.cause
+A different exception object, or nil.
+
+- err.thread
+The coroutine that has thrown the exception.
+
+- err.verbose
+Boolean, or nil; specifies if where and stacktrace should be printed.
+
+- tostring(err), or err:tostring([verbose])
+Converts the exception to string.
+
+
+
+dfhack.exception.verbose
+The default value of the verbose argument of err:tostring().
+
+
-
+
This api is intended for storing configuration options in the world itself.
It probably should be restricted to data that is world-dependent.
Entries are identified by a string key, but it is also possible to manage
@@ -838,7 +895,7 @@ functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.
-
+
A material info record has fields:
type, index, material
@@ -881,8 +938,9 @@ Accept dfhack_material_category auto-assign table.
+
-
+
Thin wrappers around C++ functions, similar to the ones for virtual methods.
One notable difference is that these explicit wrappers allow argument count
adjustment according to the usual lua rules, so trailing false/nil arguments
@@ -911,7 +969,7 @@ can be omitted.
-
+
dfhack.units.getPosition(unit)
Returns true x,y,z of the unit, or nil if invalid; may be not equal to unit.pos if caged.
@@ -1038,7 +1096,7 @@ or raws. The ignore_noble boolean disables the
-
+
dfhack.items.getPosition(item)
Returns true x,y,z of the item, or nil if invalid; may be not equal to item.pos if in inventory.
@@ -1081,7 +1139,7 @@ Returns false in case of error.
-
+
dfhack.buildings.getSize(building)
Returns width, height, centerx, centery.
@@ -1297,7 +1355,7 @@ can be determined this way, constructBuilding
-
+
dfhack.constructions.designateNew(pos,type,item_type,mat_index)
Designates a new construction at given position. If there already is
@@ -1313,7 +1371,7 @@ Returns true, was_only_planned if removed; or false if none fo
-
+
These functions are intended for the use by dfhack developers,
and are only documented here for completeness:
@@ -1356,7 +1414,7 @@ Returns: found_index, or nil if end reached.
-
+
While plugins can create any number of interpreter instances,
there is one special context managed by dfhack core. It is the
only context that can receive events from DF and plugins.
@@ -1387,7 +1445,7 @@ Using
timeout_active(id,nil) cancels the timer
-
+
An event is just a lua table with a predefined metatable that
contains a __call metamethod. When it is invoked, it loops
through the table with next and calls all contained values.
@@ -1413,14 +1471,14 @@ order using dfhack.safecall.
-
+
DFHack plugins may export native functions and events
to lua contexts. They are automatically imported by
mkmodule('plugins.<name>'); this means that a lua
module file is still necessary for require to read.
The following plugins have lua support.
-
+
Implements extended burrow manipulations.
Events:
-
+
Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items.
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 48244dedf..28571a0f7 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -426,10 +426,12 @@ static bool convert_to_exception(lua_State *L, int slevel, lua_State *thread = N
// Create a new exception for this thread
lua_newtable(L);
- luaL_where(L, 1);
+ luaL_where(L, slevel);
+ lua_setfield(L, -2, "where");
lua_pushstring(L, "coroutine resume failed");
- lua_concat(L, 2);
lua_setfield(L, -2, "message");
+ lua_getfield(L, -2, "verbose");
+ lua_setfield(L, -2, "verbose");
lua_swap(L);
lua_setfield(L, -2, "cause");
}
@@ -483,12 +485,57 @@ static int dfhack_onerror(lua_State *L)
return 1;
}
+static int dfhack_error(lua_State *L)
+{
+ luaL_checkany(L, 1);
+ lua_settop(L, 3);
+ int level = std::max(1, luaL_optint(L, 2, 1));
+
+ lua_pushvalue(L, 1);
+
+ if (convert_to_exception(L, level))
+ {
+ luaL_where(L, level);
+ lua_setfield(L, -2, "where");
+
+ if (!lua_isnil(L, 3))
+ {
+ lua_pushvalue(L, 3);
+ lua_setfield(L, -2, "verbose");
+ }
+ }
+
+ return lua_error(L);
+}
+
static int dfhack_exception_tostring(lua_State *L)
{
luaL_checktype(L, 1, LUA_TTABLE);
+ lua_settop(L, 2);
+
+ if (lua_isnil(L, 2))
+ {
+ lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN);
+ lua_getfield(L, -1, "verbose");
+ lua_insert(L, 2);
+ lua_settop(L, 2);
+ }
+
+ lua_getfield(L, 1, "verbose");
+
+ bool verbose =
+ lua_toboolean(L, 2) || lua_toboolean(L, 3) ||
+ (lua_isnil(L, 2) && lua_isnil(L, 3));
int base = lua_gettop(L);
+ if (verbose || lua_isnil(L, 3))
+ {
+ lua_getfield(L, 1, "where");
+ if (!lua_isstring(L, -1))
+ lua_pop(L, 1);
+ }
+
lua_getfield(L, 1, "message");
if (!lua_isstring(L, -1))
{
@@ -496,15 +543,26 @@ static int dfhack_exception_tostring(lua_State *L)
lua_pushstring(L, "(error message is not a string)");
}
- lua_pushstring(L, "\n");
- lua_getfield(L, 1, "stacktrace");
- if (!lua_isstring(L, -1))
- lua_pop(L, 2);
+ if (verbose)
+ {
+ lua_pushstring(L, "\n");
+ lua_getfield(L, 1, "stacktrace");
+ if (!lua_isstring(L, -1))
+ lua_pop(L, 2);
+ }
lua_pushstring(L, "\ncaused by:\n");
lua_getfield(L, 1, "cause");
if (lua_isnil(L, -1))
lua_pop(L, 2);
+ else if (lua_istable(L, -1))
+ {
+ lua_pushcfunction(L, dfhack_exception_tostring);
+ lua_swap(L);
+ lua_pushvalue(L, 2);
+ if (lua_pcall(L, 2, 1, 0) != LUA_OK)
+ error_tostring(L);
+ }
else
error_tostring(L);
@@ -655,7 +713,12 @@ static int dfhack_coauxwrap (lua_State *L) {
if (Lua::IsSuccess(r))
return lua_gettop(L);
else
+ {
+ if (lua_checkstack(L, LUA_MINSTACK))
+ convert_to_exception(L, 1);
+
return lua_error(L);
+ }
}
static int dfhack_cowrap (lua_State *L) {
@@ -1162,6 +1225,7 @@ static const luaL_Reg dfhack_funcs[] = {
{ "safecall", dfhack_safecall },
{ "saferesume", dfhack_saferesume },
{ "onerror", dfhack_onerror },
+ { "error", dfhack_error },
{ "call_with_finalizer", dfhack_call_with_finalizer },
{ "with_suspend", lua_dfhack_with_suspend },
{ "open_plugin", dfhack_open_plugin },
@@ -1362,6 +1426,8 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_newtable(state);
lua_pushcfunction(state, dfhack_exception_tostring);
lua_setfield(state, -2, "__tostring");
+ lua_pushcfunction(state, dfhack_exception_tostring);
+ lua_setfield(state, -2, "tostring");
lua_dup(state);
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_EXCEPTION_META_TOKEN);
lua_setfield(state, -2, "exception");
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index 4cdb4c950..d200a6c5c 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -49,6 +49,10 @@ function dfhack.pcall(f, ...)
return xpcall(f, dfhack.onerror, ...)
end
+function qerror(msg, level)
+ dfhack.error(msg, (level or 1) + 1, false)
+end
+
function dfhack.with_finalize(...)
return dfhack.call_with_finalizer(0,true,...)
end
diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua
index 92a3e3e80..970f821c2 100644
--- a/library/lua/memscan.lua
+++ b/library/lua/memscan.lua
@@ -235,7 +235,7 @@ function found_offset(name,val)
if not val then
print('Could not find offset '..name)
if not cval and not utils.prompt_yes_no('Continue with the script?') then
- error('User quit')
+ qerror('User quit')
end
return
end
diff --git a/library/lua/utils.lua b/library/lua/utils.lua
index 93ee840c4..f303091d6 100644
--- a/library/lua/utils.lua
+++ b/library/lua/utils.lua
@@ -379,7 +379,7 @@ function prompt_yes_no(msg,default)
elseif string.match(rv,'^[Nn]') then
return false
elseif rv == 'abort' then
- error('User abort in utils.prompt_yes_no()')
+ qerror('User abort in utils.prompt_yes_no()')
elseif rv == '' and default ~= nil then
return default
end
diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua
index bddd29dfe..6fc127351 100644
--- a/scripts/devel/find-offsets.lua
+++ b/scripts/devel/find-offsets.lua
@@ -43,12 +43,12 @@ end
local data = ms.get_data_segment()
if not data then
- error('Could not find data segment')
+ qerror('Could not find data segment')
end
print('\nData section: '..tostring(data))
if data.size < 5000000 then
- error('Data segment too short.')
+ qerror('Data segment too short.')
end
local searcher = ms.DiffSearcher.new(data)
@@ -103,7 +103,7 @@ local function exec_finder(finder, names)
if not dfhack.safecall(finder) then
if not utils.prompt_yes_no('Proceed with the rest of the script?') then
searcher:reset()
- error('Quit')
+ qerror('Quit')
end
end
else
diff --git a/scripts/fix/item-occupancy.lua b/scripts/fix/item-occupancy.lua
index b5466b7a8..09c6b3030 100644
--- a/scripts/fix/item-occupancy.lua
+++ b/scripts/fix/item-occupancy.lua
@@ -116,8 +116,7 @@ if opt then
if opt == '--fix' then
fix = true
else
- dfhack.printerr('Invalid option: '..opt)
- return
+ qerror('Invalid option: '..opt)
end
end
diff --git a/scripts/quicksave.lua b/scripts/quicksave.lua
index c54cc730b..f4886b35b 100644
--- a/scripts/quicksave.lua
+++ b/scripts/quicksave.lua
@@ -1,8 +1,7 @@
-- Makes the game immediately save the state.
if not dfhack.isMapLoaded() then
- dfhack.printerr("World and map aren't loaded.")
- return
+ qerror("World and map aren't loaded.")
end
local ui_main = df.global.ui.main
From bd37cc09c525d4db9400e224e4ca3cadcf0eed4c Mon Sep 17 00:00:00 2001
From: Alexander Gavrilov
Date: Fri, 22 Jun 2012 20:17:55 +0400
Subject: [PATCH 13/40] Update the Lua API document with info about scripts.
---
LUA_API.rst | 183 ++++++++++++++++++++++++++++++-----------
Lua API.html | 172 ++++++++++++++++++++++++++------------
library/lua/dfhack.lua | 2 +
3 files changed, 256 insertions(+), 101 deletions(-)
diff --git a/LUA_API.rst b/LUA_API.rst
index e8c413fe7..5fc653bb3 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -4,9 +4,26 @@ DFHack Lua API
.. contents::
-====================
-DF structure wrapper
-====================
+The current version of DFHack has extensive support for
+the Lua scripting language, providing access to:
+
+1. Raw data structures used by the game.
+2. Many C++ functions for high-level access to these
+ structures, and interaction with dfhack itself.
+3. Some functions exported by C++ plugins.
+
+Lua code can be used both for writing scripts, which
+are treated by DFHack command line prompt almost as
+native C++ commands, and invoked by plugins written in c++.
+
+This document describes native API available to Lua in detail.
+For the most part it does not describe utility functions
+implemented by Lua files located in hack/lua/...
+
+
+=========================
+DF data structure wrapper
+=========================
DF structures described by the xml files in library/xml are exported
to lua code as a tree of objects and functions under the ``df`` global,
@@ -479,29 +496,6 @@ Input & Output
If the interactive console is not accessible, returns *nil, error*.
-Miscellaneous
--------------
-
-* ``dfhack.run_script(name[,args...])``
-
- Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
- The ``name`` argument should be the name stem, as would be used on the command line.
- Note that the script is re-read from the file every time it is called, and errors
- are propagated to the caller.
-
-* ``dfhack.with_suspend(f[,args...])``
-
- Calls ``f`` with arguments after grabbing the DF core suspend lock.
- Suspending is necessary for accessing a consistent state of DF memory.
-
- Returned values and errors are propagated through after releasing
- the lock. It is safe to nest suspends.
-
- Every thread is allowed only one suspend per DF frame, so it is best
- to group operations together in one big critical section. A plugin
- can choose to run all lua code inside a C++-side suspend lock.
-
-
Exception handling
------------------
@@ -531,30 +525,6 @@ Exception handling
Compares to coroutine.resume like dfhack.safecall vs pcall.
-* ``dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])``
-
- Invokes ``fn`` with ``args``, and after it returns or throws an
- error calls ``cleanup_fn`` with ``cleanup_args``. Any return values from
- ``fn`` are propagated, and errors are re-thrown.
-
- The ``num_cleanup_args`` integer specifies the number of ``cleanup_args``,
- and the ``always`` boolean specifies if cleanup should be called in any case,
- or only in case of an error.
-
-* ``dfhack.with_finalize(cleanup_fn,fn[,args...])``
-
- Calls ``fn`` with arguments, then finalizes with ``cleanup_fn``.
- Implemented using ``call_with_finalizer(0,true,...)``.
-
-* ``dfhack.with_onerror(cleanup_fn,fn[,args...])``
-
- Calls ``fn`` with arguments, then finalizes with ``cleanup_fn`` on any thrown error.
- Implemented using ``call_with_finalizer(0,false,...)``.
-
-* ``dfhack.with_temp_object(obj,fn[,args...])``
-
- Calls ``fn(obj,args...)``, then finalizes with ``obj:delete()``.
-
* ``dfhack.exception``
Metatable of error objects used by dfhack. The objects have the
@@ -580,6 +550,46 @@ Exception handling
The default value of the ``verbose`` argument of ``err:tostring()``.
+Locking and finalization
+------------------------
+
+* ``dfhack.with_suspend(f[,args...])``
+
+ Calls ``f`` with arguments after grabbing the DF core suspend lock.
+ Suspending is necessary for accessing a consistent state of DF memory.
+
+ Returned values and errors are propagated through after releasing
+ the lock. It is safe to nest suspends.
+
+ Every thread is allowed only one suspend per DF frame, so it is best
+ to group operations together in one big critical section. A plugin
+ can choose to run all lua code inside a C++-side suspend lock.
+
+* ``dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])``
+
+ Invokes ``fn`` with ``args``, and after it returns or throws an
+ error calls ``cleanup_fn`` with ``cleanup_args``. Any return values from
+ ``fn`` are propagated, and errors are re-thrown.
+
+ The ``num_cleanup_args`` integer specifies the number of ``cleanup_args``,
+ and the ``always`` boolean specifies if cleanup should be called in any case,
+ or only in case of an error.
+
+* ``dfhack.with_finalize(cleanup_fn,fn[,args...])``
+
+ Calls ``fn`` with arguments, then finalizes with ``cleanup_fn``.
+ Implemented using ``call_with_finalizer(0,true,...)``.
+
+* ``dfhack.with_onerror(cleanup_fn,fn[,args...])``
+
+ Calls ``fn`` with arguments, then finalizes with ``cleanup_fn`` on any thrown error.
+ Implemented using ``call_with_finalizer(0,false,...)``.
+
+* ``dfhack.with_temp_object(obj,fn[,args...])``
+
+ Calls ``fn(obj,args...)``, then finalizes with ``obj:delete()``.
+
+
Persistent configuration storage
--------------------------------
@@ -1312,6 +1322,42 @@ Features:
Invokes all listeners contained in the event in an arbitrary
order using ``dfhack.safecall``.
+
+=======
+Modules
+=======
+
+DFHack sets up the lua interpreter so that the built-in ``require``
+function can be used to load shared lua code from hack/lua/.
+The ``dfhack`` namespace reference itself may be obtained via
+``require('dfhack')``, although it is initially created as a
+global by C++ bootstrap code.
+
+The following functions are provided:
+
+* ``mkmodule(name)``
+
+ Creates an environment table for the module. Intended to be used as::
+
+ local _ENV = mkmodule('foo')
+ ...
+ return _ENV
+
+ If called the second time, returns the same table; thus providing reload support.
+
+* ``reload(name)``
+
+ Reloads a previously ``require``-d module *"name"* from the file.
+ Intended as a help for module development.
+
+* ``dfhack.BASE_G``
+
+ This variable contains the root global environment table, which is
+ used as a base for all module and script environments. Its contents
+ should be kept limited to the standard Lua library and API described
+ in this document.
+
+
=======
Plugins
=======
@@ -1373,3 +1419,40 @@ sort
Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items.
+
+
+=======
+Scripts
+=======
+
+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
+the extension.
+
+**NOTE:** Scripts placed in subdirectories still can be accessed, but
+do not clutter the ``ls`` command list; thus it is preferred
+for obscure developer-oriented scripts and scripts used by tools.
+When calling such scripts, always use '/' as the separator for
+directories, e.g. ``devel/lua-example``.
+
+Scripts are re-read from disk every time they are used
+(this may be changed later to check the file change time); however
+the global variable values persist in memory between calls.
+Every script gets its own separate environment for global
+variables.
+
+Arguments are passed in to the scripts via the **...** built-in
+quasi-variable; when the script is called by the DFHack core,
+they are all guaranteed to be non-nil strings.
+
+DFHack core invokes the scripts in the *core context* (see above);
+however it is possible to call them from any lua code (including
+from other scripts) in any context, via the same function the core uses:
+
+* ``dfhack.run_script(name[,args...])``
+
+ Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
+ The ``name`` argument should be the name stem, as would be used on the command line.
+
+Note that this function lets errors propagate to the caller.
diff --git a/Lua API.html b/Lua API.html
index 47cf08ab6..04e899366 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -320,7 +320,7 @@ ul.auto-toc {
-
-
+
The current version of DFHack has extensive support for
+the Lua scripting language, providing access to:
+
+- Raw data structures used by the game.
+- Many C++ functions for high-level access to these
+structures, and interaction with dfhack itself.
+- Some functions exported by C++ plugins.
+
+
Lua code can be used both for writing scripts, which
+are treated by DFHack command line prompt almost as
+native C++ commands, and invoked by plugins written in c++.
+
This document describes native API available to Lua in detail.
+For the most part it does not describe utility functions
+implemented by Lua files located in hack/lua/...
+
+
DF structures described by the xml files in library/xml are exported
to lua code as a tree of objects and functions under the df global,
which broadly maps to the df namespace in C++.
@@ -764,28 +780,8 @@ string, global environment and command-line history file.
-
-
-
-dfhack.run_script(name[,args...])
-Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
-The name argument should be the name stem, as would be used on the command line.
-Note that the script is re-read from the file every time it is called, and errors
-are propagated to the caller.
-
-dfhack.with_suspend(f[,args...])
-Calls f with arguments after grabbing the DF core suspend lock.
-Suspending is necessary for accessing a consistent state of DF memory.
-Returned values and errors are propagated through after releasing
-the lock. It is safe to nest suspends.
-Every thread is allowed only one suspend per DF frame, so it is best
-to group operations together in one big critical section. A plugin
-can choose to run all lua code inside a C++-side suspend lock.
-
-
-
-
+
dfhack.error(msg[,level[,verbose]])
Throws a dfhack exception object with location and stack trace.
@@ -808,25 +804,6 @@ returning. Intended as a convenience function.
dfhack.saferesume(coroutine[,args...])
Compares to coroutine.resume like dfhack.safecall vs pcall.
-dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])
-Invokes fn with args, and after it returns or throws an
-error calls cleanup_fn with cleanup_args. Any return values from
-fn are propagated, and errors are re-thrown.
-The num_cleanup_args integer specifies the number of cleanup_args,
-and the always boolean specifies if cleanup should be called in any case,
-or only in case of an error.
-
-dfhack.with_finalize(cleanup_fn,fn[,args...])
-Calls fn with arguments, then finalizes with cleanup_fn.
-Implemented using call_with_finalizer(0,true,...).
-
-dfhack.with_onerror(cleanup_fn,fn[,args...])
-Calls fn with arguments, then finalizes with cleanup_fn on any thrown error.
-Implemented using call_with_finalizer(0,false,...).
-
-dfhack.with_temp_object(obj,fn[,args...])
-Calls fn(obj,args...), then finalizes with obj:delete().
-
dfhack.exception
Metatable of error objects used by dfhack. The objects have the
following properties:
@@ -859,6 +836,39 @@ following properties:
+
+
+
+dfhack.with_suspend(f[,args...])
+Calls f with arguments after grabbing the DF core suspend lock.
+Suspending is necessary for accessing a consistent state of DF memory.
+Returned values and errors are propagated through after releasing
+the lock. It is safe to nest suspends.
+Every thread is allowed only one suspend per DF frame, so it is best
+to group operations together in one big critical section. A plugin
+can choose to run all lua code inside a C++-side suspend lock.
+
+dfhack.call_with_finalizer(num_cleanup_args,always,cleanup_fn[,cleanup_args...],fn[,args...])
+Invokes fn with args, and after it returns or throws an
+error calls cleanup_fn with cleanup_args. Any return values from
+fn are propagated, and errors are re-thrown.
+The num_cleanup_args integer specifies the number of cleanup_args,
+and the always boolean specifies if cleanup should be called in any case,
+or only in case of an error.
+
+dfhack.with_finalize(cleanup_fn,fn[,args...])
+Calls fn with arguments, then finalizes with cleanup_fn.
+Implemented using call_with_finalizer(0,true,...).
+
+dfhack.with_onerror(cleanup_fn,fn[,args...])
+Calls fn with arguments, then finalizes with cleanup_fn on any thrown error.
+Implemented using call_with_finalizer(0,false,...).
+
+dfhack.with_temp_object(obj,fn[,args...])
+Calls fn(obj,args...), then finalizes with obj:delete().
+
+
+
This api is intended for storing configuration options in the world itself.
@@ -1470,15 +1480,45 @@ order using dfhack.safecall.
+
+
+
DFHack sets up the lua interpreter so that the built-in require
+function can be used to load shared lua code from hack/lua/.
+The dfhack namespace reference itself may be obtained via
+require('dfhack'), although it is initially created as a
+global by C++ bootstrap code.
+
The following functions are provided:
+
+mkmodule(name)
+Creates an environment table for the module. Intended to be used as:
+
+local _ENV = mkmodule('foo')
+...
+return _ENV
+
+If called the second time, returns the same table; thus providing reload support.
+
+reload(name)
+Reloads a previously require-d module "name" from the file.
+Intended as a help for module development.
+
+dfhack.BASE_G
+This variable contains the root global environment table, which is
+used as a base for all module and script environments. Its contents
+should be kept limited to the standard Lua library and API described
+in this document.
+
+
+
-
+
DFHack plugins may export native functions and events
to lua contexts. They are automatically imported by
mkmodule('plugins.<name>'); this means that a lua
module file is still necessary for require to read.
The following plugins have lua support.
-
+
Implements extended burrow manipulations.
Events:
-
+
Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items.
+
+
+
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
+the extension.
+
NOTE: Scripts placed in subdirectories still can be accessed, but
+do not clutter the ls command list; thus it is preferred
+for obscure developer-oriented scripts and scripts used by tools.
+When calling such scripts, always use '/' as the separator for
+directories, e.g. devel/lua-example.
+
Scripts are re-read from disk every time they are used
+(this may be changed later to check the file change time); however
+the global variable values persist in memory between calls.
+Every script gets its own separate environment for global
+variables.
+
Arguments are passed in to the scripts via the ... built-in
+quasi-variable; when the script is called by the DFHack core,
+they are all guaranteed to be non-nil strings.
+
DFHack core invokes the scripts in the core context (see above);
+however it is possible to call them from any lua code (including
+from other scripts) in any context, via the same function the core uses:
+
+dfhack.run_script(name[,args...])
+Run a lua script in hack/scripts/, as if it was started from dfhack command-line.
+The name argument should be the name stem, as would be used on the command line.
+
+
+
Note that this function lets errors propagate to the caller.
+