From 6bd67cc055a3e2ec9a7562536d8c51c11a2789d2 Mon Sep 17 00:00:00 2001 From: jj Date: Thu, 14 Jun 2012 14:25:16 +0200 Subject: [PATCH 01/29] 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/29] 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/29] 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 ed4acbdedbb0ddecac5b8b666ef159289697ec93 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Wed, 20 Jun 2012 10:12:26 +0400 Subject: [PATCH 04/29] 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 05/29] 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 06/29] 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 07/29] 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 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...)

            Output tab-separated args as standard lua print would do, @@ -753,20 +762,11 @@ this, forcing the function to block on input with lock held.

            string, global environment and command-line history file.

            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...])

            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.

          • +
          +
          +
          +

          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 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().

            +
          +
          -

          Persistent configuration storage

          +

          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.

          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.

          -

          Material info lookup

          +

          Material info lookup

          A material info record has fields:

          • type, index, material

            @@ -881,8 +938,9 @@ Accept dfhack_material_category auto-assign table.

          +
          -

          C++ function wrappers

          +

          C++ function wrappers

          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.

        -

        Gui module

        +

        Gui module

        • dfhack.gui.getCurViewscreen()

          Returns the viewscreen that is current in the core.

          @@ -947,7 +1005,7 @@ The is_bright boolean actually seems to invert the brightness.

        -

        Job module

        +

        Job module

        • dfhack.job.cloneJobStruct(job)

          Creates a deep copy of the given job.

          @@ -984,7 +1042,7 @@ a lua list containing them.

        -

        Units module

        +

        Units module

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

        Items module

        +

        Items module

        • 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.

        -

        Maps module

        +

        Maps module

        • dfhack.maps.getSize()

          Returns map size in blocks: x, y, z

          @@ -1122,7 +1180,7 @@ burrows, or the presence of invaders.

        -

        Burrows module

        +

        Burrows module

        • dfhack.burrows.findByName(name)

          Returns the burrow pointer or nil.

          @@ -1157,7 +1215,7 @@ burrows, or the presence of invaders.

        -

        Buildings module

        +

        Buildings module

        • dfhack.buildings.getSize(building)

          Returns width, height, centerx, centery.

          @@ -1297,7 +1355,7 @@ can be determined this way, constructBuilding
        -

        Constructions module

        +

        Constructions module

        • 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

        -

        Internal API

        +

        Internal API

        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.

        -

        Core interpreter context

        +

        Core interpreter context

        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
      -

      Event type

      +

      Event type

      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.

      -

      Plugins

      +

      Plugins

      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.

      -

      burrows

      +

      burrows

      Implements extended burrow manipulations.

      Events:

        @@ -1458,7 +1516,7 @@ set is the same as used by the command line.

        The lua module file also re-exports functions from dfhack.burrows.

      -

      sort

      +

      sort

      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 08/29] 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 { -
      -

      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. +
      3. Many C++ functions for high-level access to these +structures, and interaction with dfhack itself.
      4. +
      5. Some functions exported by C++ plugins.
      6. +
      +

      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, which broadly maps to the df namespace in C++.

      @@ -764,28 +780,8 @@ string, global environment and command-line history file.

    -
    -

    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

    +

    Exception handling

    • 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:

    +
    +

    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

    This api is intended for storing configuration options in the world itself. @@ -1470,15 +1480,45 @@ 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

    +

    Plugins

    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.

    -

    burrows

    +

    burrows

    Implements extended burrow manipulations.

    Events:

      @@ -1516,11 +1556,41 @@ set is the same as used by the command line.

      The lua module file also re-exports functions from dfhack.burrows.

    -

    sort

    +

    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/library/lua/dfhack.lua b/library/lua/dfhack.lua index d200a6c5c..d56d4df60 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -68,6 +68,8 @@ function dfhack.with_temp_object(obj,fn,...) return dfhack.call_with_finalizer(1,true,call_delete,obj,fn,obj,...) end +dfhack.exception.__index = dfhack.exception + -- Module loading function mkmodule(module,env) From 0e582901ee972853999dfc86dbe93093a65cd35b Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Fri, 22 Jun 2012 20:41:15 +0400 Subject: [PATCH 09/29] Update the compile document with some info about df-structures and RPC. --- COMPILE.rst | 23 ++++++++++++++++++++++- Compile.html | 33 ++++++++++++++++++++++++--------- Readme.html | 7 ++++++- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/COMPILE.rst b/COMPILE.rst index beb738363..8fca0e1f5 100644 --- a/COMPILE.rst +++ b/COMPILE.rst @@ -152,10 +152,13 @@ Valid and useful build types include 'Release', 'Debug' and ================================ Using the library as a developer ================================ -Currently, the only way to use the library is to write a plugin that can be loaded by it. + +Currently, the most direct way to use the library is to write a plugin that can be loaded by it. All the plugins can be found in the 'plugins' folder. There's no in-depth documentation on how to write one yet, but it should be easy enough to copy one and just follow the pattern. +Other than through plugins, it is possible to use DFHack via remote access interface, or by writing Lua scripts. + The most important parts of DFHack are the Core, Console, Modules and Plugins. * Core acts as the centerpiece of DFHack - it acts as a filter between DF and SDL and synchronizes the various plugins with DF. @@ -171,6 +174,24 @@ The main license is zlib/libpng, some bits are MIT licensed, and some are BSD li Feel free to add your own extensions and plugins. Contributing back to the dfhack repository is welcome and the right thing to do :) +DF data structure definitions +============================= + +DFHack uses information about the game data structures, represented via xml files in the library/xml/ submodule. + +Data structure layouts are described in files following the df.*.xml name pattern. This information is transformed by a perl script into C++ headers describing the structures, and associated metadata for the Lua wrapper. These headers and data are then compiled into the DFHack libraries, thus necessitating a compatibility break every time layouts change; in return it significantly boosts the efficiency and capabilities of DFHack code. + +Global object addresses are stored in symbols.xml, which is copied to the dfhack release package and loaded as data at runtime. + +Remote access interface +======================= + +DFHack supports remote access by exchanging Google protobuf messages via a TCP socket. Both the core and plugins can define remotely accessible methods. The ``dfhack-run`` command uses this interface to invoke ordinary console commands. + +Currently the supported set of requests is limited, because the developers don't know what exactly is most useful. + +Protocol client implementations exist for Java and C#. + Contributing to DFHack ====================== diff --git a/Compile.html b/Compile.html index b0f9e9c6e..e17e57e22 100644 --- a/Compile.html +++ b/Compile.html @@ -334,10 +334,12 @@ ul.auto-toc {
  • Build types
  • Using the library as a developer
      -
    • Contributing to DFHack @@ -470,9 +472,10 @@ cmake .. -DCMAKE_BUILD_TYPE:string=BUILD_TYPE

      Using the library as a developer

      -

      Currently, the only way to use the library is to write a plugin that can be loaded by it. +

      Currently, the most direct way to use the library is to write a plugin that can be loaded by it. All the plugins can be found in the 'plugins' folder. There's no in-depth documentation on how to write one yet, but it should be easy enough to copy one and just follow the pattern.

      +

      Other than through plugins, it is possible to use DFHack via remote access interface, or by writing Lua scripts.

      The most important parts of DFHack are the Core, Console, Modules and Plugins.

      • Core acts as the centerpiece of DFHack - it acts as a filter between DF and SDL and synchronizes the various plugins with DF.
      • @@ -485,18 +488,30 @@ on how to write one yet, but it should be easy enough to copy one and just follo The main license is zlib/libpng, some bits are MIT licensed, and some are BSD licensed.

        Feel free to add your own extensions and plugins. Contributing back to the dfhack repository is welcome and the right thing to do :)

        +
        +

        DF data structure definitions

        +

        DFHack uses information about the game data structures, represented via xml files in the library/xml/ submodule.

        +

        Data structure layouts are described in files following the df.*.xml name pattern. This information is transformed by a perl script into C++ headers describing the structures, and associated metadata for the Lua wrapper. These headers and data are then compiled into the DFHack libraries, thus necessitating a compatibility break every time layouts change; in return it significantly boosts the efficiency and capabilities of DFHack code.

        +

        Global object addresses are stored in symbols.xml, which is copied to the dfhack release package and loaded as data at runtime.

        +
        +
        +

        Remote access interface

        +

        DFHack supports remote access by exchanging Google protobuf messages via a TCP socket. Both the core and plugins can define remotely accessible methods. The dfhack-run command uses this interface to invoke ordinary console commands.

        +

        Currently the supported set of requests is limited, because the developers don't know what exactly is most useful.

        +

        Protocol client implementations exist for Java and C#.

        +
        -

        Contributing to DFHack

        +

        Contributing to DFHack

        Several things should be kept in mind when contributing to DFHack.

        -

        Coding style

        +

        Coding style

        DFhack uses ANSI formatting and four spaces as indentation. Line endings are UNIX. The files use UTF-8 encoding. Code not following this won't make me happy, because I'll have to fix it. There's a good chance I'll make you fix it ;)

        -

        How to get new code into DFHack

        +

        How to get new code into DFHack

        You can send patches or make a clone of the github repo and ask me on the IRC channel to pull your code in. I'll review it and see if there are any problems. I'll fix them if they are minor.

        @@ -506,7 +521,7 @@ this is also a good place to dump new ideas and/or bugs that need fixing.

        -

        Memory research

        +

        Memory research

        If you want to do memory research, you'll need some tools and some knowledge. In general, you'll need a good memory viewer and optionally something to look at machine code without getting crazy :)

        diff --git a/Readme.html b/Readme.html index cd579d374..50ceae999 100644 --- a/Readme.html +++ b/Readme.html @@ -1350,7 +1350,12 @@ produce undesirable results. There are a few good ones though.

        You are in fort game mode, managing your fortress and paused. You switch to the arena game mode, assume control of a creature and then switch to adventure game mode(1). -You just lost a fortress and gained an adventurer.

        +You just lost a fortress and gained an adventurer. +You could also do this. +You are in fort game mode, managing your fortress and paused at the esc menu. +You switch to the adventure game mode, then use Dfusion to assume control of a creature and then +save or retire. +You just created a returnable mountain home and gained an adventurer.

        I take no responsibility of anything that happens as a result of using this tool

        From 6199d6915cd6e2420dbcd84d09e1d4eb4833eeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 23 Jun 2012 00:25:30 +0200 Subject: [PATCH 10/29] Nuke some error prints. --- library/Core.cpp | 9 --------- library/xml | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 620fc81d2..8c8badf16 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -281,29 +281,20 @@ static command_result runLuaScript(color_ostream &out, std::string filename, vec command_result Core::runCommand(color_ostream &out, const std::string &command) { - fprintf(stderr,"Inside runCommand"); - fprintf(stderr," with command %s\n",command.c_str()); if (!command.empty()) { - fprintf(stderr,"Command is not empty, tokenizing\n"); vector parts; Core::cheap_tokenise(command,parts); - fprintf(stderr,"Tokenized, got %d parts\n",parts.size()); if(parts.size() == 0) return CR_NOT_IMPLEMENTED; string first = parts[0]; - fprintf(stderr,"Erasing beginning\n"); parts.erase(parts.begin()); - - fprintf(stderr,"I think we're about there\n"); if (first[0] == '#') return CR_OK; cerr << "Invoking: " << command << endl; - - fprintf(stderr,"Returning with the next recursion\n"); return runCommand(out, first, parts); } else diff --git a/library/xml b/library/xml index c38188466..ad38c5e96 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit c381884664c71adefbec44258a734def2c88dacc +Subproject commit ad38c5e96b05fedf16114fd16bd463e933f13582 From ad1ba9bf6b55251f54366d3b679737d169336870 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 24 Jun 2012 02:59:56 +0200 Subject: [PATCH 11/29] ruby: try msvc workaround for __thiscall --- plugins/ruby/ruby.cpp | 51 +++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index 06f033d2f..630cca0e6 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -761,22 +761,49 @@ static VALUE rb_dfmemory_bitarray_set(VALUE self, VALUE addr, VALUE idx, VALUE v /* call an arbitrary object virtual method */ -static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE a1, VALUE a2, VALUE a3) -{ #ifdef WIN32 - int (__fastcall *fptr)(char **me, int dummy_edx, int, int, int, int); +__declspec(naked) static int raw_vcall(char **that, unsigned long voff, unsigned long a0, + unsigned long a1, unsigned long a2, unsigned long a3) +{ + // __thiscall requires that the callee cleans up the stack + // here we dont know how many arguments it will take, so + // we simply fix esp across the funcall + __asm { + push ebp + mov ebp, esp + + push a3 + push a2 + push a1 + push a0 + + mov ecx, that + + mov eax, [ecx] + add eax, voff + call [eax] + + mov esp, ebp + pop ebp + ret + } +} #else +static int raw_vcall(char **that, unsigned long voff, unsigned long a0, + unsigned long a1, unsigned long a2, unsigned long a3) +{ int (*fptr)(char **me, int, int, int, int); + fptr = (decltype(fptr))*(void**)(*that + voff); + return fptr(that, a0, a1, a2, a3); +} #endif - char **that = (char**)rb_num2ulong(cppobj); - int ret; - fptr = (decltype(fptr))*(void**)(*that + rb_num2ulong(cppvoff)); - ret = fptr(that, -#ifdef WIN32 - 0, -#endif - rb_num2ulong(a0), rb_num2ulong(a1), rb_num2ulong(a2), rb_num2ulong(a3)); - return rb_int2inum(ret); + +// call an arbitrary vmethod, convert args/ret to native values for raw_vcall +static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE a1, VALUE a2, VALUE a3) +{ + return rb_int2inum(raw_vcall((char**)rb_num2ulong(cppobj), rb_num2ulong(cppvoff), + rb_num2ulong(a0), rb_num2ulong(a1), + rb_num2ulong(a2), rb_num2ulong(a3))); } From 4c2d4d4b86355063d0eef3c93dc6b686448123b3 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 24 Jun 2012 16:09:31 +0200 Subject: [PATCH 12/29] ruby: clean stuff up, install ruby modules in df/hack/ruby/ --- plugins/ruby/CMakeLists.txt | 7 +- plugins/ruby/{plugins => }/building.rb | 0 plugins/ruby/{plugins => }/plant.rb | 0 plugins/ruby/ruby.cpp | 105 +++---------------------- plugins/ruby/ruby.rb | 54 ++++++------- plugins/ruby/{plugins => }/unit.rb | 0 6 files changed, 43 insertions(+), 123 deletions(-) rename plugins/ruby/{plugins => }/building.rb (100%) rename plugins/ruby/{plugins => }/plant.rb (100%) rename plugins/ruby/{plugins => }/unit.rb (100%) diff --git a/plugins/ruby/CMakeLists.txt b/plugins/ruby/CMakeLists.txt index e69632e61..f33e41740 100644 --- a/plugins/ruby/CMakeLists.txt +++ b/plugins/ruby/CMakeLists.txt @@ -30,4 +30,9 @@ INCLUDE_DIRECTORIES("${dfhack_SOURCE_DIR}/depends/tthread") DFHACK_PLUGIN(ruby ruby.cpp LINK_LIBRARIES dfhack-tinythread) ADD_DEPENDENCIES(ruby ruby-autogen-rb) -INSTALL(FILES ruby.rb ruby-autogen.rb ${RUBYLIB} DESTINATION ${DFHACK_LIBRARY_DESTINATION}) +INSTALL(FILES ${RUBYLIB} DESTINATION ${DFHACK_LIBRARY_DESTINATION}) + +INSTALL(DIRECTORY . + DESTINATION hack/ruby + FILES_MATCHING PATTERN "*.rb") + diff --git a/plugins/ruby/plugins/building.rb b/plugins/ruby/building.rb similarity index 100% rename from plugins/ruby/plugins/building.rb rename to plugins/ruby/building.rb diff --git a/plugins/ruby/plugins/plant.rb b/plugins/ruby/plant.rb similarity index 100% rename from plugins/ruby/plugins/plant.rb rename to plugins/ruby/plant.rb diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index 630cca0e6..e3bed78b7 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -21,7 +21,6 @@ using namespace DFHack; static int df_loadruby(void); static void df_unloadruby(void); static void df_rubythread(void*); -static command_result df_rubyload(color_ostream &out, std::vector & parameters); static command_result df_rubyeval(color_ostream &out, std::vector & parameters); static void ruby_bind_dfhack(void); @@ -31,7 +30,6 @@ enum RB_command { RB_INIT, RB_DIE, RB_EVAL, - RB_CUSTOM, }; tthread::mutex *m_irun; tthread::mutex *m_mutex; @@ -66,10 +64,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector & parameters) -{ - if (parameters.size() == 1 && (parameters[0] == "help" || parameters[0] == "?")) - { - out.print("This command loads the ruby script whose path is given as parameter, and run it.\n"); - return CR_OK; - } - - std::string cmd = "load '"; - cmd += parameters[0]; // TODO escape singlequotes - cmd += "'"; - - return plugin_eval_rb(cmd); -} - static command_result df_rubyeval(color_ostream &out, std::vector & parameters) { command_result ret; @@ -206,13 +185,14 @@ static command_result df_rubyeval(color_ostream &out, std::vector // ruby stuff -// ruby-dev on windows is messy -// ruby.h on linux 64 is broken -// so we dynamically load libruby instead of linking it at compile time -// lib path can be set in dfhack.ini to use the system libruby, but by -// default we'll embed our own (downloaded by cmake) +// - ruby-dev on windows is messy +// - ruby.h with gcc -m32 on linux 64 is broken +// so we dynamically load libruby with dlopen/LoadLibrary +// lib path is hardcoded here, and by default downloaded by cmake +// this code should work with ruby1.9, but ruby1.9 doesn't like running +// in a dedicated non-main thread, so use ruby1.8 binaries only for now -// these ruby definitions are invalid for windows 64bit +// these ruby definitions are invalid for windows 64bit (need long long) typedef unsigned long VALUE; typedef unsigned long ID; @@ -224,23 +204,17 @@ typedef unsigned long ID; #define FIX2INT(i) (((long)i) >> 1) #define RUBY_METHOD_FUNC(func) ((VALUE(*)(...))func) -VALUE *rb_eRuntimeError; - void (*ruby_sysinit)(int *, const char ***); void (*ruby_init)(void); void (*ruby_init_loadpath)(void); void (*ruby_script)(const char*); void (*ruby_finalize)(void); ID (*rb_intern)(const char*); -VALUE (*rb_raise)(VALUE, const char*, ...); VALUE (*rb_funcall)(VALUE, ID, int, ...); VALUE (*rb_define_module)(const char*); void (*rb_define_singleton_method)(VALUE, const char*, VALUE(*)(...), int); -void (*rb_define_const)(VALUE, const char*, VALUE); -void (*rb_load_protect)(VALUE, int, int*); VALUE (*rb_gv_get)(const char*); VALUE (*rb_str_new)(const char*, long); -VALUE (*rb_str_new2)(const char*); char* (*rb_string_value_ptr)(VALUE*); VALUE (*rb_eval_string_protect)(const char*, int*); VALUE (*rb_ary_shift)(VALUE); @@ -269,11 +243,6 @@ static int df_loadruby(void) return 0; } - if (!(rb_eRuntimeError = (VALUE*)LookupPlugin(libruby_handle, "rb_eRuntimeError"))) - return 0; - - // XXX does msvc support decltype ? might need a #define decltype typeof - // or just assign to *(void**)(&s) = ... // ruby_sysinit is optional (ruby1.9 only) ruby_sysinit = (decltype(ruby_sysinit))LookupPlugin(libruby_handle, "ruby_sysinit"); #define rbloadsym(s) if (!(s = (decltype(s))LookupPlugin(libruby_handle, #s))) return 0 @@ -282,15 +251,11 @@ static int df_loadruby(void) rbloadsym(ruby_script); rbloadsym(ruby_finalize); rbloadsym(rb_intern); - rbloadsym(rb_raise); rbloadsym(rb_funcall); rbloadsym(rb_define_module); rbloadsym(rb_define_singleton_method); - rbloadsym(rb_define_const); - rbloadsym(rb_load_protect); rbloadsym(rb_gv_get); rbloadsym(rb_str_new); - rbloadsym(rb_str_new2); rbloadsym(rb_string_value_ptr); rbloadsym(rb_eval_string_protect); rbloadsym(rb_ary_shift); @@ -382,10 +347,6 @@ static void df_rubythread(void *p) if (state) dump_rb_error(); break; - - case RB_CUSTOM: - // TODO handle ruby custom commands - break; } r_result = CR_OK; @@ -431,30 +392,6 @@ static VALUE rb_dfsuspend(VALUE self) return Qtrue; } -// returns the delta to apply to dfhack xml addrs wrt actual memory addresses -// usage: real_addr = addr_from_xml + this_delta; -static VALUE rb_dfrebase_delta(void) -{ - uint32_t expected_base_address; - uint32_t actual_base_address = 0; -#ifdef WIN32 - expected_base_address = 0x00400000; - actual_base_address = (uint32_t)GetModuleHandle(0); -#else - expected_base_address = 0x08048000; - FILE *f = fopen("/proc/self/maps", "r"); - char line[256]; - while (fgets(line, sizeof(line), f)) { - if (strstr(line, "libs/Dwarf_Fortress")) { - actual_base_address = strtoul(line, 0, 16); - break; - } - } -#endif - - return rb_int2inum(actual_base_address - expected_base_address); -} - static VALUE rb_dfprint_str(VALUE self, VALUE s) { console_proxy->print("%s", rb_string_value_ptr(&s)); @@ -467,25 +404,6 @@ static VALUE rb_dfprint_err(VALUE self, VALUE s) return Qnil; } -/* TODO needs main dfhack support - this needs a custom DFHack::Plugin subclass to pass the cmdname to invoke(), to match the ruby callback -// register a ruby method as dfhack console command -// usage: DFHack.register("moo", "this commands prints moo on the console") { DFHack.puts "moo !" } -static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr) -{ - commands.push_back(PluginCommand(rb_string_value_ptr(&name), - rb_string_value_ptr(&descr), - df_rubycustom)); - - return Qtrue; -} -*/ -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) { return rb_uint2inum(Core::getInstance().vinfo->getAddress(rb_string_value_ptr(&name))); @@ -511,7 +429,7 @@ static VALUE rb_dfget_rtti_classname(VALUE self, VALUE vptr) char *typestring = *(char**)(typeinfo + 0x4); while (*typestring >= '0' && *typestring <= '9') typestring++; - return rb_str_new2(typestring); + return rb_str_new(typestring, strlen(typestring)); #endif } @@ -532,7 +450,7 @@ static VALUE rb_dfmalloc(VALUE self, VALUE len) { char *ptr = (char*)malloc(FIX2INT(len)); if (!ptr) - rb_raise(*rb_eRuntimeError, "no memory"); + return Qnil; memset(ptr, 0, FIX2INT(len)); return rb_uint2inum((long)ptr); } @@ -821,13 +739,11 @@ static void ruby_bind_dfhack(void) { rb_define_singleton_method(rb_cDFHack, "get_vtable", RUBY_METHOD_FUNC(rb_dfget_vtable), 1); rb_define_singleton_method(rb_cDFHack, "get_rtti_classname", RUBY_METHOD_FUNC(rb_dfget_rtti_classname), 1); rb_define_singleton_method(rb_cDFHack, "get_vtable_ptr", RUBY_METHOD_FUNC(rb_dfget_vtable_ptr), 1); - rb_define_singleton_method(rb_cDFHack, "register_dfcommand", RUBY_METHOD_FUNC(rb_dfregister), 2); rb_define_singleton_method(rb_cDFHack, "print_str", RUBY_METHOD_FUNC(rb_dfprint_str), 1); rb_define_singleton_method(rb_cDFHack, "print_err", RUBY_METHOD_FUNC(rb_dfprint_err), 1); rb_define_singleton_method(rb_cDFHack, "malloc", RUBY_METHOD_FUNC(rb_dfmalloc), 1); rb_define_singleton_method(rb_cDFHack, "free", RUBY_METHOD_FUNC(rb_dffree), 1); rb_define_singleton_method(rb_cDFHack, "vmethod_do_call", RUBY_METHOD_FUNC(rb_dfvcall), 6); - rb_define_const(rb_cDFHack, "REBASE_DELTA", rb_dfrebase_delta()); rb_define_singleton_method(rb_cDFHack, "memory_read", RUBY_METHOD_FUNC(rb_dfmemory_read), 2); rb_define_singleton_method(rb_cDFHack, "memory_read_int8", RUBY_METHOD_FUNC(rb_dfmemory_read_int8), 1); @@ -869,8 +785,7 @@ static void ruby_bind_dfhack(void) { // load the default ruby-level definitions int state=0; - // 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); + rb_eval_string_protect("require './hack/ruby/ruby'", &state); if (state) dump_rb_error(); } diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb index 9c11144c0..3a7d20a27 100644 --- a/plugins/ruby/ruby.rb +++ b/plugins/ruby/ruby.rb @@ -1,3 +1,26 @@ +module Kernel + def puts(*a) + a.flatten.each { |l| + DFHack.print_str(l.to_s.chomp + "\n") + } + nil + end + + def puts_err(*a) + a.flatten.each { |l| + DFHack.print_err(l.to_s.chomp + "\n") + } + nil + end + + def p(*a) + a.each { |e| + puts_err e.inspect + } + nil + end +end + module DFHack class << self # update the ruby.cpp version to accept a block @@ -14,28 +37,6 @@ module DFHack end end - module ::Kernel - def puts(*a) - a.flatten.each { |l| - DFHack.print_str(l.to_s.chomp + "\n") - } - nil - end - - def puts_err(*a) - a.flatten.each { |l| - DFHack.print_err(l.to_s.chomp + "\n") - } - nil - end - - def p(*a) - a.each { |e| - puts_err e.inspect - } - end - end - # register a callback to be called every gframe or more # ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 } def onupdate_register(&b) @@ -1047,8 +1048,7 @@ end end -# load autogen'd file -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') +# load autogenned file +require './hack/ruby/ruby-autogen' +# load all modules +Dir['./hack/ruby/*.rb'].each { |m| require m.chomp('.rb') } diff --git a/plugins/ruby/plugins/unit.rb b/plugins/ruby/unit.rb similarity index 100% rename from plugins/ruby/plugins/unit.rb rename to plugins/ruby/unit.rb From 3f4d2e4792850991ae3ab5a11e813488e993b2a4 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 24 Jun 2012 17:30:26 +0200 Subject: [PATCH 13/29] ruby: split ruby.rb in modules --- plugins/ruby/README | 111 ++-- plugins/ruby/building.rb | 499 ++++++++-------- plugins/ruby/item.rb | 21 + plugins/ruby/job.rb | 35 ++ plugins/ruby/map.rb | 54 ++ plugins/ruby/plant.rb | 296 +++++----- plugins/ruby/ruby-autogen-defs.rb | 751 ++++++++++++++++++++++++ plugins/ruby/ruby.rb | 945 +----------------------------- plugins/ruby/ui.rb | 65 ++ plugins/ruby/unit.rb | 122 ++-- 10 files changed, 1485 insertions(+), 1414 deletions(-) create mode 100644 plugins/ruby/item.rb create mode 100644 plugins/ruby/job.rb create mode 100644 plugins/ruby/map.rb create mode 100644 plugins/ruby/ruby-autogen-defs.rb create mode 100644 plugins/ruby/ui.rb diff --git a/plugins/ruby/README b/plugins/ruby/README index 9dc7d49f6..4bfc20957 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -2,61 +2,110 @@ This plugins embeds a ruby interpreter inside DFHack (ie inside Dwarf Fortress). The plugin maps all the structures available in library/xml/ to ruby objects. -These objects are described in ruby-autogen.rb, they are all in the DFHack:: +These objects are described in ruby-autogen.rb, they are all in the DFHack module. The toplevel 'df' method is a shortcut to the DFHack module. The plugin does *not* map most of dfhack methods (MapCache, ...) ; only direct access to the raw DF data structures in memory is provided. -Some library methods are stored in the ruby.rb file, with shortcuts to read a -map block, find an unit or an item, etc. +Some library methods are stored in the various .rb file, e.g. shortcuts to read +a map block, find an unit or an item, etc. -Global objects are accessible through the 'df' accessor (eg df.world). +Global dfhack objects are accessible through the 'df' accessor (eg 'df.world'). -The ruby plugin defines 2 dfhack console commands: - rb_load ; load a ruby script. Ex: rb_load hack/plants.rb (no quotes) - rb_eval ; evaluate a ruby expression, show the result in the -console. Ex: rb_eval df.find_unit.name.first_name + +DFHack console +-------------- + +The ruby plugin defines 1 dfhack console command: + rb_eval ; evaluate a ruby expression and show the result in +the console. Ex: rb_eval df.unit_find().name.first_name You can use single-quotes for strings ; avoid double-quotes that are parsed -and removed by the dfhack console. +and removed by the dfhack console code. + +Text output from ruby code, through the standard 'puts', 'p' or 'raise' are +redirected to the dfhack console window. If dfhack reports 'rb_eval is not a recognized command', check stderr.log. You need a valid 32-bit ruby library to work, and ruby1.8 is prefered (ruby1.9 may crash DF on startup for now). Install the library in the df root folder (or -hack/ on linux), the library should be named 'libruby.dll' (.so on linux). +df/hack/ on linux), the library should be named 'libruby.dll' (.so on linux). You can download a tested version at http://github.com/jjyg/dfhack/downloads/ -The plugin also interfaces with dfhack 'onupdate' hook. + +DFHack callbacks +---------------- + +The plugin interfaces with dfhack 'onupdate' hook. To register ruby code to be run every graphic frame, use: handle = df.onupdate_register { puts 'i love flooding the console' } To stop being called, use: df.onupdate_unregister handle -The same mechanism is available for onstatechange. +The same mechanism is available for 'onstatechange'. + + +C++ object manipulation +----------------------- + +The ruby classes defined in ruby-autogen.rb are accessors to the underlying +df C++ objects in-memory. To allocate a new C++ object for use in DF, use the +RubyClass.cpp_new method (see buildings.rb for exemples), works for Compounds +only. + +Deallocation is not supported. You may manually call df.free if you know +what you are doing (maps directly to the native malloc/free) + +C++ std::string fields may be directly re-allocated using standard ruby strings, +e.g. some_unit.name.nickname = 'moo' +More subtle string manipulation, e.g. changing a single character, are not +supported. Read the whole string, manipulate it in ruby, and re-assign it +instead. + +C++ std::vector<> can be iterated as standard ruby Enumerable objects, using +each/map/etc. +To append data to a vector, use vector << newelement or vector.push(newelement) +To insert at a given pos, vector.insert_at(index, value) +To delete an element, vector.delete_at(index) + +You can binary search an element in a vector for a given numeric field value: + df.world.unit.all.binsearch(42, :id) +will find the element whose 'id' field is 42 (needs the vector to be initially +sorted by this field). The binsearch 2nd argument defaults to :id. + +Any numeric field defined as being an enum value will be converted to a ruby +Symbol. This works for array indexes too. + +Virtual method calls are supported for C++ objects, with a maximum of 4 +arguments. Arguments / return value are interpreted as Compound/Enums as +specified in the vmethod definition in the xmls. + +Pointer fields are automatically dereferenced ; so a vector of pointer to +Units will yield Units directly. NULL pointers yield the 'nil' value. Exemples -------- -For more complex exemples, check the ruby/plugins/ source folder. +For more complex exemples, check the dfhack/scripts/*.rb files. Show info on the currently selected unit ('v' or 'k' DF menu) - p df.find_unit.flags1 + p df.unit_find.flags1 Set a custom nickname to unit with id '123' - df.find_unit(123).name.nickname = 'moo' + df.unit_find(123).name.nickname = 'moo' Show current unit profession - p df.find_unit.profession + p df.unit_find.profession Change current unit profession - df.find_unit.profession = :MASON + df.unit_find.profession = :MASON -Center the screen on unit '123' - df.center_viewscreen(df.find_unit(123)) +Center the screen on unit ID '123' + df.center_viewscreen(df.unit_find(123)) Find an item at a given position, show its C++ classname - df.find_item(df.cursor)._rtti_classname + p df.item_find(df.cursor)._rtti_classname Find the raws name of the plant under cursor plant = df.world.plants.all.find { |plt| df.at_cursor?(plt) } @@ -67,15 +116,14 @@ Dig a channel under the cursor df.map_block_at(df.cursor).flags.designated = true -Compilation ------------ +Plugin compilation +------------------ -The plugin consists of the ruby.rb file including user comfort functions and +The plugin consists of the *.rb file including user comfort functions and describing basic classes used by the autogenerated code, and ruby-autogen.rb, the auto-generated code. -The generated code is generated by codegen.pl, which takes the codegen.out.xml -file as input. +autogen is output by codegen.pl from dfhack/library/include/df/codegen.out.xml For exemple, @@ -89,17 +137,10 @@ Will generate field(:custom_profession, 60) { stl_string } field(:profession, 64) { number 16, true } -The syntax for the 'field' method is: +The syntax for the 'field' method in ruby-autogen.rb is: 1st argument = name of the method -2nd argument = offset of this field from the beginning of the struct. - +2nd argument = offset of this field from the beginning of the current struct. The block argument describes the type of the field: uint32, ptr to global... -Primitive type access is done through native methods in ruby.cpp (vector length, +Primitive type access is done through native methods from ruby.cpp (vector length, raw memory access, etc) - -MemHack::Pointers are automatically dereferenced ; so a vector of pointer to -Units will yield Units directly. Null pointers yield the 'nil' value. - -This allows to use code such as 'df.world.units.all[0].pos', with 'all' being -in fact a vector of *pointers* to DFHack::Unit objects. diff --git a/plugins/ruby/building.rb b/plugins/ruby/building.rb index 5dfbcdacd..29e10ae29 100644 --- a/plugins/ruby/building.rb +++ b/plugins/ruby/building.rb @@ -1,266 +1,267 @@ module DFHack + class << self + # allocate a new building object + def building_alloc(type, subtype=-1, custom=-1) + cls = rtti_n2c[BuildingType::Classname[type].to_sym] + raise "invalid building type #{type.inspect}" if not cls + bld = cls.cpp_new + bld.race = ui.race_id + bld.setSubtype(subtype) if subtype != -1 + bld.setCustomType(custom) if custom != -1 + case type + when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0 + when :Coffin; bld.initBurialFlags + when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate + end + bld + end -# allocate a new building object -def self.building_alloc(type, subtype=-1, custom=-1) - cls = rtti_n2c[BuildingType::Classname[type].to_sym] - raise "invalid building type #{type.inspect}" if not cls - bld = cls.cpp_new - bld.race = ui.race_id - bld.setSubtype(subtype) if subtype != -1 - bld.setCustomType(custom) if custom != -1 - case type - when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0 - when :Coffin; bld.initBurialFlags - when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate - end - bld -end + # used by building_setsize + def building_check_bridge_support(bld) + x1 = bld.x1-1 + x2 = bld.x2+1 + y1 = bld.y1-1 + y2 = bld.y2+1 + z = bld.z + (x1..x2).each { |x| + (y1..y2).each { |y| + next if ((x == x1 or x == x2) and + (y == y1 or y == y2)) + if mb = map_block_at(x, y, z) and tile = mb.tiletype[x%16][y%16] and TiletypeShape::BasicShape[Tiletype::Shape[tile]] != :Open + bld.gate_flags.has_support = true + return + end + } + } + bld.gate_flags.has_support = false + end -# used by building_setsize -def self.building_check_bridge_support(bld) - x1 = bld.x1-1 - x2 = bld.x2+1 - y1 = bld.y1-1 - y2 = bld.y2+1 - z = bld.z - (x1..x2).each { |x| - (y1..y2).each { |y| - next if ((x == x1 or x == x2) and - (y == y1 or y == y2)) - if mb = map_block_at(x, y, z) and tile = mb.tiletype[x%16][y%16] and TiletypeShape::BasicShape[Tiletype::Shape[tile]] == :Open - bld.gate_flags.has_support = true - return - end - } - } - bld.gate_flags.has_support = false -end + # sets x2/centerx/y2/centery from x1/y1/bldtype + # x2/y2 preserved for :FarmPlot etc + def building_setsize(bld) + bld.x2 = bld.x1 if bld.x1 > bld.x2 + bld.y2 = bld.y1 if bld.y1 > bld.y2 + case bld.getType + when :Bridge + bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 + bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 + building_check_bridge_support(bld) + when :FarmPlot, :RoadDirt, :RoadPaved, :Stockpile, :Civzone + bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 + bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 + when :TradeDepot, :Shop + bld.x2 = bld.x1+4 + bld.y2 = bld.y1+4 + bld.centerx = bld.x1+2 + bld.centery = bld.y1+2 + when :SiegeEngine, :Windmill, :Wagon + bld.x2 = bld.x1+2 + bld.y2 = bld.y1+2 + bld.centerx = bld.x1+1 + bld.centery = bld.y1+1 + when :AxleHorizontal + if bld.is_vertical == 1 + bld.x2 = bld.centerx = bld.x1 + bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 + else + bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 + bld.y2 = bld.centery = bld.y1 + end + when :WaterWheel + if bld.is_vertical == 1 + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.y1+2 + bld.centery = bld.y1+1 + else + bld.x2 = bld.x1+2 + bld.centerx = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + end + when :Workshop, :Furnace + # Furnace = Custom or default case only + case bld.type + when :Quern, :Millstone, :Tool + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + when :Siege, :Kennels + bld.x2 = bld.x1+4 + bld.y2 = bld.y1+4 + bld.centerx = bld.x1+2 + bld.centery = bld.y1+2 + when :Custom + if bdef = world.raws.buildings.all.binsearch(bld.getCustomType) + bld.x2 = bld.x1 + bdef.dim_x - 1 + bld.y2 = bld.y1 + bdef.dim_y - 1 + bld.centerx = bld.x1 + bdef.workloc_x + bld.centery = bld.y1 + bdef.workloc_y + end + else + bld.x2 = bld.x1+2 + bld.y2 = bld.y1+2 + bld.centerx = bld.x1+1 + bld.centery = bld.y1+1 + end + when :ScrewPump + case bld.direction + when :FromEast + bld.x2 = bld.centerx = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + when :FromSouth + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1+1 + when :FromWest + bld.x2 = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + bld.centerx = bld.x1 + else + bld.x2 = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + bld.centerx = bld.x1 + end + when :Well + bld.bucket_z = bld.z + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + when :Construction + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + bld.setMaterialAmount(1) + return + else + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + end + bld.setMaterialAmount((bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1) + end -# sets x2/centerx/y2/centery from x1/y1/bldtype -# x2/y2 preserved for :FarmPlot etc -def self.building_setsize(bld) - bld.x2 = bld.x1 if bld.x1 > bld.x2 - bld.y2 = bld.y1 if bld.y1 > bld.y2 - case bld.getType - when :Bridge - bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 - bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 - building_check_bridge_support(bld) - when :FarmPlot, :RoadDirt, :RoadPaved, :Stockpile, :Civzone - bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 - bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 - when :TradeDepot, :Shop - bld.x2 = bld.x1+4 - bld.y2 = bld.y1+4 - bld.centerx = bld.x1+2 - bld.centery = bld.y1+2 - when :SiegeEngine, :Windmill, :Wagon - bld.x2 = bld.x1+2 - bld.y2 = bld.y1+2 - bld.centerx = bld.x1+1 - bld.centery = bld.y1+1 - when :AxleHorizontal - if bld.is_vertical == 1 - bld.x2 = bld.centerx = bld.x1 - bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 - else - bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 - bld.y2 = bld.centery = bld.y1 - end - when :WaterWheel - if bld.is_vertical == 1 - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.y1+2 - bld.centery = bld.y1+1 - else - bld.x2 = bld.x1+2 - bld.centerx = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - end - when :Workshop, :Furnace - # Furnace = Custom or default case only - case bld.type - when :Quern, :Millstone, :Tool - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - when :Siege, :Kennels - bld.x2 = bld.x1+4 - bld.y2 = bld.y1+4 - bld.centerx = bld.x1+2 - bld.centery = bld.y1+2 - when :Custom - if bdef = world.raws.buildings.all.binsearch(bld.getCustomType) - bld.x2 = bld.x1 + bdef.dim_x - 1 - bld.y2 = bld.y1 + bdef.dim_y - 1 - bld.centerx = bld.x1 + bdef.workloc_x - bld.centery = bld.y1 + bdef.workloc_y - end - else - bld.x2 = bld.x1+2 - bld.y2 = bld.y1+2 - bld.centerx = bld.x1+1 - bld.centery = bld.y1+1 - end - when :ScrewPump - case bld.direction - when :FromEast - bld.x2 = bld.centerx = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - when :FromSouth - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1+1 - when :FromWest - bld.x2 = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - bld.centerx = bld.x1 - else - bld.x2 = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - bld.centerx = bld.x1 - end - when :Well - bld.bucket_z = bld.z - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - when :Construction - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - bld.setMaterialAmount(1) - return - else - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - end - bld.setMaterialAmount((bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1) -end + # set building at position, with optional width/height + def building_position(bld, pos, w=nil, h=nil) + bld.x1 = pos.x + bld.y1 = pos.y + bld.z = pos.z + bld.x2 = bld.x1+w-1 if w + bld.y2 = bld.y1+h-1 if h + building_setsize(bld) + end -# set building at position, with optional width/height -def self.building_position(bld, pos, w=nil, h=nil) - bld.x1 = pos.x - bld.y1 = pos.y - bld.z = pos.z - bld.x2 = bld.x1+w-1 if w - bld.y2 = bld.y1+h-1 if h - building_setsize(bld) -end + # set map occupancy/stockpile/etc for a building + def building_setoccupancy(bld) + stockpile = (bld.getType == :Stockpile) + complete = (bld.getBuildStage >= bld.getMaxBuildStage) + extents = (bld.room.extents and bld.isExtentShaped) -# set map occupancy/stockpile/etc for a building -def self.building_setoccupancy(bld) - stockpile = (bld.getType == :Stockpile) - complete = (bld.getBuildStage >= bld.getMaxBuildStage) - extents = (bld.room.extents and bld.isExtentShaped) + z = bld.z + (bld.x1..bld.x2).each { |x| + (bld.y1..bld.y2).each { |y| + next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0 + next if not mb = map_block_at(x, y, z) + des = mb.designation[x%16][y%16] + des.pile = stockpile + des.dig = :No + if complete + bld.updateOccupancy(x, y) + else + mb.occupancy[x%16][y%16].building = :Planned + end + } + } + end - z = bld.z - (bld.x1..bld.x2).each { |x| - (bld.y1..bld.y2).each { |y| - next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0 - next if not mb = map_block_at(x, y, z) - des = mb.designation[x%16][y%16] - des.pile = stockpile - des.dig = :No - if complete - bld.updateOccupancy(x, y) - else - mb.occupancy[x%16][y%16].building = :Planned - end - } - } -end + # link bld into other rooms if it is inside their extents + def building_linkrooms(bld) + didstuff = false + world.buildings.other[:ANY_FREE].each { |ob| + next if !ob.is_room or ob.z != bld.z + next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0 + didstuff = true + ob.children << bld + bld.parents << ob + } + ui.equipment.update.buildings = true if didstuff + end -# link bld into other rooms if it is inside their extents -def self.building_linkrooms(bld) - didstuff = false - world.buildings.other[:ANY_FREE].each { |ob| - next if !ob.is_room or ob.z != bld.z - next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0 - didstuff = true - ob.children << bld - bld.parents << ob - } - ui.equipment.update.buildings = true if didstuff -end + # link the building into the world, set map data, link rooms, bld.id + def building_link(bld) + bld.id = df.building_next_id + df.building_next_id += 1 -# link the building into the world, set map data, link rooms, bld.id -def self.building_link(bld) - bld.id = df.building_next_id - df.building_next_id += 1 + world.buildings.all << bld + bld.categorize(true) + building_setoccupancy(bld) if bld.isSettingOccupancy + building_linkrooms(bld) + end - world.buildings.all << bld - bld.categorize(true) - building_setoccupancy(bld) if bld.isSettingOccupancy - building_linkrooms(bld) -end + # set a design for the building + def building_createdesign(bld, rough=true) + job = bld.jobs[0] + job.mat_type = bld.mat_type + job.mat_index = bld.mat_index + if bld.needsDesign + bld.design = BuildingDesign.cpp_new + bld.design.flags.rough = rough + end + end -# set a design for the building -def self.building_createdesign(bld, rough=true) - job = bld.jobs[0] - job.mat_type = bld.mat_type - job.mat_index = bld.mat_index - if bld.needsDesign - bld.design = BuildingDesign.cpp_new - bld.design.flags.rough = rough - end -end - -# creates a job to build bld, return it -def self.building_linkforconstruct(bld) - building_link bld - ref = GeneralRefBuildingHolderst.cpp_new - ref.building_id = bld.id - job = Job.cpp_new - job.job_type = :ConstructBuilding - job.pos = [bld.centerx, bld.centery, bld.z] - job.references << ref - bld.jobs << job - job_link job - job -end + # creates a job to build bld, return it + def building_linkforconstruct(bld) + building_link bld + ref = GeneralRefBuildingHolderst.cpp_new + ref.building_id = bld.id + job = Job.cpp_new + job.job_type = :ConstructBuilding + job.pos = [bld.centerx, bld.centery, bld.z] + job.references << ref + bld.jobs << job + job_link job + job + end -# construct a building with items or JobItems -def self.building_construct(bld, items) - job = building_linkforconstruct(bld) - rough = false - items.each { |item| - if items.kind_of?(JobItem) - item.quantity = (bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1 if item.quantity < 0 - job.job_items << item - else - job_attachitem(job, item, :Hauled) - end - rough = true if item.getType == :BOULDER - bld.mat_type = item.getMaterial if bld.mat_type == -1 - bld.mat_index = item.getMaterialIndex if bld.mat_index == -1 - } - building_createdesign(bld, rough) -end + # construct a building with items or JobItems + def building_construct(bld, items) + job = building_linkforconstruct(bld) + rough = false + items.each { |item| + if items.kind_of?(JobItem) + item.quantity = (bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1 if item.quantity < 0 + job.job_items << item + else + job_attachitem(job, item, :Hauled) + end + rough = true if item.getType == :BOULDER + bld.mat_type = item.getMaterial if bld.mat_type == -1 + bld.mat_index = item.getMaterialIndex if bld.mat_index == -1 + } + building_createdesign(bld, rough) + end -# creates a job to deconstruct the building -def self.building_deconstruct(bld) - job = Job.cpp_new - refbuildingholder = GeneralRefBuildingHolderst.cpp_new - job.job_type = :DestroyBuilding - refbuildingholder.building_id = building.id - job.references << refbuildingholder - building.jobs << job - job_link job - job -end + # creates a job to deconstruct the building + def building_deconstruct(bld) + job = Job.cpp_new + refbuildingholder = GeneralRefBuildingHolderst.cpp_new + job.job_type = :DestroyBuilding + refbuildingholder.building_id = building.id + job.references << refbuildingholder + building.jobs << job + job_link job + job + end -# exemple usage -def self.buildbed(pos=cursor) - suspend { - raise 'where to ?' if pos.x < 0 + # exemple usage + def buildbed(pos=cursor) + suspend { + raise 'where to ?' if pos.x < 0 - item = world.items.all.find { |i| - i.kind_of?(ItemBedst) and - i.itemrefs.empty? and - !i.flags.in_job - } - raise 'no free bed, build more !' if not item + item = world.items.all.find { |i| + i.kind_of?(ItemBedst) and + i.itemrefs.empty? and + !i.flags.in_job + } + raise 'no free bed, build more !' if not item - bld = building_alloc(:Bed) - building_position(bld, pos) - building_construct(bld, [item]) - } -end + bld = building_alloc(:Bed) + building_position(bld, pos) + building_construct(bld, [item]) + } + end + end end diff --git a/plugins/ruby/item.rb b/plugins/ruby/item.rb new file mode 100644 index 000000000..cd95e82a9 --- /dev/null +++ b/plugins/ruby/item.rb @@ -0,0 +1,21 @@ +module DFHack + class << self + # return an Item + # arg similar to unit.rb/unit_find; no arg = 'k' menu + def item_find(what=:selected) + if what == :selected + case ui.main.mode + when :LookAround + k = ui_look_list.items[ui_look_cursor] + k.item if k.type == :Item + end + elsif what.kind_of?(Integer) + world.items.all.binsearch(what) + elsif what.respond_to?(:x) or what.respond_to?(:pos) + world.items.all.find { |i| same_pos?(what, i) } + else + raise "what what?" + end + end + end +end diff --git a/plugins/ruby/job.rb b/plugins/ruby/job.rb new file mode 100644 index 000000000..e489dcc91 --- /dev/null +++ b/plugins/ruby/job.rb @@ -0,0 +1,35 @@ +module DFHack + class << self + # link a job to the world + # allocate & set job.id, allocate a JobListLink, link to job & world.job_list + def job_link(job) + lastjob = world.job_list + lastjob = lastjob.next while lastjob.next + joblink = JobListLink.cpp_new + joblink.prev = lastjob + joblink.item = job + job.list_link = joblink + job.id = df.job_next_id + df.job_next_id += 1 + lastjob.next = joblink + end + + # attach an item to a job, flag item in_job + def job_attachitem(job, item, role=:Hauled, filter_idx=-1) + if role != :TargetContainer + item.flags.in_job = true + end + + itemlink = SpecificRef.cpp_new + itemlink.type = :JOB + itemlink.job = job + item.specific_refs << itemlink + + joblink = JobItemRef.cpp_new + joblink.item = item + joblink.role = role + joblink.job_item_idx = filter_idx + job.items << joblink + end + end +end diff --git a/plugins/ruby/map.rb b/plugins/ruby/map.rb new file mode 100644 index 000000000..af9e8b804 --- /dev/null +++ b/plugins/ruby/map.rb @@ -0,0 +1,54 @@ +module DFHack + class << self + # return a map block by tile coordinates + # you can also use find_map_block(cursor) or anything that respond to x/y/z + def map_block_at(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + if x >= 0 and x < world.map.x_count and y >= 0 and y < world.map.y_count and z >= 0 and z < world.map.z_count + world.map.block_index[x/16][y/16][z] + end + end + + def map_designation_at(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + if b = map_block_at(x, y, z) + b.designation[x%16][y%16] + end + end + + def map_occupancy_at(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + if b = map_block_at(x, y, z) + b.occupancy[x%16][y%16] + end + end + + # yields every map block + def each_map_block + (0...world.map.x_count_block).each { |xb| + xl = world.map.block_index[xb] + (0...world.map.y_count_block).each { |yb| + yl = xl[yb] + (0...world.map.z_count_block).each { |z| + p = yl[z] + yield p if p + } + } + } + end + + # yields every map block for a given z level + def each_map_block_z(z) + (0...world.map.x_count_block).each { |xb| + xl = world.map.block_index[xb] + (0...world.map.y_count_block).each { |yb| + p = xl[yb][z] + yield p if p + } + } + end + end +end diff --git a/plugins/ruby/plant.rb b/plugins/ruby/plant.rb index 64f17b493..63195e6c2 100644 --- a/plugins/ruby/plant.rb +++ b/plugins/ruby/plant.rb @@ -1,152 +1,166 @@ module DFHack -def self.each_tree(material=:any) - @raws_tree_name ||= {} - if @raws_tree_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_tree_name[idx] = p.id if p.flags[:TREE] - } - end + class << self + # return a Plant + # arg similar to unit.rb/unit_find, no menu + def plant_find(what=cursor) + if what.kind_of?(Integer) + world.items.all.binsearch(what) + elsif what.respond_to?(:x) or what.respond_to?(:pos) + world.plants.all.find { |p| same_pos?(what, p) } + else + raise "what what?" + end + end - if material != :any - mat = match_rawname(material, @raws_tree_name.values) - unless wantmat = @raws_tree_name.index(mat) - raise "invalid tree material #{material}" - end - end + def each_tree(material=:any) + @raws_tree_name ||= {} + if @raws_tree_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_tree_name[idx] = p.id if p.flags[:TREE] + } + end - world.plants.all.each { |plant| - next if not @raws_tree_name[plant.material] - next if wantmat and plant.material != wantmat - yield plant - } -end + if material != :any + mat = match_rawname(material, @raws_tree_name.values) + unless wantmat = @raws_tree_name.index(mat) + raise "invalid tree material #{material}" + end + end -def self.each_shrub(material=:any) - @raws_shrub_name ||= {} - if @raws_tree_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_shrub_name[idx] = p.id if not p.flags[:GRASS] and not p.flags[:TREE] - } - end + world.plants.all.each { |plant| + next if not @raws_tree_name[plant.material] + next if wantmat and plant.material != wantmat + yield plant + } + end - if material != :any - mat = match_rawname(material, @raws_shrub_name.values) - unless wantmat = @raws_shrub_name.index(mat) - raise "invalid shrub material #{material}" - end - end -end + def each_shrub(material=:any) + @raws_shrub_name ||= {} + if @raws_tree_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_shrub_name[idx] = p.id if not p.flags[:GRASS] and not p.flags[:TREE] + } + end -SaplingToTreeAge = 120960 -def self.cuttrees(material=nil, count_max=100) - if !material - # list trees - cnt = Hash.new(0) - suspend { - each_tree { |plant| - next if plant.grow_counter < SaplingToTreeAge - next if map_designation_at(plant).hidden - cnt[plant.material] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = @raws_tree_name[mat] - puts " #{name} #{c}" - } - else - cnt = 0 - suspend { - each_tree(material) { |plant| - next if plant.grow_counter < SaplingToTreeAge - b = map_block_at(plant) - d = b.designation[plant.pos.x%16][plant.pos.y%16] - next if d.hidden - if d.dig == :No - d.dig = :Default - b.flags.designated = true - cnt += 1 - break if cnt == count_max - end - } - } - puts "Updated #{cnt} plant designations" - end -end + if material != :any + mat = match_rawname(material, @raws_shrub_name.values) + unless wantmat = @raws_shrub_name.index(mat) + raise "invalid shrub material #{material}" + end + end + end -def self.growtrees(material=nil, count_max=100) - if !material - # list plants - cnt = Hash.new(0) - suspend { - each_tree { |plant| - next if plant.grow_counter >= SaplingToTreeAge - next if map_designation_at(plant).hidden - cnt[plant.material] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = @raws_tree_name[mat] - puts " #{name} #{c}" - } - else - cnt = 0 - suspend { - each_tree(material) { |plant| - next if plant.grow_counter >= SaplingToTreeAge - next if map_designation_at(plant).hidden - plant.grow_counter = SaplingToTreeAge - cnt += 1 - break if cnt == count_max - } - } - puts "Grown #{cnt} saplings" - end -end + SaplingToTreeAge = 120960 + def cuttrees(material=nil, count_max=100) + if !material + # list trees + cnt = Hash.new(0) + suspend { + each_tree { |plant| + next if plant.grow_counter < SaplingToTreeAge + next if map_designation_at(plant).hidden + cnt[plant.material] += 1 + } + } + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = @raws_tree_name[mat] + puts " #{name} #{c}" + } + else + cnt = 0 + suspend { + each_tree(material) { |plant| + next if plant.grow_counter < SaplingToTreeAge + b = map_block_at(plant) + d = b.designation[plant.pos.x%16][plant.pos.y%16] + next if d.hidden + if d.dig == :No + d.dig = :Default + b.flags.designated = true + cnt += 1 + break if cnt == count_max + end + } + } + puts "Updated #{cnt} plant designations" + end + end -def self.growcrops(material=nil, count_max=100) - @raws_plant_name ||= {} - @raws_plant_growdur ||= {} - if @raws_plant_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_plant_name[idx] = p.id - @raws_plant_growdur[idx] = p.growdur - } - end + def growtrees(material=nil, count_max=100) + if !material + # list plants + cnt = Hash.new(0) + suspend { + each_tree { |plant| + next if plant.grow_counter >= SaplingToTreeAge + next if map_designation_at(plant).hidden + cnt[plant.material] += 1 + } + } + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = @raws_tree_name[mat] + puts " #{name} #{c}" + } + else + cnt = 0 + suspend { + each_tree(material) { |plant| + next if plant.grow_counter >= SaplingToTreeAge + next if map_designation_at(plant).hidden + plant.grow_counter = SaplingToTreeAge + cnt += 1 + break if cnt == count_max + } + } + puts "Grown #{cnt} saplings" + end + end - if !material - cnt = Hash.new(0) - suspend { - world.items.other[:SEEDS].each { |seed| - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - cnt[seed.mat_index] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = world.raws.plants.all[mat].id - puts " #{name} #{c}" - } - else - if material != :any - mat = match_rawname(material, @raws_plant_name.values) - unless wantmat = @raws_plant_name.index(mat) - raise "invalid plant material #{material}" - end - end + def growcrops(material=nil, count_max=100) + @raws_plant_name ||= {} + @raws_plant_growdur ||= {} + if @raws_plant_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_plant_name[idx] = p.id + @raws_plant_growdur[idx] = p.growdur + } + end - cnt = 0 - suspend { - world.items.other[:SEEDS].each { |seed| - next if wantmat and seed.mat_index != wantmat - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - seed.grow_counter = @raws_plant_growdur[seed.mat_index] - cnt += 1 - } - } - puts "Grown #{cnt} crops" - end -end + if !material + cnt = Hash.new(0) + suspend { + world.items.other[:SEEDS].each { |seed| + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + cnt[seed.mat_index] += 1 + } + } + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = world.raws.plants.all[mat].id + puts " #{name} #{c}" + } + else + if material != :any + mat = match_rawname(material, @raws_plant_name.values) + unless wantmat = @raws_plant_name.index(mat) + raise "invalid plant material #{material}" + end + end + + cnt = 0 + suspend { + world.items.other[:SEEDS].each { |seed| + next if wantmat and seed.mat_index != wantmat + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + seed.grow_counter = @raws_plant_growdur[seed.mat_index] + cnt += 1 + } + } + puts "Grown #{cnt} crops" + end + end + end end diff --git a/plugins/ruby/ruby-autogen-defs.rb b/plugins/ruby/ruby-autogen-defs.rb new file mode 100644 index 000000000..482cf24ff --- /dev/null +++ b/plugins/ruby/ruby-autogen-defs.rb @@ -0,0 +1,751 @@ +# definition of classes used by ruby-autogen +module DFHack + module MemHack + INSPECT_SIZE_LIMIT=16384 + class MemStruct + attr_accessor :_memaddr + def _at(addr) ; d = dup ; d._memaddr = addr ; d ; end + def _get ; self ; end + def _cpp_init ; end + end + + class Compound < MemStruct + class << self + attr_accessor :_fields, :_rtti_classname, :_sizeof + def field(name, offset) + struct = yield + return if not struct + @_fields ||= [] + @_fields << [name, offset, struct] + define_method(name) { struct._at(@_memaddr+offset)._get } + define_method("#{name}=") { |v| struct._at(@_memaddr+offset)._set(v) } + end + def _fields_ancestors + if superclass.respond_to?(:_fields_ancestors) + superclass._fields_ancestors + _fields.to_a + else + _fields.to_a + end + end + + def number(bits, signed, initvalue=nil, enum=nil) + Number.new(bits, signed, initvalue, enum) + end + def float + Float.new + end + def bit(shift) + BitField.new(shift, 1) + end + def bits(shift, len, enum=nil) + BitField.new(shift, len, enum) + end + def pointer + Pointer.new((yield if block_given?)) + end + def pointer_ary(tglen) + PointerAry.new(tglen, yield) + end + def static_array(len, tglen, indexenum=nil) + StaticArray.new(tglen, len, indexenum, yield) + end + def static_string(len) + StaticString.new(len) + end + + def stl_vector(tglen=nil) + tg = yield if tglen + case tglen + when 1; StlVector8.new(tg) + when 2; StlVector16.new(tg) + else StlVector32.new(tg) + end + end + def stl_string + StlString.new + end + def stl_bit_vector + StlBitVector.new + end + def stl_deque(tglen) + StlDeque.new(tglen, yield) + end + + def df_flagarray(indexenum=nil) + DfFlagarray.new(indexenum) + end + def df_array(tglen) + DfArray.new(tglen, yield) + end + def df_linked_list + DfLinkedList.new(yield) + end + + def global(glob) + Global.new(glob) + end + def compound(name=nil, &b) + m = Class.new(Compound) + DFHack.const_set(name, m) if name + m.instance_eval(&b) + m.new + end + def rtti_classname(n) + DFHack.rtti_register(n, self) + @_rtti_classname = n + end + def sizeof(n) + @_sizeof = n + end + + # allocate a new c++ object, return its ruby wrapper + def cpp_new(init=nil) + ptr = DFHack.malloc(_sizeof) + if _rtti_classname and vt = DFHack.rtti_getvtable(_rtti_classname) + DFHack.memory_write_int32(ptr, vt) + # TODO call constructor + end + o = new._at(ptr) + o._cpp_init + o._set(init) if init + o + end + end + def _cpp_init + _fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_init } + end + def _set(h) + case h + when Hash; h.each { |k, v| send("#{k}=", v) } + when Array; names = _field_names ; raise 'bad size' if names.length != h.length ; names.zip(h).each { |n, a| send("#{n}=", a) } + when Compound; _field_names.each { |n| send("#{n}=", h.send(n)) } + else raise 'wut?' + end + end + def _fields ; self.class._fields.to_a ; end + def _fields_ancestors ; self.class._fields_ancestors.to_a ; end + def _field_names ; _fields_ancestors.map { |n, o, s| n } ; end + def _rtti_classname ; self.class._rtti_classname ; end + def _sizeof ; self.class._sizeof ; end + @@inspecting = {} # avoid infinite recursion on mutually-referenced objects + def inspect + cn = self.class.name.sub(/^DFHack::/, '') + cn << ' @' << ('0x%X' % _memaddr) if cn != '' + out = "#<#{cn}" + return out << ' ...>' if @@inspecting[_memaddr] + @@inspecting[_memaddr] = true + _fields_ancestors.each { |n, o, s| + out << ' ' if out.length != 0 and out[-1, 1] != ' ' + if out.length > INSPECT_SIZE_LIMIT + out << '...' + break + end + out << inspect_field(n, o, s) + } + out.chomp!(' ') + @@inspecting.delete _memaddr + out << '>' + end + def inspect_field(n, o, s) + if s.kind_of?(BitField) and s._len == 1 + send(n) ? n.to_s : '' + elsif s.kind_of?(Pointer) + "#{n}=#{s._at(@_memaddr+o).inspect}" + elsif n == :_whole + "_whole=0x#{_whole.to_s(16)}" + else + v = send(n).inspect + "#{n}=#{v}" + end + rescue + "#{n}=ERR(#{$!})" + end + end + + class Enum + # number -> symbol + def self.enum + # ruby weirdness, needed to make the constants 'virtual' + @enum ||= const_get(:ENUM) + end + # symbol -> number + def self.nume + @nume ||= const_get(:NUME) + end + + def self.int(i) + nume[i] || i + end + def self.sym(i) + enum[i] || i + end + end + + class Number < MemStruct + attr_accessor :_bits, :_signed, :_initvalue, :_enum + def initialize(bits, signed, initvalue, enum) + @_bits = bits + @_signed = signed + @_initvalue = initvalue + @_enum = enum + end + + def _get + v = case @_bits + when 32; DFHack.memory_read_int32(@_memaddr) + when 16; DFHack.memory_read_int16(@_memaddr) + when 8; DFHack.memory_read_int8( @_memaddr) + when 64;(DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + (DFHack.memory_read_int32(@_memaddr+4) << 32) + end + v &= (1 << @_bits) - 1 if not @_signed + v = @_enum.sym(v) if @_enum + v + end + + def _set(v) + v = @_enum.int(v) if @_enum + case @_bits + when 32; DFHack.memory_write_int32(@_memaddr, v) + when 16; DFHack.memory_write_int16(@_memaddr, v) + when 8; DFHack.memory_write_int8( @_memaddr, v) + when 64; DFHack.memory_write_int32(@_memaddr, v & 0xffffffff) ; DFHack.memory_write_int32(@memaddr+4, v>>32) + end + end + + def _cpp_init + _set(@_initvalue) if @_initvalue + end + end + class Float < MemStruct + def _get + DFHack.memory_read_float(@_memaddr) + end + + def _set(v) + DFHack.memory_write_float(@_memaddr, v) + end + + def _cpp_init + _set(0.0) + end + end + class BitField < MemStruct + attr_accessor :_shift, :_len, :_enum + def initialize(shift, len, enum=nil) + @_shift = shift + @_len = len + @_enum = enum + end + def _mask + (1 << @_len) - 1 + end + + def _get + v = DFHack.memory_read_int32(@_memaddr) >> @_shift + if @_len == 1 + ((v & 1) == 0) ? false : true + else + v &= _mask + v = @_enum.sym(v) if @_enum + v + end + end + + def _set(v) + if @_len == 1 + # allow 'bit = 0' + v = (v && v != 0 ? 1 : 0) + end + v = @_enum.int(v) if @_enum + v = (v & _mask) << @_shift + + ori = DFHack.memory_read_int32(@_memaddr) & 0xffffffff + DFHack.memory_write_int32(@_memaddr, ori - (ori & ((-1 & _mask) << @_shift)) + v) + end + end + + class Pointer < MemStruct + attr_accessor :_tg + def initialize(tg) + @_tg = tg + end + + def _getp + DFHack.memory_read_int32(@_memaddr) & 0xffffffff + end + + def _get + addr = _getp + return if addr == 0 + @_tg._at(addr)._get + end + + # XXX shaky... + def _set(v) + if v.kind_of?(Pointer) + DFHack.memory_write_int32(@_memaddr, v._getp) + elsif v.kind_of?(MemStruct) + DFHack.memory_write_int32(@_memaddr, v._memaddr) + else + _get._set(v) + end + end + + def inspect + ptr = _getp + if ptr == 0 + 'NULL' + else + cn = '' + cn = @_tg.class.name.sub(/^DFHack::/, '').sub(/^MemHack::/, '') if @_tg + cn = @_tg._glob if cn == 'Global' + "#" + end + end + end + class PointerAry < MemStruct + attr_accessor :_tglen, :_tg + def initialize(tglen, tg) + @_tglen = tglen + @_tg = tg + end + + def _getp(i=0) + delta = (i != 0 ? i*@_tglen : 0) + (DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + delta + end + + def _get + addr = _getp + return if addr == 0 + self + end + + def [](i) + addr = _getp(i) + return if addr == 0 + @_tg._at(addr)._get + end + def []=(i, v) + addr = _getp(i) + raise 'null pointer' if addr == 0 + @_tg._at(addr)._set(v) + end + + def inspect ; ptr = _getp ; (ptr == 0) ? 'NULL' : "#" ; end + end + module Enumerable + include ::Enumerable + attr_accessor :_indexenum + def each ; (0...length).each { |i| yield self[i] } ; end + def inspect + out = '[' + each_with_index { |e, idx| + out << ', ' if out.length > 1 + if out.length > INSPECT_SIZE_LIMIT + out << '...' + break + end + out << "#{_indexenum.sym(idx)}=" if _indexenum + out << e.inspect + } + out << ']' + end + def empty? ; length == 0 ; end + def flatten ; map { |e| e.respond_to?(:flatten) ? e.flatten : e }.flatten ; end + end + class StaticArray < MemStruct + attr_accessor :_tglen, :_length, :_indexenum, :_tg + def initialize(tglen, length, indexenum, tg) + @_tglen = tglen + @_length = length + @_indexenum = indexenum + @_tg = tg + end + def _set(a) + a.each_with_index { |v, i| self[i] = v } + end + def _cpp_init + _length.times { |i| _tgat(i)._cpp_init } + end + alias length _length + alias size _length + def _tgat(i) + @_tg._at(@_memaddr + i*@_tglen) if i >= 0 and i < @_length + end + def [](i) + i = _indexenum.int(i) if _indexenum + i += @_length if i < 0 + _tgat(i)._get + end + def []=(i, v) + i = _indexenum.int(i) if _indexenum + i += @_length if i < 0 + _tgat(i)._set(v) + end + + include Enumerable + end + class StaticString < MemStruct + attr_accessor :_length + def initialize(length) + @_length = length + end + def _get + DFHack.memory_read(@_memaddr, @_length) + end + def _set(v) + DFHack.memory_write(@_memaddr, v[0, @_length]) + end + end + + class StlVector32 < MemStruct + attr_accessor :_tg + def initialize(tg) + @_tg = tg + end + + def length + DFHack.memory_vector32_length(@_memaddr) + end + def size ; length ; end # alias wouldnt work for subclasses + def valueptr_at(idx) + DFHack.memory_vector32_ptrat(@_memaddr, idx) + end + def insert_at(idx, val) + DFHack.memory_vector32_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vector32_delete(@_memaddr, idx) + end + + def _set(v) + delete_at(length-1) while length > v.length # match lengthes + v.each_with_index { |e, i| self[i] = e } # patch entries + end + + def _cpp_init + DFHack.memory_vector_init(@_memaddr) + end + + def clear + delete_at(length-1) while length > 0 + end + def [](idx) + idx += length if idx < 0 + @_tg._at(valueptr_at(idx))._get if idx >= 0 and idx < length + end + def []=(idx, v) + idx += length if idx < 0 + if idx >= length + insert_at(idx, 0) + elsif idx < 0 + raise 'invalid idx' + end + @_tg._at(valueptr_at(idx))._set(v) + end + def push(v) + self[length] = v + self + end + def <<(v) ; push(v) ; end + def pop + l = length + if l > 0 + v = self[l-1] + delete_at(l-1) + end + v + end + + include Enumerable + # do a binary search in an ordered vector for a specific target attribute + # ex: world.history.figures.binsearch(unit.hist_figure_id) + def binsearch(target, field=:id) + o_start = 0 + o_end = length - 1 + while o_end >= o_start + o_half = o_start + (o_end-o_start)/2 + obj = self[o_half] + oval = obj.send(field) + if oval == target + return obj + elsif oval < target + o_start = o_half+1 + else + o_end = o_half-1 + end + end + end + end + class StlVector16 < StlVector32 + def length + DFHack.memory_vector16_length(@_memaddr) + end + def valueptr_at(idx) + DFHack.memory_vector16_ptrat(@_memaddr, idx) + end + def insert_at(idx, val) + DFHack.memory_vector16_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vector16_delete(@_memaddr, idx) + end + end + class StlVector8 < StlVector32 + def length + DFHack.memory_vector8_length(@_memaddr) + end + def valueptr_at(idx) + DFHack.memory_vector8_ptrat(@_memaddr, idx) + end + def insert_at(idx, val) + DFHack.memory_vector8_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vector8_delete(@_memaddr, idx) + end + end + class StlBitVector < StlVector32 + def initialize ; end + def length + DFHack.memory_vectorbool_length(@_memaddr) + end + def insert_at(idx, val) + DFHack.memory_vectorbool_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vectorbool_delete(@_memaddr, idx) + end + def [](idx) + idx += length if idx < 0 + DFHack.memory_vectorbool_at(@_memaddr, idx) if idx >= 0 and idx < length + end + def []=(idx, v) + idx += length if idx < 0 + if idx >= length + insert_at(idx, v) + elsif idx < 0 + raise 'invalid idx' + else + DFHack.memory_vectorbool_setat(@_memaddr, idx, v) + end + end + end + class StlString < MemStruct + def _get + DFHack.memory_read_stlstring(@_memaddr) + end + + def _set(v) + DFHack.memory_write_stlstring(@_memaddr, v) + end + + def _cpp_init + DFHack.memory_stlstring_init(@_memaddr) + end + end + class StlDeque < MemStruct + attr_accessor :_tglen, :_tg + def initialize(tglen, tg) + @_tglen = tglen + @_tg = tg + end + # XXX DF uses stl::deque, so to have a C binding we'd need to single-case every + # possible struct size, like for StlVector. Just ignore it for now, deque are rare enough. + def inspect ; "#" ; end + end + + class DfFlagarray < MemStruct + attr_accessor :_indexenum + def initialize(indexenum) + @_indexenum = indexenum + end + def length + DFHack.memory_bitarray_length(@_memaddr) + end + # TODO _cpp_init + def size ; length ; end + def resize(len) + DFHack.memory_bitarray_resize(@_memaddr, len) + end + def [](idx) + idx = _indexenum.int(idx) if _indexenum + idx += length if idx < 0 + DFHack.memory_bitarray_isset(@_memaddr, idx) if idx >= 0 and idx < length + end + def []=(idx, v) + idx = _indexenum.int(idx) if _indexenum + idx += length if idx < 0 + if idx >= length or idx < 0 + raise 'invalid idx' + else + DFHack.memory_bitarray_set(@_memaddr, idx, v) + end + end + + include Enumerable + end + class DfArray < Compound + attr_accessor :_tglen, :_tg + def initialize(tglen, tg) + @_tglen = tglen + @_tg = tg + end + + field(:_ptr, 0) { number 32, false } + field(:_length, 4) { number 16, false } + + def length ; _length ; end + def size ; _length ; end + # TODO _cpp_init + def _tgat(i) + @_tg._at(_ptr + i*@_tglen) if i >= 0 and i < _length + end + def [](i) + i += _length if i < 0 + _tgat(i)._get + end + def []=(i, v) + i += _length if i < 0 + _tgat(i)._set(v) + end + def _set(a) + a.each_with_index { |v, i| self[i] = v } + end + + include Enumerable + end + class DfLinkedList < Compound + attr_accessor :_tg + def initialize(tg) + @_tg = tg + end + + field(:_ptr, 0) { number 32, false } + field(:_prev, 4) { number 32, false } + field(:_next, 8) { number 32, false } + + def item + # With the current xml structure, currently _tg designate + # the type of the 'next' and 'prev' fields, not 'item'. + # List head has item == NULL, so we can safely return nil. + + #addr = _ptr + #return if addr == 0 + #@_tg._at(addr)._get + end + + def item=(v) + #addr = _ptr + #raise 'null pointer' if addr == 0 + #@_tg.at(addr)._set(v) + raise 'null pointer' + end + + def prev + addr = _prev + return if addr == 0 + @_tg._at(addr)._get + end + + def next + addr = _next + return if addr == 0 + @_tg._at(addr)._get + end + + include Enumerable + def each + o = self + while o + yield o.item if o.item + o = o.next + end + end + def inspect ; "#" ; end + end + + class Global < MemStruct + attr_accessor :_glob + def initialize(glob) + @_glob = glob + end + def _at(addr) + g = DFHack.const_get(@_glob) + g = DFHack.rtti_getclassat(g, addr) + g.new._at(addr) + end + def inspect ; "#<#{@_glob}>" ; end + end + + end + + class BooleanEnum + def self.int(v) ; ((v == true) || (v == 1)) ? 1 : 0 ; end + def self.sym(v) ; (!v || (v == 0)) ? false : true ; end + end + + # cpp rtti name -> rb class + @rtti_n2c = {} + @rtti_c2n = {} + + # cpp rtti name -> vtable ptr + @rtti_n2v = {} + @rtti_v2n = {} + + def self.rtti_n2c ; @rtti_n2c ; end + def self.rtti_c2n ; @rtti_c2n ; end + def self.rtti_n2v ; @rtti_n2v ; end + def self.rtti_v2n ; @rtti_v2n ; end + + # register a ruby class with a cpp rtti class name + def self.rtti_register(cppname, cls) + @rtti_n2c[cppname] = cls + @rtti_c2n[cls] = cppname + end + + # return the ruby class to use for the cpp object at address if rtti info is available + def self.rtti_getclassat(cls, addr) + if addr != 0 and @rtti_c2n[cls] + # rtti info exist for class => cpp object has a vtable + @rtti_n2c[rtti_readclassname(get_vtable_ptr(addr))] || cls + else + cls + end + end + + # try to read the rtti classname from an object vtable pointer + def self.rtti_readclassname(vptr) + unless n = @rtti_v2n[vptr] + n = @rtti_v2n[vptr] = get_rtti_classname(vptr).to_sym + @rtti_n2v[n] = vptr + end + n + end + + # return the vtable pointer from the cpp rtti name + def self.rtti_getvtable(cppname) + unless v = @rtti_n2v[cppname] + v = get_vtable(cppname.to_s) + @rtti_n2v[cppname] = v + @rtti_v2n[v] = cppname if v != 0 + end + v if v != 0 + end + + def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0) + vmethod_do_call(obj._memaddr, voff, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), vmethod_arg(a3)) + end + + def self.vmethod_arg(arg) + case arg + when nil, false; 0 + when true; 1 + when Integer; arg + #when String; [arg].pack('p').unpack('L')[0] # raw pointer to buffer + when MemHack::Compound; arg._memaddr + else raise "bad vmethod arg #{arg.class}" + end + end +end diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb index 3a7d20a27..1922e5bb5 100644 --- a/plugins/ruby/ruby.rb +++ b/plugins/ruby/ruby.rb @@ -1,3 +1,4 @@ +# redefine standard i/o methods to use the dfhack console module Kernel def puts(*a) a.flatten.each { |l| @@ -77,104 +78,6 @@ module DFHack @onstatechange_list.each { |cb| cb.call(newstate) } end - - # return an Unit - # with no arg, return currently selected unit in df UI ('v' or 'k' menu) - # with numeric arg, search unit by unit.id - # with an argument that respond to x/y/z (eg cursor), find first unit at this position - def find_unit(what=:selected) - if what == :selected - case ui.main.mode - when :ViewUnits - # nobody selected => idx == 0 - v = world.units.active[ui_selected_unit] - v if v and v.pos.z == cursor.z - when :LookAround - k = ui_look_list.items[ui_look_cursor] - k.unit if k.type == :Unit - end - elsif what.kind_of?(Integer) - world.units.all.binsearch(what) - elsif what.respond_to?(:x) or what.respond_to?(:pos) - what = what.pos if what.respond_to?(:pos) - x, y, z = what.x, what.y, what.z - world.units.all.find { |u| u.pos.x == x and u.pos.y == y and u.pos.z == z } - else - raise "what what?" - end - end - - # return an Item - # arg similar to find_unit; no arg = 'k' menu - def find_item(what=:selected) - if what == :selected - case ui.main.mode - when :LookAround - k = ui_look_list.items[ui_look_cursor] - k.item if k.type == :Item - end - elsif what.kind_of?(Integer) - world.items.all.binsearch(what) - elsif what.respond_to?(:x) or what.respond_to?(:pos) - what = what.pos if what.respond_to?(:pos) - x, y, z = what.x, what.y, what.z - world.items.all.find { |i| i.pos.x == x and i.pos.y == y and i.pos.z == z } - else - raise "what what?" - end - end - - # return a map block by tile coordinates - # you can also use find_map_block(cursor) or anything that respond to x/y/z - def map_block_at(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - if x >= 0 and x < world.map.x_count and y >= 0 and y < world.map.y_count and z >= 0 and z < world.map.z_count - world.map.block_index[x/16][y/16][z] - end - end - - def map_designation_at(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - if b = map_block_at(x, y, z) - b.designation[x%16][y%16] - end - end - - def map_occupancy_at(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - if b = map_block_at(x, y, z) - b.occupancy[x%16][y%16] - end - end - - # yields every map block - def each_map_block - (0...world.map.x_count_block).each { |xb| - xl = world.map.block_index[xb] - (0...world.map.y_count_block).each { |yb| - yl = xl[yb] - (0...world.map.z_count_block).each { |z| - p = yl[z] - yield p if p - } - } - } - end - - # yields every map block for a given z level - def each_map_block_z(z) - (0...world.map.x_count_block).each { |xb| - xl = world.map.block_index[xb] - (0...world.map.y_count_block).each { |yb| - p = xl[yb][z] - yield p if p - } - } - end - # return true if the argument is under the cursor def at_cursor?(obj) same_pos?(obj, cursor) @@ -187,67 +90,6 @@ module DFHack pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z end - # center the DF screen on something - # updates the cursor position if visible - def center_viewscreen(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - - # compute screen 'map' size (tiles) - menuwidth = ui_menu_width - # ui_menu_width shows only the 'tab' status - menuwidth = 1 if menuwidth == 2 and ui_area_map_width == 2 and cursor.x != -30000 - menuwidth = 2 if menuwidth == 3 and cursor.x != -30000 - w_w = gps.dimx - 2 - w_h = gps.dimy - 2 - case menuwidth - when 1; w_w -= 55 - when 2; w_w -= (ui_area_map_width == 2 ? 24 : 31) - end - - # center view - w_x = x - w_w/2 - w_y = y - w_h/2 - w_z = z - # round view coordinates (optional) - #w_x -= w_x % 10 - #w_y -= w_y % 10 - # crop to map limits - w_x = [[w_x, world.map.x_count - w_w].min, 0].max - w_y = [[w_y, world.map.y_count - w_h].min, 0].max - - self.window_x = w_x - self.window_y = w_y - self.window_z = w_z - - if cursor.x != -30000 - cursor.x, cursor.y, cursor.z = x, y, z - end - end - - # add an announcement - # color = integer, bright = bool - def add_announcement(str, color=nil, bright=nil) - cont = false - while str.length > 0 - rep = Report.cpp_new - rep.color = color if color - rep.bright = ((bright && bright != 0) ? 1 : 0) if bright != nil - rep.year = cur_year - rep.time = cur_year_tick - rep.flags.continuation = cont - cont = true - rep.flags.announcement = true - rep.text = str[0, 73] - str = str[73..-1].to_s - rep.id = world.status.next_report_id - world.status.next_report_id += 1 - world.status.reports << rep - world.status.announcements << rep - world.status.display_timer = 2000 - end - end - # try to match a user-specified name to one from the raws # uses case-switching and substring matching # eg match_rawname('coal', ['COAL_BITUMINOUS', 'BAUXITE']) => 'COAL_BITUMINOUS' @@ -257,38 +99,6 @@ module DFHack may = rawlist.find_all { |r| r.downcase.index(name.downcase) } may.first if may.length == 1 end - - # link a job to the world - # allocate & set job.id, allocate a JobListLink, link to job & world.job_list - def job_link(job) - lastjob = world.job_list - lastjob = lastjob.next while lastjob.next - joblink = JobListLink.cpp_new - joblink.prev = lastjob - joblink.item = job - job.list_link = joblink - job.id = df.job_next_id - df.job_next_id += 1 - lastjob.next = joblink - end - - # attach an item to a job, flag item in_job - def job_attachitem(job, item, role=:Hauled, filter_idx=-1) - if role != :TargetContainer - item.flags.in_job = true - end - - itemlink = SpecificRef.cpp_new - itemlink.type = :JOB - itemlink.job = job - item.specific_refs << itemlink - - joblink = JobItemRef.cpp_new - joblink.item = item - joblink.role = role - joblink.job_item_idx = filter_idx - job.items << joblink - end end end @@ -297,758 +107,9 @@ def df DFHack end -# following: definitions used by ruby-autogen.rb -module DFHack -module MemHack -INSPECT_SIZE_LIMIT=16384 -class MemStruct - attr_accessor :_memaddr - def _at(addr) ; d = dup ; d._memaddr = addr ; d ; end - def _get ; self ; end - def _cpp_init ; end -end - -class Compound < MemStruct - class << self - attr_accessor :_fields, :_rtti_classname, :_sizeof - def field(name, offset) - struct = yield - return if not struct - @_fields ||= [] - @_fields << [name, offset, struct] - define_method(name) { struct._at(@_memaddr+offset)._get } - define_method("#{name}=") { |v| struct._at(@_memaddr+offset)._set(v) } - end - def _fields_ancestors - if superclass.respond_to?(:_fields_ancestors) - superclass._fields_ancestors + _fields.to_a - else - _fields.to_a - end - end - - def number(bits, signed, initvalue=nil, enum=nil) - Number.new(bits, signed, initvalue, enum) - end - def float - Float.new - end - def bit(shift) - BitField.new(shift, 1) - end - def bits(shift, len, enum=nil) - BitField.new(shift, len, enum) - end - def pointer - Pointer.new((yield if block_given?)) - end - def pointer_ary(tglen) - PointerAry.new(tglen, yield) - end - def static_array(len, tglen, indexenum=nil) - StaticArray.new(tglen, len, indexenum, yield) - end - def static_string(len) - StaticString.new(len) - end - - def stl_vector(tglen=nil) - tg = yield if tglen - case tglen - when 1; StlVector8.new(tg) - when 2; StlVector16.new(tg) - else StlVector32.new(tg) - end - end - def stl_string - StlString.new - end - def stl_bit_vector - StlBitVector.new - end - def stl_deque(tglen) - StlDeque.new(tglen, yield) - end - - def df_flagarray(indexenum=nil) - DfFlagarray.new(indexenum) - end - def df_array(tglen) - DfArray.new(tglen, yield) - end - def df_linked_list - DfLinkedList.new(yield) - end - - def global(glob) - Global.new(glob) - end - def compound(name=nil, &b) - m = Class.new(Compound) - DFHack.const_set(name, m) if name - m.instance_eval(&b) - m.new - end - def rtti_classname(n) - DFHack.rtti_register(n, self) - @_rtti_classname = n - end - def sizeof(n) - @_sizeof = n - end - - # allocate a new c++ object, return its ruby wrapper - def cpp_new - ptr = DFHack.malloc(_sizeof) - if _rtti_classname and vt = DFHack.rtti_getvtable(_rtti_classname) - DFHack.memory_write_int32(ptr, vt) - # TODO call constructor - end - o = new._at(ptr) - o._cpp_init - o - end - end - def _cpp_init - _fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_init } - end - def _set(h) - case h - when Hash; h.each { |k, v| send("_#{k}=", v) } - when Array; names = _field_names ; raise 'bad size' if names.length != h.length ; names.zip(h).each { |n, a| send("#{n}=", a) } - when Compound; _field_names.each { |n| send("#{n}=", h.send(n)) } - else raise 'wut?' - end - end - def _fields ; self.class._fields.to_a ; end - def _fields_ancestors ; self.class._fields_ancestors.to_a ; end - def _field_names ; _fields_ancestors.map { |n, o, s| n } ; end - def _rtti_classname ; self.class._rtti_classname ; end - def _sizeof ; self.class._sizeof ; end - @@inspecting = {} # avoid infinite recursion on mutually-referenced objects - def inspect - cn = self.class.name.sub(/^DFHack::/, '') - cn << ' @' << ('0x%X' % _memaddr) if cn != '' - out = "#<#{cn}" - return out << ' ...>' if @@inspecting[_memaddr] - @@inspecting[_memaddr] = true - _fields_ancestors.each { |n, o, s| - out << ' ' if out.length != 0 and out[-1, 1] != ' ' - if out.length > INSPECT_SIZE_LIMIT - out << '...' - break - end - out << inspect_field(n, o, s) - } - out.chomp!(' ') - @@inspecting.delete _memaddr - out << '>' - end - def inspect_field(n, o, s) - if s.kind_of?(BitField) and s._len == 1 - send(n) ? n.to_s : '' - elsif s.kind_of?(Pointer) - "#{n}=#{s._at(@_memaddr+o).inspect}" - elsif n == :_whole - "_whole=0x#{_whole.to_s(16)}" - else - v = send(n).inspect - "#{n}=#{v}" - end - rescue - "#{n}=ERR(#{$!})" - end -end - -class Enum - # number -> symbol - def self.enum - # ruby weirdness, needed to make the constants 'virtual' - @enum ||= const_get(:ENUM) - end - # symbol -> number - def self.nume - @nume ||= const_get(:NUME) - end - - def self.int(i) - nume[i] || i - end - def self.sym(i) - enum[i] || i - end -end - -class Number < MemStruct - attr_accessor :_bits, :_signed, :_initvalue, :_enum - def initialize(bits, signed, initvalue, enum) - @_bits = bits - @_signed = signed - @_initvalue = initvalue - @_enum = enum - end - - def _get - v = case @_bits - when 32; DFHack.memory_read_int32(@_memaddr) - when 16; DFHack.memory_read_int16(@_memaddr) - when 8; DFHack.memory_read_int8( @_memaddr) - when 64;(DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + (DFHack.memory_read_int32(@_memaddr+4) << 32) - end - v &= (1 << @_bits) - 1 if not @_signed - v = @_enum.sym(v) if @_enum - v - end - - def _set(v) - v = @_enum.int(v) if @_enum - case @_bits - when 32; DFHack.memory_write_int32(@_memaddr, v) - when 16; DFHack.memory_write_int16(@_memaddr, v) - when 8; DFHack.memory_write_int8( @_memaddr, v) - when 64; DFHack.memory_write_int32(@_memaddr, v & 0xffffffff) ; DFHack.memory_write_int32(@memaddr+4, v>>32) - end - end - - def _cpp_init - _set(@_initvalue) if @_initvalue - end -end -class Float < MemStruct - def _get - DFHack.memory_read_float(@_memaddr) - end - - def _set(v) - DFHack.memory_write_float(@_memaddr, v) - end - - def _cpp_init - _set(0.0) - end -end -class BitField < MemStruct - attr_accessor :_shift, :_len, :_enum - def initialize(shift, len, enum=nil) - @_shift = shift - @_len = len - @_enum = enum - end - def _mask - (1 << @_len) - 1 - end - - def _get - v = DFHack.memory_read_int32(@_memaddr) >> @_shift - if @_len == 1 - ((v & 1) == 0) ? false : true - else - v &= _mask - v = @_enum.sym(v) if @_enum - v - end - end - - def _set(v) - if @_len == 1 - # allow 'bit = 0' - v = (v && v != 0 ? 1 : 0) - end - v = @_enum.int(v) if @_enum - v = (v & _mask) << @_shift - - ori = DFHack.memory_read_int32(@_memaddr) & 0xffffffff - DFHack.memory_write_int32(@_memaddr, ori - (ori & ((-1 & _mask) << @_shift)) + v) - end -end - -class Pointer < MemStruct - attr_accessor :_tg - def initialize(tg) - @_tg = tg - end - - def _getp - DFHack.memory_read_int32(@_memaddr) & 0xffffffff - end - - def _get - addr = _getp - return if addr == 0 - @_tg._at(addr)._get - end - - # XXX shaky... - def _set(v) - if v.kind_of?(Pointer) - DFHack.memory_write_int32(@_memaddr, v._getp) - elsif v.kind_of?(MemStruct) - DFHack.memory_write_int32(@_memaddr, v._memaddr) - else - _get._set(v) - end - end - - def inspect - ptr = _getp - if ptr == 0 - 'NULL' - else - cn = '' - cn = @_tg.class.name.sub(/^DFHack::/, '').sub(/^MemHack::/, '') if @_tg - cn = @_tg._glob if cn == 'Global' - "#" - end - end -end -class PointerAry < MemStruct - attr_accessor :_tglen, :_tg - def initialize(tglen, tg) - @_tglen = tglen - @_tg = tg - end - - def _getp(i=0) - delta = (i != 0 ? i*@_tglen : 0) - (DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + delta - end - - def _get - addr = _getp - return if addr == 0 - self - end - - def [](i) - addr = _getp(i) - return if addr == 0 - @_tg._at(addr)._get - end - def []=(i, v) - addr = _getp(i) - raise 'null pointer' if addr == 0 - @_tg._at(addr)._set(v) - end - - def inspect ; ptr = _getp ; (ptr == 0) ? 'NULL' : "#" ; end -end -module Enumerable - include ::Enumerable - attr_accessor :_indexenum - def each ; (0...length).each { |i| yield self[i] } ; end - def inspect - out = '[' - each_with_index { |e, idx| - out << ', ' if out.length > 1 - if out.length > INSPECT_SIZE_LIMIT - out << '...' - break - end - out << "#{_indexenum.sym(idx)}=" if _indexenum - out << e.inspect - } - out << ']' - end - def empty? ; length == 0 ; end - def flatten ; map { |e| e.respond_to?(:flatten) ? e.flatten : e }.flatten ; end -end -class StaticArray < MemStruct - attr_accessor :_tglen, :_length, :_indexenum, :_tg - def initialize(tglen, length, indexenum, tg) - @_tglen = tglen - @_length = length - @_indexenum = indexenum - @_tg = tg - end - def _set(a) - a.each_with_index { |v, i| self[i] = v } - end - def _cpp_init - _length.times { |i| _tgat(i)._cpp_init } - end - alias length _length - alias size _length - def _tgat(i) - @_tg._at(@_memaddr + i*@_tglen) if i >= 0 and i < @_length - end - def [](i) - i = _indexenum.int(i) if _indexenum - i += @_length if i < 0 - _tgat(i)._get - end - def []=(i, v) - i = _indexenum.int(i) if _indexenum - i += @_length if i < 0 - _tgat(i)._set(v) - end - - include Enumerable -end -class StaticString < MemStruct - attr_accessor :_length - def initialize(length) - @_length = length - end - def _get - DFHack.memory_read(@_memaddr, @_length) - end - def _set(v) - DFHack.memory_write(@_memaddr, v[0, @_length]) - end -end - -class StlVector32 < MemStruct - attr_accessor :_tg - def initialize(tg) - @_tg = tg - end - - def length - DFHack.memory_vector32_length(@_memaddr) - end - def size ; length ; end # alias wouldnt work for subclasses - def valueptr_at(idx) - DFHack.memory_vector32_ptrat(@_memaddr, idx) - end - def insert_at(idx, val) - DFHack.memory_vector32_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vector32_delete(@_memaddr, idx) - end - - def _set(v) - delete_at(length-1) while length > v.length # match lengthes - v.each_with_index { |e, i| self[i] = e } # patch entries - end - - def _cpp_init - DFHack.memory_vector_init(@_memaddr) - end - - def clear - delete_at(length-1) while length > 0 - end - def [](idx) - idx += length if idx < 0 - @_tg._at(valueptr_at(idx))._get if idx >= 0 and idx < length - end - def []=(idx, v) - idx += length if idx < 0 - if idx >= length - insert_at(idx, 0) - elsif idx < 0 - raise 'invalid idx' - end - @_tg._at(valueptr_at(idx))._set(v) - end - def push(v) - self[length] = v - self - end - def <<(v) ; push(v) ; end - def pop - l = length - if l > 0 - v = self[l-1] - delete_at(l-1) - end - v - end - - include Enumerable - # do a binary search in an ordered vector for a specific target attribute - # ex: world.history.figures.binsearch(unit.hist_figure_id) - def binsearch(target, field=:id) - o_start = 0 - o_end = length - 1 - while o_end >= o_start - o_half = o_start + (o_end-o_start)/2 - obj = self[o_half] - oval = obj.send(field) - if oval == target - return obj - elsif oval < target - o_start = o_half+1 - else - o_end = o_half-1 - end - end - end -end -class StlVector16 < StlVector32 - def length - DFHack.memory_vector16_length(@_memaddr) - end - def valueptr_at(idx) - DFHack.memory_vector16_ptrat(@_memaddr, idx) - end - def insert_at(idx, val) - DFHack.memory_vector16_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vector16_delete(@_memaddr, idx) - end -end -class StlVector8 < StlVector32 - def length - DFHack.memory_vector8_length(@_memaddr) - end - def valueptr_at(idx) - DFHack.memory_vector8_ptrat(@_memaddr, idx) - end - def insert_at(idx, val) - DFHack.memory_vector8_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vector8_delete(@_memaddr, idx) - end -end -class StlBitVector < StlVector32 - def initialize ; end - def length - DFHack.memory_vectorbool_length(@_memaddr) - end - def insert_at(idx, val) - DFHack.memory_vectorbool_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vectorbool_delete(@_memaddr, idx) - end - def [](idx) - idx += length if idx < 0 - DFHack.memory_vectorbool_at(@_memaddr, idx) if idx >= 0 and idx < length - end - def []=(idx, v) - idx += length if idx < 0 - if idx >= length - insert_at(idx, v) - elsif idx < 0 - raise 'invalid idx' - else - DFHack.memory_vectorbool_setat(@_memaddr, idx, v) - end - end -end -class StlString < MemStruct - def _get - DFHack.memory_read_stlstring(@_memaddr) - end - - def _set(v) - DFHack.memory_write_stlstring(@_memaddr, v) - end - - def _cpp_init - DFHack.memory_stlstring_init(@_memaddr) - end -end -class StlDeque < MemStruct - attr_accessor :_tglen, :_tg - def initialize(tglen, tg) - @_tglen = tglen - @_tg = tg - end - # XXX DF uses stl::deque, so to have a C binding we'd need to single-case every - # possible struct size, like for StlVector. Just ignore it for now, deque are rare enough. - def inspect ; "#" ; end -end - -class DfFlagarray < MemStruct - attr_accessor :_indexenum - def initialize(indexenum) - @_indexenum = indexenum - end - def length - DFHack.memory_bitarray_length(@_memaddr) - end - # TODO _cpp_init - def size ; length ; end - def resize(len) - DFHack.memory_bitarray_resize(@_memaddr, len) - end - def [](idx) - idx = _indexenum.int(idx) if _indexenum - idx += length if idx < 0 - DFHack.memory_bitarray_isset(@_memaddr, idx) if idx >= 0 and idx < length - end - def []=(idx, v) - idx = _indexenum.int(idx) if _indexenum - idx += length if idx < 0 - if idx >= length or idx < 0 - raise 'invalid idx' - else - DFHack.memory_bitarray_set(@_memaddr, idx, v) - end - end - - include Enumerable -end -class DfArray < Compound - attr_accessor :_tglen, :_tg - def initialize(tglen, tg) - @_tglen = tglen - @_tg = tg - end - - field(:_ptr, 0) { number 32, false } - field(:_length, 4) { number 16, false } - - def length ; _length ; end - def size ; _length ; end - # TODO _cpp_init - def _tgat(i) - @_tg._at(_ptr + i*@_tglen) if i >= 0 and i < _length - end - def [](i) - i += _length if i < 0 - _tgat(i)._get - end - def []=(i, v) - i += _length if i < 0 - _tgat(i)._set(v) - end - def _set(a) - a.each_with_index { |v, i| self[i] = v } - end - - include Enumerable -end -class DfLinkedList < Compound - attr_accessor :_tg - def initialize(tg) - @_tg = tg - end - - field(:_ptr, 0) { number 32, false } - field(:_prev, 4) { number 32, false } - field(:_next, 8) { number 32, false } - - def item - # With the current xml structure, currently _tg designate - # the type of the 'next' and 'prev' fields, not 'item'. - # List head has item == NULL, so we can safely return nil. - - #addr = _ptr - #return if addr == 0 - #@_tg._at(addr)._get - end - - def item=(v) - #addr = _ptr - #raise 'null pointer' if addr == 0 - #@_tg.at(addr)._set(v) - raise 'null pointer' - end - - def prev - addr = _prev - return if addr == 0 - @_tg._at(addr)._get - end - - def next - addr = _next - return if addr == 0 - @_tg._at(addr)._get - end - - include Enumerable - def each - o = self - while o - yield o.item if o.item - o = o.next - end - end - def inspect ; "#" ; end -end - -class Global < MemStruct - attr_accessor :_glob - def initialize(glob) - @_glob = glob - end - def _at(addr) - g = DFHack.const_get(@_glob) - g = DFHack.rtti_getclassat(g, addr) - g.new._at(addr) - end - def inspect ; "#<#{@_glob}>" ; end -end -end # module MemHack - -class BooleanEnum - def self.int(v) ; ((v == true) || (v == 1)) ? 1 : 0 ; end - def self.sym(v) ; (!v || (v == 0)) ? false : true ; end -end - -# cpp rtti name -> rb class -@rtti_n2c = {} -@rtti_c2n = {} - -# cpp rtti name -> vtable ptr -@rtti_n2v = {} -@rtti_v2n = {} - -def self.rtti_n2c ; @rtti_n2c ; end -def self.rtti_c2n ; @rtti_c2n ; end -def self.rtti_n2v ; @rtti_n2v ; end -def self.rtti_v2n ; @rtti_v2n ; end - -# register a ruby class with a cpp rtti class name -def self.rtti_register(cppname, cls) - @rtti_n2c[cppname] = cls - @rtti_c2n[cls] = cppname -end - -# return the ruby class to use for the cpp object at address if rtti info is available -def self.rtti_getclassat(cls, addr) - if addr != 0 and @rtti_c2n[cls] - # rtti info exist for class => cpp object has a vtable - @rtti_n2c[rtti_readclassname(get_vtable_ptr(addr))] || cls - else - cls - end -end - -# try to read the rtti classname from an object vtable pointer -def self.rtti_readclassname(vptr) - unless n = @rtti_v2n[vptr] - n = @rtti_v2n[vptr] = get_rtti_classname(vptr).to_sym - @rtti_n2v[n] = vptr - end - n -end - -# return the vtable pointer from the cpp rtti name -def self.rtti_getvtable(cppname) - unless v = @rtti_n2v[cppname] - v = get_vtable(cppname.to_s) - @rtti_n2v[cppname] = v - @rtti_v2n[v] = cppname if v != 0 - end - v if v != 0 -end - -def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0) - vmethod_do_call(obj._memaddr, voff, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), vmethod_arg(a3)) -end - -def self.vmethod_arg(arg) - case arg - when nil, false; 0 - when true; 1 - when Integer; arg - #when String; [arg].pack('p').unpack('L')[0] # raw pointer to buffer - when MemHack::Compound; arg._memaddr - else raise "bad vmethod arg #{arg.class}" - end -end - -end - # load autogenned file +require './hack/ruby/ruby-autogen-defs' require './hack/ruby/ruby-autogen' + # load all modules Dir['./hack/ruby/*.rb'].each { |m| require m.chomp('.rb') } diff --git a/plugins/ruby/ui.rb b/plugins/ruby/ui.rb new file mode 100644 index 000000000..fbe7ced77 --- /dev/null +++ b/plugins/ruby/ui.rb @@ -0,0 +1,65 @@ +# df user-interface related methods +module DFHack + class << self + # center the DF screen on something + # updates the cursor position if visible + def center_viewscreen(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + + # compute screen 'map' size (tiles) + menuwidth = ui_menu_width + # ui_menu_width shows only the 'tab' status + menuwidth = 1 if menuwidth == 2 and ui_area_map_width == 2 and cursor.x != -30000 + menuwidth = 2 if menuwidth == 3 and cursor.x != -30000 + w_w = gps.dimx - 2 + w_h = gps.dimy - 2 + case menuwidth + when 1; w_w -= 55 + when 2; w_w -= (ui_area_map_width == 2 ? 24 : 31) + end + + # center view + w_x = x - w_w/2 + w_y = y - w_h/2 + w_z = z + # round view coordinates (optional) + #w_x -= w_x % 10 + #w_y -= w_y % 10 + # crop to map limits + w_x = [[w_x, world.map.x_count - w_w].min, 0].max + w_y = [[w_y, world.map.y_count - w_h].min, 0].max + + self.window_x = w_x + self.window_y = w_y + self.window_z = w_z + + if cursor.x != -30000 + cursor.x, cursor.y, cursor.z = x, y, z + end + end + + # add an announcement + # color = integer, bright = bool + def add_announcement(str, color=nil, bright=nil) + cont = false + while str.length > 0 + rep = Report.cpp_new + rep.color = color if color + rep.bright = ((bright && bright != 0) ? 1 : 0) if bright != nil + rep.year = cur_year + rep.time = cur_year_tick + rep.flags.continuation = cont + cont = true + rep.flags.announcement = true + rep.text = str[0, 73] + str = str[73..-1].to_s + rep.id = world.status.next_report_id + world.status.next_report_id += 1 + world.status.reports << rep + world.status.announcements << rep + world.status.display_timer = 2000 + end + end + end +end diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 9a00b2bfa..41b7bf8a6 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -1,52 +1,80 @@ module DFHack -# returns an Array of all units that are current fort citizen (dwarves, on map, not hostile) -def self.unit_citizens - race = ui.race_id - civ = ui.civ_id - world.units.active.find_all { |u| - u.race == race and u.civ_id == civ and !u.flags1.dead and !u.flags1.merchant and - !u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and - !u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and - u.mood != :Berserk - # TODO check curse ; currently this should keep vampires, but may include werebeasts - } -end + class << self + # return an Unit + # with no arg, return currently selected unit in df UI ('v' or 'k' menu) + # with numeric arg, search unit by unit.id + # with an argument that respond to x/y/z (eg cursor), find first unit at this position + def unit_find(what=:selected) + if what == :selected + case ui.main.mode + when :ViewUnits + # nobody selected => idx == 0 + v = world.units.active[ui_selected_unit] + v if v and v.pos.z == cursor.z + when :LookAround + k = ui_look_list.items[ui_look_cursor] + k.unit if k.type == :Unit + else + raise "bad UI mode #{ui.main.mode.inspect}" + end + elsif what.kind_of?(Integer) + world.units.all.binsearch(what) + elsif what.respond_to?(:x) or what.respond_to?(:pos) + world.units.all.find { |u| same_pos?(what, u) } + else + raise "what what?" + end + end -# list workers (citizen, not crazy / child / inmood / noble) -def self.unit_workers - unit_citizens.find_all { |u| - u.mood == :None and - u.profession != :CHILD and - u.profession != :BABY and - # TODO MENIAL_WORK_EXEMPTION_SPOUSE - !unit_entitypositions(u).find { |pos| pos.flags[:MENIAL_WORK_EXEMPTION] } - } -end + # returns an Array of all units that are current fort citizen (dwarves, on map, not hostile) + def unit_citizens + race = ui.race_id + civ = ui.civ_id + world.units.active.find_all { |u| + u.race == race and u.civ_id == civ and !u.flags1.dead and !u.flags1.merchant and + !u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and + !u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and + u.mood != :Berserk + # TODO check curse ; currently this should keep vampires, but may include werebeasts + } + end -# list currently idle workers -def self.unit_idlers - unit_workers.find_all { |u| - # current_job includes eat/drink/sleep/pickupequip - !u.job.current_job._getv and - # filter 'attend meeting' - u.meetings.length == 0 and - # filter soldiers (TODO check schedule) - u.military.squad_index == -1 and - # filter 'on break' - !u.status.misc_traits.find { |t| id == :OnBreak } - } -end + # list workers (citizen, not crazy / child / inmood / noble) + def unit_workers + unit_citizens.find_all { |u| + u.mood == :None and + u.profession != :CHILD and + u.profession != :BABY and + # TODO MENIAL_WORK_EXEMPTION_SPOUSE + !unit_entitypositions(u).find { |pos| pos.flags[:MENIAL_WORK_EXEMPTION] } + } + end -def self.unit_entitypositions(unit) - list = [] - return list if not hf = world.history.figures.binsearch(unit.hist_figure_id) - hf.entity_links.each { |el| - next if el._rtti_classname != :histfig_entity_link_positionst - next if not ent = world.entities.all.binsearch(el.entity_id) - next if not pa = ent.positions.assignments.binsearch(el.assignment_id) - next if not pos = ent.positions.own.binsearch(pa.position_id) - list << pos - } - list -end + # list currently idle workers + def unit_idlers + unit_workers.find_all { |u| + # current_job includes eat/drink/sleep/pickupequip + !u.job.current_job._getv and + # filter 'attend meeting' + u.meetings.length == 0 and + # filter soldiers (TODO check schedule) + u.military.squad_index == -1 and + # filter 'on break' + !u.status.misc_traits.find { |t| id == :OnBreak } + } + end + + def unit_entitypositions(unit) + list = [] + return list if not hf = world.history.figures.binsearch(unit.hist_figure_id) + hf.entity_links.each { |el| + next if el._rtti_classname != :histfig_entity_link_positionst + next if not ent = world.entities.all.binsearch(el.entity_id) + next if not pa = ent.positions.assignments.binsearch(el.assignment_id) + next if not pos = ent.positions.own.binsearch(pa.position_id) + list << pos + } + list + end + end end From 552da8417e35cf5e22312b663aec5e266d4fafa9 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 24 Jun 2012 19:52:40 +0200 Subject: [PATCH 14/29] ruby: handle .rb files in df/hack/scripts/ --- library/Core.cpp | 50 +++++++++++++++++++++++-------- library/PluginManager.cpp | 8 ++++- library/include/PluginManager.h | 2 ++ plugins/ruby/README | 14 +++++++++ plugins/ruby/ruby-autogen-defs.rb | 1 + plugins/ruby/ruby.cpp | 16 +++++----- scripts/slayrace.rb | 21 +++++++++++++ 7 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 scripts/slayrace.rb diff --git a/library/Core.cpp b/library/Core.cpp index df11ca5b2..ad9fdc61a 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -204,7 +204,7 @@ struct sortable }; }; -static std::string getLuaHelp(std::string path) +static std::string getScriptHelp(std::string path, std::string helpprefix) { ifstream script(path.c_str()); @@ -212,14 +212,14 @@ static std::string getLuaHelp(std::string path) { std::string help; if (getline(script, help) && - help.substr(0,3) == "-- ") - return help.substr(3); + help.substr(0,helpprefix.length()) == helpprefix) + return help.substr(helpprefix.length()); } - return "Lua script."; + return "No help available."; } -static std::map listLuaScripts(std::string path) +static std::map listScripts(PluginManager *plug_mgr, std::string path) { std::vector files; getdir(path, files); @@ -229,10 +229,16 @@ static std::map listLuaScripts(std::string path) { if (hasEnding(files[i], ".lua")) { - std::string help = getLuaHelp(path + files[i]); + std::string help = getScriptHelp(path + files[i], "-- "); pset[files[i].substr(0, files[i].size()-4)] = help; } + else if (plug_mgr->eval_ruby && hasEnding(files[i], ".rb")) + { + std::string help = getScriptHelp(path + files[i], "# "); + + pset[files[i].substr(0, files[i].size()-3)] = help; + } } return pset; } @@ -275,6 +281,18 @@ static command_result runLuaScript(color_ostream &out, std::string name, vector< return ok ? CR_OK : CR_FAILURE; } +static command_result runRubyScript(PluginManager *plug_mgr, std::string name, vector &args) +{ + std::string rbcmd = "$script_args = ["; + for (size_t i = 0; i < args.size(); i++) + rbcmd += "'" + args[i] + "', "; + rbcmd += "]\n"; + + rbcmd += "load './hack/scripts/" + name + ".rb'"; + + return plug_mgr->eval_ruby(rbcmd.c_str()); +} + command_result Core::runCommand(color_ostream &out, const std::string &command) { if (!command.empty()) @@ -348,10 +366,16 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve return CR_OK; } } - auto filename = getHackPath() + "scripts/" + parts[0] + ".lua"; - if (fileExists(filename)) + auto filename = getHackPath() + "scripts/" + parts[0]; + if (fileExists(filename + ".lua")) + { + string help = getScriptHelp(filename + ".lua", "-- "); + con.print("%s: %s\n", parts[0].c_str(), help.c_str()); + return CR_OK; + } + if (plug_mgr->eval_ruby && fileExists(filename + ".rb")) { - string help = getLuaHelp(filename); + string help = getScriptHelp(filename + ".rb", "# "); con.print("%s: %s\n", parts[0].c_str(), help.c_str()); return CR_OK; } @@ -499,7 +523,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve con.print(" %-22s- %s\n",(*iter).name.c_str(), (*iter).description.c_str()); con.reset_color(); } - auto scripts = listLuaScripts(getHackPath() + "scripts/"); + auto scripts = listScripts(plug_mgr, getHackPath() + "scripts/"); if (!scripts.empty()) { con.print("\nscripts:\n"); @@ -604,9 +628,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve command_result res = plug_mgr->InvokeCommand(con, first, parts); if(res == CR_NOT_IMPLEMENTED) { - auto filename = getHackPath() + "scripts/" + first + ".lua"; - if (fileExists(filename)) + auto filename = getHackPath() + "scripts/" + first; + if (fileExists(filename + ".lua")) res = runLuaScript(con, first, parts); + else if (plug_mgr->eval_ruby && fileExists(filename + ".rb")) + res = runRubyScript(plug_mgr, first, parts); else con.printerr("%s is not a recognized command.\n", first.c_str()); } diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index ae8cc755f..a314883e1 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -188,6 +188,7 @@ bool Plugin::load(color_ostream &con) plugin_shutdown = (command_result (*)(color_ostream &)) LookupPlugin(plug, "plugin_shutdown"); plugin_onstatechange = (command_result (*)(color_ostream &, state_change_event)) LookupPlugin(plug, "plugin_onstatechange"); plugin_rpcconnect = (RPCService* (*)(color_ostream &)) LookupPlugin(plug, "plugin_rpcconnect"); + plugin_eval_ruby = (command_result (*)(const char*)) LookupPlugin(plug, "plugin_eval_ruby"); index_lua(plug); this->name = *plug_name; plugin_lib = plug; @@ -538,6 +539,7 @@ PluginManager::PluginManager(Core * core) const string searchstr = ".plug.dll"; #endif cmdlist_mutex = new mutex(); + eval_ruby = NULL; vector filez; getdir(path, filez); for(size_t i = 0; i < filez.size();i++) @@ -620,6 +622,8 @@ void PluginManager::registerCommands( Plugin * p ) { belongs[cmds[i].name] = p; } + if (p->plugin_eval_ruby) + eval_ruby = p->plugin_eval_ruby; cmdlist_mutex->unlock(); } @@ -632,5 +636,7 @@ void PluginManager::unregisterCommands( Plugin * p ) { belongs.erase(cmds[i].name); } + if (p->plugin_eval_ruby) + eval_ruby = NULL; cmdlist_mutex->unlock(); -} \ No newline at end of file +} diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index b76df437d..5da9fc92f 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -209,6 +209,7 @@ namespace DFHack command_result (*plugin_onupdate)(color_ostream &); command_result (*plugin_onstatechange)(color_ostream &, state_change_event); RPCService* (*plugin_rpcconnect)(color_ostream &); + command_result (*plugin_eval_ruby)(const char*); }; class DFHACK_EXPORT PluginManager { @@ -237,6 +238,7 @@ namespace DFHack { return all_plugins.size(); } + command_result (*eval_ruby)(const char*); // DATA private: tthread::mutex * cmdlist_mutex; diff --git a/plugins/ruby/README b/plugins/ruby/README index 4bfc20957..619d85cf2 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -33,6 +33,20 @@ df/hack/ on linux), the library should be named 'libruby.dll' (.so on linux). You can download a tested version at http://github.com/jjyg/dfhack/downloads/ +Ruby scripts +------------ + +The ruby plugin allows the creation of '.rb' scripts in df/hack/scripts/. + +If you create such a script, e.g. 'test.rb', that will add a new dfhack console +command 'test'. +The script can access the console command arguments through the global variable +'$script_args', which is an array of ruby Strings. + +The help string displayed in dfhack 'ls' command is the first line of the +script, if it is a comment (starts with '# '). + + DFHack callbacks ---------------- diff --git a/plugins/ruby/ruby-autogen-defs.rb b/plugins/ruby/ruby-autogen-defs.rb index 482cf24ff..64da12ff9 100644 --- a/plugins/ruby/ruby-autogen-defs.rb +++ b/plugins/ruby/ruby-autogen-defs.rb @@ -353,6 +353,7 @@ module DFHack end def empty? ; length == 0 ; end def flatten ; map { |e| e.respond_to?(:flatten) ? e.flatten : e }.flatten ; end + def index(elem=nil, &b) ; (0...length).find { |i| b ? b[self[i]] : self[i] == elem } ; end end class StaticArray < MemStruct attr_accessor :_tglen, :_length, :_indexenum, :_tg diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index e3bed78b7..f4a553380 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -96,8 +96,11 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) } // send a single ruby line to be evaluated by the ruby thread -static command_result plugin_eval_rb(const char *command) +DFhackCExport command_result plugin_eval_ruby(const char *command) { + if (!r_thread) + return CR_FAILURE; + command_result ret; // serialize 'accesses' to the ruby thread @@ -123,11 +126,6 @@ static command_result plugin_eval_rb(const char *command) return ret; } -static command_result plugin_eval_rb(std::string &command) -{ - return plugin_eval_rb(command.c_str()); -} - DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { if (!r_thread) @@ -136,7 +134,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (!onupdate_active) return CR_OK; - return plugin_eval_rb("DFHack.onupdate"); + return plugin_eval_ruby("DFHack.onupdate"); } DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event e) @@ -157,7 +155,7 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch #undef SCASE } - return plugin_eval_rb(cmd); + return plugin_eval_ruby(cmd.c_str()); } static command_result df_rubyeval(color_ostream &out, std::vector & parameters) @@ -178,7 +176,7 @@ static command_result df_rubyeval(color_ostream &out, std::vector full += " "; } - return plugin_eval_rb(full); + return plugin_eval_ruby(full.c_str()); } diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb new file mode 100644 index 000000000..b58835257 --- /dev/null +++ b/scripts/slayrace.rb @@ -0,0 +1,21 @@ +# slay all creatures of a given race (default = goblins) + +race = $script_args[0] || 'GOBLIN' + +all_races = df.world.raws.creatures.all.map { |cr| cr.creature_id } +raw_race = df.match_rawname(race, all_races) +raise 'invalid race' if not raw_race + +race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race } +count = 0 + +df.suspend { + df.world.units.active.each { |u| + if u.race == race_nr and u.body.blood_count != 0 + u.body.blood_count = 0 + count += 1 + end + } +} + +puts "slain #{count} #{raw_race}" From d1762e3cb8b16cc90760f8e56517292b7291c08f Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 24 Jun 2012 21:41:43 +0200 Subject: [PATCH 15/29] ruby: add growcrops script, add doc for methods in README --- plugins/ruby/README | 62 +++++++++++++++++++++++++++++++++++++++++++ plugins/ruby/plant.rb | 47 -------------------------------- plugins/ruby/unit.rb | 2 +- scripts/growcrops.rb | 53 ++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 48 deletions(-) create mode 100644 scripts/growcrops.rb diff --git a/plugins/ruby/README b/plugins/ruby/README index 619d85cf2..bda352e14 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -13,6 +13,10 @@ a map block, find an unit or an item, etc. Global dfhack objects are accessible through the 'df' accessor (eg 'df.world'). +DFHack structures are renamed in CamelCase in the ruby namespace. + +For a list of the structures and their methods, grep the ruby-autogen.rb file. + DFHack console -------------- @@ -47,6 +51,64 @@ The help string displayed in dfhack 'ls' command is the first line of the script, if it is a comment (starts with '# '). +Ruby helper functions +--------------------- + +This is an excerpt of the functions defined in dfhack/plugins/ruby/*.rb. Check +the files and the comments for a complete list. + + df.suspend { } +Ensures that the main game thread is paused while exec'ing the content of the +block. Similar to df.suspend() ; ; df.resume(). + + df.same_pos?(obj1, obj2) +Returns true if both objects are at the same game coordinates. +obj1 and 2 should respond to #pos and #x #y #z. + + df.map_block_at(pos) / map_block_at(x, y, z) +Returns the MapBlock for the coordinates or nil. + + df.each_map_block { |b| } + df.each_map_block_z(zlevel) { |b| } +Iterates over every map block (opt. on a single z-level). + + df.center_viewscreen(coords) +Centers the DF view on the given coordinates. Accepts x/y/z arguments, or a +single argument responding to pos/x/y/z, eg an Unit, Item, ... + + df.unit_find(arg) +Returns an Unit. +With no arg, returns the currently selected unit (through the (v) or (k) menus) +With a number, returns the unit with this ID +With something else, returns the first unit at the same game coordinates + + df.unit_workers +Returns a list of worker citizen: units of your race & civilization, adults, +not dead, crazy, ghosts or nobles exempted of work. + + df.unit_entitypositions(unit) +Returns the list of EntityPosition occupied by the unit. +Check the 'code' field for a readable name (MANAGER, CHIEF_MEDICAL_DWARF, ...) + + df.match_rawname(name, list) +String fuzzy matching. Returns the list entry most similar to 'name'. +First searches for an exact match, then for a case-insensitive match, and +finally for a case-insensitive substring. +Returns the element from list if there is only one match, or nil. +Most useful to allow the user to specify a raw-defined name, +eg 'gob' for 'GOBLIN' or 'coal' for 'COAL_BITUMINOUS', hence the name. + + df.building_alloc(type, subtype, customtype) + df.building_position(bld, pos, w, h) + df.building_construct(bld, item_list) +Allocates a new building in DF memory, define its position / dimensions, and +create a dwarf job to construct it from the given list of items. +See buildings.rb/buildbed for an exemple. + + df.each_tree(material) { |t| } +Iterates over every tree of the given material (eg 'maple'). + + DFHack callbacks ---------------- diff --git a/plugins/ruby/plant.rb b/plugins/ruby/plant.rb index 63195e6c2..db0f9a817 100644 --- a/plugins/ruby/plant.rb +++ b/plugins/ruby/plant.rb @@ -115,52 +115,5 @@ module DFHack puts "Grown #{cnt} saplings" end end - - def growcrops(material=nil, count_max=100) - @raws_plant_name ||= {} - @raws_plant_growdur ||= {} - if @raws_plant_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_plant_name[idx] = p.id - @raws_plant_growdur[idx] = p.growdur - } - end - - if !material - cnt = Hash.new(0) - suspend { - world.items.other[:SEEDS].each { |seed| - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - cnt[seed.mat_index] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = world.raws.plants.all[mat].id - puts " #{name} #{c}" - } - else - if material != :any - mat = match_rawname(material, @raws_plant_name.values) - unless wantmat = @raws_plant_name.index(mat) - raise "invalid plant material #{material}" - end - end - - cnt = 0 - suspend { - world.items.other[:SEEDS].each { |seed| - next if wantmat and seed.mat_index != wantmat - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - seed.grow_counter = @raws_plant_growdur[seed.mat_index] - cnt += 1 - } - } - puts "Grown #{cnt} crops" - end - end end end diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 41b7bf8a6..833cb4625 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -54,7 +54,7 @@ module DFHack def unit_idlers unit_workers.find_all { |u| # current_job includes eat/drink/sleep/pickupequip - !u.job.current_job._getv and + !u.job.current_job and # filter 'attend meeting' u.meetings.length == 0 and # filter soldiers (TODO check schedule) diff --git a/scripts/growcrops.rb b/scripts/growcrops.rb new file mode 100644 index 000000000..855600d7c --- /dev/null +++ b/scripts/growcrops.rb @@ -0,0 +1,53 @@ +# grow crops in farm plots. ex: growcrops helmet_plump 20 + +material = $script_args[0] +count_max = $script_args[1].to_i +count_max = 100 if count_max == 0 + +# cache information from the raws +@raws_plant_name ||= {} +@raws_plant_growdur ||= {} +if @raws_plant_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_plant_name[idx] = p.id + @raws_plant_growdur[idx] = p.growdur + } +end + +if !material or material == 'help' or material == 'list' + # show a list of available crop types + cnt = Hash.new(0) + df.suspend { + df.world.items.other[:SEEDS].each { |seed| + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + cnt[seed.mat_index] += 1 + } + } + + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = df.world.raws.plants.all[mat].id + puts " #{name} #{c}" + } + +else + + mat = df.match_rawname(material, @raws_plant_name.values) + unless wantmat = @raws_plant_name.index(mat) + raise "invalid plant material #{material}" + end + + cnt = 0 + df.suspend { + df.world.items.other[:SEEDS].each { |seed| + next if seed.mat_index != wantmat + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + seed.grow_counter = @raws_plant_growdur[seed.mat_index] + cnt += 1 + } + } + puts "Grown #{cnt} #{mat}" +end From 8fb139a2f471fb61fd69efddc016bdc48d6e55a5 Mon Sep 17 00:00:00 2001 From: jj Date: Mon, 25 Jun 2012 01:45:50 +0200 Subject: [PATCH 16/29] ruby: document mutex use, load ruby-autogen in the background --- plugins/ruby/ruby.cpp | 70 +++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index f4a553380..7e6c4b413 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -33,9 +33,9 @@ enum RB_command { }; tthread::mutex *m_irun; tthread::mutex *m_mutex; -static RB_command r_type; +static volatile RB_command r_type; +static volatile command_result r_result; static const char *r_command; -static command_result r_result; static tthread::thread *r_thread; static int onupdate_active; @@ -43,27 +43,39 @@ DFHACK_PLUGIN("ruby") DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - // fail silently instead of spamming the console with 'failed to initialize' if libruby is not present - // the error is still logged in stderr.log + onupdate_active = 0; + + // fail silently instead of spamming the console with 'failed to initialize' + // if libruby is not present, the error is still logged in stderr.log if (!df_loadruby()) return CR_OK; + // the ruby thread sleeps trying to lock this + // when it gets it, it runs according to r_type + // when finished, it sets r_type to IDLE and unlocks m_irun = new tthread::mutex(); + + // when any thread is going to request something to the ruby thread, + // lock this before anything, and release when everything is done m_mutex = new tthread::mutex(); + r_type = RB_INIT; + // create the dedicated ruby thread + // df_rubythread starts the ruby interpreter and goes to type=IDLE when done r_thread = new tthread::thread(df_rubythread, 0); + // wait until init phase 1 is done while (r_type != RB_IDLE) tthread::this_thread::yield(); + // ensure the ruby thread sleeps until we have a command to handle m_irun->lock(); + // check return value from rbinit if (r_result == CR_FAILURE) return CR_FAILURE; - onupdate_active = 0; - commands.push_back(PluginCommand("rb_eval", "Ruby interpreter. Eval() a ruby string.", df_rubyeval)); @@ -73,23 +85,30 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector lock(); r_type = RB_DIE; - r_command = 0; + r_command = NULL; + // start ruby thread m_irun->unlock(); + // wait until ruby thread ends after RB_DIE r_thread->join(); + // cleanup everything delete r_thread; r_thread = 0; delete m_irun; + // we can release m_mutex, other users will check r_thread m_mutex->unlock(); delete m_mutex; + // dlclose libruby df_unloadruby(); return CR_OK; @@ -98,29 +117,32 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) // send a single ruby line to be evaluated by the ruby thread DFhackCExport command_result plugin_eval_ruby(const char *command) { + // if dlopen failed if (!r_thread) return CR_FAILURE; command_result ret; - // serialize 'accesses' to the ruby thread + // ensure ruby thread is idle m_mutex->lock(); if (!r_thread) - // raced with plugin_shutdown ? + // raced with plugin_shutdown return CR_OK; r_type = RB_EVAL; r_command = command; + // wake ruby thread up m_irun->unlock(); - // could use a condition_variable or something... + // semi-active loop until ruby thread is done while (r_type != RB_IDLE) tthread::this_thread::yield(); - // XXX non-atomic with previous r_type change check ret = r_result; + // block ruby thread m_irun->lock(); + // let other plugin_eval_ruby run m_mutex->unlock(); return ret; @@ -131,6 +153,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (!r_thread) return CR_OK; + // ruby sets this flag when needed, to avoid lag running ruby code every + // frame if not necessary + // TODO dynamic check on df::cur_year{_tick} if (!onupdate_active) return CR_OK; @@ -168,6 +193,7 @@ static command_result df_rubyeval(color_ostream &out, std::vector return CR_OK; } + // reconstruct the text from dfhack console line std::string full = ""; for (unsigned i=0 ; ilock(); + + // tell the main thread our initialization is finished r_result = CR_OK; r_type = RB_IDLE; + // load the default ruby-level definitions in the background + state=0; + rb_eval_string_protect("require './hack/ruby/ruby'", &state); + if (state) + dump_rb_error(); + + // ready to go + m_mutex->unlock(); + running = 1; while (running) { - // wait for new command + // sleep waiting for new command m_irun->lock(); switch (r_type) { @@ -728,7 +767,6 @@ static VALUE rb_dfvcall(VALUE self, VALUE cppobj, VALUE cppvoff, VALUE a0, VALUE static void ruby_bind_dfhack(void) { rb_cDFHack = rb_define_module("DFHack"); - // global DFHack commands rb_define_singleton_method(rb_cDFHack, "onupdate_active", RUBY_METHOD_FUNC(rb_dfonupdateactive), 0); rb_define_singleton_method(rb_cDFHack, "onupdate_active=", RUBY_METHOD_FUNC(rb_dfonupdateactiveset), 1); rb_define_singleton_method(rb_cDFHack, "resume", RUBY_METHOD_FUNC(rb_dfresume), 0); @@ -780,10 +818,4 @@ static void ruby_bind_dfhack(void) { rb_define_singleton_method(rb_cDFHack, "memory_bitarray_resize", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_resize), 2); rb_define_singleton_method(rb_cDFHack, "memory_bitarray_isset", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_isset), 2); rb_define_singleton_method(rb_cDFHack, "memory_bitarray_set", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_set), 3); - - // load the default ruby-level definitions - int state=0; - rb_eval_string_protect("require './hack/ruby/ruby'", &state); - if (state) - dump_rb_error(); } From 17d06b969b7a903cab7643aeace3d164c0c5afb9 Mon Sep 17 00:00:00 2001 From: jj Date: Mon, 25 Jun 2012 17:06:37 +0200 Subject: [PATCH 17/29] ruby: move all ruby invocations inside a CoreSuspend lock, remove ruby suspend method --- plugins/ruby/README | 6 ++---- plugins/ruby/building.rb | 22 ++++++++++------------ plugins/ruby/plant.rb | 16 ++++------------ plugins/ruby/ruby.cpp | 19 +++++-------------- plugins/ruby/ruby.rb | 14 -------------- scripts/growcrops.rb | 28 ++++++++++++---------------- scripts/slayrace.rb | 12 +++++------- 7 files changed, 38 insertions(+), 79 deletions(-) diff --git a/plugins/ruby/README b/plugins/ruby/README index bda352e14..e4f22c71d 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -17,6 +17,8 @@ DFHack structures are renamed in CamelCase in the ruby namespace. For a list of the structures and their methods, grep the ruby-autogen.rb file. +All ruby code runs while the main DF process and other plugins are suspended. + DFHack console -------------- @@ -57,10 +59,6 @@ Ruby helper functions This is an excerpt of the functions defined in dfhack/plugins/ruby/*.rb. Check the files and the comments for a complete list. - df.suspend { } -Ensures that the main game thread is paused while exec'ing the content of the -block. Similar to df.suspend() ; ; df.resume(). - df.same_pos?(obj1, obj2) Returns true if both objects are at the same game coordinates. obj1 and 2 should respond to #pos and #x #y #z. diff --git a/plugins/ruby/building.rb b/plugins/ruby/building.rb index 29e10ae29..826cd26b9 100644 --- a/plugins/ruby/building.rb +++ b/plugins/ruby/building.rb @@ -248,20 +248,18 @@ module DFHack # exemple usage def buildbed(pos=cursor) - suspend { - raise 'where to ?' if pos.x < 0 + raise 'where to ?' if pos.x < 0 - item = world.items.all.find { |i| - i.kind_of?(ItemBedst) and - i.itemrefs.empty? and - !i.flags.in_job - } - raise 'no free bed, build more !' if not item - - bld = building_alloc(:Bed) - building_position(bld, pos) - building_construct(bld, [item]) + item = world.items.all.find { |i| + i.kind_of?(ItemBedst) and + i.itemrefs.empty? and + !i.flags.in_job } + raise 'no free bed, build more !' if not item + + bld = building_alloc(:Bed) + building_position(bld, pos) + building_construct(bld, [item]) end end end diff --git a/plugins/ruby/plant.rb b/plugins/ruby/plant.rb index db0f9a817..5d6b9d724 100644 --- a/plugins/ruby/plant.rb +++ b/plugins/ruby/plant.rb @@ -55,21 +55,18 @@ module DFHack if !material # list trees cnt = Hash.new(0) - suspend { - each_tree { |plant| + each_tree { |plant| next if plant.grow_counter < SaplingToTreeAge next if map_designation_at(plant).hidden cnt[plant.material] += 1 } - } cnt.sort_by { |mat, c| c }.each { |mat, c| name = @raws_tree_name[mat] puts " #{name} #{c}" } else cnt = 0 - suspend { - each_tree(material) { |plant| + each_tree(material) { |plant| next if plant.grow_counter < SaplingToTreeAge b = map_block_at(plant) d = b.designation[plant.pos.x%16][plant.pos.y%16] @@ -81,7 +78,6 @@ module DFHack break if cnt == count_max end } - } puts "Updated #{cnt} plant designations" end end @@ -90,28 +86,24 @@ module DFHack if !material # list plants cnt = Hash.new(0) - suspend { - each_tree { |plant| + each_tree { |plant| next if plant.grow_counter >= SaplingToTreeAge next if map_designation_at(plant).hidden cnt[plant.material] += 1 } - } cnt.sort_by { |mat, c| c }.each { |mat, c| name = @raws_tree_name[mat] puts " #{name} #{c}" } else cnt = 0 - suspend { - each_tree(material) { |plant| + each_tree(material) { |plant| next if plant.grow_counter >= SaplingToTreeAge next if map_designation_at(plant).hidden plant.grow_counter = SaplingToTreeAge cnt += 1 break if cnt == count_max } - } puts "Grown #{cnt} saplings" end end diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index 7e6c4b413..d4271c54a 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -121,6 +121,11 @@ DFhackCExport command_result plugin_eval_ruby(const char *command) if (!r_thread) return CR_FAILURE; + // wrap all ruby code inside a suspend block + // if we dont do that and rely on ruby code doing it, we'll deadlock in + // onupdate + CoreSuspender suspend; + command_result ret; // ensure ruby thread is idle @@ -417,18 +422,6 @@ static VALUE rb_dfonupdateactiveset(VALUE self, VALUE val) return Qtrue; } -static VALUE rb_dfresume(VALUE self) -{ - Core::getInstance().Resume(); - return Qtrue; -} - -static VALUE rb_dfsuspend(VALUE self) -{ - Core::getInstance().Suspend(); - return Qtrue; -} - static VALUE rb_dfprint_str(VALUE self, VALUE s) { console_proxy->print("%s", rb_string_value_ptr(&s)); @@ -769,8 +762,6 @@ static void ruby_bind_dfhack(void) { rb_define_singleton_method(rb_cDFHack, "onupdate_active", RUBY_METHOD_FUNC(rb_dfonupdateactive), 0); rb_define_singleton_method(rb_cDFHack, "onupdate_active=", RUBY_METHOD_FUNC(rb_dfonupdateactiveset), 1); - rb_define_singleton_method(rb_cDFHack, "resume", RUBY_METHOD_FUNC(rb_dfresume), 0); - rb_define_singleton_method(rb_cDFHack, "do_suspend", RUBY_METHOD_FUNC(rb_dfsuspend), 0); rb_define_singleton_method(rb_cDFHack, "get_global_address", RUBY_METHOD_FUNC(rb_dfget_global_address), 1); rb_define_singleton_method(rb_cDFHack, "get_vtable", RUBY_METHOD_FUNC(rb_dfget_vtable), 1); rb_define_singleton_method(rb_cDFHack, "get_rtti_classname", RUBY_METHOD_FUNC(rb_dfget_rtti_classname), 1); diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb index 1922e5bb5..64592e3eb 100644 --- a/plugins/ruby/ruby.rb +++ b/plugins/ruby/ruby.rb @@ -24,20 +24,6 @@ end module DFHack class << self - # update the ruby.cpp version to accept a block - def suspend - if block_given? - begin - do_suspend - yield - ensure - resume - end - else - do_suspend - end - end - # register a callback to be called every gframe or more # ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 } def onupdate_register(&b) diff --git a/scripts/growcrops.rb b/scripts/growcrops.rb index 855600d7c..5994c3d94 100644 --- a/scripts/growcrops.rb +++ b/scripts/growcrops.rb @@ -17,13 +17,11 @@ end if !material or material == 'help' or material == 'list' # show a list of available crop types cnt = Hash.new(0) - df.suspend { - df.world.items.other[:SEEDS].each { |seed| - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - cnt[seed.mat_index] += 1 - } + df.world.items.other[:SEEDS].each { |seed| + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + cnt[seed.mat_index] += 1 } cnt.sort_by { |mat, c| c }.each { |mat, c| @@ -39,15 +37,13 @@ else end cnt = 0 - df.suspend { - df.world.items.other[:SEEDS].each { |seed| - next if seed.mat_index != wantmat - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - seed.grow_counter = @raws_plant_growdur[seed.mat_index] - cnt += 1 - } + df.world.items.other[:SEEDS].each { |seed| + next if seed.mat_index != wantmat + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + seed.grow_counter = @raws_plant_growdur[seed.mat_index] + cnt += 1 } puts "Grown #{cnt} #{mat}" end diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb index b58835257..eba57f0ef 100644 --- a/scripts/slayrace.rb +++ b/scripts/slayrace.rb @@ -9,13 +9,11 @@ raise 'invalid race' if not raw_race race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race } count = 0 -df.suspend { - df.world.units.active.each { |u| - if u.race == race_nr and u.body.blood_count != 0 - u.body.blood_count = 0 - count += 1 - end - } +df.world.units.active.each { |u| + if u.race == race_nr and u.body.blood_count != 0 + u.body.blood_count = 0 + count += 1 + end } puts "slain #{count} #{raw_race}" From 412e8608d8ac2c63c12590f9585b6566cf307642 Mon Sep 17 00:00:00 2001 From: jj Date: Mon, 25 Jun 2012 19:05:50 +0200 Subject: [PATCH 18/29] ruby: add method for basic xml ref-target support, tweak scripts/slayrace --- plugins/ruby/codegen.pl | 27 +++++++++++++++++++++++++++ scripts/slayrace.rb | 35 +++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index 45fcd8fa4..e5847a841 100755 --- a/plugins/ruby/codegen.pl +++ b/plugins/ruby/codegen.pl @@ -260,6 +260,9 @@ sub render_struct_fields { render_item($field); }; push @lines_rb, "}"; + + my $reftg = $field->getAttribute('ref-target'); + render_field_reftarget($type, $field, $name, $reftg) if ($reftg); } } @@ -267,6 +270,30 @@ sub render_struct_fields { } } +sub render_field_reftarget { + my ($parent, $field, $name, $reftg) = @_; + + my $aux = $field->getAttribute('aux-value'); + return if ($aux); # TODO + + my $tg = $global_types{$reftg}; + return if (!$tg); + my $tgvec = $tg->getAttribute('instance-vector'); + return if (!$tgvec); + $tgvec =~ s/\$global/df/; + return if $tgvec !~ /^[\w\.]+$/; + + my $tgname = "${name}_tg"; + $tgname =~ s/_id_tg//; + + for my $otherfield ($parent->findnodes('child::ld:field')) { + my $othername = $otherfield->getAttribute('name'); + $tgname .= '_' if ($othername and $tgname eq $othername); + } + + push @lines_rb, "def $tgname ; ${tgvec}[$name] ; end"; +} + sub render_class_vmethods { my ($vms) = @_; my $voff = 0; diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb index eba57f0ef..bf8ccb939 100644 --- a/scripts/slayrace.rb +++ b/scripts/slayrace.rb @@ -1,19 +1,26 @@ -# slay all creatures of a given race (default = goblins) +# slay all creatures of a given race -race = $script_args[0] || 'GOBLIN' +race = $script_args[0] -all_races = df.world.raws.creatures.all.map { |cr| cr.creature_id } -raw_race = df.match_rawname(race, all_races) -raise 'invalid race' if not raw_race +all_races = df.world.units.active.map { |u| + u.race_tg.creature_id if not u.flags1.dead and not df.map_designation_at(u).hidden +}.compact.uniq.sort -race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race } -count = 0 +if !race + puts all_races +else + raw_race = df.match_rawname(race, all_races) + raise 'invalid race' if not raw_race -df.world.units.active.each { |u| - if u.race == race_nr and u.body.blood_count != 0 - u.body.blood_count = 0 - count += 1 - end -} + race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race } + count = 0 -puts "slain #{count} #{raw_race}" + df.world.units.active.each { |u| + if u.race == race_nr and u.body.blood_count != 0 and not u.flags1.dead + u.body.blood_count = 0 + count += 1 + end + } + + puts "slain #{count} #{raw_race}" +end From c2c29af95981a9b60cba9fa92c47be3870409a65 Mon Sep 17 00:00:00 2001 From: jj Date: Mon, 25 Jun 2012 19:16:35 +0200 Subject: [PATCH 19/29] ruby: fix deadlock on df exit due to BEGIN_UNLOAD + Core.Suspend() --- plugins/ruby/README | 3 ++- plugins/ruby/ruby.cpp | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/ruby/README b/plugins/ruby/README index e4f22c71d..690e83ca0 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -116,7 +116,8 @@ To register ruby code to be run every graphic frame, use: To stop being called, use: df.onupdate_unregister handle -The same mechanism is available for 'onstatechange'. +The same mechanism is available for 'onstatechange', but the +SC_BEGIN_UNLOAD event is not propagated to the ruby handler. C++ object manipulation diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index d4271c54a..8ec6ac464 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -181,7 +181,9 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch SCASE(MAP_UNLOADED); SCASE(VIEWSCREEN_CHANGED); SCASE(CORE_INITIALIZED); - SCASE(BEGIN_UNLOAD); + // if we go through plugin_eval at BEGIN_UNLOAD, it'll + // try to get the suspend lock and deadlock at df exit + case SC_BEGIN_UNLOAD : return CR_OK; #undef SCASE } From 24aa3827c11dca4a1cfa7cdd8c18b7c527b81430 Mon Sep 17 00:00:00 2001 From: jj Date: Wed, 27 Jun 2012 14:52:34 +0200 Subject: [PATCH 20/29] ruby: generate accessors for refers-to and vector of ref-target --- plugins/ruby/codegen.pl | 63 ++++++++++++++++++++++++++++++++++++----- plugins/ruby/ruby.cpp | 6 ++-- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index e5847a841..265db47c5 100755 --- a/plugins/ruby/codegen.pl +++ b/plugins/ruby/codegen.pl @@ -261,8 +261,7 @@ sub render_struct_fields { }; push @lines_rb, "}"; - my $reftg = $field->getAttribute('ref-target'); - render_field_reftarget($type, $field, $name, $reftg) if ($reftg); + render_struct_field_refs($type, $field, $name); } } @@ -270,6 +269,24 @@ sub render_struct_fields { } } +# handle generating accessor for xml attributes ref-target, refers-to etc +sub render_struct_field_refs { + my ($parent, $field, $name) = @_; + + my $reftg = $field->getAttribute('ref-target'); + render_field_reftarget($parent, $field, $name, $reftg) if ($reftg); + + my $refto = $field->getAttribute('refers-to'); + render_field_refto($parent, $name, $refto) if ($refto); + + my $meta = $field->getAttribute('ld:meta'); + my $item = $field->findnodes('child::ld:item')->[0]; + if ($meta and $meta eq 'container' and $item) { + my $itemreftg = $item->getAttribute('ref-target'); + render_container_reftarget($parent, $item, $name, $itemreftg) if $itemreftg; + } +} + sub render_field_reftarget { my ($parent, $field, $name, $reftg) = @_; @@ -280,20 +297,52 @@ sub render_field_reftarget { return if (!$tg); my $tgvec = $tg->getAttribute('instance-vector'); return if (!$tgvec); - $tgvec =~ s/\$global/df/; + + render_field_refto($parent, $name, $tgvec); +} + +sub render_field_refto { + my ($parent, $name, $tgvec) = @_; + + $tgvec =~ s/^\$global/df/; + $tgvec =~ s/\[\$\]$//; return if $tgvec !~ /^[\w\.]+$/; my $tgname = "${name}_tg"; - $tgname =~ s/_id_tg//; + $tgname =~ s/_id(.?.?)_tg/_tg$1/; - for my $otherfield ($parent->findnodes('child::ld:field')) { - my $othername = $otherfield->getAttribute('name'); + for my $othername (map { $_->getAttribute('name') } $parent->findnodes('child::ld:field')) { $tgname .= '_' if ($othername and $tgname eq $othername); } push @lines_rb, "def $tgname ; ${tgvec}[$name] ; end"; } +sub render_container_reftarget { + my ($parent, $item, $name, $reftg) = @_; + + my $aux = $item->getAttribute('aux-value'); + return if ($aux); # TODO + + my $tg = $global_types{$reftg}; + return if (!$tg); + my $tgvec = $tg->getAttribute('instance-vector'); + return if (!$tgvec); + + $tgvec =~ s/^\$global/df/; + $tgvec =~ s/\[\$\]$//; + return if $tgvec !~ /^[\w\.]+$/; + + my $tgname = "${name}_tg"; + $tgname =~ s/_id(.?.?)_tg/_tg$1/; + + for my $othername (map { $_->getAttribute('name') } $parent->findnodes('child::ld:field')) { + $tgname .= '_' if ($othername and $tgname eq $othername); + } + + push @lines_rb, "def $tgname ; $name.map { |i| ${tgvec}[i] } ; end"; +} + sub render_class_vmethods { my ($vms) = @_; my $voff = 0; @@ -742,7 +791,7 @@ sub render_item_number { push @lines_rb, 'number 8, false'; } elsif ($subtype eq 'bool') { push @lines_rb, 'number 8, true'; - $initvalue ||= 'nil'; + $initvalue ||= 'nil'; $typename ||= 'BooleanEnum'; } elsif ($subtype eq 's-float') { push @lines_rb, 'float'; diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index 8ec6ac464..87d0d0917 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -181,9 +181,9 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch SCASE(MAP_UNLOADED); SCASE(VIEWSCREEN_CHANGED); SCASE(CORE_INITIALIZED); - // if we go through plugin_eval at BEGIN_UNLOAD, it'll - // try to get the suspend lock and deadlock at df exit - case SC_BEGIN_UNLOAD : return CR_OK; + // if we go through plugin_eval at BEGIN_UNLOAD, it'll + // try to get the suspend lock and deadlock at df exit + case SC_BEGIN_UNLOAD : return CR_OK; #undef SCASE } From ccbebdafb0f345275ccddfb96c93d12cdd0b953d Mon Sep 17 00:00:00 2001 From: jj Date: Thu, 28 Jun 2012 15:02:48 +0200 Subject: [PATCH 21/29] fix error spam when using Zoom hotkeys --- library/Core.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index ad9fdc61a..09344135c 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1224,7 +1224,8 @@ bool Core::ncurses_wgetch(int in, int & out) { df::viewscreen * ws = Gui::GetCurrentScreen(); if (strict_virtual_cast(ws) && - df::global::ui->main.mode != ui_sidebar_mode::Hotkeys) + df::global::ui->main.mode != ui_sidebar_mode::Hotkeys && + df::global::ui->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) { setHotkeyCmd(df::global::ui->main.hotkeys[idx].name); return false; @@ -1372,7 +1373,8 @@ bool Core::SelectHotkey(int sym, int modifiers) idx += 8; if (strict_virtual_cast(screen) && - df::global::ui->main.mode != ui_sidebar_mode::Hotkeys) + df::global::ui->main.mode != ui_sidebar_mode::Hotkeys && + df::global::ui->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) { cmd = df::global::ui->main.hotkeys[idx].name; } From 56ff129ee00296693e338368cf98e4c74189bc5a Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 11:25:41 +0200 Subject: [PATCH 22/29] ruby: fix enum + base-type --- plugins/ruby/codegen.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index 265db47c5..3b11565e8 100755 --- a/plugins/ruby/codegen.pl +++ b/plugins/ruby/codegen.pl @@ -772,7 +772,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 ||= $g->getAttribute('base-type') if ($g); $subtype = 'int32_t' if (!$subtype); if ($subtype eq 'int64_t') { From 592c0a41ac0b0732ee7e5633e110fbcd03f2ad93 Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 11:29:36 +0200 Subject: [PATCH 23/29] ruby: remove useless raise in unit_find --- plugins/ruby/unit.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 833cb4625..e7d4335f0 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -14,8 +14,6 @@ module DFHack when :LookAround k = ui_look_list.items[ui_look_cursor] k.unit if k.type == :Unit - else - raise "bad UI mode #{ui.main.mode.inspect}" end elsif what.kind_of?(Integer) world.units.all.binsearch(what) From a72a59b2f9ef898ed2613bf603b0109c35bd295d Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 11:38:58 +0200 Subject: [PATCH 24/29] add scripts/removebadthoughts --- scripts/removebadthoughts.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/removebadthoughts.rb diff --git a/scripts/removebadthoughts.rb b/scripts/removebadthoughts.rb new file mode 100644 index 000000000..99b742643 --- /dev/null +++ b/scripts/removebadthoughts.rb @@ -0,0 +1,27 @@ +# remove bad thoughts for the selected unit or the whole fort + +# with removebadthoughts -v, dump the bad thoughts types we removed +verbose = $script_args.delete('-v') + +if u = df.unit_find(:selected) + targets = [u] +else + targets = df.unit_citizens +end + +seenbad = Hash.new(0) + +targets.each { |u| + u.status.recent_events.each { |e| + next if DFHack::UnitThoughtType::Value[e.type].to_s[0, 1] != '-' + seenbad[e.type] += 1 + e.age = 0x1000_0000 + } +} + +if verbose + seenbad.sort_by { |k, v| v }.each { |k, v| puts " #{v} #{k}" } +end + +count = seenbad.values.inject(0) { |s, v| s+v } +puts "removed #{count} bad thought#{'s' if count != 1}" From 7971925f7accdd2c21256ebeb6f69968ed14177d Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 11:51:54 +0200 Subject: [PATCH 25/29] slayrace: ignore caged units --- scripts/slayrace.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb index bf8ccb939..27b1ba3cc 100644 --- a/scripts/slayrace.rb +++ b/scripts/slayrace.rb @@ -2,8 +2,15 @@ race = $script_args[0] +checkunit = lambda { |u| + u.body.blood_count != 0 and + not u.flags1.dead and + not u.flags1.caged and + not df.map_designation_at(u).hidden +} + all_races = df.world.units.active.map { |u| - u.race_tg.creature_id if not u.flags1.dead and not df.map_designation_at(u).hidden + u.race_tg.creature_id if checkunit[u] }.compact.uniq.sort if !race @@ -13,10 +20,10 @@ else raise 'invalid race' if not raw_race race_nr = df.world.raws.creatures.all.index { |cr| cr.creature_id == raw_race } - count = 0 + count = 0 df.world.units.active.each { |u| - if u.race == race_nr and u.body.blood_count != 0 and not u.flags1.dead + if u.race == race_nr and checkunit[u] u.body.blood_count = 0 count += 1 end From b421c08d31a8aa935969ec73466449b8d2d65090 Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 14:09:11 +0200 Subject: [PATCH 26/29] growcrops: allow any unambiguous substring among existing seed types --- scripts/growcrops.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/growcrops.rb b/scripts/growcrops.rb index 5994c3d94..e3abe54ac 100644 --- a/scripts/growcrops.rb +++ b/scripts/growcrops.rb @@ -14,36 +14,36 @@ if @raws_plant_name.empty? } end +inventory = Hash.new(0) +df.world.items.other[:SEEDS].each { |seed| + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + inventory[seed.mat_index] += 1 +} + if !material or material == 'help' or material == 'list' # show a list of available crop types - cnt = Hash.new(0) - df.world.items.other[:SEEDS].each { |seed| - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - cnt[seed.mat_index] += 1 - } - - cnt.sort_by { |mat, c| c }.each { |mat, c| + inventory.sort_by { |mat, c| c }.each { |mat, c| name = df.world.raws.plants.all[mat].id puts " #{name} #{c}" } else - mat = df.match_rawname(material, @raws_plant_name.values) + mat = df.match_rawname(material, inventory.keys.map { |k| @raws_plant_name[k] }) unless wantmat = @raws_plant_name.index(mat) raise "invalid plant material #{material}" end - cnt = 0 + count = 0 df.world.items.other[:SEEDS].each { |seed| next if seed.mat_index != wantmat next if not seed.flags.in_building next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] seed.grow_counter = @raws_plant_growdur[seed.mat_index] - cnt += 1 + count += 1 } - puts "Grown #{cnt} #{mat}" + puts "Grown #{count} #{mat}" end From 125de9509300c4442b31e09d4e6e69149da01632 Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 14:10:07 +0200 Subject: [PATCH 27/29] document ruby scripts usage in the README --- README.rst | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.rst b/README.rst index d57cf2a72..9543e63a0 100644 --- a/README.rst +++ b/README.rst @@ -1361,3 +1361,60 @@ also tries to have dwarves specialize in specific skills. while it is enabled. For detailed usage information, see 'help autolabor'. + + +growcrops +========= +Instantly grow seeds inside farming plots. + +With no argument, this command list the various seed types currently in +use in your farming plots. +With a seed type, the script will grow 100 of these seeds, ready to be +harvested. You can change the number with a 2nd argument. + +For exemple, to grow 40 plump helmet spawn: +:: + + growcrops plump 40 + +This is a ruby script and needs the ruby plugin. + + +removebadthoughts +================= +This script remove negative thoughts from your dwarves. Very useful against +tantrum spirals. + +With a selected unit in 'v' mode, will clear this unit's mind, otherwise +clear all your fort's units minds. + +Individual dwarf happiness may not increase right after this command is run, +but in the short term your dwarves will get much more joyful. +The thoughts are set to be very old, and the game will remove them soon when +you unpause. + +With the optional ``-v`` parameter, the script will dump the negative thoughts +it removed. + +This is a ruby script and needs the ruby plugin. + + +slayrace +======== +Kills any unit of a given race. + +With no argument, lists the available races. + +Any non-dead non-caged unit of the specified race gets its ``blood_count`` +set to 0, which means immediate death at the next game tick. May not work +on vampires and other weird creatures. + +Targets any unit on a revealed tile of the map, including ambushers. Ex: +:: + slayrace gob + +To kill a single creature in the same way, you can use the following line, +after selecting the unit with the 'v' cursor: +:: + rb_eval df.unit_find.body.blood_count = 0 + From 31a5495031bf54d971e02abb14ad7a8420f21b9a Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 29 Jun 2012 17:51:26 +0200 Subject: [PATCH 28/29] ruby: osx support (maybe) --- plugins/ruby/CMakeLists.txt | 5 ++--- plugins/ruby/ruby.cpp | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/ruby/CMakeLists.txt b/plugins/ruby/CMakeLists.txt index f33e41740..a9a85636c 100644 --- a/plugins/ruby/CMakeLists.txt +++ b/plugins/ruby/CMakeLists.txt @@ -1,5 +1,5 @@ OPTION(DL_RUBY "download libruby from the internet" ON) -IF (DL_RUBY) +IF (DL_RUBY AND NOT APPLE) IF (UNIX) FILE(DOWNLOAD http://cloud.github.com/downloads/jjyg/dfhack/libruby187.tar.gz ${CMAKE_CURRENT_SOURCE_DIR}/libruby187.tar.gz EXPECTED_MD5 eb2adea59911f68e6066966c1352f291) @@ -15,7 +15,7 @@ IF (DL_RUBY) FILE(RENAME msvcrt-ruby18.dll libruby.dll) SET(RUBYLIB libruby.dll) ENDIF(UNIX) -ENDIF(DL_RUBY) +ENDIF(DL_RUBY AND NOT APPLE) ADD_CUSTOM_COMMAND( OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/ruby-autogen.rb @@ -35,4 +35,3 @@ INSTALL(FILES ${RUBYLIB} DESTINATION ${DFHACK_LIBRARY_DESTINATION}) INSTALL(DIRECTORY . DESTINATION hack/ruby FILES_MATCHING PATTERN "*.rb") - diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index 87d0d0917..0f5264515 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -262,8 +262,10 @@ DFHack::DFLibrary *libruby_handle; static int df_loadruby(void) { const char *libpath = -#ifdef WIN32 +#if defined(WIN32) "./libruby.dll"; +#elif defined(__APPLE__) + "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/libruby.1.dylib"; #else "hack/libruby.so"; #endif From 554683929f850adfd60ed43fd0b8290a176b90c4 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 1 Jul 2012 14:20:55 +0200 Subject: [PATCH 29/29] ruby: allow explicit target-os override in codegen --- plugins/ruby/codegen.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index 3b11565e8..c7fb210c9 100755 --- a/plugins/ruby/codegen.pl +++ b/plugins/ruby/codegen.pl @@ -13,6 +13,7 @@ if ($^O =~ /linux/i) { } else { $os = 'windows'; } +$os = $ARGV[2] if ($ARGV[2]); sub indent_rb(&) { my ($sub) = @_;