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
    -

    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. @@ -1411,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:

      @@ -1457,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/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 + 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

    diff --git a/library/Core.cpp b/library/Core.cpp index f30e19c27..09344135c 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,31 +281,34 @@ 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) { - //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 @@ -357,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; } @@ -508,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"); @@ -613,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()); } @@ -680,7 +697,7 @@ void fIOthread(void * iodata) { string command = ""; int ret = con.lineedit("[DFHack]# ",command, main_history); - //fprintf(stderr,"Command: [%s]\n",command.c_str()); + fprintf(stderr,"Command: [%s]\n",command.c_str()); if(ret == -2) { cerr << "Console is shutting down properly." << endl; @@ -694,13 +711,13 @@ void fIOthread(void * iodata) else if(ret) { // a proper, non-empty command was entered - //fprintf(stderr,"Adding command to history\n"); + fprintf(stderr,"Adding command to history\n"); main_history.add(command); - //fprintf(stderr,"Saving history\n"); + fprintf(stderr,"Saving history\n"); main_history.save("dfhack.history"); } - //fprintf(stderr,"Running command\n"); + fprintf(stderr,"Running command\n"); auto rv = core->runCommand(con, command); @@ -1207,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; @@ -1355,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; } 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..28571a0f7 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; } @@ -423,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"); } @@ -480,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)) { @@ -493,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); @@ -652,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) { @@ -1159,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 }, @@ -1359,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/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/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/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/library/lua/dfhack.lua b/library/lua/dfhack.lua index 4cdb4c950..d56d4df60 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 @@ -64,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) diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua index 4bd01c8f7..970f821c2 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 @@ -168,7 +169,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) @@ -234,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 @@ -251,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 @@ -453,4 +464,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/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/library/xml b/library/xml index 18c1d3e3d..ad38c5e96 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 18c1d3e3d0185d0bf80161d1e8410f08dd46d1e1 +Subproject commit ad38c5e96b05fedf16114fd16bd463e933f13582 diff --git a/plugins/ruby/CMakeLists.txt b/plugins/ruby/CMakeLists.txt index e69632e61..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 @@ -30,4 +30,8 @@ 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/README b/plugins/ruby/README index 9dc7d49f6..690e83ca0 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -2,61 +2,185 @@ 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 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 +-------------- + +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. + +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 '# '). + + +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.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 +---------------- + +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', but the +SC_BEGIN_UNLOAD event is not propagated to the ruby handler. + + +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 +191,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 +212,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 new file mode 100644 index 000000000..826cd26b9 --- /dev/null +++ b/plugins/ruby/building.rb @@ -0,0 +1,265 @@ +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 + + # 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 + + # 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 + + # 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 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) + + 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 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 + + 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 + + # 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 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 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 buildbed(pos=cursor) + 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]) + end + end +end diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index 5cdeeedd9..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) = @_; @@ -260,6 +261,8 @@ sub render_struct_fields { render_item($field); }; push @lines_rb, "}"; + + render_struct_field_refs($type, $field, $name); } } @@ -267,6 +270,80 @@ 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) = @_; + + 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); + + 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/_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 ; ${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; @@ -687,6 +764,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 +773,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') { @@ -713,7 +792,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/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 new file mode 100644 index 000000000..5d6b9d724 --- /dev/null +++ b/plugins/ruby/plant.rb @@ -0,0 +1,111 @@ +module DFHack + 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 + + 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 + + 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 + + world.plants.all.each { |plant| + next if not @raws_tree_name[plant.material] + next if wantmat and plant.material != wantmat + yield plant + } + 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 + + 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 + + SaplingToTreeAge = 120960 + def cuttrees(material=nil, count_max=100) + if !material + # list trees + cnt = Hash.new(0) + 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 + 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 growtrees(material=nil, count_max=100) + if !material + # list plants + cnt = Hash.new(0) + 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 + 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 + end +end diff --git a/plugins/ruby/plugins/building.rb b/plugins/ruby/plugins/building.rb deleted file mode 100644 index 5dfbcdacd..000000000 --- a/plugins/ruby/plugins/building.rb +++ /dev/null @@ -1,266 +0,0 @@ -module DFHack - -# 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 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 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 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 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 - -# 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 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 - -# 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 - -# 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 - -# 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 - -# exemple usage -def self.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 - - bld = building_alloc(:Bed) - building_position(bld, pos) - building_construct(bld, [item]) - } -end -end diff --git a/plugins/ruby/plugins/plant.rb b/plugins/ruby/plugins/plant.rb deleted file mode 100644 index 64f17b493..000000000 --- a/plugins/ruby/plugins/plant.rb +++ /dev/null @@ -1,152 +0,0 @@ -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 - - 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 - - world.plants.all.each { |plant| - next if not @raws_tree_name[plant.material] - next if wantmat and plant.material != wantmat - yield plant - } -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 - - 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 - -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 - -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 - -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 - - 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 diff --git a/plugins/ruby/plugins/unit.rb b/plugins/ruby/plugins/unit.rb deleted file mode 100644 index 9a00b2bfa..000000000 --- a/plugins/ruby/plugins/unit.rb +++ /dev/null @@ -1,52 +0,0 @@ -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 - -# 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 - -# 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 - -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 -end diff --git a/plugins/ruby/ruby-autogen-defs.rb b/plugins/ruby/ruby-autogen-defs.rb new file mode 100644 index 000000000..64da12ff9 --- /dev/null +++ b/plugins/ruby/ruby-autogen-defs.rb @@ -0,0 +1,752 @@ +# 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 + 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 + 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.cpp b/plugins/ruby/ruby.cpp index 49119c9aa..0f5264515 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,13 +30,12 @@ enum RB_command { RB_INIT, RB_DIE, RB_EVAL, - RB_CUSTOM, }; 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; @@ -45,31 +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_load", - "Ruby interpreter. Loads the given ruby script.", - df_rubyload)); - commands.push_back(PluginCommand("rb_eval", "Ruby interpreter. Eval() a ruby string.", df_rubyeval)); @@ -79,70 +85,86 @@ 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; } // 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 dlopen failed + 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; - // 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; } -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) 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; - return plugin_eval_rb("DFHack.onupdate"); + return plugin_eval_ruby("DFHack.onupdate"); } DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event e) @@ -159,26 +181,13 @@ 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 } - return plugin_eval_rb(cmd); -} - -static command_result df_rubyload(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); + return plugin_eval_ruby(cmd.c_str()); } static command_result df_rubyeval(color_ostream &out, std::vector & parameters) @@ -191,6 +200,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 ; i full += " "; } - return plugin_eval_rb(full); + return plugin_eval_ruby(full.c_str()); } // 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 +235,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); @@ -257,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 @@ -269,11 +276,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 +284,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); @@ -358,12 +356,25 @@ static void df_rubythread(void *p) console_proxy = new color_ostream_proxy(Core::getInstance().getConsole()); + // ensure noone bothers us while we load data defs in the background + m_mutex->lock(); + + // 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) { @@ -382,10 +393,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; @@ -419,42 +426,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; -} - -// 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,24 +438,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"); -} - static VALUE rb_dfget_global_address(VALUE self, VALUE name) { return rb_uint2inum(Core::getInstance().vinfo->getAddress(rb_string_value_ptr(&name))); @@ -510,7 +463,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 } @@ -531,7 +484,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); } @@ -760,17 +713,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 - __thiscall -#endif +__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); - 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)); - return rb_int2inum(ret); + fptr = (decltype(fptr))*(void**)(*that + voff); + return fptr(that, a0, a1, a2, a3); +} +#endif + +// 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))); } @@ -779,22 +764,17 @@ 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); - 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); 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); @@ -833,10 +813,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_load_protect(rb_str_new2("./hack/ruby.rb"), Qfalse, &state); - if (state) - dump_rb_error(); } diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb index fd597e43f..64592e3eb 100644 --- a/plugins/ruby/ruby.rb +++ b/plugins/ruby/ruby.rb @@ -1,41 +1,29 @@ -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 - - module ::Kernel - def puts(*a) - a.flatten.each { |l| - DFHack.print_str(l.to_s.chomp + "\n") - } - nil - end +# redefine standard i/o methods to use the dfhack console +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 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 + def p(*a) + a.each { |e| + puts_err e.inspect + } + nil + end +end +module DFHack + class << self # 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) @@ -76,104 +64,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) @@ -186,67 +76,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' @@ -256,38 +85,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 @@ -296,759 +93,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 autogen'd file -require 'hack/ruby-autogen' +# load autogenned file +require './hack/ruby/ruby-autogen-defs' +require './hack/ruby/ruby-autogen' -# load optional user-specified startup file -load 'ruby_custom.rb' if File.exist?('ruby_custom.rb') +# 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 new file mode 100644 index 000000000..e7d4335f0 --- /dev/null +++ b/plugins/ruby/unit.rb @@ -0,0 +1,78 @@ +module DFHack + 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 + 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 + + # 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 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 + + # list currently idle workers + def unit_idlers + unit_workers.find_all { |u| + # current_job includes eat/drink/sleep/pickupequip + !u.job.current_job 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 diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua index ef9c98299..6fc127351 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 @@ -40,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) @@ -100,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 @@ -249,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 -- @@ -387,6 +467,88 @@ 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 +-- + +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 +831,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 +872,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 @@ -737,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') @@ -744,9 +908,11 @@ 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') +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') + 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/growcrops.rb b/scripts/growcrops.rb new file mode 100644 index 000000000..e3abe54ac --- /dev/null +++ b/scripts/growcrops.rb @@ -0,0 +1,49 @@ +# 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 + +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 + 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, inventory.keys.map { |k| @raws_plant_name[k] }) + unless wantmat = @raws_plant_name.index(mat) + raise "invalid plant material #{material}" + end + + 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] + count += 1 + } + puts "Grown #{count} #{mat}" +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 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}" diff --git a/scripts/slayrace.rb b/scripts/slayrace.rb new file mode 100644 index 000000000..27b1ba3cc --- /dev/null +++ b/scripts/slayrace.rb @@ -0,0 +1,33 @@ +# slay all creatures of a given race + +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 checkunit[u] +}.compact.uniq.sort + +if !race + puts all_races +else + 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.world.units.active.each { |u| + if u.race == race_nr and checkunit[u] + u.body.blood_count = 0 + count += 1 + end + } + + puts "slain #{count} #{raw_race}" +end