diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 6c2a537d1..39fdcba9e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -35,6 +35,11 @@ if (BUILD_DWARFEXPORT) add_subdirectory (dwarfexport) endif() +OPTION(BUILD_RUBY "Build ruby binding." OFF) +if (BUILD_RUBY) + add_subdirectory (ruby) +endif() + # Protobuf FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto) diff --git a/plugins/ruby/CMakeLists.txt b/plugins/ruby/CMakeLists.txt new file mode 100644 index 000000000..dc5f0284a --- /dev/null +++ b/plugins/ruby/CMakeLists.txt @@ -0,0 +1,7 @@ +find_package(Ruby) +if(RUBY_FOUND) + include_directories("${dfhack_SOURCE_DIR}/depends/tthread" ${RUBY_INCLUDE_PATH}) + DFHACK_PLUGIN(ruby ruby.cpp LINK_LIBRARIES dfhack-tinythread) +else(RUBY_FOUND) + MESSAGE(STATUS "Required library (ruby) not found - ruby plugin can't be built.") +endif(RUBY_FOUND) diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp new file mode 100644 index 000000000..7ecda28ca --- /dev/null +++ b/plugins/ruby/ruby.cpp @@ -0,0 +1,609 @@ +// blindly copied imports from fastdwarf +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" + +#include "DataDefs.h" +#include "df/world.h" +#include "df/unit.h" + +#include "tinythread.h" + +#include + +using std::string; +using std::vector; +using namespace DFHack; + + +static void df_rubythread(void*); +static command_result df_rubyload(color_ostream &out, vector & parameters); +static command_result df_rubyeval(color_ostream &out, vector & parameters); +static void ruby_bind_dfhack(void); + +// inter-thread communication stuff +enum RB_command { + RB_IDLE, + RB_INIT, + RB_DIE, + RB_LOAD, + 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 interface +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)); + + commands.push_back(PluginCommand("r", + "Ruby interpreter dev. 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; +} + +DFhackCExport command_result plugin_onupdate ( color_ostream &out ) +{ + if (!onupdate_active) + return CR_OK; + + command_result ret; + + m_mutex->lock(); + if (!r_thread) + return CR_OK; + + r_type = RB_EVAL; + r_command = "DFHack.onupdate"; + m_irun->unlock(); + + while (r_type != RB_IDLE) + tthread::this_thread::yield(); + + ret = r_result; + + m_irun->lock(); + m_mutex->unlock(); + + return ret; +} + +static command_result df_rubyload(color_ostream &out, vector & parameters) +{ + command_result ret; + + 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; + } + + // serialize 'accesses' to the ruby thread + m_mutex->lock(); + if (!r_thread) + // raced with plugin_shutdown ? + return CR_OK; + + r_type = RB_LOAD; + r_command = parameters[0].c_str(); + 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 df_rubyeval(color_ostream &out, 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 = ""; + full += "DFHack.puts(("; + + for (unsigned i=0 ; ilock(); + if (!r_thread) + return CR_OK; + + r_type = RB_EVAL; + r_command = full.c_str(); + m_irun->unlock(); + + while (r_type != RB_IDLE) + tthread::this_thread::yield(); + + ret = r_result; + + m_irun->lock(); + m_mutex->unlock(); + + return ret; +} + + + +// ruby thread code +static void dump_rb_error(void) +{ + VALUE s, err; + + err = rb_gv_get("$!"); + + s = rb_funcall(err, rb_intern("class"), 0); + s = rb_funcall(s, rb_intern("name"), 0); + Core::printerr("E: %s: ", rb_string_value_ptr(&s)); + + s = rb_funcall(err, rb_intern("message"), 0); + Core::printerr("%s\n", rb_string_value_ptr(&s)); + + err = rb_funcall(err, rb_intern("backtrace"), 0); + for (int i=0 ; i<8 ; ++i) + if ((s = rb_ary_shift(err)) != Qnil) + Core::printerr(" %s\n", rb_string_value_ptr(&s)); +} + +// ruby thread main loop +static void df_rubythread(void *p) +{ + int state, running; + + // initialize the ruby interpreter + ruby_init(); + ruby_init_loadpath(); + // default value for the $0 "current script name" + ruby_script("dfhack"); + + // create the ruby objects to map DFHack to ruby methods + ruby_bind_dfhack(); + + r_result = CR_OK; + r_type = RB_IDLE; + + running = 1; + while (running) { + // wait for new command + m_irun->lock(); + + switch (r_type) { + case RB_IDLE: + case RB_INIT: + break; + + case RB_DIE: + running = 0; + ruby_finalize(); + break; + + case RB_LOAD: + state = 0; + rb_load_protect(rb_str_new2(r_command), Qfalse, &state); + if (state) + dump_rb_error(); + 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(); + } +} + + + +// ruby classes +static VALUE rb_cDFHack; +static VALUE rb_c_WrapData; + + +// 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 = (val == Qtrue || val == INT2FIX(1)) ? 1 : 0; + return Qtrue; +} + +static VALUE rb_dfresume(VALUE self) +{ + Core::getInstance().Resume(); + return Qtrue; +} + +static VALUE rb_dfsuspend(VALUE self) +{ + Core::getInstance().Suspend(); + return Qtrue; +} + +/* +static VALUE rb_dfgetversion(VALUE self) +{ + return rb_str_new2(getcore().vinfo->getVersion().c_str()); +} +*/ + +// TODO color_ostream proxy yadda yadda +static VALUE rb_dfprint_str(VALUE self, VALUE s) +{ + //getcore().con.print("%s", rb_string_value_ptr(&s)); + Core::printerr("%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; +} + +// raw memory access +// WARNING: may cause game crash ! double-check your addresses ! +static VALUE rb_dfmemread(VALUE self, VALUE addr, VALUE len) +{ + return rb_str_new((char*)rb_num2ulong(addr), rb_num2ulong(len)); +} + +static VALUE rb_dfmemwrite(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; +} + +static VALUE rb_dfmalloc(VALUE self, VALUE len) +{ + return rb_uint2inum((long)malloc(FIX2INT(len))); +} + +static VALUE rb_dffree(VALUE self, VALUE ptr) +{ + free((void*)rb_num2ulong(ptr)); + return Qtrue; +} + +// raw c++ wrappers +// return the nth element of a vector +static VALUE rb_dfvectorat(VALUE self, VALUE vect_addr, VALUE idx) +{ + vector *v = (vector*)rb_num2ulong(vect_addr); + return rb_uint2inum(v->at(FIX2INT(idx))); +} + +// return a c++ string as a ruby string (nul-terminated) +static VALUE rb_dfreadstring(VALUE self, VALUE str_addr) +{ + string *s = (string*)rb_num2ulong(str_addr); + return rb_str_new2(s->c_str()); +} + + + + +/* XXX this needs a custom DFHack::Plugin subclass to pass the cmdname to invoke(), to match the ruby callback +// register a ruby method as dfhack console command +// usage: DFHack.register("moo", "this commands prints moo on the console") { DFHack.puts "moo !" } +static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr) +{ + commands.push_back(PluginCommand(rb_string_value_ptr(&name), + rb_string_value_ptr(&descr), + df_rubycustom)); + + return Qtrue; +} +*/ +static VALUE rb_dfregister(VALUE self, VALUE name, VALUE descr) +{ + rb_raise(rb_eRuntimeError, "not implemented"); +} + + +// return the address of the struct in DF memory (for raw memread/write) +static VALUE rb_memaddr(VALUE self) +{ + void *data; + Data_Get_Struct(self, void, data); + + return rb_uint2inum((uint32_t)data); +} + + + + +// BEGIN GENERATED SECTION + +// begin generated T_cursor binding +static VALUE rb_c_T_cursor; + +static VALUE rb_m_T_cursor_x(VALUE self) { + struct df::global::T_cursor *var; + Data_Get_Struct(self, struct df::global::T_cursor, var); + return rb_uint2inum(var->x); +} +static VALUE rb_m_T_cursor_x_set(VALUE self, VALUE val) { + struct df::global::T_cursor *var; + Data_Get_Struct(self, struct df::global::T_cursor, var); + var->x = rb_num2ulong(val); + return Qtrue; +} + +static VALUE rb_m_T_cursor_y(VALUE self) { + struct df::global::T_cursor *var; + Data_Get_Struct(self, struct df::global::T_cursor, var); + return rb_uint2inum(var->y); +} +static VALUE rb_m_T_cursor_y_set(VALUE self, VALUE val) { + struct df::global::T_cursor *var; + Data_Get_Struct(self, struct df::global::T_cursor, var); + var->y = rb_num2ulong(val); + return Qtrue; +} + +static VALUE rb_m_T_cursor_z(VALUE self) { + struct df::global::T_cursor *var; + Data_Get_Struct(self, struct df::global::T_cursor, var); + return rb_uint2inum(var->z); +} +static VALUE rb_m_T_cursor_z_set(VALUE self, VALUE val) { + struct df::global::T_cursor *var; + Data_Get_Struct(self, struct df::global::T_cursor, var); + var->z = rb_num2ulong(val); + return Qtrue; +} + +// link methods to the class +static void ruby_bind_T_cursor(void) { + // create a class, child of WrapData, in module DFHack + rb_c_T_cursor = rb_define_class_under(rb_cDFHack, "T_cursor", rb_c_WrapData); + + // reader for 'x' (0 = no arg) + rb_define_method(rb_c_T_cursor, "x", RUBY_METHOD_FUNC(rb_m_T_cursor_x), 0); + // writer for 'x' (1 arg) + rb_define_method(rb_c_T_cursor, "x=", RUBY_METHOD_FUNC(rb_m_T_cursor_x_set), 1); + rb_define_method(rb_c_T_cursor, "y", RUBY_METHOD_FUNC(rb_m_T_cursor_y), 0); + rb_define_method(rb_c_T_cursor, "y=", RUBY_METHOD_FUNC(rb_m_T_cursor_y_set), 1); + rb_define_method(rb_c_T_cursor, "z", RUBY_METHOD_FUNC(rb_m_T_cursor_z), 0); + rb_define_method(rb_c_T_cursor, "z=", RUBY_METHOD_FUNC(rb_m_T_cursor_z_set), 1); +} + + +// create an instance of T_cursor from global::cursor +// this method is linked to DFHack.cursor in ruby_bind_dfhack() +static VALUE rb_global_cursor(VALUE self) { + return Data_Wrap_Struct(rb_c_T_cursor, 0, 0, df::global::cursor); +} + + +// begin generated unit binding +static VALUE rb_c_unit; + +static VALUE rb_m_unit_id(VALUE self) { + struct df::unit *var; + Data_Get_Struct(self, struct df::unit, var); + + return rb_uint2inum(var->id); +} +static VALUE rb_m_unit_id_set(VALUE self, VALUE val) { + struct df::unit *var; + Data_Get_Struct(self, struct df::unit, var); + var->id = rb_num2ulong(val); + return Qtrue; +} + +static void ruby_bind_unit(void) { + // ruby class name must begin with uppercase + rb_c_unit = rb_define_class_under(rb_cDFHack, "Unit", rb_c_WrapData); + + rb_define_method(rb_c_unit, "id", RUBY_METHOD_FUNC(rb_m_unit_id), 0); + rb_define_method(rb_c_unit, "id=", RUBY_METHOD_FUNC(rb_m_unit_id_set), 1); +} + + +// begin generated world binding +static VALUE rb_c_world; +static VALUE rb_c_world_T_units; + +static VALUE rb_m_world_T_units_all(VALUE self) { + struct df::world::T_units *var; + Data_Get_Struct(self, struct df::world::T_units, var); + + // read a vector + VALUE ret = rb_ary_new(); + for (unsigned i=0 ; iall.size() ; ++i) + rb_ary_push(ret, Data_Wrap_Struct(rb_c_unit, 0, 0, var->all[i])); + + return ret; +} + +static VALUE rb_m_world_units(VALUE self) { + struct df::world *var; + Data_Get_Struct(self, struct df::world, var); + return Data_Wrap_Struct(rb_c_world_T_units, 0, 0, &var->units); +} + +static void ruby_bind_world(void) { + rb_c_world = rb_define_class_under(rb_cDFHack, "World", rb_c_WrapData); + rb_c_world_T_units = rb_define_class_under(rb_c_world, "T_units", rb_c_WrapData); + + rb_define_method(rb_c_world, "units", RUBY_METHOD_FUNC(rb_m_world_units), 0); +} + +static VALUE rb_global_world(VALUE self) { + return Data_Wrap_Struct(rb_c_world, 0, 0, df::global::world); +} + +/* +static VALUE rb_dfcreatures(VALUE self) +{ + OffsetGroup *ogc = getcore().vinfo->getGroup("Creatures"); + vector *v = (vector*)ogc->getAddress("vector"); + + VALUE ret = rb_ary_new(); + for (unsigned i=0 ; isize() ; ++i) + rb_ary_push(ret, Data_Wrap_Struct(rb_cCreature, 0, 0, v->at(i))); + + return ret; +} + +static VALUE rb_getlaborname(VALUE self, VALUE idx) +{ + return rb_str_new2(getcore().vinfo->getLabor(FIX2INT(idx)).c_str()); +} + +static VALUE rb_getskillname(VALUE self, VALUE idx) +{ + return rb_str_new2(getcore().vinfo->getSkill(FIX2INT(idx)).c_str()); +} + +static VALUE rb_mapblock(VALUE self, VALUE x, VALUE y, VALUE z) +{ + Maps *map; + Data_Get_Struct(self, Maps, map); + df_block *block; + + block = map->getBlock(FIX2INT(x)/16, FIX2INT(y)/16, FIX2INT(z)); + if (!block) + return Qnil; + + return Data_Wrap_Struct(rb_cMapBlock, 0, 0, block); +} +*/ + + + + +// 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, "resume", RUBY_METHOD_FUNC(rb_dfresume), 0); + //rb_define_singleton_method(rb_cDFHack, "version", RUBY_METHOD_FUNC(rb_dfgetversion), 0); + 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, "memread", RUBY_METHOD_FUNC(rb_dfmemread), 2); + rb_define_singleton_method(rb_cDFHack, "memwrite", RUBY_METHOD_FUNC(rb_dfmemwrite), 2); + 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, "vectorat", RUBY_METHOD_FUNC(rb_dfvectorat), 2); + rb_define_singleton_method(rb_cDFHack, "readstring", RUBY_METHOD_FUNC(rb_dfreadstring), 1); + rb_define_singleton_method(rb_cDFHack, "register_dfcommand", RUBY_METHOD_FUNC(rb_dfregister), 2); + + // accessors for dfhack globals + rb_define_singleton_method(rb_cDFHack, "cursor", RUBY_METHOD_FUNC(rb_global_cursor), 0); + rb_define_singleton_method(rb_cDFHack, "world", RUBY_METHOD_FUNC(rb_global_world), 0); + + // parent class for all wrapped objects + rb_c_WrapData = rb_define_class_under(rb_cDFHack, "WrapData", rb_cObject); + rb_define_method(rb_c_WrapData, "memaddr", RUBY_METHOD_FUNC(rb_memaddr), 0); + + // call generated bindings + ruby_bind_T_cursor(); + ruby_bind_unit(); + ruby_bind_world(); + + // load the default ruby-level definitions + int state=0; + rb_load_protect(rb_str_new2("./hack/plugins/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..1ad7dbab3 --- /dev/null +++ b/plugins/ruby/ruby.rb @@ -0,0 +1,44 @@ +module DFHack + def suspend + if block_given? + begin + do_suspend + yield + ensure + resume + end + else + do_suspend + end + end + + def puts(*a) + a.flatten.each { |l| + print_str(l.to_s.chomp + "\n") + } + end + + def puts_err(*a) + a.flatten.each { |l| + print_err(l.to_s.chomp + "\n") + } + end + + def test + puts "starting" + + suspend { + c = cursor + puts "cursor pos: #{c.x} #{c.y} #{c.z}" + + puts "unit[0] id: #{world.units.all[0].id}" + } + + puts "done" + end +end + +end + +# load user-specified startup file +load 'ruby_custom.rb' if File.exist?('ruby_custom.rb')