Skip to content

Latest commit

 

History

History

ruby

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
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 various .rb file, e.g. shortcuts to read
a map block, find an unit or an item, etc.

Global dfhack objects are accessible through the 'df' accessor (eg 'df.world').

DFHack structures are renamed in CamelCase in the ruby namespace.

For a list of the structures and their methods, grep the ruby-autogen.rb file.

All ruby code runs while the main DF process and other plugins are suspended.


DFHack console
--------------

The ruby plugin defines one new dfhack console command:
 rb_eval <ruby expression> ; 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 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
df/hack/ on linux), the library should be named 'libruby.dll' (.so on linux).
You can download a tested version at http://github.com/jjyg/dfhack/downloads/


Ruby scripts
------------

The ruby plugin allows the creation of '.rb' scripts in df/hack/scripts/.

If you create such a script, e.g. 'test.rb', that will add a new dfhack console
command 'test'.
The script can access the console command arguments through the global variable
'$script_args', which is an array of ruby Strings.
To exit early from a script, use 'throw :script_finished'

The help string displayed in dfhack 'ls' command is the first line of the
script, if it is a comment (ie starts with '# ').


Calling dfhack commands
-----------------------

The ruby plugin allows the calling of arbitrary dfhack commands, as if typed
directly on the dfhack prompt.
However due to locks and stuff, the dfhack command is delayed until the current
ruby command is finished, so it is restricted to interactive uses.
It is possible to call the method many times, this will queue dfhack commands
to be run in order.

 df.dfhack_run "reveal"


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.map_tile_at(pos)
Returns a MapTile, holding all informations wrt the map tile (read&write).
This class is a ruby specific extention, to facilitate interaction with the
DF map data. Check out hack/ruby/map.rb.

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

 df.each_tree(material) { |t|  }
Iterates over every tree of the given material (eg 'maple').

 df.translate_name(name, in_english=true, only_lastpart=false)
Decode the LanguageName structure as a String as displayed in the game UI.
A shortcut is available through name.to_s

 df.decode_mat(obj)
Returns a MaterialInfo definition for the given object, using its mat_type
and mat_index fields. Also works with a token string argument ('STONE:DOLOMITE')


DFHack callbacks
----------------

The plugin interfaces with dfhack 'onupdate' hook.
To register ruby code to be run every graphic frame, use:
 handle = df.onupdate_register('log') { puts 'i love flooding the console' }
You can also rate-limit when your callback is called to a number of game ticks:
 handle = df.onupdate_register('myname', 10) { puts '10 more in-game ticks elapsed' }
In this case, the callback is called immediately, and then every X in-game
ticks (advances only when the game is unpaused).
To stop being called, use:
 df.onupdate_unregister handle

The same mechanism is available for 'onstatechange', but the
SC_BEGIN_UNLOAD event is not propagated to the ruby handler.

Available states:
 :WORLD_LOADED, :WORLD_UNLOADED, :MAP_LOADED, :MAP_UNLOADED,
 :VIEWSCREEN_CHANGED, :CORE_INITIALIZED, :PAUSED, :UNPAUSED


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 examples), works for Compounds
only.
A special Compound DFHack::StlString is available for allocating a single c++
stl::string, so that you can call vmethods that take a string pointer argument
(eg getName).
 ex: s = DFHack::StlString.cpp_new ; df.building_find.getName(s) ; p s.str

Deallocation may work, using the compound method _cpp_delete. Use with caution,
may crash your DF session. It may be simpler to just leak the memory.
_cpp_delete will try to free all memory directly used by the compound, eg
strings and vectors. It will *not* call the class destructor, and will not free
stuff behind pointers.

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 entry 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.
 ex: df.unit_find(:selected).status.labors[:HAUL_FOOD] = true
     df.map_tile_at(df.cursor).designation.liquid_type = :Water

Virtual method calls are supported for C++ objects, with a maximum of 6
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.


Examples
--------

For more complex examples, check the dfhack/scripts/*.rb files.

Show info on the currently selected unit ('v' or 'k' DF menu)
 p df.unit_find.flags1

Set a custom nickname to unit with id '123'
 df.unit_find(123).name.nickname = 'moo'

Show current unit profession
 p df.unit_find.profession

Change current unit profession
 df.unit_find.profession = :MASON

Center the screen on unit ID '123'
 df.center_viewscreen(df.unit_find(123))

Find an item under the game cursor and show its C++ 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) }
 p df.world.raws.plants.all[plant.mat_index].id

Dig a channel under the cursor
 df.map_tile_at(df.cursor).dig(:Channel)

Spawn 2/7 magma on the tile of the dwarf nicknamed 'hotfeet'
 hot = df.unit_citizens.find { |u| u.name.nickname == 'hotfeet' }
 df.map_tile_at(hot).spawn_magma(2)


Plugin compilation
------------------

The plugin consists of the main ruby.cpp native plugin and the *.rb files.

The native plugin handles only low-level ruby-to-df interaction (eg raw memory
read/write, and dfhack integration), and the .rb files hold end-user helper
functions.

On dfhack start, the native plugin will initialize the ruby interpreter, and
load hack/ruby/ruby.rb. This one then loads all other .rb files.

The DF internal structures are described in ruby-autogen.rb .
It is output by ruby/codegen.pl, from dfhack/library/include/df/codegen.out.xml
It contains architecture-specific data (eg DF internal structures field offsets,
which differ between Windows and Linux. Linux and Macosx are the same, as they
both use gcc).
It is stored inside the build directory (eg build/plugins/ruby/ruby-autogen.rb)

For example,
 <ld:global-type ld:meta="struct-type" type-name="unit">
   <ld:field type-name="language_name" name="name" ld:meta="global"/>
   <ld:field name="custom_profession" ld:meta="primitive" ld:subtype="stl-string"/>
   <ld:field ld:subtype="enum" base-type="int16_t" name="profession" type-name="profession" ld:meta="global"/>

Will generate
 class Unit < MemHack::Compound
  field(:name, 0) { global :LanguageName }
  field(:custom_profession, 60) { stl_string }
  field(:profession, 64) { number 16, true }

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 current struct.
 This field depends on the compiler used by Toady to generate DF.
The block argument describes the type of the field: uint32, ptr to global...

Primitive type access is done through native methods from ruby.cpp (vector length,
raw memory access, etc)