From 3f4d2e4792850991ae3ab5a11e813488e993b2a4 Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 24 Jun 2012 17:30:26 +0200 Subject: [PATCH] ruby: split ruby.rb in modules --- plugins/ruby/README | 111 ++-- plugins/ruby/building.rb | 499 ++++++++-------- plugins/ruby/item.rb | 21 + plugins/ruby/job.rb | 35 ++ plugins/ruby/map.rb | 54 ++ plugins/ruby/plant.rb | 296 +++++----- plugins/ruby/ruby-autogen-defs.rb | 751 ++++++++++++++++++++++++ plugins/ruby/ruby.rb | 945 +----------------------------- plugins/ruby/ui.rb | 65 ++ plugins/ruby/unit.rb | 122 ++-- 10 files changed, 1485 insertions(+), 1414 deletions(-) create mode 100644 plugins/ruby/item.rb create mode 100644 plugins/ruby/job.rb create mode 100644 plugins/ruby/map.rb create mode 100644 plugins/ruby/ruby-autogen-defs.rb create mode 100644 plugins/ruby/ui.rb diff --git a/plugins/ruby/README b/plugins/ruby/README index 9dc7d49f6..4bfc20957 100644 --- a/plugins/ruby/README +++ b/plugins/ruby/README @@ -2,61 +2,110 @@ This plugins embeds a ruby interpreter inside DFHack (ie inside Dwarf Fortress). The plugin maps all the structures available in library/xml/ to ruby objects. -These objects are described in ruby-autogen.rb, they are all in the DFHack:: +These objects are described in ruby-autogen.rb, they are all in the DFHack module. The toplevel 'df' method is a shortcut to the DFHack module. The plugin does *not* map most of dfhack methods (MapCache, ...) ; only direct access to the raw DF data structures in memory is provided. -Some library methods are stored in the ruby.rb file, with shortcuts to read a -map block, find an unit or an item, etc. +Some library methods are stored in the various .rb file, e.g. shortcuts to read +a map block, find an unit or an item, etc. -Global objects are accessible through the 'df' accessor (eg df.world). +Global dfhack objects are accessible through the 'df' accessor (eg 'df.world'). -The ruby plugin defines 2 dfhack console commands: - rb_load ; load a ruby script. Ex: rb_load hack/plants.rb (no quotes) - rb_eval ; evaluate a ruby expression, show the result in the -console. Ex: rb_eval df.find_unit.name.first_name + +DFHack console +-------------- + +The ruby plugin defines 1 dfhack console command: + rb_eval ; evaluate a ruby expression and show the result in +the console. Ex: rb_eval df.unit_find().name.first_name You can use single-quotes for strings ; avoid double-quotes that are parsed -and removed by the dfhack console. +and removed by the dfhack console code. + +Text output from ruby code, through the standard 'puts', 'p' or 'raise' are +redirected to the dfhack console window. If dfhack reports 'rb_eval is not a recognized command', check stderr.log. You need a valid 32-bit ruby library to work, and ruby1.8 is prefered (ruby1.9 may crash DF on startup for now). Install the library in the df root folder (or -hack/ on linux), the library should be named 'libruby.dll' (.so on linux). +df/hack/ on linux), the library should be named 'libruby.dll' (.so on linux). You can download a tested version at http://github.com/jjyg/dfhack/downloads/ -The plugin also interfaces with dfhack 'onupdate' hook. + +DFHack callbacks +---------------- + +The plugin interfaces with dfhack 'onupdate' hook. To register ruby code to be run every graphic frame, use: handle = df.onupdate_register { puts 'i love flooding the console' } To stop being called, use: df.onupdate_unregister handle -The same mechanism is available for onstatechange. +The same mechanism is available for 'onstatechange'. + + +C++ object manipulation +----------------------- + +The ruby classes defined in ruby-autogen.rb are accessors to the underlying +df C++ objects in-memory. To allocate a new C++ object for use in DF, use the +RubyClass.cpp_new method (see buildings.rb for exemples), works for Compounds +only. + +Deallocation is not supported. You may manually call df.free if you know +what you are doing (maps directly to the native malloc/free) + +C++ std::string fields may be directly re-allocated using standard ruby strings, +e.g. some_unit.name.nickname = 'moo' +More subtle string manipulation, e.g. changing a single character, are not +supported. Read the whole string, manipulate it in ruby, and re-assign it +instead. + +C++ std::vector<> can be iterated as standard ruby Enumerable objects, using +each/map/etc. +To append data to a vector, use vector << newelement or vector.push(newelement) +To insert at a given pos, vector.insert_at(index, value) +To delete an element, vector.delete_at(index) + +You can binary search an element in a vector for a given numeric field value: + df.world.unit.all.binsearch(42, :id) +will find the element whose 'id' field is 42 (needs the vector to be initially +sorted by this field). The binsearch 2nd argument defaults to :id. + +Any numeric field defined as being an enum value will be converted to a ruby +Symbol. This works for array indexes too. + +Virtual method calls are supported for C++ objects, with a maximum of 4 +arguments. Arguments / return value are interpreted as Compound/Enums as +specified in the vmethod definition in the xmls. + +Pointer fields are automatically dereferenced ; so a vector of pointer to +Units will yield Units directly. NULL pointers yield the 'nil' value. Exemples -------- -For more complex exemples, check the ruby/plugins/ source folder. +For more complex exemples, check the dfhack/scripts/*.rb files. Show info on the currently selected unit ('v' or 'k' DF menu) - p df.find_unit.flags1 + p df.unit_find.flags1 Set a custom nickname to unit with id '123' - df.find_unit(123).name.nickname = 'moo' + df.unit_find(123).name.nickname = 'moo' Show current unit profession - p df.find_unit.profession + p df.unit_find.profession Change current unit profession - df.find_unit.profession = :MASON + df.unit_find.profession = :MASON -Center the screen on unit '123' - df.center_viewscreen(df.find_unit(123)) +Center the screen on unit ID '123' + df.center_viewscreen(df.unit_find(123)) Find an item at a given position, show its C++ classname - df.find_item(df.cursor)._rtti_classname + p df.item_find(df.cursor)._rtti_classname Find the raws name of the plant under cursor plant = df.world.plants.all.find { |plt| df.at_cursor?(plt) } @@ -67,15 +116,14 @@ Dig a channel under the cursor df.map_block_at(df.cursor).flags.designated = true -Compilation ------------ +Plugin compilation +------------------ -The plugin consists of the ruby.rb file including user comfort functions and +The plugin consists of the *.rb file including user comfort functions and describing basic classes used by the autogenerated code, and ruby-autogen.rb, the auto-generated code. -The generated code is generated by codegen.pl, which takes the codegen.out.xml -file as input. +autogen is output by codegen.pl from dfhack/library/include/df/codegen.out.xml For exemple, @@ -89,17 +137,10 @@ Will generate field(:custom_profession, 60) { stl_string } field(:profession, 64) { number 16, true } -The syntax for the 'field' method is: +The syntax for the 'field' method in ruby-autogen.rb is: 1st argument = name of the method -2nd argument = offset of this field from the beginning of the struct. - +2nd argument = offset of this field from the beginning of the current struct. The block argument describes the type of the field: uint32, ptr to global... -Primitive type access is done through native methods in ruby.cpp (vector length, +Primitive type access is done through native methods from ruby.cpp (vector length, raw memory access, etc) - -MemHack::Pointers are automatically dereferenced ; so a vector of pointer to -Units will yield Units directly. Null pointers yield the 'nil' value. - -This allows to use code such as 'df.world.units.all[0].pos', with 'all' being -in fact a vector of *pointers* to DFHack::Unit objects. diff --git a/plugins/ruby/building.rb b/plugins/ruby/building.rb index 5dfbcdacd..29e10ae29 100644 --- a/plugins/ruby/building.rb +++ b/plugins/ruby/building.rb @@ -1,266 +1,267 @@ module DFHack + class << self + # allocate a new building object + def building_alloc(type, subtype=-1, custom=-1) + cls = rtti_n2c[BuildingType::Classname[type].to_sym] + raise "invalid building type #{type.inspect}" if not cls + bld = cls.cpp_new + bld.race = ui.race_id + bld.setSubtype(subtype) if subtype != -1 + bld.setCustomType(custom) if custom != -1 + case type + when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0 + when :Coffin; bld.initBurialFlags + when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate + end + bld + end -# allocate a new building object -def self.building_alloc(type, subtype=-1, custom=-1) - cls = rtti_n2c[BuildingType::Classname[type].to_sym] - raise "invalid building type #{type.inspect}" if not cls - bld = cls.cpp_new - bld.race = ui.race_id - bld.setSubtype(subtype) if subtype != -1 - bld.setCustomType(custom) if custom != -1 - case type - when :Furnace; bld.melt_remainder[world.raws.inorganics.length] = 0 - when :Coffin; bld.initBurialFlags - when :Trap; bld.unk_cc = 500 if bld.trap_type == :PressurePlate - end - bld -end + # used by building_setsize + def building_check_bridge_support(bld) + x1 = bld.x1-1 + x2 = bld.x2+1 + y1 = bld.y1-1 + y2 = bld.y2+1 + z = bld.z + (x1..x2).each { |x| + (y1..y2).each { |y| + next if ((x == x1 or x == x2) and + (y == y1 or y == y2)) + if mb = map_block_at(x, y, z) and tile = mb.tiletype[x%16][y%16] and TiletypeShape::BasicShape[Tiletype::Shape[tile]] != :Open + bld.gate_flags.has_support = true + return + end + } + } + bld.gate_flags.has_support = false + end -# used by building_setsize -def self.building_check_bridge_support(bld) - x1 = bld.x1-1 - x2 = bld.x2+1 - y1 = bld.y1-1 - y2 = bld.y2+1 - z = bld.z - (x1..x2).each { |x| - (y1..y2).each { |y| - next if ((x == x1 or x == x2) and - (y == y1 or y == y2)) - if mb = map_block_at(x, y, z) and tile = mb.tiletype[x%16][y%16] and TiletypeShape::BasicShape[Tiletype::Shape[tile]] == :Open - bld.gate_flags.has_support = true - return - end - } - } - bld.gate_flags.has_support = false -end + # sets x2/centerx/y2/centery from x1/y1/bldtype + # x2/y2 preserved for :FarmPlot etc + def building_setsize(bld) + bld.x2 = bld.x1 if bld.x1 > bld.x2 + bld.y2 = bld.y1 if bld.y1 > bld.y2 + case bld.getType + when :Bridge + bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 + bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 + building_check_bridge_support(bld) + when :FarmPlot, :RoadDirt, :RoadPaved, :Stockpile, :Civzone + bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 + bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 + when :TradeDepot, :Shop + bld.x2 = bld.x1+4 + bld.y2 = bld.y1+4 + bld.centerx = bld.x1+2 + bld.centery = bld.y1+2 + when :SiegeEngine, :Windmill, :Wagon + bld.x2 = bld.x1+2 + bld.y2 = bld.y1+2 + bld.centerx = bld.x1+1 + bld.centery = bld.y1+1 + when :AxleHorizontal + if bld.is_vertical == 1 + bld.x2 = bld.centerx = bld.x1 + bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 + else + bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 + bld.y2 = bld.centery = bld.y1 + end + when :WaterWheel + if bld.is_vertical == 1 + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.y1+2 + bld.centery = bld.y1+1 + else + bld.x2 = bld.x1+2 + bld.centerx = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + end + when :Workshop, :Furnace + # Furnace = Custom or default case only + case bld.type + when :Quern, :Millstone, :Tool + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + when :Siege, :Kennels + bld.x2 = bld.x1+4 + bld.y2 = bld.y1+4 + bld.centerx = bld.x1+2 + bld.centery = bld.y1+2 + when :Custom + if bdef = world.raws.buildings.all.binsearch(bld.getCustomType) + bld.x2 = bld.x1 + bdef.dim_x - 1 + bld.y2 = bld.y1 + bdef.dim_y - 1 + bld.centerx = bld.x1 + bdef.workloc_x + bld.centery = bld.y1 + bdef.workloc_y + end + else + bld.x2 = bld.x1+2 + bld.y2 = bld.y1+2 + bld.centerx = bld.x1+1 + bld.centery = bld.y1+1 + end + when :ScrewPump + case bld.direction + when :FromEast + bld.x2 = bld.centerx = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + when :FromSouth + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1+1 + when :FromWest + bld.x2 = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + bld.centerx = bld.x1 + else + bld.x2 = bld.x1+1 + bld.y2 = bld.centery = bld.y1 + bld.centerx = bld.x1 + end + when :Well + bld.bucket_z = bld.z + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + when :Construction + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + bld.setMaterialAmount(1) + return + else + bld.x2 = bld.centerx = bld.x1 + bld.y2 = bld.centery = bld.y1 + end + bld.setMaterialAmount((bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1) + end -# sets x2/centerx/y2/centery from x1/y1/bldtype -# x2/y2 preserved for :FarmPlot etc -def self.building_setsize(bld) - bld.x2 = bld.x1 if bld.x1 > bld.x2 - bld.y2 = bld.y1 if bld.y1 > bld.y2 - case bld.getType - when :Bridge - bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 - bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 - building_check_bridge_support(bld) - when :FarmPlot, :RoadDirt, :RoadPaved, :Stockpile, :Civzone - bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 - bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 - when :TradeDepot, :Shop - bld.x2 = bld.x1+4 - bld.y2 = bld.y1+4 - bld.centerx = bld.x1+2 - bld.centery = bld.y1+2 - when :SiegeEngine, :Windmill, :Wagon - bld.x2 = bld.x1+2 - bld.y2 = bld.y1+2 - bld.centerx = bld.x1+1 - bld.centery = bld.y1+1 - when :AxleHorizontal - if bld.is_vertical == 1 - bld.x2 = bld.centerx = bld.x1 - bld.centery = bld.y1 + (bld.y2+1-bld.y1)/2 - else - bld.centerx = bld.x1 + (bld.x2+1-bld.x1)/2 - bld.y2 = bld.centery = bld.y1 - end - when :WaterWheel - if bld.is_vertical == 1 - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.y1+2 - bld.centery = bld.y1+1 - else - bld.x2 = bld.x1+2 - bld.centerx = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - end - when :Workshop, :Furnace - # Furnace = Custom or default case only - case bld.type - when :Quern, :Millstone, :Tool - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - when :Siege, :Kennels - bld.x2 = bld.x1+4 - bld.y2 = bld.y1+4 - bld.centerx = bld.x1+2 - bld.centery = bld.y1+2 - when :Custom - if bdef = world.raws.buildings.all.binsearch(bld.getCustomType) - bld.x2 = bld.x1 + bdef.dim_x - 1 - bld.y2 = bld.y1 + bdef.dim_y - 1 - bld.centerx = bld.x1 + bdef.workloc_x - bld.centery = bld.y1 + bdef.workloc_y - end - else - bld.x2 = bld.x1+2 - bld.y2 = bld.y1+2 - bld.centerx = bld.x1+1 - bld.centery = bld.y1+1 - end - when :ScrewPump - case bld.direction - when :FromEast - bld.x2 = bld.centerx = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - when :FromSouth - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1+1 - when :FromWest - bld.x2 = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - bld.centerx = bld.x1 - else - bld.x2 = bld.x1+1 - bld.y2 = bld.centery = bld.y1 - bld.centerx = bld.x1 - end - when :Well - bld.bucket_z = bld.z - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - when :Construction - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - bld.setMaterialAmount(1) - return - else - bld.x2 = bld.centerx = bld.x1 - bld.y2 = bld.centery = bld.y1 - end - bld.setMaterialAmount((bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1) -end + # set building at position, with optional width/height + def building_position(bld, pos, w=nil, h=nil) + bld.x1 = pos.x + bld.y1 = pos.y + bld.z = pos.z + bld.x2 = bld.x1+w-1 if w + bld.y2 = bld.y1+h-1 if h + building_setsize(bld) + end -# set building at position, with optional width/height -def self.building_position(bld, pos, w=nil, h=nil) - bld.x1 = pos.x - bld.y1 = pos.y - bld.z = pos.z - bld.x2 = bld.x1+w-1 if w - bld.y2 = bld.y1+h-1 if h - building_setsize(bld) -end + # set map occupancy/stockpile/etc for a building + def building_setoccupancy(bld) + stockpile = (bld.getType == :Stockpile) + complete = (bld.getBuildStage >= bld.getMaxBuildStage) + extents = (bld.room.extents and bld.isExtentShaped) -# set map occupancy/stockpile/etc for a building -def self.building_setoccupancy(bld) - stockpile = (bld.getType == :Stockpile) - complete = (bld.getBuildStage >= bld.getMaxBuildStage) - extents = (bld.room.extents and bld.isExtentShaped) + z = bld.z + (bld.x1..bld.x2).each { |x| + (bld.y1..bld.y2).each { |y| + next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0 + next if not mb = map_block_at(x, y, z) + des = mb.designation[x%16][y%16] + des.pile = stockpile + des.dig = :No + if complete + bld.updateOccupancy(x, y) + else + mb.occupancy[x%16][y%16].building = :Planned + end + } + } + end - z = bld.z - (bld.x1..bld.x2).each { |x| - (bld.y1..bld.y2).each { |y| - next if !extents or bld.room.extents[bld.room.width*(y-bld.room.y)+(x-bld.room.x)] == 0 - next if not mb = map_block_at(x, y, z) - des = mb.designation[x%16][y%16] - des.pile = stockpile - des.dig = :No - if complete - bld.updateOccupancy(x, y) - else - mb.occupancy[x%16][y%16].building = :Planned - end - } - } -end + # link bld into other rooms if it is inside their extents + def building_linkrooms(bld) + didstuff = false + world.buildings.other[:ANY_FREE].each { |ob| + next if !ob.is_room or ob.z != bld.z + next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0 + didstuff = true + ob.children << bld + bld.parents << ob + } + ui.equipment.update.buildings = true if didstuff + end -# link bld into other rooms if it is inside their extents -def self.building_linkrooms(bld) - didstuff = false - world.buildings.other[:ANY_FREE].each { |ob| - next if !ob.is_room or ob.z != bld.z - next if !ob.room.extents or !ob.isExtentShaped or ob.room.extents[ob.room.width*(bld.y1-ob.room.y)+(bld.x1-ob.room.x)] == 0 - didstuff = true - ob.children << bld - bld.parents << ob - } - ui.equipment.update.buildings = true if didstuff -end + # link the building into the world, set map data, link rooms, bld.id + def building_link(bld) + bld.id = df.building_next_id + df.building_next_id += 1 -# link the building into the world, set map data, link rooms, bld.id -def self.building_link(bld) - bld.id = df.building_next_id - df.building_next_id += 1 + world.buildings.all << bld + bld.categorize(true) + building_setoccupancy(bld) if bld.isSettingOccupancy + building_linkrooms(bld) + end - world.buildings.all << bld - bld.categorize(true) - building_setoccupancy(bld) if bld.isSettingOccupancy - building_linkrooms(bld) -end + # set a design for the building + def building_createdesign(bld, rough=true) + job = bld.jobs[0] + job.mat_type = bld.mat_type + job.mat_index = bld.mat_index + if bld.needsDesign + bld.design = BuildingDesign.cpp_new + bld.design.flags.rough = rough + end + end -# set a design for the building -def self.building_createdesign(bld, rough=true) - job = bld.jobs[0] - job.mat_type = bld.mat_type - job.mat_index = bld.mat_index - if bld.needsDesign - bld.design = BuildingDesign.cpp_new - bld.design.flags.rough = rough - end -end - -# creates a job to build bld, return it -def self.building_linkforconstruct(bld) - building_link bld - ref = GeneralRefBuildingHolderst.cpp_new - ref.building_id = bld.id - job = Job.cpp_new - job.job_type = :ConstructBuilding - job.pos = [bld.centerx, bld.centery, bld.z] - job.references << ref - bld.jobs << job - job_link job - job -end + # creates a job to build bld, return it + def building_linkforconstruct(bld) + building_link bld + ref = GeneralRefBuildingHolderst.cpp_new + ref.building_id = bld.id + job = Job.cpp_new + job.job_type = :ConstructBuilding + job.pos = [bld.centerx, bld.centery, bld.z] + job.references << ref + bld.jobs << job + job_link job + job + end -# construct a building with items or JobItems -def self.building_construct(bld, items) - job = building_linkforconstruct(bld) - rough = false - items.each { |item| - if items.kind_of?(JobItem) - item.quantity = (bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1 if item.quantity < 0 - job.job_items << item - else - job_attachitem(job, item, :Hauled) - end - rough = true if item.getType == :BOULDER - bld.mat_type = item.getMaterial if bld.mat_type == -1 - bld.mat_index = item.getMaterialIndex if bld.mat_index == -1 - } - building_createdesign(bld, rough) -end + # construct a building with items or JobItems + def building_construct(bld, items) + job = building_linkforconstruct(bld) + rough = false + items.each { |item| + if items.kind_of?(JobItem) + item.quantity = (bld.x2-bld.x1+1)*(bld.y2-bld.y1+1)/4+1 if item.quantity < 0 + job.job_items << item + else + job_attachitem(job, item, :Hauled) + end + rough = true if item.getType == :BOULDER + bld.mat_type = item.getMaterial if bld.mat_type == -1 + bld.mat_index = item.getMaterialIndex if bld.mat_index == -1 + } + building_createdesign(bld, rough) + end -# creates a job to deconstruct the building -def self.building_deconstruct(bld) - job = Job.cpp_new - refbuildingholder = GeneralRefBuildingHolderst.cpp_new - job.job_type = :DestroyBuilding - refbuildingholder.building_id = building.id - job.references << refbuildingholder - building.jobs << job - job_link job - job -end + # creates a job to deconstruct the building + def building_deconstruct(bld) + job = Job.cpp_new + refbuildingholder = GeneralRefBuildingHolderst.cpp_new + job.job_type = :DestroyBuilding + refbuildingholder.building_id = building.id + job.references << refbuildingholder + building.jobs << job + job_link job + job + end -# exemple usage -def self.buildbed(pos=cursor) - suspend { - raise 'where to ?' if pos.x < 0 + # exemple usage + def buildbed(pos=cursor) + suspend { + raise 'where to ?' if pos.x < 0 - item = world.items.all.find { |i| - i.kind_of?(ItemBedst) and - i.itemrefs.empty? and - !i.flags.in_job - } - raise 'no free bed, build more !' if not item + item = world.items.all.find { |i| + i.kind_of?(ItemBedst) and + i.itemrefs.empty? and + !i.flags.in_job + } + raise 'no free bed, build more !' if not item - bld = building_alloc(:Bed) - building_position(bld, pos) - building_construct(bld, [item]) - } -end + bld = building_alloc(:Bed) + building_position(bld, pos) + building_construct(bld, [item]) + } + end + end end diff --git a/plugins/ruby/item.rb b/plugins/ruby/item.rb new file mode 100644 index 000000000..cd95e82a9 --- /dev/null +++ b/plugins/ruby/item.rb @@ -0,0 +1,21 @@ +module DFHack + class << self + # return an Item + # arg similar to unit.rb/unit_find; no arg = 'k' menu + def item_find(what=:selected) + if what == :selected + case ui.main.mode + when :LookAround + k = ui_look_list.items[ui_look_cursor] + k.item if k.type == :Item + end + elsif what.kind_of?(Integer) + world.items.all.binsearch(what) + elsif what.respond_to?(:x) or what.respond_to?(:pos) + world.items.all.find { |i| same_pos?(what, i) } + else + raise "what what?" + end + end + end +end diff --git a/plugins/ruby/job.rb b/plugins/ruby/job.rb new file mode 100644 index 000000000..e489dcc91 --- /dev/null +++ b/plugins/ruby/job.rb @@ -0,0 +1,35 @@ +module DFHack + class << self + # link a job to the world + # allocate & set job.id, allocate a JobListLink, link to job & world.job_list + def job_link(job) + lastjob = world.job_list + lastjob = lastjob.next while lastjob.next + joblink = JobListLink.cpp_new + joblink.prev = lastjob + joblink.item = job + job.list_link = joblink + job.id = df.job_next_id + df.job_next_id += 1 + lastjob.next = joblink + end + + # attach an item to a job, flag item in_job + def job_attachitem(job, item, role=:Hauled, filter_idx=-1) + if role != :TargetContainer + item.flags.in_job = true + end + + itemlink = SpecificRef.cpp_new + itemlink.type = :JOB + itemlink.job = job + item.specific_refs << itemlink + + joblink = JobItemRef.cpp_new + joblink.item = item + joblink.role = role + joblink.job_item_idx = filter_idx + job.items << joblink + end + end +end diff --git a/plugins/ruby/map.rb b/plugins/ruby/map.rb new file mode 100644 index 000000000..af9e8b804 --- /dev/null +++ b/plugins/ruby/map.rb @@ -0,0 +1,54 @@ +module DFHack + class << self + # return a map block by tile coordinates + # you can also use find_map_block(cursor) or anything that respond to x/y/z + def map_block_at(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + if x >= 0 and x < world.map.x_count and y >= 0 and y < world.map.y_count and z >= 0 and z < world.map.z_count + world.map.block_index[x/16][y/16][z] + end + end + + def map_designation_at(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + if b = map_block_at(x, y, z) + b.designation[x%16][y%16] + end + end + + def map_occupancy_at(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + if b = map_block_at(x, y, z) + b.occupancy[x%16][y%16] + end + end + + # yields every map block + def each_map_block + (0...world.map.x_count_block).each { |xb| + xl = world.map.block_index[xb] + (0...world.map.y_count_block).each { |yb| + yl = xl[yb] + (0...world.map.z_count_block).each { |z| + p = yl[z] + yield p if p + } + } + } + end + + # yields every map block for a given z level + def each_map_block_z(z) + (0...world.map.x_count_block).each { |xb| + xl = world.map.block_index[xb] + (0...world.map.y_count_block).each { |yb| + p = xl[yb][z] + yield p if p + } + } + end + end +end diff --git a/plugins/ruby/plant.rb b/plugins/ruby/plant.rb index 64f17b493..63195e6c2 100644 --- a/plugins/ruby/plant.rb +++ b/plugins/ruby/plant.rb @@ -1,152 +1,166 @@ module DFHack -def self.each_tree(material=:any) - @raws_tree_name ||= {} - if @raws_tree_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_tree_name[idx] = p.id if p.flags[:TREE] - } - end + class << self + # return a Plant + # arg similar to unit.rb/unit_find, no menu + def plant_find(what=cursor) + if what.kind_of?(Integer) + world.items.all.binsearch(what) + elsif what.respond_to?(:x) or what.respond_to?(:pos) + world.plants.all.find { |p| same_pos?(what, p) } + else + raise "what what?" + end + end - if material != :any - mat = match_rawname(material, @raws_tree_name.values) - unless wantmat = @raws_tree_name.index(mat) - raise "invalid tree material #{material}" - end - end + def each_tree(material=:any) + @raws_tree_name ||= {} + if @raws_tree_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_tree_name[idx] = p.id if p.flags[:TREE] + } + end - world.plants.all.each { |plant| - next if not @raws_tree_name[plant.material] - next if wantmat and plant.material != wantmat - yield plant - } -end + if material != :any + mat = match_rawname(material, @raws_tree_name.values) + unless wantmat = @raws_tree_name.index(mat) + raise "invalid tree material #{material}" + end + end -def self.each_shrub(material=:any) - @raws_shrub_name ||= {} - if @raws_tree_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_shrub_name[idx] = p.id if not p.flags[:GRASS] and not p.flags[:TREE] - } - end + world.plants.all.each { |plant| + next if not @raws_tree_name[plant.material] + next if wantmat and plant.material != wantmat + yield plant + } + end - if material != :any - mat = match_rawname(material, @raws_shrub_name.values) - unless wantmat = @raws_shrub_name.index(mat) - raise "invalid shrub material #{material}" - end - end -end + def each_shrub(material=:any) + @raws_shrub_name ||= {} + if @raws_tree_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_shrub_name[idx] = p.id if not p.flags[:GRASS] and not p.flags[:TREE] + } + end -SaplingToTreeAge = 120960 -def self.cuttrees(material=nil, count_max=100) - if !material - # list trees - cnt = Hash.new(0) - suspend { - each_tree { |plant| - next if plant.grow_counter < SaplingToTreeAge - next if map_designation_at(plant).hidden - cnt[plant.material] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = @raws_tree_name[mat] - puts " #{name} #{c}" - } - else - cnt = 0 - suspend { - each_tree(material) { |plant| - next if plant.grow_counter < SaplingToTreeAge - b = map_block_at(plant) - d = b.designation[plant.pos.x%16][plant.pos.y%16] - next if d.hidden - if d.dig == :No - d.dig = :Default - b.flags.designated = true - cnt += 1 - break if cnt == count_max - end - } - } - puts "Updated #{cnt} plant designations" - end -end + if material != :any + mat = match_rawname(material, @raws_shrub_name.values) + unless wantmat = @raws_shrub_name.index(mat) + raise "invalid shrub material #{material}" + end + end + end -def self.growtrees(material=nil, count_max=100) - if !material - # list plants - cnt = Hash.new(0) - suspend { - each_tree { |plant| - next if plant.grow_counter >= SaplingToTreeAge - next if map_designation_at(plant).hidden - cnt[plant.material] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = @raws_tree_name[mat] - puts " #{name} #{c}" - } - else - cnt = 0 - suspend { - each_tree(material) { |plant| - next if plant.grow_counter >= SaplingToTreeAge - next if map_designation_at(plant).hidden - plant.grow_counter = SaplingToTreeAge - cnt += 1 - break if cnt == count_max - } - } - puts "Grown #{cnt} saplings" - end -end + SaplingToTreeAge = 120960 + def cuttrees(material=nil, count_max=100) + if !material + # list trees + cnt = Hash.new(0) + suspend { + each_tree { |plant| + next if plant.grow_counter < SaplingToTreeAge + next if map_designation_at(plant).hidden + cnt[plant.material] += 1 + } + } + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = @raws_tree_name[mat] + puts " #{name} #{c}" + } + else + cnt = 0 + suspend { + each_tree(material) { |plant| + next if plant.grow_counter < SaplingToTreeAge + b = map_block_at(plant) + d = b.designation[plant.pos.x%16][plant.pos.y%16] + next if d.hidden + if d.dig == :No + d.dig = :Default + b.flags.designated = true + cnt += 1 + break if cnt == count_max + end + } + } + puts "Updated #{cnt} plant designations" + end + end -def self.growcrops(material=nil, count_max=100) - @raws_plant_name ||= {} - @raws_plant_growdur ||= {} - if @raws_plant_name.empty? - df.world.raws.plants.all.each_with_index { |p, idx| - @raws_plant_name[idx] = p.id - @raws_plant_growdur[idx] = p.growdur - } - end + def growtrees(material=nil, count_max=100) + if !material + # list plants + cnt = Hash.new(0) + suspend { + each_tree { |plant| + next if plant.grow_counter >= SaplingToTreeAge + next if map_designation_at(plant).hidden + cnt[plant.material] += 1 + } + } + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = @raws_tree_name[mat] + puts " #{name} #{c}" + } + else + cnt = 0 + suspend { + each_tree(material) { |plant| + next if plant.grow_counter >= SaplingToTreeAge + next if map_designation_at(plant).hidden + plant.grow_counter = SaplingToTreeAge + cnt += 1 + break if cnt == count_max + } + } + puts "Grown #{cnt} saplings" + end + end - if !material - cnt = Hash.new(0) - suspend { - world.items.other[:SEEDS].each { |seed| - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - cnt[seed.mat_index] += 1 - } - } - cnt.sort_by { |mat, c| c }.each { |mat, c| - name = world.raws.plants.all[mat].id - puts " #{name} #{c}" - } - else - if material != :any - mat = match_rawname(material, @raws_plant_name.values) - unless wantmat = @raws_plant_name.index(mat) - raise "invalid plant material #{material}" - end - end + def growcrops(material=nil, count_max=100) + @raws_plant_name ||= {} + @raws_plant_growdur ||= {} + if @raws_plant_name.empty? + df.world.raws.plants.all.each_with_index { |p, idx| + @raws_plant_name[idx] = p.id + @raws_plant_growdur[idx] = p.growdur + } + end - cnt = 0 - suspend { - world.items.other[:SEEDS].each { |seed| - next if wantmat and seed.mat_index != wantmat - next if not seed.flags.in_building - next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } - next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] - seed.grow_counter = @raws_plant_growdur[seed.mat_index] - cnt += 1 - } - } - puts "Grown #{cnt} crops" - end -end + if !material + cnt = Hash.new(0) + suspend { + world.items.other[:SEEDS].each { |seed| + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + cnt[seed.mat_index] += 1 + } + } + cnt.sort_by { |mat, c| c }.each { |mat, c| + name = world.raws.plants.all[mat].id + puts " #{name} #{c}" + } + else + if material != :any + mat = match_rawname(material, @raws_plant_name.values) + unless wantmat = @raws_plant_name.index(mat) + raise "invalid plant material #{material}" + end + end + + cnt = 0 + suspend { + world.items.other[:SEEDS].each { |seed| + next if wantmat and seed.mat_index != wantmat + next if not seed.flags.in_building + next if not seed.itemrefs.find { |ref| ref._rtti_classname == :general_ref_building_holderst } + next if seed.grow_counter >= @raws_plant_growdur[seed.mat_index] + seed.grow_counter = @raws_plant_growdur[seed.mat_index] + cnt += 1 + } + } + puts "Grown #{cnt} crops" + end + end + end end diff --git a/plugins/ruby/ruby-autogen-defs.rb b/plugins/ruby/ruby-autogen-defs.rb new file mode 100644 index 000000000..482cf24ff --- /dev/null +++ b/plugins/ruby/ruby-autogen-defs.rb @@ -0,0 +1,751 @@ +# definition of classes used by ruby-autogen +module DFHack + module MemHack + INSPECT_SIZE_LIMIT=16384 + class MemStruct + attr_accessor :_memaddr + def _at(addr) ; d = dup ; d._memaddr = addr ; d ; end + def _get ; self ; end + def _cpp_init ; end + end + + class Compound < MemStruct + class << self + attr_accessor :_fields, :_rtti_classname, :_sizeof + def field(name, offset) + struct = yield + return if not struct + @_fields ||= [] + @_fields << [name, offset, struct] + define_method(name) { struct._at(@_memaddr+offset)._get } + define_method("#{name}=") { |v| struct._at(@_memaddr+offset)._set(v) } + end + def _fields_ancestors + if superclass.respond_to?(:_fields_ancestors) + superclass._fields_ancestors + _fields.to_a + else + _fields.to_a + end + end + + def number(bits, signed, initvalue=nil, enum=nil) + Number.new(bits, signed, initvalue, enum) + end + def float + Float.new + end + def bit(shift) + BitField.new(shift, 1) + end + def bits(shift, len, enum=nil) + BitField.new(shift, len, enum) + end + def pointer + Pointer.new((yield if block_given?)) + end + def pointer_ary(tglen) + PointerAry.new(tglen, yield) + end + def static_array(len, tglen, indexenum=nil) + StaticArray.new(tglen, len, indexenum, yield) + end + def static_string(len) + StaticString.new(len) + end + + def stl_vector(tglen=nil) + tg = yield if tglen + case tglen + when 1; StlVector8.new(tg) + when 2; StlVector16.new(tg) + else StlVector32.new(tg) + end + end + def stl_string + StlString.new + end + def stl_bit_vector + StlBitVector.new + end + def stl_deque(tglen) + StlDeque.new(tglen, yield) + end + + def df_flagarray(indexenum=nil) + DfFlagarray.new(indexenum) + end + def df_array(tglen) + DfArray.new(tglen, yield) + end + def df_linked_list + DfLinkedList.new(yield) + end + + def global(glob) + Global.new(glob) + end + def compound(name=nil, &b) + m = Class.new(Compound) + DFHack.const_set(name, m) if name + m.instance_eval(&b) + m.new + end + def rtti_classname(n) + DFHack.rtti_register(n, self) + @_rtti_classname = n + end + def sizeof(n) + @_sizeof = n + end + + # allocate a new c++ object, return its ruby wrapper + def cpp_new(init=nil) + ptr = DFHack.malloc(_sizeof) + if _rtti_classname and vt = DFHack.rtti_getvtable(_rtti_classname) + DFHack.memory_write_int32(ptr, vt) + # TODO call constructor + end + o = new._at(ptr) + o._cpp_init + o._set(init) if init + o + end + end + def _cpp_init + _fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_init } + end + def _set(h) + case h + when Hash; h.each { |k, v| send("#{k}=", v) } + when Array; names = _field_names ; raise 'bad size' if names.length != h.length ; names.zip(h).each { |n, a| send("#{n}=", a) } + when Compound; _field_names.each { |n| send("#{n}=", h.send(n)) } + else raise 'wut?' + end + end + def _fields ; self.class._fields.to_a ; end + def _fields_ancestors ; self.class._fields_ancestors.to_a ; end + def _field_names ; _fields_ancestors.map { |n, o, s| n } ; end + def _rtti_classname ; self.class._rtti_classname ; end + def _sizeof ; self.class._sizeof ; end + @@inspecting = {} # avoid infinite recursion on mutually-referenced objects + def inspect + cn = self.class.name.sub(/^DFHack::/, '') + cn << ' @' << ('0x%X' % _memaddr) if cn != '' + out = "#<#{cn}" + return out << ' ...>' if @@inspecting[_memaddr] + @@inspecting[_memaddr] = true + _fields_ancestors.each { |n, o, s| + out << ' ' if out.length != 0 and out[-1, 1] != ' ' + if out.length > INSPECT_SIZE_LIMIT + out << '...' + break + end + out << inspect_field(n, o, s) + } + out.chomp!(' ') + @@inspecting.delete _memaddr + out << '>' + end + def inspect_field(n, o, s) + if s.kind_of?(BitField) and s._len == 1 + send(n) ? n.to_s : '' + elsif s.kind_of?(Pointer) + "#{n}=#{s._at(@_memaddr+o).inspect}" + elsif n == :_whole + "_whole=0x#{_whole.to_s(16)}" + else + v = send(n).inspect + "#{n}=#{v}" + end + rescue + "#{n}=ERR(#{$!})" + end + end + + class Enum + # number -> symbol + def self.enum + # ruby weirdness, needed to make the constants 'virtual' + @enum ||= const_get(:ENUM) + end + # symbol -> number + def self.nume + @nume ||= const_get(:NUME) + end + + def self.int(i) + nume[i] || i + end + def self.sym(i) + enum[i] || i + end + end + + class Number < MemStruct + attr_accessor :_bits, :_signed, :_initvalue, :_enum + def initialize(bits, signed, initvalue, enum) + @_bits = bits + @_signed = signed + @_initvalue = initvalue + @_enum = enum + end + + def _get + v = case @_bits + when 32; DFHack.memory_read_int32(@_memaddr) + when 16; DFHack.memory_read_int16(@_memaddr) + when 8; DFHack.memory_read_int8( @_memaddr) + when 64;(DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + (DFHack.memory_read_int32(@_memaddr+4) << 32) + end + v &= (1 << @_bits) - 1 if not @_signed + v = @_enum.sym(v) if @_enum + v + end + + def _set(v) + v = @_enum.int(v) if @_enum + case @_bits + when 32; DFHack.memory_write_int32(@_memaddr, v) + when 16; DFHack.memory_write_int16(@_memaddr, v) + when 8; DFHack.memory_write_int8( @_memaddr, v) + when 64; DFHack.memory_write_int32(@_memaddr, v & 0xffffffff) ; DFHack.memory_write_int32(@memaddr+4, v>>32) + end + end + + def _cpp_init + _set(@_initvalue) if @_initvalue + end + end + class Float < MemStruct + def _get + DFHack.memory_read_float(@_memaddr) + end + + def _set(v) + DFHack.memory_write_float(@_memaddr, v) + end + + def _cpp_init + _set(0.0) + end + end + class BitField < MemStruct + attr_accessor :_shift, :_len, :_enum + def initialize(shift, len, enum=nil) + @_shift = shift + @_len = len + @_enum = enum + end + def _mask + (1 << @_len) - 1 + end + + def _get + v = DFHack.memory_read_int32(@_memaddr) >> @_shift + if @_len == 1 + ((v & 1) == 0) ? false : true + else + v &= _mask + v = @_enum.sym(v) if @_enum + v + end + end + + def _set(v) + if @_len == 1 + # allow 'bit = 0' + v = (v && v != 0 ? 1 : 0) + end + v = @_enum.int(v) if @_enum + v = (v & _mask) << @_shift + + ori = DFHack.memory_read_int32(@_memaddr) & 0xffffffff + DFHack.memory_write_int32(@_memaddr, ori - (ori & ((-1 & _mask) << @_shift)) + v) + end + end + + class Pointer < MemStruct + attr_accessor :_tg + def initialize(tg) + @_tg = tg + end + + def _getp + DFHack.memory_read_int32(@_memaddr) & 0xffffffff + end + + def _get + addr = _getp + return if addr == 0 + @_tg._at(addr)._get + end + + # XXX shaky... + def _set(v) + if v.kind_of?(Pointer) + DFHack.memory_write_int32(@_memaddr, v._getp) + elsif v.kind_of?(MemStruct) + DFHack.memory_write_int32(@_memaddr, v._memaddr) + else + _get._set(v) + end + end + + def inspect + ptr = _getp + if ptr == 0 + 'NULL' + else + cn = '' + cn = @_tg.class.name.sub(/^DFHack::/, '').sub(/^MemHack::/, '') if @_tg + cn = @_tg._glob if cn == 'Global' + "#" + end + end + end + class PointerAry < MemStruct + attr_accessor :_tglen, :_tg + def initialize(tglen, tg) + @_tglen = tglen + @_tg = tg + end + + def _getp(i=0) + delta = (i != 0 ? i*@_tglen : 0) + (DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + delta + end + + def _get + addr = _getp + return if addr == 0 + self + end + + def [](i) + addr = _getp(i) + return if addr == 0 + @_tg._at(addr)._get + end + def []=(i, v) + addr = _getp(i) + raise 'null pointer' if addr == 0 + @_tg._at(addr)._set(v) + end + + def inspect ; ptr = _getp ; (ptr == 0) ? 'NULL' : "#" ; end + end + module Enumerable + include ::Enumerable + attr_accessor :_indexenum + def each ; (0...length).each { |i| yield self[i] } ; end + def inspect + out = '[' + each_with_index { |e, idx| + out << ', ' if out.length > 1 + if out.length > INSPECT_SIZE_LIMIT + out << '...' + break + end + out << "#{_indexenum.sym(idx)}=" if _indexenum + out << e.inspect + } + out << ']' + end + def empty? ; length == 0 ; end + def flatten ; map { |e| e.respond_to?(:flatten) ? e.flatten : e }.flatten ; end + end + class StaticArray < MemStruct + attr_accessor :_tglen, :_length, :_indexenum, :_tg + def initialize(tglen, length, indexenum, tg) + @_tglen = tglen + @_length = length + @_indexenum = indexenum + @_tg = tg + end + def _set(a) + a.each_with_index { |v, i| self[i] = v } + end + def _cpp_init + _length.times { |i| _tgat(i)._cpp_init } + end + alias length _length + alias size _length + def _tgat(i) + @_tg._at(@_memaddr + i*@_tglen) if i >= 0 and i < @_length + end + def [](i) + i = _indexenum.int(i) if _indexenum + i += @_length if i < 0 + _tgat(i)._get + end + def []=(i, v) + i = _indexenum.int(i) if _indexenum + i += @_length if i < 0 + _tgat(i)._set(v) + end + + include Enumerable + end + class StaticString < MemStruct + attr_accessor :_length + def initialize(length) + @_length = length + end + def _get + DFHack.memory_read(@_memaddr, @_length) + end + def _set(v) + DFHack.memory_write(@_memaddr, v[0, @_length]) + end + end + + class StlVector32 < MemStruct + attr_accessor :_tg + def initialize(tg) + @_tg = tg + end + + def length + DFHack.memory_vector32_length(@_memaddr) + end + def size ; length ; end # alias wouldnt work for subclasses + def valueptr_at(idx) + DFHack.memory_vector32_ptrat(@_memaddr, idx) + end + def insert_at(idx, val) + DFHack.memory_vector32_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vector32_delete(@_memaddr, idx) + end + + def _set(v) + delete_at(length-1) while length > v.length # match lengthes + v.each_with_index { |e, i| self[i] = e } # patch entries + end + + def _cpp_init + DFHack.memory_vector_init(@_memaddr) + end + + def clear + delete_at(length-1) while length > 0 + end + def [](idx) + idx += length if idx < 0 + @_tg._at(valueptr_at(idx))._get if idx >= 0 and idx < length + end + def []=(idx, v) + idx += length if idx < 0 + if idx >= length + insert_at(idx, 0) + elsif idx < 0 + raise 'invalid idx' + end + @_tg._at(valueptr_at(idx))._set(v) + end + def push(v) + self[length] = v + self + end + def <<(v) ; push(v) ; end + def pop + l = length + if l > 0 + v = self[l-1] + delete_at(l-1) + end + v + end + + include Enumerable + # do a binary search in an ordered vector for a specific target attribute + # ex: world.history.figures.binsearch(unit.hist_figure_id) + def binsearch(target, field=:id) + o_start = 0 + o_end = length - 1 + while o_end >= o_start + o_half = o_start + (o_end-o_start)/2 + obj = self[o_half] + oval = obj.send(field) + if oval == target + return obj + elsif oval < target + o_start = o_half+1 + else + o_end = o_half-1 + end + end + end + end + class StlVector16 < StlVector32 + def length + DFHack.memory_vector16_length(@_memaddr) + end + def valueptr_at(idx) + DFHack.memory_vector16_ptrat(@_memaddr, idx) + end + def insert_at(idx, val) + DFHack.memory_vector16_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vector16_delete(@_memaddr, idx) + end + end + class StlVector8 < StlVector32 + def length + DFHack.memory_vector8_length(@_memaddr) + end + def valueptr_at(idx) + DFHack.memory_vector8_ptrat(@_memaddr, idx) + end + def insert_at(idx, val) + DFHack.memory_vector8_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vector8_delete(@_memaddr, idx) + end + end + class StlBitVector < StlVector32 + def initialize ; end + def length + DFHack.memory_vectorbool_length(@_memaddr) + end + def insert_at(idx, val) + DFHack.memory_vectorbool_insert(@_memaddr, idx, val) + end + def delete_at(idx) + DFHack.memory_vectorbool_delete(@_memaddr, idx) + end + def [](idx) + idx += length if idx < 0 + DFHack.memory_vectorbool_at(@_memaddr, idx) if idx >= 0 and idx < length + end + def []=(idx, v) + idx += length if idx < 0 + if idx >= length + insert_at(idx, v) + elsif idx < 0 + raise 'invalid idx' + else + DFHack.memory_vectorbool_setat(@_memaddr, idx, v) + end + end + end + class StlString < MemStruct + def _get + DFHack.memory_read_stlstring(@_memaddr) + end + + def _set(v) + DFHack.memory_write_stlstring(@_memaddr, v) + end + + def _cpp_init + DFHack.memory_stlstring_init(@_memaddr) + end + end + class StlDeque < MemStruct + attr_accessor :_tglen, :_tg + def initialize(tglen, tg) + @_tglen = tglen + @_tg = tg + end + # XXX DF uses stl::deque, so to have a C binding we'd need to single-case every + # possible struct size, like for StlVector. Just ignore it for now, deque are rare enough. + def inspect ; "#" ; end + end + + class DfFlagarray < MemStruct + attr_accessor :_indexenum + def initialize(indexenum) + @_indexenum = indexenum + end + def length + DFHack.memory_bitarray_length(@_memaddr) + end + # TODO _cpp_init + def size ; length ; end + def resize(len) + DFHack.memory_bitarray_resize(@_memaddr, len) + end + def [](idx) + idx = _indexenum.int(idx) if _indexenum + idx += length if idx < 0 + DFHack.memory_bitarray_isset(@_memaddr, idx) if idx >= 0 and idx < length + end + def []=(idx, v) + idx = _indexenum.int(idx) if _indexenum + idx += length if idx < 0 + if idx >= length or idx < 0 + raise 'invalid idx' + else + DFHack.memory_bitarray_set(@_memaddr, idx, v) + end + end + + include Enumerable + end + class DfArray < Compound + attr_accessor :_tglen, :_tg + def initialize(tglen, tg) + @_tglen = tglen + @_tg = tg + end + + field(:_ptr, 0) { number 32, false } + field(:_length, 4) { number 16, false } + + def length ; _length ; end + def size ; _length ; end + # TODO _cpp_init + def _tgat(i) + @_tg._at(_ptr + i*@_tglen) if i >= 0 and i < _length + end + def [](i) + i += _length if i < 0 + _tgat(i)._get + end + def []=(i, v) + i += _length if i < 0 + _tgat(i)._set(v) + end + def _set(a) + a.each_with_index { |v, i| self[i] = v } + end + + include Enumerable + end + class DfLinkedList < Compound + attr_accessor :_tg + def initialize(tg) + @_tg = tg + end + + field(:_ptr, 0) { number 32, false } + field(:_prev, 4) { number 32, false } + field(:_next, 8) { number 32, false } + + def item + # With the current xml structure, currently _tg designate + # the type of the 'next' and 'prev' fields, not 'item'. + # List head has item == NULL, so we can safely return nil. + + #addr = _ptr + #return if addr == 0 + #@_tg._at(addr)._get + end + + def item=(v) + #addr = _ptr + #raise 'null pointer' if addr == 0 + #@_tg.at(addr)._set(v) + raise 'null pointer' + end + + def prev + addr = _prev + return if addr == 0 + @_tg._at(addr)._get + end + + def next + addr = _next + return if addr == 0 + @_tg._at(addr)._get + end + + include Enumerable + def each + o = self + while o + yield o.item if o.item + o = o.next + end + end + def inspect ; "#" ; end + end + + class Global < MemStruct + attr_accessor :_glob + def initialize(glob) + @_glob = glob + end + def _at(addr) + g = DFHack.const_get(@_glob) + g = DFHack.rtti_getclassat(g, addr) + g.new._at(addr) + end + def inspect ; "#<#{@_glob}>" ; end + end + + end + + class BooleanEnum + def self.int(v) ; ((v == true) || (v == 1)) ? 1 : 0 ; end + def self.sym(v) ; (!v || (v == 0)) ? false : true ; end + end + + # cpp rtti name -> rb class + @rtti_n2c = {} + @rtti_c2n = {} + + # cpp rtti name -> vtable ptr + @rtti_n2v = {} + @rtti_v2n = {} + + def self.rtti_n2c ; @rtti_n2c ; end + def self.rtti_c2n ; @rtti_c2n ; end + def self.rtti_n2v ; @rtti_n2v ; end + def self.rtti_v2n ; @rtti_v2n ; end + + # register a ruby class with a cpp rtti class name + def self.rtti_register(cppname, cls) + @rtti_n2c[cppname] = cls + @rtti_c2n[cls] = cppname + end + + # return the ruby class to use for the cpp object at address if rtti info is available + def self.rtti_getclassat(cls, addr) + if addr != 0 and @rtti_c2n[cls] + # rtti info exist for class => cpp object has a vtable + @rtti_n2c[rtti_readclassname(get_vtable_ptr(addr))] || cls + else + cls + end + end + + # try to read the rtti classname from an object vtable pointer + def self.rtti_readclassname(vptr) + unless n = @rtti_v2n[vptr] + n = @rtti_v2n[vptr] = get_rtti_classname(vptr).to_sym + @rtti_n2v[n] = vptr + end + n + end + + # return the vtable pointer from the cpp rtti name + def self.rtti_getvtable(cppname) + unless v = @rtti_n2v[cppname] + v = get_vtable(cppname.to_s) + @rtti_n2v[cppname] = v + @rtti_v2n[v] = cppname if v != 0 + end + v if v != 0 + end + + def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0) + vmethod_do_call(obj._memaddr, voff, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), vmethod_arg(a3)) + end + + def self.vmethod_arg(arg) + case arg + when nil, false; 0 + when true; 1 + when Integer; arg + #when String; [arg].pack('p').unpack('L')[0] # raw pointer to buffer + when MemHack::Compound; arg._memaddr + else raise "bad vmethod arg #{arg.class}" + end + end +end diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb index 3a7d20a27..1922e5bb5 100644 --- a/plugins/ruby/ruby.rb +++ b/plugins/ruby/ruby.rb @@ -1,3 +1,4 @@ +# redefine standard i/o methods to use the dfhack console module Kernel def puts(*a) a.flatten.each { |l| @@ -77,104 +78,6 @@ module DFHack @onstatechange_list.each { |cb| cb.call(newstate) } end - - # return an Unit - # with no arg, return currently selected unit in df UI ('v' or 'k' menu) - # with numeric arg, search unit by unit.id - # with an argument that respond to x/y/z (eg cursor), find first unit at this position - def find_unit(what=:selected) - if what == :selected - case ui.main.mode - when :ViewUnits - # nobody selected => idx == 0 - v = world.units.active[ui_selected_unit] - v if v and v.pos.z == cursor.z - when :LookAround - k = ui_look_list.items[ui_look_cursor] - k.unit if k.type == :Unit - end - elsif what.kind_of?(Integer) - world.units.all.binsearch(what) - elsif what.respond_to?(:x) or what.respond_to?(:pos) - what = what.pos if what.respond_to?(:pos) - x, y, z = what.x, what.y, what.z - world.units.all.find { |u| u.pos.x == x and u.pos.y == y and u.pos.z == z } - else - raise "what what?" - end - end - - # return an Item - # arg similar to find_unit; no arg = 'k' menu - def find_item(what=:selected) - if what == :selected - case ui.main.mode - when :LookAround - k = ui_look_list.items[ui_look_cursor] - k.item if k.type == :Item - end - elsif what.kind_of?(Integer) - world.items.all.binsearch(what) - elsif what.respond_to?(:x) or what.respond_to?(:pos) - what = what.pos if what.respond_to?(:pos) - x, y, z = what.x, what.y, what.z - world.items.all.find { |i| i.pos.x == x and i.pos.y == y and i.pos.z == z } - else - raise "what what?" - end - end - - # return a map block by tile coordinates - # you can also use find_map_block(cursor) or anything that respond to x/y/z - def map_block_at(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - if x >= 0 and x < world.map.x_count and y >= 0 and y < world.map.y_count and z >= 0 and z < world.map.z_count - world.map.block_index[x/16][y/16][z] - end - end - - def map_designation_at(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - if b = map_block_at(x, y, z) - b.designation[x%16][y%16] - end - end - - def map_occupancy_at(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - if b = map_block_at(x, y, z) - b.occupancy[x%16][y%16] - end - end - - # yields every map block - def each_map_block - (0...world.map.x_count_block).each { |xb| - xl = world.map.block_index[xb] - (0...world.map.y_count_block).each { |yb| - yl = xl[yb] - (0...world.map.z_count_block).each { |z| - p = yl[z] - yield p if p - } - } - } - end - - # yields every map block for a given z level - def each_map_block_z(z) - (0...world.map.x_count_block).each { |xb| - xl = world.map.block_index[xb] - (0...world.map.y_count_block).each { |yb| - p = xl[yb][z] - yield p if p - } - } - end - # return true if the argument is under the cursor def at_cursor?(obj) same_pos?(obj, cursor) @@ -187,67 +90,6 @@ module DFHack pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z end - # center the DF screen on something - # updates the cursor position if visible - def center_viewscreen(x, y=nil, z=nil) - x = x.pos if x.respond_to?(:pos) - x, y, z = x.x, x.y, x.z if x.respond_to?(:x) - - # compute screen 'map' size (tiles) - menuwidth = ui_menu_width - # ui_menu_width shows only the 'tab' status - menuwidth = 1 if menuwidth == 2 and ui_area_map_width == 2 and cursor.x != -30000 - menuwidth = 2 if menuwidth == 3 and cursor.x != -30000 - w_w = gps.dimx - 2 - w_h = gps.dimy - 2 - case menuwidth - when 1; w_w -= 55 - when 2; w_w -= (ui_area_map_width == 2 ? 24 : 31) - end - - # center view - w_x = x - w_w/2 - w_y = y - w_h/2 - w_z = z - # round view coordinates (optional) - #w_x -= w_x % 10 - #w_y -= w_y % 10 - # crop to map limits - w_x = [[w_x, world.map.x_count - w_w].min, 0].max - w_y = [[w_y, world.map.y_count - w_h].min, 0].max - - self.window_x = w_x - self.window_y = w_y - self.window_z = w_z - - if cursor.x != -30000 - cursor.x, cursor.y, cursor.z = x, y, z - end - end - - # add an announcement - # color = integer, bright = bool - def add_announcement(str, color=nil, bright=nil) - cont = false - while str.length > 0 - rep = Report.cpp_new - rep.color = color if color - rep.bright = ((bright && bright != 0) ? 1 : 0) if bright != nil - rep.year = cur_year - rep.time = cur_year_tick - rep.flags.continuation = cont - cont = true - rep.flags.announcement = true - rep.text = str[0, 73] - str = str[73..-1].to_s - rep.id = world.status.next_report_id - world.status.next_report_id += 1 - world.status.reports << rep - world.status.announcements << rep - world.status.display_timer = 2000 - end - end - # try to match a user-specified name to one from the raws # uses case-switching and substring matching # eg match_rawname('coal', ['COAL_BITUMINOUS', 'BAUXITE']) => 'COAL_BITUMINOUS' @@ -257,38 +99,6 @@ module DFHack may = rawlist.find_all { |r| r.downcase.index(name.downcase) } may.first if may.length == 1 end - - # link a job to the world - # allocate & set job.id, allocate a JobListLink, link to job & world.job_list - def job_link(job) - lastjob = world.job_list - lastjob = lastjob.next while lastjob.next - joblink = JobListLink.cpp_new - joblink.prev = lastjob - joblink.item = job - job.list_link = joblink - job.id = df.job_next_id - df.job_next_id += 1 - lastjob.next = joblink - end - - # attach an item to a job, flag item in_job - def job_attachitem(job, item, role=:Hauled, filter_idx=-1) - if role != :TargetContainer - item.flags.in_job = true - end - - itemlink = SpecificRef.cpp_new - itemlink.type = :JOB - itemlink.job = job - item.specific_refs << itemlink - - joblink = JobItemRef.cpp_new - joblink.item = item - joblink.role = role - joblink.job_item_idx = filter_idx - job.items << joblink - end end end @@ -297,758 +107,9 @@ def df DFHack end -# following: definitions used by ruby-autogen.rb -module DFHack -module MemHack -INSPECT_SIZE_LIMIT=16384 -class MemStruct - attr_accessor :_memaddr - def _at(addr) ; d = dup ; d._memaddr = addr ; d ; end - def _get ; self ; end - def _cpp_init ; end -end - -class Compound < MemStruct - class << self - attr_accessor :_fields, :_rtti_classname, :_sizeof - def field(name, offset) - struct = yield - return if not struct - @_fields ||= [] - @_fields << [name, offset, struct] - define_method(name) { struct._at(@_memaddr+offset)._get } - define_method("#{name}=") { |v| struct._at(@_memaddr+offset)._set(v) } - end - def _fields_ancestors - if superclass.respond_to?(:_fields_ancestors) - superclass._fields_ancestors + _fields.to_a - else - _fields.to_a - end - end - - def number(bits, signed, initvalue=nil, enum=nil) - Number.new(bits, signed, initvalue, enum) - end - def float - Float.new - end - def bit(shift) - BitField.new(shift, 1) - end - def bits(shift, len, enum=nil) - BitField.new(shift, len, enum) - end - def pointer - Pointer.new((yield if block_given?)) - end - def pointer_ary(tglen) - PointerAry.new(tglen, yield) - end - def static_array(len, tglen, indexenum=nil) - StaticArray.new(tglen, len, indexenum, yield) - end - def static_string(len) - StaticString.new(len) - end - - def stl_vector(tglen=nil) - tg = yield if tglen - case tglen - when 1; StlVector8.new(tg) - when 2; StlVector16.new(tg) - else StlVector32.new(tg) - end - end - def stl_string - StlString.new - end - def stl_bit_vector - StlBitVector.new - end - def stl_deque(tglen) - StlDeque.new(tglen, yield) - end - - def df_flagarray(indexenum=nil) - DfFlagarray.new(indexenum) - end - def df_array(tglen) - DfArray.new(tglen, yield) - end - def df_linked_list - DfLinkedList.new(yield) - end - - def global(glob) - Global.new(glob) - end - def compound(name=nil, &b) - m = Class.new(Compound) - DFHack.const_set(name, m) if name - m.instance_eval(&b) - m.new - end - def rtti_classname(n) - DFHack.rtti_register(n, self) - @_rtti_classname = n - end - def sizeof(n) - @_sizeof = n - end - - # allocate a new c++ object, return its ruby wrapper - def cpp_new - ptr = DFHack.malloc(_sizeof) - if _rtti_classname and vt = DFHack.rtti_getvtable(_rtti_classname) - DFHack.memory_write_int32(ptr, vt) - # TODO call constructor - end - o = new._at(ptr) - o._cpp_init - o - end - end - def _cpp_init - _fields_ancestors.each { |n, o, s| s._at(@_memaddr+o)._cpp_init } - end - def _set(h) - case h - when Hash; h.each { |k, v| send("_#{k}=", v) } - when Array; names = _field_names ; raise 'bad size' if names.length != h.length ; names.zip(h).each { |n, a| send("#{n}=", a) } - when Compound; _field_names.each { |n| send("#{n}=", h.send(n)) } - else raise 'wut?' - end - end - def _fields ; self.class._fields.to_a ; end - def _fields_ancestors ; self.class._fields_ancestors.to_a ; end - def _field_names ; _fields_ancestors.map { |n, o, s| n } ; end - def _rtti_classname ; self.class._rtti_classname ; end - def _sizeof ; self.class._sizeof ; end - @@inspecting = {} # avoid infinite recursion on mutually-referenced objects - def inspect - cn = self.class.name.sub(/^DFHack::/, '') - cn << ' @' << ('0x%X' % _memaddr) if cn != '' - out = "#<#{cn}" - return out << ' ...>' if @@inspecting[_memaddr] - @@inspecting[_memaddr] = true - _fields_ancestors.each { |n, o, s| - out << ' ' if out.length != 0 and out[-1, 1] != ' ' - if out.length > INSPECT_SIZE_LIMIT - out << '...' - break - end - out << inspect_field(n, o, s) - } - out.chomp!(' ') - @@inspecting.delete _memaddr - out << '>' - end - def inspect_field(n, o, s) - if s.kind_of?(BitField) and s._len == 1 - send(n) ? n.to_s : '' - elsif s.kind_of?(Pointer) - "#{n}=#{s._at(@_memaddr+o).inspect}" - elsif n == :_whole - "_whole=0x#{_whole.to_s(16)}" - else - v = send(n).inspect - "#{n}=#{v}" - end - rescue - "#{n}=ERR(#{$!})" - end -end - -class Enum - # number -> symbol - def self.enum - # ruby weirdness, needed to make the constants 'virtual' - @enum ||= const_get(:ENUM) - end - # symbol -> number - def self.nume - @nume ||= const_get(:NUME) - end - - def self.int(i) - nume[i] || i - end - def self.sym(i) - enum[i] || i - end -end - -class Number < MemStruct - attr_accessor :_bits, :_signed, :_initvalue, :_enum - def initialize(bits, signed, initvalue, enum) - @_bits = bits - @_signed = signed - @_initvalue = initvalue - @_enum = enum - end - - def _get - v = case @_bits - when 32; DFHack.memory_read_int32(@_memaddr) - when 16; DFHack.memory_read_int16(@_memaddr) - when 8; DFHack.memory_read_int8( @_memaddr) - when 64;(DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + (DFHack.memory_read_int32(@_memaddr+4) << 32) - end - v &= (1 << @_bits) - 1 if not @_signed - v = @_enum.sym(v) if @_enum - v - end - - def _set(v) - v = @_enum.int(v) if @_enum - case @_bits - when 32; DFHack.memory_write_int32(@_memaddr, v) - when 16; DFHack.memory_write_int16(@_memaddr, v) - when 8; DFHack.memory_write_int8( @_memaddr, v) - when 64; DFHack.memory_write_int32(@_memaddr, v & 0xffffffff) ; DFHack.memory_write_int32(@memaddr+4, v>>32) - end - end - - def _cpp_init - _set(@_initvalue) if @_initvalue - end -end -class Float < MemStruct - def _get - DFHack.memory_read_float(@_memaddr) - end - - def _set(v) - DFHack.memory_write_float(@_memaddr, v) - end - - def _cpp_init - _set(0.0) - end -end -class BitField < MemStruct - attr_accessor :_shift, :_len, :_enum - def initialize(shift, len, enum=nil) - @_shift = shift - @_len = len - @_enum = enum - end - def _mask - (1 << @_len) - 1 - end - - def _get - v = DFHack.memory_read_int32(@_memaddr) >> @_shift - if @_len == 1 - ((v & 1) == 0) ? false : true - else - v &= _mask - v = @_enum.sym(v) if @_enum - v - end - end - - def _set(v) - if @_len == 1 - # allow 'bit = 0' - v = (v && v != 0 ? 1 : 0) - end - v = @_enum.int(v) if @_enum - v = (v & _mask) << @_shift - - ori = DFHack.memory_read_int32(@_memaddr) & 0xffffffff - DFHack.memory_write_int32(@_memaddr, ori - (ori & ((-1 & _mask) << @_shift)) + v) - end -end - -class Pointer < MemStruct - attr_accessor :_tg - def initialize(tg) - @_tg = tg - end - - def _getp - DFHack.memory_read_int32(@_memaddr) & 0xffffffff - end - - def _get - addr = _getp - return if addr == 0 - @_tg._at(addr)._get - end - - # XXX shaky... - def _set(v) - if v.kind_of?(Pointer) - DFHack.memory_write_int32(@_memaddr, v._getp) - elsif v.kind_of?(MemStruct) - DFHack.memory_write_int32(@_memaddr, v._memaddr) - else - _get._set(v) - end - end - - def inspect - ptr = _getp - if ptr == 0 - 'NULL' - else - cn = '' - cn = @_tg.class.name.sub(/^DFHack::/, '').sub(/^MemHack::/, '') if @_tg - cn = @_tg._glob if cn == 'Global' - "#" - end - end -end -class PointerAry < MemStruct - attr_accessor :_tglen, :_tg - def initialize(tglen, tg) - @_tglen = tglen - @_tg = tg - end - - def _getp(i=0) - delta = (i != 0 ? i*@_tglen : 0) - (DFHack.memory_read_int32(@_memaddr) & 0xffffffff) + delta - end - - def _get - addr = _getp - return if addr == 0 - self - end - - def [](i) - addr = _getp(i) - return if addr == 0 - @_tg._at(addr)._get - end - def []=(i, v) - addr = _getp(i) - raise 'null pointer' if addr == 0 - @_tg._at(addr)._set(v) - end - - def inspect ; ptr = _getp ; (ptr == 0) ? 'NULL' : "#" ; end -end -module Enumerable - include ::Enumerable - attr_accessor :_indexenum - def each ; (0...length).each { |i| yield self[i] } ; end - def inspect - out = '[' - each_with_index { |e, idx| - out << ', ' if out.length > 1 - if out.length > INSPECT_SIZE_LIMIT - out << '...' - break - end - out << "#{_indexenum.sym(idx)}=" if _indexenum - out << e.inspect - } - out << ']' - end - def empty? ; length == 0 ; end - def flatten ; map { |e| e.respond_to?(:flatten) ? e.flatten : e }.flatten ; end -end -class StaticArray < MemStruct - attr_accessor :_tglen, :_length, :_indexenum, :_tg - def initialize(tglen, length, indexenum, tg) - @_tglen = tglen - @_length = length - @_indexenum = indexenum - @_tg = tg - end - def _set(a) - a.each_with_index { |v, i| self[i] = v } - end - def _cpp_init - _length.times { |i| _tgat(i)._cpp_init } - end - alias length _length - alias size _length - def _tgat(i) - @_tg._at(@_memaddr + i*@_tglen) if i >= 0 and i < @_length - end - def [](i) - i = _indexenum.int(i) if _indexenum - i += @_length if i < 0 - _tgat(i)._get - end - def []=(i, v) - i = _indexenum.int(i) if _indexenum - i += @_length if i < 0 - _tgat(i)._set(v) - end - - include Enumerable -end -class StaticString < MemStruct - attr_accessor :_length - def initialize(length) - @_length = length - end - def _get - DFHack.memory_read(@_memaddr, @_length) - end - def _set(v) - DFHack.memory_write(@_memaddr, v[0, @_length]) - end -end - -class StlVector32 < MemStruct - attr_accessor :_tg - def initialize(tg) - @_tg = tg - end - - def length - DFHack.memory_vector32_length(@_memaddr) - end - def size ; length ; end # alias wouldnt work for subclasses - def valueptr_at(idx) - DFHack.memory_vector32_ptrat(@_memaddr, idx) - end - def insert_at(idx, val) - DFHack.memory_vector32_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vector32_delete(@_memaddr, idx) - end - - def _set(v) - delete_at(length-1) while length > v.length # match lengthes - v.each_with_index { |e, i| self[i] = e } # patch entries - end - - def _cpp_init - DFHack.memory_vector_init(@_memaddr) - end - - def clear - delete_at(length-1) while length > 0 - end - def [](idx) - idx += length if idx < 0 - @_tg._at(valueptr_at(idx))._get if idx >= 0 and idx < length - end - def []=(idx, v) - idx += length if idx < 0 - if idx >= length - insert_at(idx, 0) - elsif idx < 0 - raise 'invalid idx' - end - @_tg._at(valueptr_at(idx))._set(v) - end - def push(v) - self[length] = v - self - end - def <<(v) ; push(v) ; end - def pop - l = length - if l > 0 - v = self[l-1] - delete_at(l-1) - end - v - end - - include Enumerable - # do a binary search in an ordered vector for a specific target attribute - # ex: world.history.figures.binsearch(unit.hist_figure_id) - def binsearch(target, field=:id) - o_start = 0 - o_end = length - 1 - while o_end >= o_start - o_half = o_start + (o_end-o_start)/2 - obj = self[o_half] - oval = obj.send(field) - if oval == target - return obj - elsif oval < target - o_start = o_half+1 - else - o_end = o_half-1 - end - end - end -end -class StlVector16 < StlVector32 - def length - DFHack.memory_vector16_length(@_memaddr) - end - def valueptr_at(idx) - DFHack.memory_vector16_ptrat(@_memaddr, idx) - end - def insert_at(idx, val) - DFHack.memory_vector16_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vector16_delete(@_memaddr, idx) - end -end -class StlVector8 < StlVector32 - def length - DFHack.memory_vector8_length(@_memaddr) - end - def valueptr_at(idx) - DFHack.memory_vector8_ptrat(@_memaddr, idx) - end - def insert_at(idx, val) - DFHack.memory_vector8_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vector8_delete(@_memaddr, idx) - end -end -class StlBitVector < StlVector32 - def initialize ; end - def length - DFHack.memory_vectorbool_length(@_memaddr) - end - def insert_at(idx, val) - DFHack.memory_vectorbool_insert(@_memaddr, idx, val) - end - def delete_at(idx) - DFHack.memory_vectorbool_delete(@_memaddr, idx) - end - def [](idx) - idx += length if idx < 0 - DFHack.memory_vectorbool_at(@_memaddr, idx) if idx >= 0 and idx < length - end - def []=(idx, v) - idx += length if idx < 0 - if idx >= length - insert_at(idx, v) - elsif idx < 0 - raise 'invalid idx' - else - DFHack.memory_vectorbool_setat(@_memaddr, idx, v) - end - end -end -class StlString < MemStruct - def _get - DFHack.memory_read_stlstring(@_memaddr) - end - - def _set(v) - DFHack.memory_write_stlstring(@_memaddr, v) - end - - def _cpp_init - DFHack.memory_stlstring_init(@_memaddr) - end -end -class StlDeque < MemStruct - attr_accessor :_tglen, :_tg - def initialize(tglen, tg) - @_tglen = tglen - @_tg = tg - end - # XXX DF uses stl::deque, so to have a C binding we'd need to single-case every - # possible struct size, like for StlVector. Just ignore it for now, deque are rare enough. - def inspect ; "#" ; end -end - -class DfFlagarray < MemStruct - attr_accessor :_indexenum - def initialize(indexenum) - @_indexenum = indexenum - end - def length - DFHack.memory_bitarray_length(@_memaddr) - end - # TODO _cpp_init - def size ; length ; end - def resize(len) - DFHack.memory_bitarray_resize(@_memaddr, len) - end - def [](idx) - idx = _indexenum.int(idx) if _indexenum - idx += length if idx < 0 - DFHack.memory_bitarray_isset(@_memaddr, idx) if idx >= 0 and idx < length - end - def []=(idx, v) - idx = _indexenum.int(idx) if _indexenum - idx += length if idx < 0 - if idx >= length or idx < 0 - raise 'invalid idx' - else - DFHack.memory_bitarray_set(@_memaddr, idx, v) - end - end - - include Enumerable -end -class DfArray < Compound - attr_accessor :_tglen, :_tg - def initialize(tglen, tg) - @_tglen = tglen - @_tg = tg - end - - field(:_ptr, 0) { number 32, false } - field(:_length, 4) { number 16, false } - - def length ; _length ; end - def size ; _length ; end - # TODO _cpp_init - def _tgat(i) - @_tg._at(_ptr + i*@_tglen) if i >= 0 and i < _length - end - def [](i) - i += _length if i < 0 - _tgat(i)._get - end - def []=(i, v) - i += _length if i < 0 - _tgat(i)._set(v) - end - def _set(a) - a.each_with_index { |v, i| self[i] = v } - end - - include Enumerable -end -class DfLinkedList < Compound - attr_accessor :_tg - def initialize(tg) - @_tg = tg - end - - field(:_ptr, 0) { number 32, false } - field(:_prev, 4) { number 32, false } - field(:_next, 8) { number 32, false } - - def item - # With the current xml structure, currently _tg designate - # the type of the 'next' and 'prev' fields, not 'item'. - # List head has item == NULL, so we can safely return nil. - - #addr = _ptr - #return if addr == 0 - #@_tg._at(addr)._get - end - - def item=(v) - #addr = _ptr - #raise 'null pointer' if addr == 0 - #@_tg.at(addr)._set(v) - raise 'null pointer' - end - - def prev - addr = _prev - return if addr == 0 - @_tg._at(addr)._get - end - - def next - addr = _next - return if addr == 0 - @_tg._at(addr)._get - end - - include Enumerable - def each - o = self - while o - yield o.item if o.item - o = o.next - end - end - def inspect ; "#" ; end -end - -class Global < MemStruct - attr_accessor :_glob - def initialize(glob) - @_glob = glob - end - def _at(addr) - g = DFHack.const_get(@_glob) - g = DFHack.rtti_getclassat(g, addr) - g.new._at(addr) - end - def inspect ; "#<#{@_glob}>" ; end -end -end # module MemHack - -class BooleanEnum - def self.int(v) ; ((v == true) || (v == 1)) ? 1 : 0 ; end - def self.sym(v) ; (!v || (v == 0)) ? false : true ; end -end - -# cpp rtti name -> rb class -@rtti_n2c = {} -@rtti_c2n = {} - -# cpp rtti name -> vtable ptr -@rtti_n2v = {} -@rtti_v2n = {} - -def self.rtti_n2c ; @rtti_n2c ; end -def self.rtti_c2n ; @rtti_c2n ; end -def self.rtti_n2v ; @rtti_n2v ; end -def self.rtti_v2n ; @rtti_v2n ; end - -# register a ruby class with a cpp rtti class name -def self.rtti_register(cppname, cls) - @rtti_n2c[cppname] = cls - @rtti_c2n[cls] = cppname -end - -# return the ruby class to use for the cpp object at address if rtti info is available -def self.rtti_getclassat(cls, addr) - if addr != 0 and @rtti_c2n[cls] - # rtti info exist for class => cpp object has a vtable - @rtti_n2c[rtti_readclassname(get_vtable_ptr(addr))] || cls - else - cls - end -end - -# try to read the rtti classname from an object vtable pointer -def self.rtti_readclassname(vptr) - unless n = @rtti_v2n[vptr] - n = @rtti_v2n[vptr] = get_rtti_classname(vptr).to_sym - @rtti_n2v[n] = vptr - end - n -end - -# return the vtable pointer from the cpp rtti name -def self.rtti_getvtable(cppname) - unless v = @rtti_n2v[cppname] - v = get_vtable(cppname.to_s) - @rtti_n2v[cppname] = v - @rtti_v2n[v] = cppname if v != 0 - end - v if v != 0 -end - -def self.vmethod_call(obj, voff, a0=0, a1=0, a2=0, a3=0, a4=0) - vmethod_do_call(obj._memaddr, voff, vmethod_arg(a0), vmethod_arg(a1), vmethod_arg(a2), vmethod_arg(a3)) -end - -def self.vmethod_arg(arg) - case arg - when nil, false; 0 - when true; 1 - when Integer; arg - #when String; [arg].pack('p').unpack('L')[0] # raw pointer to buffer - when MemHack::Compound; arg._memaddr - else raise "bad vmethod arg #{arg.class}" - end -end - -end - # load autogenned file +require './hack/ruby/ruby-autogen-defs' require './hack/ruby/ruby-autogen' + # load all modules Dir['./hack/ruby/*.rb'].each { |m| require m.chomp('.rb') } diff --git a/plugins/ruby/ui.rb b/plugins/ruby/ui.rb new file mode 100644 index 000000000..fbe7ced77 --- /dev/null +++ b/plugins/ruby/ui.rb @@ -0,0 +1,65 @@ +# df user-interface related methods +module DFHack + class << self + # center the DF screen on something + # updates the cursor position if visible + def center_viewscreen(x, y=nil, z=nil) + x = x.pos if x.respond_to?(:pos) + x, y, z = x.x, x.y, x.z if x.respond_to?(:x) + + # compute screen 'map' size (tiles) + menuwidth = ui_menu_width + # ui_menu_width shows only the 'tab' status + menuwidth = 1 if menuwidth == 2 and ui_area_map_width == 2 and cursor.x != -30000 + menuwidth = 2 if menuwidth == 3 and cursor.x != -30000 + w_w = gps.dimx - 2 + w_h = gps.dimy - 2 + case menuwidth + when 1; w_w -= 55 + when 2; w_w -= (ui_area_map_width == 2 ? 24 : 31) + end + + # center view + w_x = x - w_w/2 + w_y = y - w_h/2 + w_z = z + # round view coordinates (optional) + #w_x -= w_x % 10 + #w_y -= w_y % 10 + # crop to map limits + w_x = [[w_x, world.map.x_count - w_w].min, 0].max + w_y = [[w_y, world.map.y_count - w_h].min, 0].max + + self.window_x = w_x + self.window_y = w_y + self.window_z = w_z + + if cursor.x != -30000 + cursor.x, cursor.y, cursor.z = x, y, z + end + end + + # add an announcement + # color = integer, bright = bool + def add_announcement(str, color=nil, bright=nil) + cont = false + while str.length > 0 + rep = Report.cpp_new + rep.color = color if color + rep.bright = ((bright && bright != 0) ? 1 : 0) if bright != nil + rep.year = cur_year + rep.time = cur_year_tick + rep.flags.continuation = cont + cont = true + rep.flags.announcement = true + rep.text = str[0, 73] + str = str[73..-1].to_s + rep.id = world.status.next_report_id + world.status.next_report_id += 1 + world.status.reports << rep + world.status.announcements << rep + world.status.display_timer = 2000 + end + end + end +end diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 9a00b2bfa..41b7bf8a6 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -1,52 +1,80 @@ module DFHack -# returns an Array of all units that are current fort citizen (dwarves, on map, not hostile) -def self.unit_citizens - race = ui.race_id - civ = ui.civ_id - world.units.active.find_all { |u| - u.race == race and u.civ_id == civ and !u.flags1.dead and !u.flags1.merchant and - !u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and - !u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and - u.mood != :Berserk - # TODO check curse ; currently this should keep vampires, but may include werebeasts - } -end + class << self + # return an Unit + # with no arg, return currently selected unit in df UI ('v' or 'k' menu) + # with numeric arg, search unit by unit.id + # with an argument that respond to x/y/z (eg cursor), find first unit at this position + def unit_find(what=:selected) + if what == :selected + case ui.main.mode + when :ViewUnits + # nobody selected => idx == 0 + v = world.units.active[ui_selected_unit] + v if v and v.pos.z == cursor.z + when :LookAround + k = ui_look_list.items[ui_look_cursor] + k.unit if k.type == :Unit + else + raise "bad UI mode #{ui.main.mode.inspect}" + end + elsif what.kind_of?(Integer) + world.units.all.binsearch(what) + elsif what.respond_to?(:x) or what.respond_to?(:pos) + world.units.all.find { |u| same_pos?(what, u) } + else + raise "what what?" + end + end -# list workers (citizen, not crazy / child / inmood / noble) -def self.unit_workers - unit_citizens.find_all { |u| - u.mood == :None and - u.profession != :CHILD and - u.profession != :BABY and - # TODO MENIAL_WORK_EXEMPTION_SPOUSE - !unit_entitypositions(u).find { |pos| pos.flags[:MENIAL_WORK_EXEMPTION] } - } -end + # returns an Array of all units that are current fort citizen (dwarves, on map, not hostile) + def unit_citizens + race = ui.race_id + civ = ui.civ_id + world.units.active.find_all { |u| + u.race == race and u.civ_id == civ and !u.flags1.dead and !u.flags1.merchant and + !u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and + !u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and + u.mood != :Berserk + # TODO check curse ; currently this should keep vampires, but may include werebeasts + } + end -# list currently idle workers -def self.unit_idlers - unit_workers.find_all { |u| - # current_job includes eat/drink/sleep/pickupequip - !u.job.current_job._getv and - # filter 'attend meeting' - u.meetings.length == 0 and - # filter soldiers (TODO check schedule) - u.military.squad_index == -1 and - # filter 'on break' - !u.status.misc_traits.find { |t| id == :OnBreak } - } -end + # list workers (citizen, not crazy / child / inmood / noble) + def unit_workers + unit_citizens.find_all { |u| + u.mood == :None and + u.profession != :CHILD and + u.profession != :BABY and + # TODO MENIAL_WORK_EXEMPTION_SPOUSE + !unit_entitypositions(u).find { |pos| pos.flags[:MENIAL_WORK_EXEMPTION] } + } + end -def self.unit_entitypositions(unit) - list = [] - return list if not hf = world.history.figures.binsearch(unit.hist_figure_id) - hf.entity_links.each { |el| - next if el._rtti_classname != :histfig_entity_link_positionst - next if not ent = world.entities.all.binsearch(el.entity_id) - next if not pa = ent.positions.assignments.binsearch(el.assignment_id) - next if not pos = ent.positions.own.binsearch(pa.position_id) - list << pos - } - list -end + # list currently idle workers + def unit_idlers + unit_workers.find_all { |u| + # current_job includes eat/drink/sleep/pickupequip + !u.job.current_job._getv and + # filter 'attend meeting' + u.meetings.length == 0 and + # filter soldiers (TODO check schedule) + u.military.squad_index == -1 and + # filter 'on break' + !u.status.misc_traits.find { |t| id == :OnBreak } + } + end + + def unit_entitypositions(unit) + list = [] + return list if not hf = world.history.figures.binsearch(unit.hist_figure_id) + hf.entity_links.each { |el| + next if el._rtti_classname != :histfig_entity_link_positionst + next if not ent = world.entities.all.binsearch(el.entity_id) + next if not pa = ent.positions.assignments.binsearch(el.assignment_id) + next if not pos = ent.positions.own.binsearch(pa.position_id) + list << pos + } + list + end + end end