diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 628e8f8e7..2c54aebad 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -36,6 +36,11 @@ if (BUILD_DWARFEXPORT) add_subdirectory (dwarfexport) endif() +OPTION(BUILD_RUBY "Build ruby binding." ON) +if (BUILD_RUBY) + add_subdirectory (ruby) +endif() + install(DIRECTORY lua/ DESTINATION ${DFHACK_LUA_DESTINATION}/plugins FILES_MATCHING PATTERN "*.lua") diff --git a/plugins/ruby/CMakeLists.txt b/plugins/ruby/CMakeLists.txt new file mode 100644 index 000000000..7057cb2dc --- /dev/null +++ b/plugins/ruby/CMakeLists.txt @@ -0,0 +1,29 @@ +find_package(Ruby) +if(RUBY_FOUND) + ADD_CUSTOM_COMMAND( + OUTPUT ruby-autogen.cpp + COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/codegen.pl ${dfhack_SOURCE_DIR}/library/include/df/codegen.out.xml ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.cpp + DEPENDS ${dfhack_SOURCE_DIR}/library/include/df/codegen.out.xml codegen.pl + ) + ADD_EXECUTABLE(ruby-autogen ruby-autogen.cpp) + if(CMAKE_COMPILER_IS_GNUCC) + set_target_properties (ruby-autogen PROPERTIES COMPILE_FLAGS "-Wno-invalid-offsetof") + endif(CMAKE_COMPILER_IS_GNUCC) + ADD_CUSTOM_COMMAND( + OUTPUT ruby-autogen.offsets + COMMAND ruby-autogen ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.offsets + DEPENDS ruby-autogen + ) + ADD_CUSTOM_COMMAND( + OUTPUT ruby-autogen.rb + COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/codegen.pl ${dfhack_SOURCE_DIR}/library/include/df/codegen.out.xml ${CMAKE_CURRENT_SOURCE_DIR}/ruby-autogen.rb ${CMAKE_CURRENT_BINARY_DIR}/ruby-autogen.offsets ${CMAKE_CURRENT_SOURCE_DIR}/ruby-memstruct.rb + DEPENDS ruby-autogen.offsets ruby-memstruct.rb + ) + ADD_CUSTOM_TARGET(ruby-autogen-rb ALL DEPENDS ruby-autogen.rb) + include_directories("${dfhack_SOURCE_DIR}/depends/tthread" ${RUBY_INCLUDE_PATH}) + DFHACK_PLUGIN(ruby ruby.cpp LINK_LIBRARIES dfhack-tinythread) + target_link_libraries(ruby ${RUBY_LIBRARY}) + install(FILES ruby.rb ruby-autogen.rb DESTINATION ${DFHACK_LIBRARY_DESTINATION}) +else(RUBY_FOUND) + MESSAGE(STATUS "Required library (ruby) not found - ruby plugin can't be built.") +endif(RUBY_FOUND) diff --git a/plugins/ruby/README b/plugins/ruby/README new file mode 100644 index 000000000..7238c161d --- /dev/null +++ b/plugins/ruby/README @@ -0,0 +1,113 @@ +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 returs 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. + +Global objects are stored in the GlobalObjects class ; each object accessor is +mirrored as a DFHack module method. + +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 +You can use single-quotes for strings ; avoid double-quotes that are parsed +and removed by the dfhack console. + +The plugin also interfaces with dfhack 'onupdate' hook. +To register ruby code to be run every graphic frame, use: + handle = df.onupdate_register { puts 'i love flood' } +To stop being called, use: + df.onupdate_unregister handle + + +Exemples +-------- + +For more complex exemples, check the ruby/plugins/ folder. + +Show info on the currently selected unit ('v' or 'k' DF menu) + p df.find_unit.flags1 + +Set a custom nickname to unit with id '123' + df.find_unit(123).name.nickname = 'moo' + +Show current unit profession + p df.find_unit.profession + +Center the screen on unit '123' + df.center_viewscreen(df.find_unit(123)) + +Find an item at a given position, show its C++ classname + df.find_item(df.cursor)._rtti_classname + +Find the raws name of the plant under cursor + plant = df.world.plants.all.find { |p| df.at_cursor?(p) } + df.world.raws.plants.all[plant.mat_index].id + +Dig a channel under the cursor + df.map_designation_at(df.cursor).dig = TileDigDesignation::Channel + df.map_block_at(df.cursor).flags.designated = true + + +Compilation +----------- + +The plugin consists of the ruby.rb file including user comfort functions ; +ruby-memstruct.rb describing basic classes used by the autogenerated code, and +embedded at the beginnig of ruby-autogen.rb, and the generated code. + +The generated code is generated by codegen.pl, which takes the codegen.out.xml +file as input. + +One of the limitations of the xml file is that it does not include structure +offsets, as they depend on the compiler. To overcome that, codegen runs in two +passes. The first pass generates a ruby-autogen.cpp file, that will output the +structure offsets ; the second pass will generate the ruby-autogen.rb using the +output of the compiled ruby-autogen.cpp. + +For exemple, from + + + + + +We generate the cpp + printf("%s = %d", "offsetof(df::unit, language_name)", offsetof(df::unit, language_name)); + printf("%s = %d", "offsetof(df::unit, custom_profession)", offsetof(df::unit, custom_profession)); + printf("%s = %d", "offsetof(df::unit, profession)", offsetof(df::unit, profession)); + +Which generates (on linux) + offsetof(df::unit, name) = 0 + offsetof(df::unit, custom_profession) = 60 + offsetof(df::unit, profession) = 64 + +Which generates + class Unit < MemHack::Compound + field(:name, 0) { global :LanguageName } + field(:custom_profession, 60) { stl_string } + field(:profession, 64) { number 16, true } + +The field method has 2 arguments: the name of the method and the member offset ; +the block specifies the member type. See ruby-memstruct.rb for more information. +Primitive type access is done through native methods in 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 +really a vector of pointer. + + +Todo +---- + +Correct c++ object (de)allocation (call ctor etc) ; ability to call vtable methods diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl new file mode 100755 index 000000000..5c60fcb41 --- /dev/null +++ b/plugins/ruby/codegen.pl @@ -0,0 +1,708 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use XML::LibXML; + +our @lines_rb; +my @lines_cpp; +my @include_cpp; +my %offsets; + +sub indent_rb(&) { + my ($sub) = @_; + my @lines; + { + local @lines_rb; + $sub->(); + @lines = map { " " . $_ } @lines_rb; + } + push @lines_rb, @lines +} + +sub rb_ucase { + my ($name) = @_; + return $name if ($name eq uc($name)); + return join("", map { ucfirst $_ } (split('_', $name))); +} + +my %item_renderer = ( + 'global' => \&render_item_global, + 'number' => \&render_item_number, + 'container' => \&render_item_container, + 'compound' => \&render_item_compound, + 'pointer' => \&render_item_pointer, + 'static-array' => \&render_item_staticarray, + 'primitive' => \&render_item_primitive, + 'bytes' => \&render_item_bytes, +); + +my %global_types; + +sub render_global_enum { + my ($name, $type) = @_; + + my $rbname = rb_ucase($name); + push @lines_rb, "class $rbname < MemHack::Enum"; + indent_rb { + render_enum_fields($type); + }; + push @lines_rb, "end\n"; +} +sub render_enum_fields { + my ($type) = @_; + + my $value = -1; + push @lines_rb, "ENUM = Hash.new"; + push @lines_rb, "NUME = Hash.new"; + + my %attr_type; + my %attr_list; + for my $attr ($type->findnodes('child::enum-attr')) { + my $rbattr = rb_ucase($attr->getAttribute('name')); + my $typeattr = $attr->getAttribute('type-name'); + # find how we need to encode the attribute values: string, symbol (for enums), raw (number, bool) + if ($typeattr) { + if ($global_types{$typeattr}) { + $attr_type{$rbattr} = 'symbol'; + } else { + $attr_type{$rbattr} = 'naked'; + } + } else { + $attr_type{$rbattr} = 'quote'; + } + + my $def = $attr->getAttribute('default-value'); + if ($attr->getAttribute('is-list')) { + push @lines_rb, "$rbattr = Hash.new { |h, k| h[k] = [] }"; + $attr_list{$rbattr} = 1; + } elsif ($def) { + $def = ":$def" if ($attr_type{$rbattr} eq 'symbol'); + $def =~ s/'/\\'/g if ($attr_type{$rbattr} eq 'quote'); + $def = "'$def'" if ($attr_type{$rbattr} eq 'quote'); + push @lines_rb, "$rbattr = Hash.new($def)"; + } else { + push @lines_rb, "$rbattr = Hash.new"; + } + } + + for my $item ($type->findnodes('child::enum-item')) { + $value = $item->getAttribute('value') || ($value+1); + my $elemname = $item->getAttribute('name'); # || "unk_$value"; + + if ($elemname) { + my $rbelemname = rb_ucase($elemname); + push @lines_rb, "ENUM[$value] = :$rbelemname ; NUME[:$rbelemname] = $value"; + for my $iattr ($item->findnodes('child::item-attr')) { + my $ian = $iattr->getAttribute('name'); + my $iav = $iattr->getAttribute('value'); + my $rbattr = rb_ucase($ian); + my $op = ($attr_list{$rbattr} ? '<<' : '='); + $iav = ":$iav" if ($attr_type{$rbattr} eq 'symbol'); + $iav =~ s/'/\\'/g if ($attr_type{$rbattr} eq 'quote'); + $iav = "'$iav'" if ($attr_type{$rbattr} eq 'quote'); + $lines_rb[$#lines_rb] .= " ; ${rbattr}[:$rbelemname] $op $iav"; + } + } + } +} + + +sub render_global_bitfield { + my ($name, $type) = @_; + + push @lines_cpp, "}" if @include_cpp; + push @lines_cpp, "void cpp_$name(FILE *fout) {"; + push @include_cpp, $name; + + my $rbname = rb_ucase($name); + push @lines_rb, "class $rbname < MemHack::Compound"; + indent_rb { + render_bitfield_fields($type); + }; + push @lines_rb, "end\n"; +} +sub render_bitfield_fields { + my ($type) = @_; + + push @lines_rb, "field(:_whole, 0) {"; + indent_rb { + render_item_number($type, ''); + }; + push @lines_rb, "}"; + + my $shift = 0; + for my $field ($type->findnodes('child::ld:field')) { + my $count = $field->getAttribute('count') || 1; + my $name = $field->getAttribute('name'); + my $type = $field->getAttribute('type-name'); + my $enum = rb_ucase($type) if ($type and $global_types{$type}); + $name = $field->getAttribute('ld:anon-name') if (!$name); + print "bitfield $name !number\n" if (!($field->getAttribute('ld:meta') eq 'number')); + if ($count == 1) { + push @lines_rb, "field(:$name, 0) { bit $shift }" if ($name); + } elsif ($enum) { + push @lines_rb, "field(:$name, 0) { bits $shift, $count, $enum }" if ($name); + } else { + push @lines_rb, "field(:$name, 0) { bits $shift, $count }" if ($name); + } + $shift += $count; + } +} + + +sub render_global_struct { + my ($name, $type) = @_; + + my $rbname = rb_ucase($name); + + my $cppns = "df::$name"; + push @lines_cpp, "}" if @include_cpp; + push @lines_cpp, "void cpp_$name(FILE *fout) {"; + push @include_cpp, $name; + + push @lines_rb, "class $rbname < MemHack::Compound"; + indent_rb { + my $sz = query_cpp("sizeof($cppns)"); + push @lines_rb, "sizeof $sz"; + render_struct_fields($type, "$cppns"); + }; + push @lines_rb, "end\n"; +} +my %seen_class; +sub render_global_class { + my ($name, $type) = @_; + + my $rbname = rb_ucase($name); + + # ensure pre-definition of ancestors + my $parent = $type->getAttribute('inherits-from'); + render_global_class($parent, $global_types{$parent}) if ($parent and !$seen_class{$parent}); + + return if $seen_class{$name}; + $seen_class{$name}++; + + my $rtti_name; + if ($type->getAttribute('ld:meta') eq 'class-type') { + $rtti_name = $type->getAttribute('original-name') || + $type->getAttribute('type-name') || + $name; + } + + my $rbparent = ($parent ? rb_ucase($parent) : 'MemHack::Compound'); + + my $cppns = "df::$name"; + push @lines_cpp, "}" if @include_cpp; + push @lines_cpp, "void cpp_$name(FILE *fout) {"; + push @include_cpp, $name; + + push @lines_rb, "class $rbname < $rbparent"; + indent_rb { + my $sz = query_cpp("sizeof($cppns)"); + push @lines_rb, "sizeof $sz"; + push @lines_rb, "rtti_classname :$rtti_name" if $rtti_name; + render_struct_fields($type, "$cppns"); + my $vms = $type->findnodes('child::virtual-methods')->[0]; + render_class_vmethods($vms) if $vms; + }; + push @lines_rb, "end\n"; +} +sub render_struct_fields { + my ($type, $cppns) = @_; + + for my $field ($type->findnodes('child::ld:field')) { + my $name = $field->getAttribute('name'); + $name = $field->getAttribute('ld:anon-name') if (!$name); + if (!$name and $field->getAttribute('ld:anon-compound')) { + render_struct_fields($field, $cppns); + } + next if (!$name); + my $offset = get_offset($cppns, $name); + + push @lines_rb, "field(:$name, $offset) {"; + indent_rb { + render_item($field, "$cppns"); + }; + push @lines_rb, "}"; + } +} +sub render_class_vmethods { + my ($vms) = @_; + my $voff = 0; + for my $meth ($vms->findnodes('child::vmethod')) { + my $name = $meth->getAttribute('name'); + if ($name) { + my @argnames; + my @argargs; + for my $arg ($meth->findnodes('child::ld:field')) { + my $nr = $#argnames + 1; + my $argname = lcfirst($arg->getAttribute('name') || "arg$nr"); + push @argnames, $argname; + if ($arg->getAttribute('ld:meta') eq 'global' and $arg->getAttribute('ld:subtype') eq 'enum') { + push @argargs, rb_ucase($arg->getAttribute('type-name')) . ".to_i($argname)"; + } else { + push @argargs, $argname; + } + } + push @lines_rb, "def $name(" . join(', ', @argnames) . ')'; + indent_rb { + my $args = join('', map { ", $_" } @argargs); + my $call = "DFHack.vmethod_call(self, $voff$args)"; + my $ret = $meth->findnodes('child::ret-type')->[0]; + render_class_vmethod_ret($call, $ret); + }; + push @lines_rb, 'end'; + } + # on linux, the destructor uses 2 entries + $voff += 4 if $meth->getAttribute('is-destructor') and $^O =~ /linux/i; + $voff += 4; + } +} + +sub render_class_vmethod_ret { + my ($call, $ret) = @_; + if (!$ret) { + push @lines_rb, "$call ; nil"; + return; + } + my $retmeta = $ret->getAttribute('ld:meta') || ''; + if ($retmeta eq 'global') { # enum + my $retname = $ret->getAttribute('type-name'); + if ($retname and $global_types{$retname} and + $global_types{$retname}->getAttribute('ld:meta') eq 'enum-type') { + push @lines_rb, rb_ucase($retname) . ".to_sym($call)"; + } else { + print "vmethod global nonenum $call\n"; + push @lines_rb, $call; + } + } elsif ($retmeta eq 'number') { + my $retsubtype = $ret->getAttribute('ld:subtype'); + my $retbits = $ret->getAttribute('ld:bits'); + push @lines_rb, "val = $call"; + if ($retsubtype eq 'bool') { + push @lines_rb, "(val & 1) != 0"; + } elsif ($ret->getAttribute('ld:unsigned')) { + push @lines_rb, "val & ((1 << $retbits) - 1)"; + } else { # signed + push @lines_rb, "val &= ((1 << $retbits) - 1)"; + push @lines_rb, "((val >> ($retbits-1)) & 1) == 0 ? val : val - (1 << $retbits)"; + } + } elsif ($retmeta eq 'pointer') { + push @lines_rb, "ptr = $call"; + push @lines_rb, "class << self"; + indent_rb { + render_item($ret->findnodes('child::ld:item')->[0]); + }; + push @lines_rb, "end._at(ptr) if ptr != 0"; + } else { + print "vmethod unkret $call\n"; + push @lines_rb, $call; + } +} + +sub render_global_objects { + my (@objects) = @_; + my @global_objects; + + my $sname = 'global_objects'; + my $rbname = rb_ucase($sname); + + push @lines_cpp, "}" if @include_cpp; + push @lines_cpp, "void cpp_$sname(FILE *fout) {"; + push @include_cpp, $sname; + + push @lines_rb, "class $rbname < MemHack::Compound"; + indent_rb { + for my $obj (@objects) { + my $oname = $obj->getAttribute('name'); + my $addr = "DFHack.get_global_address('$oname')"; + push @lines_rb, "addr = $addr"; + push @lines_rb, "if addr != 0"; + indent_rb { + push @lines_rb, "field(:$oname, addr) {"; + my $item = $obj->findnodes('child::ld:item')->[0]; + indent_rb { + render_item($item, 'df::global'); + }; + push @lines_rb, "}"; + }; + push @lines_rb, "end"; + + push @global_objects, $oname; + } + }; + push @lines_rb, "end"; + + indent_rb { + push @lines_rb, "Global = GlobalObjects.new._at(0)"; + for my $obj (@global_objects) { + push @lines_rb, "def self.$obj ; Global.$obj ; end"; + push @lines_rb, "def self.$obj=(v) ; Global.$obj = v ; end"; + } + }; +} + + +sub render_item { + my ($item, $pns) = @_; + return if (!$item); + + my $meta = $item->getAttribute('ld:meta'); + + my $renderer = $item_renderer{$meta}; + if ($renderer) { + $renderer->($item, $pns); + } else { + print "no render item $meta\n"; + } +} + +sub render_item_global { + my ($item, $pns) = @_; + + my $typename = $item->getAttribute('type-name'); + my $subtype = $item->getAttribute('ld:subtype'); + + if ($subtype and $subtype eq 'enum') { + render_item_number($item, $pns); + } else { + my $rbname = rb_ucase($typename); + push @lines_rb, "global :$rbname"; + } +} + +sub render_item_number { + my ($item, $pns) = @_; + + my $subtype = $item->getAttribute('ld:subtype'); + my $meta = $item->getAttribute('ld:meta'); + my $initvalue = $item->getAttribute('init-value'); + my $typename = $item->getAttribute('type-name'); + undef $typename if ($meta and $meta eq 'bitfield-type'); + $typename = rb_ucase($typename) if $typename; + $typename = $pns if (!$typename and $subtype and $subtype eq 'enum'); # compound enum + + $initvalue = 1 if ($initvalue and $initvalue eq 'true'); + $initvalue = ":$initvalue" if ($initvalue and $typename and $initvalue =~ /[a-zA-Z]/); + $initvalue ||= 'nil' if $typename; + + $subtype = $item->getAttribute('base-type') if (!$subtype or $subtype eq 'enum' or $subtype eq 'bitfield'); + $subtype = 'int32_t' if (!$subtype); + + if ($subtype eq 'int64_t') { + push @lines_rb, 'number 64, true'; + } elsif ($subtype eq 'uint32_t') { + push @lines_rb, 'number 32, false'; + } elsif ($subtype eq 'int32_t') { + push @lines_rb, 'number 32, true'; + } elsif ($subtype eq 'uint16_t') { + push @lines_rb, 'number 16, false'; + } elsif ($subtype eq 'int16_t') { + push @lines_rb, 'number 16, true'; + } elsif ($subtype eq 'uint8_t') { + push @lines_rb, 'number 8, false'; + } elsif ($subtype eq 'int8_t') { + push @lines_rb, 'number 8, false'; + } elsif ($subtype eq 'bool') { + push @lines_rb, 'number 8, true'; + } elsif ($subtype eq 's-float') { + push @lines_rb, 'float'; + return; + } else { + print "no render number $subtype\n"; + return; + } + $lines_rb[$#lines_rb] .= ", $initvalue" if ($initvalue); + $lines_rb[$#lines_rb] .= ", $typename" if ($typename); +} + +sub render_item_compound { + my ($item, $pns) = @_; + + my $cppns = $pns . '::' . $item->getAttribute('ld:typedef-name'); + my $subtype = $item->getAttribute('ld:subtype'); + + my @namecomponents = split('::', $cppns); + shift @namecomponents; + my $classname = join('_', map { rb_ucase($_) } @namecomponents); + + if (!$subtype || $subtype eq 'bitfield') { + push @lines_rb, "compound(:$classname) {"; + indent_rb { + if (!$subtype) { + render_struct_fields($item, $cppns); + } else { + render_bitfield_fields($item); + } + }; + push @lines_rb, "}" + } elsif ($subtype eq 'enum') { + push @lines_rb, "class ::DFHack::$classname < MemHack::Enum"; + indent_rb { + # declare constants + render_enum_fields($item); + }; + push @lines_rb, "end\n"; + + # actual field + render_item_number($item, $classname); + } else { + print "no render compound $subtype\n"; + } +} + +sub render_item_container { + my ($item, $pns) = @_; + + my $subtype = $item->getAttribute('ld:subtype'); + my $rbmethod = join('_', split('-', $subtype)); + my $tg = $item->findnodes('child::ld:item')->[0]; + my $indexenum = $item->getAttribute('index-enum'); + if ($tg) { + if ($rbmethod eq 'df_linked_list') { + push @lines_rb, "$rbmethod {"; + } else { + my $tglen = get_tglen($tg, $pns); + push @lines_rb, "$rbmethod($tglen) {"; + } + indent_rb { + render_item($tg, $pns); + }; + push @lines_rb, "}"; + } elsif ($indexenum) { + $indexenum = rb_ucase($indexenum); + push @lines_rb, "$rbmethod($indexenum)"; + } else { + push @lines_rb, "$rbmethod"; + } +} + +sub render_item_pointer { + my ($item, $pns) = @_; + + my $tg = $item->findnodes('child::ld:item')->[0]; + my $ary = $item->getAttribute('is-array'); + if ($ary and $ary eq 'true') { + my $tglen = get_tglen($tg, $pns); + push @lines_rb, "pointer_ary($tglen) {"; + } else { + push @lines_rb, "pointer {"; + } + indent_rb { + render_item($tg, $pns); + }; + push @lines_rb, "}"; +} + +sub render_item_staticarray { + my ($item, $pns) = @_; + + my $count = $item->getAttribute('count'); + my $tg = $item->findnodes('child::ld:item')->[0]; + my $tglen = get_tglen($tg, $pns); + my $indexenum = $item->getAttribute('index-enum'); + if ($indexenum) { + $indexenum = rb_ucase($indexenum); + push @lines_rb, "static_array($count, $tglen, $indexenum) {"; + } else { + push @lines_rb, "static_array($count, $tglen) {"; + } + indent_rb { + render_item($tg, $pns); + }; + push @lines_rb, "}"; +} + +sub render_item_primitive { + my ($item, $pns) = @_; + + my $subtype = $item->getAttribute('ld:subtype'); + if ($subtype eq 'stl-string') { + push @lines_rb, "stl_string"; + } else { + print "no render primitive $subtype\n"; + } +} + +sub render_item_bytes { + my ($item, $pns) = @_; + + my $subtype = $item->getAttribute('ld:subtype'); + if ($subtype eq 'padding') { + } elsif ($subtype eq 'static-string') { + my $size = $item->getAttribute('size'); + push @lines_rb, "static_string($size)"; + } else { + print "no render bytes $subtype\n"; + } +} + +sub get_offset { + my ($cppns, $fname) = @_; + + return query_cpp("offsetof($cppns, $fname)"); +} + +sub get_tglen { + my ($tg, $cppns) = @_; + + if (!$tg) { + return 'nil'; + } + + my $meta = $tg->getAttribute('ld:meta'); + if ($meta eq 'number') { + return $tg->getAttribute('ld:bits')/8; + } elsif ($meta eq 'pointer') { + return 4; + } elsif ($meta eq 'container') { + my $subtype = $tg->getAttribute('ld:subtype'); + if ($subtype eq 'stl-vector') { + return query_cpp("sizeof(std::vector)"); + } elsif ($subtype eq 'df-linked-list') { + return 12; + } else { + print "cannot tglen container $subtype\n"; + } + } elsif ($meta eq 'compound') { + my $cname = $tg->getAttribute('ld:typedef-name'); + return query_cpp("sizeof(${cppns}::$cname)"); + } elsif ($meta eq 'static-array') { + my $count = $tg->getAttribute('count'); + my $ttg = $tg->findnodes('child::ld:item')->[0]; + my $ttgl = get_tglen($ttg, $cppns); + if ($ttgl =~ /^\d+$/) { + return $count * $ttgl; + } else { + return "$count*$ttgl"; + } + } elsif ($meta eq 'global') { + my $typename = $tg->getAttribute('type-name'); + my $subtype = $tg->getAttribute('ld:subtype'); + if ($subtype and $subtype eq 'enum') { + my $base = $tg->getAttribute('base-type') || 'int32_t'; + if ($base eq 'int32_t') { + return 4; + } elsif ($base eq 'int16_t') { + return 2; + } elsif ($base eq 'int8_t') { + return 1; + } else { + print "cannot tglen enum $base\n"; + } + } else { + return query_cpp("sizeof(df::$typename)"); + } + } elsif ($meta eq 'primitive') { + my $subtype = $tg->getAttribute('ld:subtype'); + if ($subtype eq 'stl-string') { + return query_cpp("sizeof(std::string)"); + } else { + print "cannot tglen primitive $subtype\n"; + } + } else { + print "cannot tglen $meta\n"; + } +} + +my %query_cpp_cache; +sub query_cpp { + my ($query) = @_; + + my $ans = $offsets{$query}; + return $ans if (defined($ans)); + + my $cached = $query_cpp_cache{$query}; + return $cached if (defined($cached)); + $query_cpp_cache{$query} = 1; + + push @lines_cpp, " fprintf(fout, \"%s = %d\\n\", \"$query\", $query);"; + return "'$query'"; +} + + + +my $input = $ARGV[0] || '../../library/include/df/codegen.out.xml'; + +# run once with output = 'ruby-autogen.cpp' +# compile +# execute, save output to 'ruby-autogen.offsets' +# re-run this script with output = 'ruby-autogen.rb' and offsetfile = 'ruby-autogen.offsets' +# delete binary +# delete offsets +my $output = $ARGV[1] or die "need output file"; +my $offsetfile = $ARGV[2]; +my $memstruct = $ARGV[3]; + +if ($offsetfile) { + open OF, "<$offsetfile"; + while (my $line = ) { + chomp($line); + my ($key, $val) = split(' = ', $line); + $offsets{$key} = $val; + } + close OF; +} + + +my $doc = XML::LibXML->new()->parse_file($input); +$global_types{$_->getAttribute('type-name')} = $_ foreach $doc->findnodes('/ld:data-definition/ld:global-type'); + +my @nonenums; +for my $name (sort { $a cmp $b } keys %global_types) { + my $type = $global_types{$name}; + my $meta = $type->getAttribute('ld:meta'); + if ($meta eq 'enum-type') { + render_global_enum($name, $type); + } else { + push @nonenums, $name; + } +} + +for my $name (@nonenums) { + my $type = $global_types{$name}; + my $meta = $type->getAttribute('ld:meta'); + if ($meta eq 'struct-type') { + render_global_struct($name, $type); + } elsif ($meta eq 'class-type') { + render_global_class($name, $type); + } elsif ($meta eq 'bitfield-type') { + render_global_bitfield($name, $type); + } else { + print "no render global type $meta\n"; + } +} + + +render_global_objects($doc->findnodes('/ld:data-definition/ld:global-object')); + + +open FH, ">$output"; +if ($output =~ /\.cpp$/) { + print FH "#include \"DataDefs.h\"\n"; + print FH "#include \"df/$_.h\"\n" for @include_cpp; + print FH "#include \n"; + print FH "#include \n"; + print FH "$_\n" for @lines_cpp; + print FH "}\n"; + print FH "int main(int argc, char **argv) {\n"; + print FH " FILE *fout;\n"; + print FH " if (argc < 2) return 1;\n"; + print FH " fout = fopen(argv[1], \"w\");\n"; + print FH " cpp_$_(fout);\n" for @include_cpp; + print FH " fclose(fout);\n"; + print FH " return 0;\n"; + print FH "}\n"; + +} else { + if ($memstruct) { + open MH, "<$memstruct"; + print FH "$_" while(); + close MH; + } + print FH "module DFHack\n"; + print FH "$_\n" for @lines_rb; + print FH "end\n"; +} +close FH; diff --git a/plugins/ruby/plugins/building.rb b/plugins/ruby/plugins/building.rb new file mode 100644 index 000000000..15212092d --- /dev/null +++ b/plugins/ruby/plugins/building.rb @@ -0,0 +1,267 @@ +module DFHack + +# allocate a new building object +def self.building_alloc(type, subtype=-1, custom=-1) + type = BuildingType.to_sym(type) + 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 new file mode 100644 index 000000000..64f17b493 --- /dev/null +++ b/plugins/ruby/plugins/plant.rb @@ -0,0 +1,152 @@ +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 new file mode 100644 index 000000000..9a00b2bfa --- /dev/null +++ b/plugins/ruby/plugins/unit.rb @@ -0,0 +1,52 @@ +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-memstruct.rb b/plugins/ruby/ruby-memstruct.rb new file mode 100644 index 000000000..40f35bb42 --- /dev/null +++ b/plugins/ruby/ruby-memstruct.rb @@ -0,0 +1,747 @@ +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.to_i(i) + nume[i] || i + end + def self.to_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.to_sym(v) if @_enum + v + end + + def _set(v) + v = @_enum.to_i(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.to_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.to_i(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.to_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.to_i(i) if _indexenum + i += @_length if i < 0 + _tgat(i)._get + end + def []=(i, v) + i = _indexenum.to_i(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.to_i(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.to_i(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 + + +# 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 new file mode 100644 index 000000000..76d6a0431 --- /dev/null +++ b/plugins/ruby/ruby.cpp @@ -0,0 +1,713 @@ +// blindly copied imports from fastdwarf +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" +#include "VersionInfo.h" + +#include "DataDefs.h" +#include "df/world.h" +#include "df/unit.h" + +#include "tinythread.h" + +#include + +using namespace DFHack; + + + +// DFHack stuff + + +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); + +// inter-thread communication stuff +enum RB_command { + RB_IDLE, + RB_INIT, + RB_DIE, + RB_EVAL, + RB_CUSTOM, +}; +tthread::mutex *m_irun; +tthread::mutex *m_mutex; +static RB_command r_type; +static const char *r_command; +static command_result r_result; +static tthread::thread *r_thread; +static int onupdate_active; + +DFHACK_PLUGIN("ruby") + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + m_irun = new tthread::mutex(); + m_mutex = new tthread::mutex(); + r_type = RB_INIT; + + r_thread = new tthread::thread(df_rubythread, 0); + + while (r_type != RB_IDLE) + tthread::this_thread::yield(); + + m_irun->lock(); + + 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)); + + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + m_mutex->lock(); + if (!r_thread) + return CR_OK; + + r_type = RB_DIE; + r_command = 0; + m_irun->unlock(); + + r_thread->join(); + + delete r_thread; + r_thread = 0; + delete m_irun; + m_mutex->unlock(); + delete m_mutex; + + return CR_OK; +} + +// send a single ruby line to be evaluated by the ruby thread +static command_result plugin_eval_rb(const char *command) +{ + command_result ret; + + // serialize 'accesses' to the ruby thread + m_mutex->lock(); + if (!r_thread) + // raced with plugin_shutdown ? + return CR_OK; + + r_type = RB_EVAL; + r_command = command; + m_irun->unlock(); + + // could use a condition_variable or something... + while (r_type != RB_IDLE) + tthread::this_thread::yield(); + + // XXX non-atomic with previous r_type change check + ret = r_result; + + m_irun->lock(); + 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 (!onupdate_active) + return CR_OK; + + return plugin_eval_rb("DFHack.onupdate"); +} + +DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event e) +{ + std::string cmd = "DFHack.onstatechange "; + switch (e) { +#define SCASE(s) case SC_ ## s : cmd += ":" # s ; break + SCASE(WORLD_LOADED); + SCASE(WORLD_UNLOADED); + SCASE(MAP_LOADED); + SCASE(MAP_UNLOADED); + SCASE(VIEWSCREEN_CHANGED); + SCASE(CORE_INITIALIZED); + SCASE(BEGIN_UNLOAD); + } + + 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); +} + +static command_result df_rubyeval(color_ostream &out, std::vector & parameters) +{ + command_result ret; + + if (parameters.size() == 1 && (parameters[0] == "help" || parameters[0] == "?")) + { + out.print("This command executes an arbitrary ruby statement.\n"); + return CR_OK; + } + + std::string full = ""; + + for (unsigned i=0 ; ilock(); + + switch (r_type) { + case RB_IDLE: + case RB_INIT: + break; + + case RB_DIE: + running = 0; + ruby_finalize(); + break; + + case RB_EVAL: + state = 0; + rb_eval_string_protect(r_command, &state); + if (state) + dump_rb_error(); + break; + + case RB_CUSTOM: + // TODO handle ruby custom commands + break; + } + + r_result = CR_OK; + r_type = RB_IDLE; + m_irun->unlock(); + tthread::this_thread::yield(); + } +} + + +#define BOOL_ISFALSE(v) ((v) == Qfalse || (v) == Qnil || (v) == INT2FIX(0)) + +// main DFHack ruby module +static VALUE rb_cDFHack; + + +// DFHack module ruby methods, binds specific dfhack methods + +// enable/disable calls to DFHack.onupdate() +static VALUE rb_dfonupdateactive(VALUE self) +{ + if (onupdate_active) + return Qtrue; + else + return Qfalse; +} + +static VALUE rb_dfonupdateactiveset(VALUE self, VALUE val) +{ + onupdate_active = (BOOL_ISFALSE(val) ? 0 : 1); + 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)); + return Qnil; +} + +static VALUE rb_dfprint_err(VALUE self, VALUE s) +{ + Core::printerr("%s", rb_string_value_ptr(&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))); +} + +static VALUE rb_dfget_vtable(VALUE self, VALUE name) +{ + return rb_uint2inum((uint32_t)Core::getInstance().vinfo->getVTable(rb_string_value_ptr(&name))); +} + +// read the c++ class name from a vtable pointer, inspired from doReadClassName +// XXX virtual classes only! dark pointer arithmetic, use with caution ! +static VALUE rb_dfget_rtti_classname(VALUE self, VALUE vptr) +{ + char *ptr = (char*)rb_num2ulong(vptr); +#ifdef WIN32 + char *rtti = *(char**)(ptr - 0x4); + char *typeinfo = *(char**)(rtti + 0xC); + // skip the .?AV, trim @@ from end + return rb_str_new(typeinfo+0xc, strlen(typeinfo+0xc)-2); +#else + char *typeinfo = *(char**)(ptr - 0x4); + char *typestring = *(char**)(typeinfo + 0x4); + while (*typestring >= '0' && *typestring <= '9') + typestring++; + return rb_str_new2(typestring); +#endif +} + +static VALUE rb_dfget_vtable_ptr(VALUE self, VALUE objptr) +{ + // actually, rb_dfmemory_read_int32 + return rb_uint2inum(*(uint32_t*)rb_num2ulong(objptr)); +} + + + + +// raw memory access +// used by the ruby class definitions +// XXX may cause game crash ! double-check your addresses ! + +static VALUE rb_dfmalloc(VALUE self, VALUE len) +{ + char *ptr = (char*)malloc(FIX2INT(len)); + if (!ptr) + rb_raise(rb_eRuntimeError, "no memory"); + memset(ptr, 0, FIX2INT(len)); + return rb_uint2inum((long)ptr); +} + +static VALUE rb_dffree(VALUE self, VALUE ptr) +{ + free((void*)rb_num2ulong(ptr)); + return Qtrue; +} + +// memory reading (buffer) +static VALUE rb_dfmemory_read(VALUE self, VALUE addr, VALUE len) +{ + return rb_str_new((char*)rb_num2ulong(addr), rb_num2ulong(len)); +} + +// memory reading (integers/floats) +static VALUE rb_dfmemory_read_int8(VALUE self, VALUE addr) +{ + return rb_int2inum(*(char*)rb_num2ulong(addr)); +} +static VALUE rb_dfmemory_read_int16(VALUE self, VALUE addr) +{ + return rb_int2inum(*(short*)rb_num2ulong(addr)); +} +static VALUE rb_dfmemory_read_int32(VALUE self, VALUE addr) +{ + return rb_int2inum(*(int*)rb_num2ulong(addr)); +} + +static VALUE rb_dfmemory_read_float(VALUE self, VALUE addr) +{ + return rb_float_new(*(float*)rb_num2ulong(addr)); +} + + +// memory writing (buffer) +static VALUE rb_dfmemory_write(VALUE self, VALUE addr, VALUE raw) +{ + // no stable api for raw.length between rb1.8/rb1.9 ... + int strlen = FIX2INT(rb_funcall(raw, rb_intern("length"), 0)); + + memcpy((void*)rb_num2ulong(addr), rb_string_value_ptr(&raw), strlen); + + return Qtrue; +} + +// memory writing (integers/floats) +static VALUE rb_dfmemory_write_int8(VALUE self, VALUE addr, VALUE val) +{ + *(char*)rb_num2ulong(addr) = rb_num2ulong(val); + return Qtrue; +} +static VALUE rb_dfmemory_write_int16(VALUE self, VALUE addr, VALUE val) +{ + *(short*)rb_num2ulong(addr) = rb_num2ulong(val); + return Qtrue; +} +static VALUE rb_dfmemory_write_int32(VALUE self, VALUE addr, VALUE val) +{ + *(int*)rb_num2ulong(addr) = rb_num2ulong(val); + return Qtrue; +} + +static VALUE rb_dfmemory_write_float(VALUE self, VALUE addr, VALUE val) +{ + *(float*)rb_num2ulong(addr) = rb_num2dbl(val); + return Qtrue; +} + + +// stl::string +static VALUE rb_dfmemory_stlstring_init(VALUE self, VALUE addr) +{ + // XXX THIS IS TERRIBLE + std::string *ptr = new std::string; + memcpy((void*)rb_num2ulong(addr), (void*)ptr, sizeof(*ptr)); + return Qtrue; +} +static VALUE rb_dfmemory_read_stlstring(VALUE self, VALUE addr) +{ + std::string *s = (std::string*)rb_num2ulong(addr); + return rb_str_new(s->c_str(), s->length()); +} +static VALUE rb_dfmemory_write_stlstring(VALUE self, VALUE addr, VALUE val) +{ + std::string *s = (std::string*)rb_num2ulong(addr); + int strlen = FIX2INT(rb_funcall(val, rb_intern("length"), 0)); + s->assign(rb_string_value_ptr(&val), strlen); + return Qtrue; +} + + +// vector access +static VALUE rb_dfmemory_vec_init(VALUE self, VALUE addr) +{ + std::vector *ptr = new std::vector; + memcpy((void*)rb_num2ulong(addr), (void*)ptr, sizeof(*ptr)); + return Qtrue; +} +// vector +static VALUE rb_dfmemory_vec8_length(VALUE self, VALUE addr) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum(v->size()); +} +static VALUE rb_dfmemory_vec8_ptrat(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum((uint32_t)&v->at(FIX2INT(idx))); +} +static VALUE rb_dfmemory_vec8_insert(VALUE self, VALUE addr, VALUE idx, VALUE val) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->insert(v->begin()+FIX2INT(idx), rb_num2ulong(val)); + return Qtrue; +} +static VALUE rb_dfmemory_vec8_delete(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->erase(v->begin()+FIX2INT(idx)); + return Qtrue; +} + +// vector +static VALUE rb_dfmemory_vec16_length(VALUE self, VALUE addr) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum(v->size()); +} +static VALUE rb_dfmemory_vec16_ptrat(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum((uint32_t)&v->at(FIX2INT(idx))); +} +static VALUE rb_dfmemory_vec16_insert(VALUE self, VALUE addr, VALUE idx, VALUE val) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->insert(v->begin()+FIX2INT(idx), rb_num2ulong(val)); + return Qtrue; +} +static VALUE rb_dfmemory_vec16_delete(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->erase(v->begin()+FIX2INT(idx)); + return Qtrue; +} + +// vector +static VALUE rb_dfmemory_vec32_length(VALUE self, VALUE addr) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum(v->size()); +} +static VALUE rb_dfmemory_vec32_ptrat(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum((uint32_t)&v->at(FIX2INT(idx))); +} +static VALUE rb_dfmemory_vec32_insert(VALUE self, VALUE addr, VALUE idx, VALUE val) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->insert(v->begin()+FIX2INT(idx), rb_num2ulong(val)); + return Qtrue; +} +static VALUE rb_dfmemory_vec32_delete(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->erase(v->begin()+FIX2INT(idx)); + return Qtrue; +} + +// vector +static VALUE rb_dfmemory_vecbool_length(VALUE self, VALUE addr) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return rb_uint2inum(v->size()); +} +static VALUE rb_dfmemory_vecbool_at(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + return v->at(FIX2INT(idx)) ? Qtrue : Qfalse; +} +static VALUE rb_dfmemory_vecbool_setat(VALUE self, VALUE addr, VALUE idx, VALUE val) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->at(FIX2INT(idx)) = (BOOL_ISFALSE(val) ? 0 : 1); + return Qtrue; +} +static VALUE rb_dfmemory_vecbool_insert(VALUE self, VALUE addr, VALUE idx, VALUE val) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->insert(v->begin()+FIX2INT(idx), (BOOL_ISFALSE(val) ? 0 : 1)); + return Qtrue; +} +static VALUE rb_dfmemory_vecbool_delete(VALUE self, VALUE addr, VALUE idx) +{ + std::vector *v = (std::vector*)rb_num2ulong(addr); + v->erase(v->begin()+FIX2INT(idx)); + return Qtrue; +} + +// BitArray +static VALUE rb_dfmemory_bitarray_length(VALUE self, VALUE addr) +{ + DFHack::BitArray *b = (DFHack::BitArray*)rb_num2ulong(addr); + return rb_uint2inum(b->size*8); // b->size is in bytes +} +static VALUE rb_dfmemory_bitarray_resize(VALUE self, VALUE addr, VALUE sz) +{ + DFHack::BitArray *b = (DFHack::BitArray*)rb_num2ulong(addr); + b->resize(rb_num2ulong(sz)); + return Qtrue; +} +static VALUE rb_dfmemory_bitarray_isset(VALUE self, VALUE addr, VALUE idx) +{ + DFHack::BitArray *b = (DFHack::BitArray*)rb_num2ulong(addr); + return b->is_set(rb_num2ulong(idx)) ? Qtrue : Qfalse; +} +static VALUE rb_dfmemory_bitarray_set(VALUE self, VALUE addr, VALUE idx, VALUE val) +{ + DFHack::BitArray *b = (DFHack::BitArray*)rb_num2ulong(addr); + b->set(rb_num2ulong(idx), (BOOL_ISFALSE(val) ? 0 : 1)); + return Qtrue; +} + + +/* 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 + 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_uint2inum(ret); +} + + + +// define module DFHack and its methods +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); + rb_define_singleton_method(rb_cDFHack, "memory_read_int16", RUBY_METHOD_FUNC(rb_dfmemory_read_int16), 1); + rb_define_singleton_method(rb_cDFHack, "memory_read_int32", RUBY_METHOD_FUNC(rb_dfmemory_read_int32), 1); + rb_define_singleton_method(rb_cDFHack, "memory_read_float", RUBY_METHOD_FUNC(rb_dfmemory_read_float), 1); + + rb_define_singleton_method(rb_cDFHack, "memory_write", RUBY_METHOD_FUNC(rb_dfmemory_write), 2); + rb_define_singleton_method(rb_cDFHack, "memory_write_int8", RUBY_METHOD_FUNC(rb_dfmemory_write_int8), 2); + rb_define_singleton_method(rb_cDFHack, "memory_write_int16", RUBY_METHOD_FUNC(rb_dfmemory_write_int16), 2); + rb_define_singleton_method(rb_cDFHack, "memory_write_int32", RUBY_METHOD_FUNC(rb_dfmemory_write_int32), 2); + rb_define_singleton_method(rb_cDFHack, "memory_write_float", RUBY_METHOD_FUNC(rb_dfmemory_write_float), 2); + + rb_define_singleton_method(rb_cDFHack, "memory_stlstring_init", RUBY_METHOD_FUNC(rb_dfmemory_stlstring_init), 1); + rb_define_singleton_method(rb_cDFHack, "memory_read_stlstring", RUBY_METHOD_FUNC(rb_dfmemory_read_stlstring), 1); + rb_define_singleton_method(rb_cDFHack, "memory_write_stlstring", RUBY_METHOD_FUNC(rb_dfmemory_write_stlstring), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vector_init", RUBY_METHOD_FUNC(rb_dfmemory_vec_init), 1); + rb_define_singleton_method(rb_cDFHack, "memory_vector8_length", RUBY_METHOD_FUNC(rb_dfmemory_vec8_length), 1); + rb_define_singleton_method(rb_cDFHack, "memory_vector8_ptrat", RUBY_METHOD_FUNC(rb_dfmemory_vec8_ptrat), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vector8_insert", RUBY_METHOD_FUNC(rb_dfmemory_vec8_insert), 3); + rb_define_singleton_method(rb_cDFHack, "memory_vector8_delete", RUBY_METHOD_FUNC(rb_dfmemory_vec8_delete), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vector16_length", RUBY_METHOD_FUNC(rb_dfmemory_vec16_length), 1); + rb_define_singleton_method(rb_cDFHack, "memory_vector16_ptrat", RUBY_METHOD_FUNC(rb_dfmemory_vec16_ptrat), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vector16_insert", RUBY_METHOD_FUNC(rb_dfmemory_vec16_insert), 3); + rb_define_singleton_method(rb_cDFHack, "memory_vector16_delete", RUBY_METHOD_FUNC(rb_dfmemory_vec16_delete), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vector32_length", RUBY_METHOD_FUNC(rb_dfmemory_vec32_length), 1); + rb_define_singleton_method(rb_cDFHack, "memory_vector32_ptrat", RUBY_METHOD_FUNC(rb_dfmemory_vec32_ptrat), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vector32_insert", RUBY_METHOD_FUNC(rb_dfmemory_vec32_insert), 3); + rb_define_singleton_method(rb_cDFHack, "memory_vector32_delete", RUBY_METHOD_FUNC(rb_dfmemory_vec32_delete), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vectorbool_length", RUBY_METHOD_FUNC(rb_dfmemory_vecbool_length), 1); + rb_define_singleton_method(rb_cDFHack, "memory_vectorbool_at", RUBY_METHOD_FUNC(rb_dfmemory_vecbool_at), 2); + rb_define_singleton_method(rb_cDFHack, "memory_vectorbool_setat", RUBY_METHOD_FUNC(rb_dfmemory_vecbool_setat), 3); + rb_define_singleton_method(rb_cDFHack, "memory_vectorbool_insert", RUBY_METHOD_FUNC(rb_dfmemory_vecbool_insert), 3); + rb_define_singleton_method(rb_cDFHack, "memory_vectorbool_delete", RUBY_METHOD_FUNC(rb_dfmemory_vecbool_delete), 2); + rb_define_singleton_method(rb_cDFHack, "memory_bitarray_length", RUBY_METHOD_FUNC(rb_dfmemory_bitarray_length), 1); + 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 new file mode 100644 index 000000000..8c218eb63 --- /dev/null +++ b/plugins/ruby/ruby.rb @@ -0,0 +1,302 @@ +require 'hack/ruby-autogen' + +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 + + def puts_err(*a) + a.flatten.each { |l| + DFHack.print_err(l.to_s.chomp + "\n") + } + nil + end + + def p(*a) + a.each { |e| + puts_err e.inspect + } + end + end + + # register a callback to be called every gframe or more + # ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 } + def onupdate_register(&b) + @onupdate_list ||= [] + @onupdate_list << b + DFHack.onupdate_active = true + @onupdate_list.last + end + + # delete the callback for onupdate ; use the value returned by onupdate_register + def onupdate_unregister(b) + @onupdate_list.delete b + DFHack.onupdate_active = false if @onupdate_list.empty? + end + + # this method is called by dfhack every 'onupdate' if onupdate_active is true + def onupdate + @onupdate_list ||= [] + @onupdate_list.each { |cb| cb.call } + end + + # register a callback to be called every gframe or more + # ex: DFHack.onstatechange_register { |newstate| puts "state changed to #{newstate}" } + def onstatechange_register(&b) + @onstatechange_list ||= [] + @onstatechange_list << b + @onstatechange_list.last + end + + # delete the callback for onstatechange ; use the value returned by onstatechange_register + def onstatechange_unregister(b) + @onstatechange_list.delete b + end + + # this method is called by dfhack every 'onstatechange' + def onstatechange(newstate) + @onstatechange_list ||= [] + @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) + end + + # returns true if both arguments are at the same x/y/z + def same_pos?(pos1, pos2) + pos1 = pos1.pos if pos1.respond_to?(:pos) + pos2 = pos2.pos if pos2.respond_to?(:pos) + 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' + def match_rawname(name, rawlist) + rawlist.each { |r| return r if name == r } + rawlist.each { |r| return r if name.downcase == r.downcase } + 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 + +# global alias so we can write 'df.world.units.all[0]' +def df + DFHack +end + +# load user-specified startup file +load 'ruby_custom.rb' if File.exist?('ruby_custom.rb')