diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 64bea03c3a..f3bda1654a 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -6520,8 +6520,11 @@ The names of the functions are also available as the keys of the building-hacks ============== -This plugin overwrites some methods in workshop df class so that mechanical workshops are -possible. Although plugin export a function it's recommended to use lua decorated function. +This plugin extends DF workshops to support custom powered buildings. + +.. note:: + When using numeric ids for workshops be aware that those id can change between worlds, + depending on what other custom types exist in the raws for that world. .. contents:: :local: @@ -6529,78 +6532,142 @@ possible. Although plugin export a function it's recommended to use lua decorate Functions --------- -``registerBuilding(table)`` where table must contain name, as a workshop raw name, -the rest are optional: - - :name: - custom workshop id e.g., ``SOAPMAKER`` - - .. note:: this is the only mandatory field. - - :fix_impassible: - if true make impassable tiles impassable to liquids too - :consume: - how much machine power is needed to work. - Disables reactions if not supplied enough and ``needs_power==1`` - :produce: - how much machine power is produced. - :needs_power: - if produced in network < consumed stop working, default true - :gears: - a table or ``{x=?,y=?}`` of connection points for machines. - :action: - a table of number (how much ticks to skip) and a function which - gets called on shop update - :animate: - a table of frames which can be a table of: - - a. tables of 4 numbers ``{tile,fore,back,bright}`` OR - b. empty table (tile not modified) OR - c. ``{x= y= + 4 numbers like in first case}``, - this generates full frame useful for animations that change little (1-2 tiles) - - :canBeRoomSubset: - a flag if this building can be counted in room. 1 means it can, - 0 means it can't and -1 default building behaviour - :auto_gears: - a flag that automatically fills up gears and animations. - It looks over the building definition for gear icons and maps them. - - Animate table also might contain: - - :frameLength: - how many ticks does one frame take OR - :isMechanical: - a bool that says to try to match to mechanical system (i.e., how gears are turning) - -``getPower(building)`` returns two number - produced and consumed power if building can be -modified and returns nothing otherwise - -``setPower(building,produced,consumed)`` sets current power production and -consumption for a building. +* ``setOwnableBuilding(workshop_type)`` + + Set workshop to be included in zones (such as a bedroom or tavern). + + :workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id + +* ``fixImpassible(workshop_type)`` + + Set workshop non walkable tiles to also block liquids (i.e. water and magma). + + :workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id + +* ``setMachineInfo(workshop_type, needs_power, power_consumed, power_produced, connection_points)`` + + Setup and enable machine-like functionality for the workshop. All workshops of this type will have + this as their default power consumption/production. Note: due to implementation limitations, + workshops only connect to other machines if the other machines are planned after than this one. + + :workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id + :needs_power: true if the workshop should only be usable if it has sufficient power + :power_consumed: buildings of this type consume this amount of power by default + :power_produced: buildings of this type output this amount of power by default + :connection_points: a table of ``{x=?,y=?}`` zero-based coordinates that can connect to other machines + +* ``setMachineInfoAuto(workshop_type, needs_power, power_consumed, power_produced, [gear_tiles])`` + + Same as ``setMachineInfo`` but fills out the ``connection_points`` table based on the + building definition in the raws. It places connection points on tiles which have the gear + tile. ``gear_tiles`` is an optional array of two tiles that are counted as gears in the + workshop ascii tile raws. The default gear tiles are ``42`` and ``15``. + +* ``setAnimationInfo(workshop_type, frames, frame_skip)`` + + Animate workshop by replacing displayed tiles (or graphical tiles). There are two ways this works: + if ``frame_skip>0`` then it shows each frame for ``frame_skip`` of frames or if ``frame_skip<=0`` + Frames are synchronized with the machines this building is connected to. + + :workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id + :frames: table of frames. Each frame is array of rows of tiles with ids from ``1`` to ``31``. + Each frame tile is table of with following optional members: ``ch``, ``fg``, + ``bg``, ``bold``, ``tile``, ``tile_overlay``, ``tile_signpost``, ``tile_item``. + First 4 are function same as ascii workshop definition. The latter 4 are graphics + layers. ``tile_signpost`` is only valid in first row and it shows up above the workshop. + + :frame_skip: How many ticks to display one frame. If set to negative number, zero or skipped, frames + are synchronized with machine animation. + +* ``setAnimationInfoAuto(workshop_type, make_graphics_too[, frame_length][, gear_tiles])`` + + Animate workshop as with function above but generate frames automatically. This works by finding + tiles which have gears and animating them with alternating gear tiles. + + :workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id + :make_graphics_too: replace same tiles in graphics mode with tiles from vanilla df mechanism + :frame_length: How many ticks to display one frame. If set to negative number, zero or skipped, frames + are synchronized with machine animation. + :gear_tiles: Optional table with ``ch``, ``ch_alt``, ``tile``, ``tile_alt``. First two are ascii + gear tiles and are used to find tiles in workshop raw and animate them. Second two are + used to animate graphical tiles. + +* ``setOnUpdate(workshop_type, interval, callback)`` + + Register callback to be called every ``interval`` ticks for each building of this + type. This can be very expensive if the interval is low and/or there are many + workshops of this type. Keep these callbacks light! + + :workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id + :interval: how many ticks to skip between event triggers + :callback: function to call. Function signature is ``func(workshop)`` where ``workshop`` is of type + ``df.building_workshopst`` + +* ``getPower(building)`` + + If this building is of a type registered with building-hacks, returns values for + consumed and produced power. Otherwise, returns ``nil``. + + :building: specific workshop that produces or consumes power + +* ``setPower(building, power_consumed, power_produced)`` + + Dynamically sets current power production and consumption for a specific workshop + (which must be of a type registered with building-hacks). + + :building: specific workshop that produces or consumes power + :power_consumed: set building to consume this amount of power + :power_produced: output this amount of power + + +Events +------ + +This module exports two events. However only one is documented here and is intended to be used directly. To use +``onUpdateAction`` instead call ``setOnUpdate`` function. + +* ``onSetTriggerState(workshop, state)`` + + Notify when building is triggered from linked lever or trap. + + :workshop: object of type ``df.building_workshopst`` that is triggered. + :state: integer value of new state. Examples -------- Simple mechanical workshop:: - require('plugins.building-hacks').registerBuilding{name="BONE_GRINDER", - consume=15, - gears={x=0,y=0}, --connection point - animate={ - isMechanical=true, --animate the same conn. point as vanilla gear - frames={ - {{x=0,y=0,42,7,0,0}}, --first frame, 1 changed tile - {{x=0,y=0,15,7,0,0}} -- second frame, same - } - } + local bhacks = require('plugins.building-hacks') + + --work only powered, consume 15 power and one connection point at 0, 0 + bhacks.setMachineInfo("BONE_GRINDER", true, 15, 0, {{x=0, y=0}}) + + --load custom graphical tiles for use if graphics is enabled + local tile1=dfhack.screen.findGraphicsTile('DRAGON_ENGINE_TILES', 0, 0) + local tile2=dfhack.screen.findGraphicsTile('DRAGON_ENGINE_TILES', 1, 0) + + local frames={} + --first frame - tile (1, 1) changed to character 42 + ensure_key(frames, 1, 1)[1]={ch=42, fg=7, bg=0, bold=0, tile=tile1} + --second frame - tile (1,1) changed to character 15 + ensure_key(frames, 2, 1)[1]={ch=15, fg=7, bg=0, bold=0, tile=tile2} + + --set animation to switch between gear tiles at 1,1 + bhacks.setAnimationInfo("BONE_GRINDER", frames) Or with auto_gears:: - require('plugins.building-hacks').registerBuilding{name="BONE_GRINDER", - consume=15, - auto_gears=true - } + local bhacks = require('plugins.building-hacks') + + --load custom graphical tiles for use if graphics is enabled + local tile1=dfhack.screen.findGraphicsTile('DRAGON_ENGINE_TILES', 0, 0) + local tile2=dfhack.screen.findGraphicsTile('DRAGON_ENGINE_TILES', 1, 0) + + --work only powered, consume 15 power and find connection point from building raws + bhacks.setMachineInfoAuto("BONE_GRINDER", true, 15) + --set animation to switch between default ascii gears and specific graphic tiles loaded above + bhacks.setAnimationInfoAuto("BONE_GRINDER", true, -1, {tile=tile1, tile_alt=tile2}) buildingplan ============ diff --git a/docs/plugins/building-hacks.rst b/docs/plugins/building-hacks.rst index ae38cb3d99..12f49fbbe9 100644 --- a/docs/plugins/building-hacks.rst +++ b/docs/plugins/building-hacks.rst @@ -3,7 +3,7 @@ building-hacks .. dfhack-tool:: :summary: Provides a Lua API for creating powered workshops. - :tags: unavailable + :tags: fort gameplay buildings :no-command: See `building-hacks-api` for more details. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index bb1b140aea..ae27c18592 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -78,7 +78,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(autoslab autoslab.cpp) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) dfhack_plugin(burrow burrow.cpp LINK_LIBRARIES lua) - #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) + dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) add_subdirectory(buildingplan) dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) diff --git a/plugins/building-hacks.cpp b/plugins/building-hacks.cpp index 72c6e50f50..1f3ea3d6a8 100644 --- a/plugins/building-hacks.cpp +++ b/plugins/building-hacks.cpp @@ -19,50 +19,63 @@ #include "df/tile_building_occ.h" #include "df/building_drawbuffer.h" #include "df/general_ref_creaturest.h" // needed for power information storage +#include "df/building_def_workshopst.h" #include "modules/Buildings.h" -#include - +#include +#include using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("building-hacks"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + REQUIRE_GLOBAL(world); +constexpr int32_t invalid_tile = 0; struct graphic_tile //could do just 31x31 and be done, but it's nicer to have flexible imho. { - int16_t tile; //originally uint8_t but we need to indicate non-animated tiles + int16_t tile=-1; //originally uint8_t but we need to indicate non-animated tiles int8_t fore; int8_t back; int8_t bright; + //index of texpos + int32_t graphics_tile = invalid_tile; + int32_t overlay_tile = invalid_tile; + int32_t item_tile = invalid_tile; + //only for first line + int32_t signpost_tile = invalid_tile; }; struct workshop_hack_data { - int32_t myType; - bool impassible_fix; + bool impassible_fix = false; //machine stuff + bool is_machine = false; df::machine_tile_set connections; df::power_info powerInfo; bool needs_power; //animation std::vector > frames; - bool machine_timing; //6 frames used in vanilla + bool machine_timing=false; //6 frames used in vanilla int frame_skip; // e.g. 2 means have to ticks between frames //updateCallback: - int skip_updates; - int room_subset; //0 no, 1 yes, -1 default + int skip_updates=0; + int room_subset=-1; //0 no, 1 yes, -1 default }; -typedef std::map workshops_data_t; +typedef std::unordered_map workshops_data_t; workshops_data_t hacked_workshops; -static void handle_update_action(color_ostream &out,df::building_workshopst*){}; +DEFINE_LUA_EVENT_NH_1(onUpdateAction,df::building_workshopst*); +DEFINE_LUA_EVENT_NH_2(onSetTriggerState,df::building_workshopst*,int32_t); -DEFINE_LUA_EVENT_1(onUpdateAction,handle_update_action,df::building_workshopst*); -DFHACK_PLUGIN_LUA_EVENTS { +DFHACK_PLUGIN_LUA_EVENTS{ DFHACK_LUA_EVENT(onUpdateAction), + DFHACK_LUA_EVENT(onSetTriggerState), DFHACK_LUA_END }; +static void enable_hooks(bool enable); + struct work_hook : df::building_workshopst{ typedef df::building_workshopst interpose_base; @@ -85,10 +98,11 @@ struct work_hook : df::building_workshopst{ if (workshop_hack_data* def = find_def()) { df::general_ref_creaturest* ref = static_cast(DFHack::Buildings::getGeneralRef(this, general_ref_type::CREATURE)); + //try getting ref, if not return from definition if (ref) { - info->produced = ref->unk_1; - info->consumed = ref->unk_2; + info->produced = ref->race; + info->consumed = ref->caste; return true; } else @@ -97,7 +111,6 @@ struct work_hook : df::building_workshopst{ info->consumed = def->powerInfo.consumed; return true; } - //try getting ref, if not return from def } return false; } @@ -115,20 +128,21 @@ struct work_hook : df::building_workshopst{ } } df::general_ref_creaturest* ref = static_cast(DFHack::Buildings::getGeneralRef(this, general_ref_type::CREATURE)); + //if we have a setting then update it, else create a new ref for dynamic power tracking if (ref) { - ref->unk_1 = produced; - ref->unk_2 = consumed; + ref->race = produced; + ref->caste = consumed; } else { ref = df::allocate(); - ref->unk_1 = produced; - ref->unk_2 = consumed; + ref->race = produced; + ref->caste = consumed; general_refs.push_back(ref); } } - DEFINE_VMETHOD_INTERPOSE(uint32_t,getImpassableOccupancy,()) + DEFINE_VMETHOD_INTERPOSE(df::tile_building_occ,getImpassableOccupancy,()) { if(auto def = find_def()) { @@ -140,7 +154,8 @@ struct work_hook : df::building_workshopst{ DEFINE_VMETHOD_INTERPOSE(void, getPowerInfo, (df::power_info *info)) { - if (find_def()) + auto def = find_def(); + if (def && def->is_machine) { df::power_info power; get_current_power(info); @@ -150,7 +165,8 @@ struct work_hook : df::building_workshopst{ } DEFINE_VMETHOD_INTERPOSE(df::machine_info*, getMachineInfo, ()) { - if (find_def()) + auto def = find_def(); + if (def && def->is_machine) return &machine; return INTERPOSE_NEXT(getMachineInfo)(); @@ -167,10 +183,21 @@ struct work_hook : df::building_workshopst{ } DEFINE_VMETHOD_INTERPOSE(void, categorize, (bool free)) { - if (find_def()) + /* + there are two ways to enter this: + a) we have plugin enabled and building added from script (thus def exists) and we are placing a new building + b) we are loading a game thus buildings are not added from script yet + */ + auto def = find_def(); + //in "b" case this ref is used as indicator to signal that script added this building last time before we saved + df::general_ref_creaturest* ref = static_cast(DFHack::Buildings::getGeneralRef(this, general_ref_type::CREATURE)); + if( ref || (def && def->is_machine)) { auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE]; insert_into_vector(vec, &df::building::id, (df::building*)this); + //in "a" case we add a ref and set it's values to signal later that we are a modified workshop + if (!ref) + set_current_power(def->powerInfo.produced, def->powerInfo.consumed); } INTERPOSE_NEXT(categorize)(free); @@ -178,7 +205,8 @@ struct work_hook : df::building_workshopst{ DEFINE_VMETHOD_INTERPOSE(void, uncategorize, ()) { - if (find_def()) + auto def = find_def(); + if (def && def->is_machine) { auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE]; erase_from_vector(vec, &df::building::id, id); @@ -188,8 +216,10 @@ struct work_hook : df::building_workshopst{ } DEFINE_VMETHOD_INTERPOSE(bool, canConnectToMachine, (df::machine_tile_set *info)) { - if (auto def = find_def()) + auto def = find_def(); + if (def && def->is_machine) { + int real_cx = centerx, real_cy = centery; bool ok = false; @@ -258,9 +288,15 @@ struct work_hook : df::building_workshopst{ } INTERPOSE_NEXT(updateAction)(); } - DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, int16_t unk)) + DEFINE_VMETHOD_INTERPOSE(void, setTriggerState,(int32_t state)) { - INTERPOSE_NEXT(drawBuilding)(db, unk); + color_ostream_proxy out(Core::getInstance().getConsole()); + onSetTriggerState(out, this, state); + INTERPOSE_NEXT(setTriggerState)(state); //pretty sure default workshop has nothing related to this, but to be future proof lets do it like this + } + DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (uint32_t curtick,df::building_drawbuffer *db, int16_t z_offset)) + { + INTERPOSE_NEXT(drawBuilding)(curtick,db, z_offset); if (auto def = find_def()) { @@ -283,19 +319,28 @@ struct work_hook : df::building_workshopst{ } } } - int w=db->x2-db->x1+1; std::vector &cur_frame=def->frames[frame]; for(size_t i=0;i=0) + int tx = i % 31; + int ty = i / 31; + const auto& cf = cur_frame[i]; + if(cf.tile>=0) { - int tx=i % w; - int ty=i / w; - db->tile[tx][ty]=cur_frame[i].tile; - db->back[tx][ty]=cur_frame[i].back; - db->bright[tx][ty]=cur_frame[i].bright; - db->fore[tx][ty]=cur_frame[i].fore; + db->tile[tx][ty]= cf.tile; + db->back[tx][ty]= cf.back; + db->bright[tx][ty]= cf.bright; + db->fore[tx][ty]= cf.fore; } + if (cf.graphics_tile != invalid_tile) + db->building_one_texpos[tx][ty] = cf.graphics_tile; + if (cf.overlay_tile != invalid_tile) + db->building_two_texpos[tx][ty] = cf.overlay_tile; + if (cf.item_tile != invalid_tile) + db->item_texpos[tx][ty] = cf.item_tile; + //only first line has signpost graphics + if (cf.item_tile != invalid_tile && ty==0) + db->signpost_texpos[tx] = cf.signpost_tile; } } } @@ -311,114 +356,211 @@ IMPLEMENT_VMETHOD_INTERPOSE(work_hook, canConnectToMachine); IMPLEMENT_VMETHOD_INTERPOSE(work_hook, isUnpowered); IMPLEMENT_VMETHOD_INTERPOSE(work_hook, canBeRoomSubset); IMPLEMENT_VMETHOD_INTERPOSE(work_hook, updateAction); +IMPLEMENT_VMETHOD_INTERPOSE(work_hook, setTriggerState); IMPLEMENT_VMETHOD_INTERPOSE(work_hook, drawBuilding); - +int get_workshop_type(lua_State* L,int arg) +{ + size_t len; + int is_num; + int type; + type=lua_tointegerx(L, arg, &is_num); + if (is_num) + { + return type; + } + auto str = lua_tolstring(L, arg, &len); + if (len) + { + std::string lua_name(str, len); + const auto& raws = world->raws.buildings.workshops; + for (size_t i = 0; i < raws.size(); i++) + { + if (raws[i]->code == lua_name) + return raws[i]->id; + } + } + luaL_argerror(L, arg, "expected int or string workshop id"); + return 0; +} void clear_mapping() { hacked_workshops.clear(); } +static void load_graphics_tile(lua_State* L,graphic_tile& t) +{ + lua_getfield(L, -1, "ch"); + t.tile = luaL_optinteger(L, -1, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "fg"); + t.fore = luaL_optinteger(L, -1, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "bg"); + t.back = luaL_optinteger(L, -1, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "bold"); + t.bright = luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + + lua_getfield(L, -1, "tile"); + t.graphics_tile = luaL_optinteger(L, -1, invalid_tile); + lua_pop(L, 1); + + lua_getfield(L, -1, "tile_overlay"); + t.overlay_tile = luaL_optinteger(L, -1, invalid_tile); + lua_pop(L, 1); + + lua_getfield(L, -1, "tile_signpost"); + t.signpost_tile = luaL_optinteger(L, -1, invalid_tile); + lua_pop(L, 1); + + lua_getfield(L, -1, "tile_item"); + t.item_tile = luaL_optinteger(L, -1, invalid_tile); + lua_pop(L, 1); +} static void loadFrames(lua_State* L,workshop_hack_data& def,int stack_pos) { + const int max_side = 31; + const int max_idx = max_side * max_side; + luaL_checktype(L,stack_pos,LUA_TTABLE); - lua_pushvalue(L,stack_pos); - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - luaL_checktype(L,-1,LUA_TTABLE); - lua_pushnil(L); - std::vector frame; - while (lua_next(L, -2) != 0) { - graphic_tile t; - lua_pushnumber(L,1); - lua_gettable(L,-2); - if(lua_isnil(L,-1)) + + int frame_index = 1; + def.frames.clear(); + + while (lua_geti(L,stack_pos,frame_index) != LUA_TNIL) { //get frame[i] + luaL_checktype(L,-1,LUA_TTABLE); //ensure that it's a table + std::vector frame(max_idx); + for (int x = 1; x <= max_side; x++) + { + lua_geti(L, -1, x); //get row at x + if (lua_isnil(L, -1))//allow sparse indexing { - t.tile=-1; - lua_pop(L,1); + lua_pop(L, 1); //pop current tile (nil in this case) + continue; } - else - { - t.tile=lua_tonumber(L,-1); - lua_pop(L,1); - lua_pushnumber(L,2); - lua_gettable(L,-2); - t.fore=lua_tonumber(L,-1); - lua_pop(L,1); - - lua_pushnumber(L,3); - lua_gettable(L,-2); - t.back=lua_tonumber(L,-1); - lua_pop(L,1); + for (int y = 1; y <= max_side; y++) + { + lua_geti(L, -1, y); //get cell at y + if (lua_isnil(L, -1))//allow sparse indexing + { + lua_pop(L, 1); //pop current tile (nil in this case) + continue; + } - lua_pushnumber(L,4); - lua_gettable(L,-2); - t.bright=lua_tonumber(L,-1); - lua_pop(L,1); + load_graphics_tile(L, frame[(x-1)+(y-1)*max_side]); + lua_pop(L, 1); //pop current tile } - frame.push_back(t); - lua_pop(L,1); + lua_pop(L, 1); //pop current row } - lua_pop(L,1); def.frames.push_back(frame); + frame_index++; + lua_pop(L, 1); //pop current frame } - lua_pop(L,1); - return ; + + return; +} + +//fixImpassible(workshop_type) - changes how impassible tiles work with liquids. +static int fixImpassible(lua_State* L) +{ + int workshop_type = get_workshop_type(L, 1); + + auto& def = hacked_workshops[workshop_type]; + def.impassible_fix = true; + + enable_hooks(true); + + return 0; } -//arguments: custom type,impassible fix (bool), consumed power, produced power, list of connection points, update skip(0/nil to disable) -// table of frames,frame to tick ratio (-1 for machine control) -static int addBuilding(lua_State* L) +//setMachineInfo(workshop_type,bool needs_power,int power_consumed=0,int power_produced=0,table [x=int,y=int] connection_points) -setups and enables machine (i.e. connected to gears, and co) behaviour of the building +static int setMachineInfo(lua_State* L) { - workshop_hack_data newDefinition; - newDefinition.myType=luaL_checkint(L,1); - newDefinition.impassible_fix=luaL_checkint(L,2); - newDefinition.powerInfo.consumed=luaL_checkint(L,3); - newDefinition.powerInfo.produced=luaL_checkint(L,4); - newDefinition.needs_power = luaL_optinteger(L, 5, 1); + int workshop_type = get_workshop_type(L, 1); + auto& def = hacked_workshops[workshop_type]; + def.is_machine = true; + + def.needs_power = lua_toboolean(L, 2); + def.powerInfo.consumed = luaL_optinteger(L, 3,0); + def.powerInfo.produced = luaL_optinteger(L, 4,0); + //table of machine connection points - luaL_checktype(L,6,LUA_TTABLE); - lua_pushvalue(L,6); + luaL_checktype(L, 5, LUA_TTABLE); + lua_pushvalue(L, 5); lua_pushnil(L); while (lua_next(L, -2) != 0) { - lua_getfield(L,-1,"x"); - int x=lua_tonumber(L,-1); - lua_pop(L,1); - lua_getfield(L,-1,"y"); - int y=lua_tonumber(L,-1); - lua_pop(L,1); + lua_getfield(L, -1, "x"); + int x = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "y"); + int y = lua_tonumber(L, -1); + lua_pop(L, 1); df::machine_conn_modes modes; modes.whole = -1; - newDefinition.connections.can_connect.push_back(modes);//TODO add this too... - newDefinition.connections.tiles.push_back(df::coord(x,y,0)); + def.connections.can_connect.push_back(modes);//TODO add this too... + def.connections.tiles.push_back(df::coord(x, y, 0)); - lua_pop(L,1); + lua_pop(L, 1); } - lua_pop(L,1); - //updates - newDefinition.skip_updates=luaL_optinteger(L,7,0); + lua_pop(L, 1); + + enable_hooks(true); + + return 0; +} +//setUpdateSkip(workshop_type,int skip_frames) - skips frames to lower onupdate event call rate, 0 to disable +static int setUpdateSkip(lua_State* L) +{ + int workshop_type = get_workshop_type(L, 1); + auto& def = hacked_workshops[workshop_type]; + + def.skip_updates = luaL_optinteger(L, 2, 0); + + enable_hooks(true); + + return 0; +} +//setAnimationInfo(workshop_type,table frames, [frame_skip]) - define animation and it's timing. If frame_skip is not set or set to -1, it will use machine timing (i.e. like gears/axels etc) +static int setAnimationInfo(lua_State* L) +{ + int workshop_type = get_workshop_type(L, 1); + auto& def = hacked_workshops[workshop_type]; //animation - if(!lua_isnil(L,8)) - { - loadFrames(L,newDefinition,8); - newDefinition.frame_skip=luaL_optinteger(L,9,-1); - if(newDefinition.frame_skip==0) - newDefinition.frame_skip=1; - if(newDefinition.frame_skip<0) - newDefinition.machine_timing=true; - else - newDefinition.machine_timing=false; - } - newDefinition.room_subset=luaL_optinteger(L,10,-1); - hacked_workshops[newDefinition.myType]=newDefinition; + loadFrames(L, def, 2); + def.frame_skip = luaL_optinteger(L, 3, -1); + if (def.frame_skip <= 0) + def.machine_timing = true; + else + def.machine_timing = false; + + enable_hooks(true); + + return 0; +} +//setOwnableBuilding(workshop_type) +static int setOwnableBuilding(lua_State* L) +{ + int workshop_type = get_workshop_type(L, 1); + + auto& def = hacked_workshops[workshop_type]; + def.room_subset = true; + + enable_hooks(true); + return 0; } static void setPower(df::building_workshopst* workshop, int power_produced, int power_consumed) { work_hook* ptr = static_cast(workshop); - if (ptr->find_def()) // check if it's really hacked workshop + auto def = ptr->find_def(); + if (def && def->is_machine) // check if it's really hacked workshop { ptr->set_current_power(power_produced, power_consumed); } @@ -429,12 +571,13 @@ static int getPower(lua_State*L) work_hook* ptr = static_cast(workshop); if (!ptr) return 0; - if (ptr->find_def()) // check if it's really hacked workshop + auto def = ptr->find_def(); + if (def && def->is_machine) // check if it's really hacked workshop { df::power_info info; ptr->get_current_power(&info); - lua_pushinteger(L, info.produced); lua_pushinteger(L, info.consumed); + lua_pushinteger(L, info.produced); return 2; } return 0; @@ -444,32 +587,37 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_END }; DFHACK_PLUGIN_LUA_COMMANDS{ - DFHACK_LUA_COMMAND(addBuilding), DFHACK_LUA_COMMAND(getPower), + DFHACK_LUA_COMMAND(setOwnableBuilding), + DFHACK_LUA_COMMAND(setAnimationInfo), + DFHACK_LUA_COMMAND(setUpdateSkip), + DFHACK_LUA_COMMAND(setMachineInfo), + DFHACK_LUA_COMMAND(fixImpassible), DFHACK_LUA_END }; static void enable_hooks(bool enable) { + if (is_enabled == enable) + return; + is_enabled = enable; + INTERPOSE_HOOK(work_hook,getImpassableOccupancy).apply(enable); //machine part INTERPOSE_HOOK(work_hook,getPowerInfo).apply(enable); INTERPOSE_HOOK(work_hook,getMachineInfo).apply(enable); INTERPOSE_HOOK(work_hook,isPowerSource).apply(enable); - INTERPOSE_HOOK(work_hook,categorize).apply(enable); - INTERPOSE_HOOK(work_hook,uncategorize).apply(enable); + INTERPOSE_HOOK(work_hook,canConnectToMachine).apply(enable); INTERPOSE_HOOK(work_hook,isUnpowered).apply(enable); INTERPOSE_HOOK(work_hook,canBeRoomSubset).apply(enable); //update n render INTERPOSE_HOOK(work_hook,updateAction).apply(enable); + INTERPOSE_HOOK(work_hook,setTriggerState).apply(enable); INTERPOSE_HOOK(work_hook,drawBuilding).apply(enable); } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { - case SC_WORLD_LOADED: - enable_hooks(true); - break; case SC_WORLD_UNLOADED: enable_hooks(false); clear_mapping(); @@ -480,14 +628,22 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan return CR_OK; } +//this is needed as all other methods depend on world being loaded but categorize happens when we are loading stuff in +static void init_categorize(bool enable) +{ + INTERPOSE_HOOK(work_hook, categorize).apply(enable); + INTERPOSE_HOOK(work_hook, uncategorize).apply(enable); +} DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { enable_hooks(true); + init_categorize(true); return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { plugin_onstatechange(out,SC_WORLD_UNLOADED); + init_categorize(false); return CR_OK; } diff --git a/plugins/lua/building-hacks.lua b/plugins/lua/building-hacks.lua index 7304dfae33..45a3826da2 100644 --- a/plugins/lua/building-hacks.lua +++ b/plugins/lua/building-hacks.lua @@ -1,50 +1,61 @@ local _ENV = mkmodule('plugins.building-hacks') --[[ from native: - addBuilding(custom type,impassible fix (bool), consumed power, produced power, list of connection points, - update skip(0/nil to disable),table of frames,frame to tick ratio (-1 for machine control)) - getPower(bld) -- 2 or 0 returns, produced and consumed - setPower(bld,produced, consumed) + setOwnableBuilding(workshop_type) + setAnimationInfo(workshop_type,table frames,int frameskip=-1) + setUpdateSkip(workshop_type,int=0) + setMachineInfo(workshop_type,bool need_power,int consume=0,int produce=0,table connection_points) + fixImpassible(workshop_type) + getPower(building) -- 2 or 0 returns, consumed and produced + setPower(building, consumed, produced) from here: - registerBuilding{ - name -- custom workshop id e.g. SOAPMAKER << required! - fix_impassible -- make impassible tiles impassible to liquids too - consume -- how much machine power is needed to work - produce -- how much machine power is produced - needs_power -- needs power to be able to add jobs - action -- a table of number (how much ticks to skip) and a function which gets called on shop update - canBeRoomSubset -- room is considered in to be part of the building defined by chairs etc... - auto_gears -- find the gears automatically and animate them - gears -- a table or {x=?,y=?} of connection points for machines - animate -- a table of - frames -- a table of - tables of 4 numbers (tile,fore,back,bright) OR - empty table (tile not modified) OR - {x= y= + 4 numbers like in first case} -- this generates full frame even, usefull for animations that change little (1-2 tiles) - frameLenght -- how many ticks does one frame take OR - isMechanical -- a bool that says to try to match to mechanical system (i.e. how gears are turning) - } + setMachineInfoAuto(name,int consume,int produce,bool need_power) + setAnimationInfoAuto(name,bool make_graphics_too) + setOnUpdate(name,int interval,callback) ]] + _registeredStuff={} + +local CHAR_GEAR=42 +local CHAR_GEAR_ALT=15 + +--cache graphics tiles for mechanical gears +local graphics_cache +function reload_graphics_cache( ) + graphics_cache={} + graphics_cache[1]=dfhack.screen.findGraphicsTile('AXLES_GEARS',0,2) + graphics_cache[2]=dfhack.screen.findGraphicsTile('AXLES_GEARS',1,2) +end + +--on world unload unreg callbacks and invalidate cache local function unregall(state) if state==SC_WORLD_UNLOADED then + graphics_cache=nil onUpdateAction._library=nil dfhack.onStateChange.building_hacks= nil _registeredStuff={} end end + local function onUpdateLocal(workshop) local f=_registeredStuff[workshop:getCustomType()] if f then f(workshop) end end -local function findCustomWorkshop(name) - local raws=df.global.world.raws.buildings.all - for k,v in ipairs(raws) do - if v.code==name then - return v +local function findCustomWorkshop(name_or_id) + if type(name_or_id) == "string" then + local raws=df.global.world.raws.buildings.all + for k,v in ipairs(raws) do + if v.code==name_or_id then + return v + end end + error("Building def:"..name_or_id.." not found") + elseif type(name_or_id)=="number" then + return df.building_def.find(name_or_id) + else + error("Expected string or integer id for workshop definition") end end local function registerUpdateAction(shopId,callback) @@ -52,54 +63,44 @@ local function registerUpdateAction(shopId,callback) onUpdateAction._library=onUpdateLocal dfhack.onStateChange.building_hacks=unregall end -local function generateFrame(tiles,w,h) +--take in tiles with {x=?, y=? ,...} and output a table in format needed for setAnimationInfo +local function generateFrame(tiles) local mTiles={} - for k,v in ipairs(tiles) do - mTiles[v.x]=mTiles[v.x] or {} - mTiles[v.x][v.y]=v - end local ret={} - for ty=0,h-1 do - for tx=0,w-1 do - if mTiles[tx] and mTiles[tx][ty] then - table.insert(ret,mTiles[tx][ty]) -- leaves x and y in but who cares - else - table.insert(ret,{}) - end - end + for k,v in ipairs(tiles) do + ensure_key(ret, v.x)[v.y]=v end return ret end -local function processFrames(shop_def,frames) - local w,h=shop_def.dim_x,shop_def.dim_y - for frame_id,frame in ipairs(frames) do - if frame[1].x~=nil then - frames[frame_id]=generateFrame(frame,w,h) - end - end - return frames -end -local function findGears( shop_def ) --finds positions of all gears and inverted gears + +--locate gears on the workshop from the raws definition +local function findGears( shop_def ,gear_tiles) --finds positions of all gears and inverted gears + gear_tiles=gear_tiles or {ch=CHAR_GEAR,ch_alt=CHAR_GEAR_ALT} local w,h=shop_def.dim_x,shop_def.dim_y local stage=shop_def.build_stages local ret={} for x=0,w-1 do for y=0,h-1 do local tile=shop_def.tile[stage][x][y] - if tile==42 then --gear icon + if tile==gear_tiles.ch then --gear icon table.insert(ret,{x=x,y=y,invert=false}) - elseif tile==15 then --inverted gear icon + elseif tile==gear_tiles.ch_alt then --inverted gear icon table.insert(ret,{x=x,y=y,invert=true}) end end end + if #ret==0 then + error(string.format("Could not find gears in a workshop (%s) that was marked for auto-gear finding",shop_def.code)) + end return ret end +--helper for reading tile color info from raws local function lookup_color( shop_def,x,y,stage ) return shop_def.tile_color[0][stage][x][y],shop_def.tile_color[1][stage][x][y],shop_def.tile_color[2][stage][x][y] end -local function processFramesAuto( shop_def ,gears) --adds frames for all gear icons and inverted gear icons - local w,h=shop_def.dim_x,shop_def.dim_y +--adds frames for all gear icons and inverted gear icons +local function processFramesAuto( shop_def ,gears,auto_graphics,gear_tiles) + gear_tiles=gear_tiles or {ch=CHAR_GEAR,ch_alt=CHAR_GEAR_ALT,tile=graphics_cache[1],tile_alt=graphics_cache[2]} local frames={{},{}} --two frames only local stage=shop_def.build_stages @@ -107,73 +108,47 @@ local function processFramesAuto( shop_def ,gears) --adds frames for all gear ic local tile,tile_inv if v.inverted then - tile=42 - tile_inv=15 + tile=gear_tiles.ch or CHAR_GEAR + tile_inv=gear_tiles.ch_alt or CHAR_GEAR_ALT else - tile=15 - tile_inv=42 + tile=gear_tiles.ch_alt or CHAR_GEAR_ALT + tile_inv=gear_tiles.ch or CHAR_GEAR end - table.insert(frames[1],{x=v.x,y=v.y,tile,lookup_color(shop_def,v.x,v.y,stage)}) - table.insert(frames[2],{x=v.x,y=v.y,tile_inv,lookup_color(shop_def,v.x,v.y,stage)}) + table.insert(frames[1],{x=v.x,y=v.y,ch=tile,fg=lookup_color(shop_def,v.x,v.y,stage)}) + table.insert(frames[2],{x=v.x,y=v.y,ch=tile_inv,fg=lookup_color(shop_def,v.x,v.y,stage)}) + + --insert default gear graphics if auto graphics is on + if auto_graphics then + frames[1][#frames[1]].tile=gear_tiles.tile or graphics_cache[1] + frames[2][#frames[2]].tile=gear_tiles.tile_alt or graphics_cache[2] + end end for frame_id,frame in ipairs(frames) do - frames[frame_id]=generateFrame(frame,w,h) + frames[frame_id]=generateFrame(frame) end return frames end -function registerBuilding(args) - local shop_def=findCustomWorkshop(args.name) - local shop_id=shop_def.id - --misc - local fix_impassible - if args.fix_impassible then - fix_impassible=1 - else - fix_impassible=0 - end - local roomSubset=args.canBeRoomSubset or -1 - --power - local consume=args.consume or 0 - local produce=args.produce or 0 - local needs_power=args.needs_power or 1 - local auto_gears=args.auto_gears or false - local updateSkip=0 - local action=args.action --could be nil - if action~=nil then - updateSkip=action[1] - registerUpdateAction(shop_id,action[2]) - end - --animations and connections next: - local gears - local frames - - local frameLength - local animate=args.animate - if not auto_gears then - gears=args.gears or {} - frameLength=1 - if animate~=nil then - frameLength=animate.frameLength - if animate.isMechanical then - frameLength=-1 - end - frames=processFrames(shop_def,animate.frames) - end - else - frameLength=-1 - if animate~=nil then - frameLength=animate.frameLength or frameLength - if animate.isMechanical then - frameLength=-1 - end - end - gears=findGears(shop_def) - frames=processFramesAuto(shop_def,gears) +function setMachineInfoAuto( name,need_power,consume,produce,gear_tiles) + local shop_def=findCustomWorkshop(name) + local gears=findGears(shop_def,gear_tiles) + setMachineInfo(name,need_power,consume,produce,gears) +end +function setAnimationInfoAuto( name,make_graphics_too,frame_length,gear_tiles ) + if graphics_cache==nil then + reload_graphics_cache() end - - addBuilding(shop_id,fix_impassible,consume,produce,needs_power,gears,updateSkip,frames,frameLength,roomSubset) + local shop_def=findCustomWorkshop(name) + local gears=findGears(shop_def,gear_tiles) + local frames=processFramesAuto(shop_def,gears,make_graphics_too,gear_tiles) + setAnimationInfo(name,frames,frame_length) +end +function setOnUpdate(name,interval,callback) + local shop_def=findCustomWorkshop(name) + local shop_id=shop_def.id + setUpdateSkip(name,interval) + registerUpdateAction(shop_id,callback) end return _ENV