Merge branch 'develop' of github.com:DFHack/dfhack into remote_reader

develop
JapaMala 2014-07-15 17:50:11 +05:30
commit 48e714b65b
55 changed files with 6470 additions and 1747 deletions

@ -3161,6 +3161,22 @@ These events are straight from EventManager module. Each of them first needs to
Gets called when someone picks up an item, puts one down, or changes the way they are holding it. If an item is picked up, old_equip will be null. If an item is dropped, new_equip will be null. If an item is re-equipped in a new way, then neither will be null. You absolutely must NOT alter either old_equip or new_equip or you might break other plugins.
10. ``onReport(reportId)``
Gets called when a report happens. This happens more often than you probably think, even if it doesn't show up in the announcements.
11. ``onUnitAttack(attackerId, defenderId, woundId)``
Called when a unit wounds another with a weapon. Is NOT called if blocked, dodged, deflected, or parried.
12. ``onUnload()``
A convenience event in case you don't want to register for every onStateChange event.
13. ``onInteraction(attackVerb, defendVerb, attackerId, defenderId, attackReportId, defendReportId)``
Called when a unit uses an interaction on another.
Functions
---------

103
NEWS

@ -1,12 +1,101 @@
DFHack future
Internals:
New scripts:
New commands:
New tweaks:
New plugins:
Misc improvements:
- outsideOnly: now buildings have to be registered as inside or outside only, and it checks periodically to see when buildings change outsideness
Internals:
supported per save script folders
Items module: added createItem function
Sorted CMakeList for plugins and plugins/devel
diggingInvaders no longer builds if plugin building is disabled
EventManager:
EQUIPMENT_CHANGE now triggers for new units
new events:
ON_REPORT
UNIT_ATTACK
UNLOAD
INTERACTION
sorted CMakeLists for plugins and devel plugins
New scripts:
lua/ //lua module folder
repeat-util.lua
makes it easier to make things repeat indefinitely
syndrome-util.lua
makes it easier to deal with unit syndromes
scripts/
forum-dwarves.lua
helps copies df viewscreens to a file
full-heal.lua
fully heals a unit
remove-wear.lua
removes wear from all items in the fort
repeat.lua
repeatedly calls a script or a plugin
ShowUnitSyndromes.rb
shows syndromes affecting units and other relevant info
teleport.lua
teleports units
scripts/devel/
print-args.lua
scripts/fix/
blood-del.lua
makes it so civs don't bring barrels full of blood ichor or goo
feeding-timers.lua
reset the feeding timers of all units
growth-bug.lua
fixes the growth bug
scripts/gui/
hack-wish.lua
creates items out of any material
unit-info-viewer.lua
displays information about units
scripts/modtools/
add-syndrome.lua
add a syndrome to a unit or remove one
force.lua
forces events: caravan, migrants, diplomat, megabeast, curiousbeast, mischievousbeast, flier, siege, nightcreature
item-trigger.lua
triggers commands based on equipping, unequipping, and wounding units with items
interaction-trigger.lua
triggers commands when interactions happen
invader-item-destroyer.lua
destroys invaders' items when they die
moddable-gods.lua
standardized version of Putnam's moddable gods script
outside-only.lua
register buildings as outside only or inside only
replaces outsideOnly plugin
projectile-trigger.lua
standardized version of projectileExpansion
reaction-trigger.lua
trigger commands when custom reactions complete
replaces autoSyndrome
reaction-trigger-transition.lua
a tool for converting mods from autoSyndrome to reaction-trigger
random-trigger.lua
triggers random scripts that you register
skill-change.lua
for incrementing and setting skills
spawn-flow.lua
creates flows, like mist or dragonfire
syndrome-trigger.lua
trigger commands when syndromes happen
transform-unit.lua
shapeshifts a unit, possibly permanently
New commands:
New tweaks:
New plugins:
Misc improvements:
new function in utils.lua for standardized argument processing
Removed
digmat.rb: digFlood does the same functionality with less FPS impact
scripts/invasionNow: scripts/modtools/force.lua does it better
autoSyndrome replaced with scripts/modtools/reaction-trigger.lua
syndromeTrigger replaced with scripts/modtools/syndrome-trigger.lua
devel/printArgs plugin converted to scripts/devel/printArgs.lua
DFHack v0.34.11-r5

File diff suppressed because it is too large Load Diff

@ -57,7 +57,7 @@ keybinding add Ctrl-D@legends "exportlegends maps"
keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show"
# open an overview window summarising some stocks (dfstatus)
keybinding add Ctrl-Shift-I@dwarfmode/Default dfstatus
keybinding add Ctrl-Shift-I@dwarfmode/Default "gui/dfstatus"
# q->stockpile; p - copy & paste stockpiles
keybinding add Alt-P copystock
@ -204,6 +204,12 @@ log-region
# patch the material objects in memory to fix cloth stockpiles
fix/cloth-stockpile enable
# civs don't bring blood
#:lua dfhack.onStateChange.onLoadBloodDel = function(state) if state == SC_WORLD_LOADED then dfhack.run_command('repeat -time 1 -timeUnits months -command fix/blood-del') end end
# run growth bug regularly
#:lua dfhack.onStateChange.onLoadGrowthBug = function(state) if state == SC_WORLD_LOADED then dfhack.run_command('repeat -time 1 -timeUnits months -command fix/growth-bug') end end
#######################################################
# Apply binary patches at runtime #
#######################################################

@ -393,6 +393,26 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std::
return false;
}
string findScript(string path, string name) {
//first try the save folder if it exists
string save = World::ReadWorldFolder();
if ( save != "" ) {
string file = path + "/data/save/" + save + "/raw/scripts/" + name;
if (fileExists(file)) {
return file;
}
}
string file = path + "/raw/scripts/" + name;
if (fileExists(file)) {
return file;
}
file = path + "/hack/scripts/" + name;
if (fileExists(file)) {
return file;
}
return "";
}
command_result Core::runCommand(color_ostream &con, const std::string &first, vector<string> &parts)
{
if (!first.empty())
@ -446,18 +466,20 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
return CR_OK;
}
}
auto filename = getHackPath() + "scripts/" + parts[0];
if (fileExists(filename + ".lua"))
{
string help = getScriptHelp(filename + ".lua", "-- ");
string path = this->p->getPath();
string file = findScript(path, parts[0] + ".lua");
if ( file != "" ) {
string help = getScriptHelp(file, "-- ");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
}
if (plug_mgr->ruby && plug_mgr->ruby->is_enabled() && fileExists(filename + ".rb"))
{
string help = getScriptHelp(filename + ".rb", "# ");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
if (plug_mgr->ruby && plug_mgr->ruby->is_enabled() ) {
file = findScript(path, parts[0] + ".rb");
if ( file != "" ) {
string help = getScriptHelp(file, "# ");
con.print("%s: %s\n", parts[0].c_str(), help.c_str());
return CR_OK;
}
}
con.printerr("Unknown command: %s\n", parts[0].c_str());
}
@ -765,15 +787,19 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
command_result res = plug_mgr->InvokeCommand(con, first, parts);
if(res == CR_NOT_IMPLEMENTED)
{
auto filename = getHackPath() + "scripts/" + first;
std::string completed;
if (fileExists(filename + ".lua"))
string completed;
string path = this->p->getPath();
string filename = findScript(path, first + ".lua");
bool lua = filename != "";
if ( !lua ) {
filename = findScript(path, first + ".rb");
}
if ( lua )
res = runLuaScript(con, first, parts);
else if (plug_mgr->ruby && plug_mgr->ruby->is_enabled() && fileExists(filename + ".rb"))
else if ( filename != "" && plug_mgr->ruby && plug_mgr->ruby->is_enabled() )
res = runRubyScript(con, plug_mgr, first, parts);
else if (try_autocomplete(con, first, completed))
return CR_NOT_IMPLEMENTED;// runCommand(con, completed, parts);
else if ( try_autocomplete(con, first, completed) )
return CR_NOT_IMPLEMENTED;
else
con.printerr("%s is not a recognized command.\n", first.c_str());
}

@ -1465,6 +1465,20 @@ static df::proj_itemst *items_makeProjectile(df::item *item)
return Items::makeProjectile(mc, item);
}
static int16_t items_findType(std::string token)
{
DFHack::ItemTypeInfo result;
result.find(token);
return result.type;
}
static int32_t items_findSubtype(std::string token)
{
DFHack::ItemTypeInfo result;
result.find(token);
return result.subtype;
}
static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPM(Items, getGeneralRef),
WRAPM(Items, getSpecificRef),
@ -1479,12 +1493,15 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPM(Items, getSubtypeDef),
WRAPM(Items, getItemBaseValue),
WRAPM(Items, getValue),
WRAPM(Items, createItem),
WRAPN(moveToGround, items_moveToGround),
WRAPN(moveToContainer, items_moveToContainer),
WRAPN(moveToBuilding, items_moveToBuilding),
WRAPN(moveToInventory, items_moveToInventory),
WRAPN(makeProjectile, items_makeProjectile),
WRAPN(remove, items_remove),
WRAPN(findType, items_findType),
WRAPN(findSubtype, items_findSubtype),
{ NULL, NULL }
};

@ -9,8 +9,10 @@
#include "Console.h"
#include "DataDefs.h"
#include <df/coord.h>
#include <df/unit_inventory_item.h>
#include "df/coord.h"
#include "df/unit.h"
#include "df/unit_inventory_item.h"
#include "df/unit_wound.h"
namespace DFHack {
namespace EventManager {
@ -26,6 +28,10 @@ namespace DFHack {
SYNDROME,
INVASION,
INVENTORY_CHANGE,
REPORT,
UNIT_ATTACK,
UNLOAD,
INTERACTION,
EVENT_MAX
};
}
@ -69,6 +75,21 @@ namespace DFHack {
InventoryChangeData(int32_t id_in, InventoryItem* old_in, InventoryItem* new_in): unitId(id_in), item_old(old_in), item_new(new_in) {}
};
struct UnitAttackData {
int32_t attacker;
int32_t defender;
int32_t wound;
};
struct InteractionData {
std::string attackVerb;
std::string defendVerb;
int32_t attackReport;
int32_t defendReport;
int32_t attacker;
int32_t defender;
};
DFHACK_EXPORT void registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin);
DFHACK_EXPORT int32_t registerTick(EventHandler handler, int32_t when, Plugin* plugin, bool absolute=false);
DFHACK_EXPORT void unregister(EventType::EventType e, EventHandler handler, Plugin* plugin);

@ -179,5 +179,8 @@ DFHACK_EXPORT int getItemBaseValue(int16_t item_type, int16_t item_subtype, int1
/// Gets the value of a specific item, ignoring civ values and trade agreements
DFHACK_EXPORT int getValue(df::item *item);
DFHACK_EXPORT int32_t createItem(df::item_type type, int16_t item_subtype, int16_t mat_type, int32_t mat_index, df::unit* creator);
}
}

@ -371,23 +371,44 @@ internal.scripts = internal.scripts or {}
local scripts = internal.scripts
local hack_path = dfhack.getHackPath()
function dfhack.run_script(name,...)
local key = string.lower(name)
local function findScript(name)
local file = hack_path..'scripts/'..name..'.lua'
local env = scripts[key]
if dfhack.filesystem.exists(file) then
return file
end
file = dfhack.getSavePath()
if file then
file = file .. '/raw/scripts/' .. name .. '.lua'
if dfhack.filesystem.exists(file) then
return file
end
end
file = hack_path..'../raw/scripts/' .. name .. '.lua'
if dfhack.filesystem.exists(file) then
return file
end
return nil
end
function dfhack.run_script(name,...)
local file = findScript(name)
if not file then
error('Could not find script ' .. name)
end
local env = scripts[file]
if env == nil then
env = {}
setmetatable(env, { __index = base_env })
end
local f,perr = loadfile(file, 't', env)
if f == nil then
error(perr)
if f then
scripts[file] = env
return f(...)
end
scripts[key] = env
return f(...)
error(perr)
end
function dfhack.run_command(...)
local function _run_command(...)
args = {...}
if type(args[1]) == 'table' then
command = args[1]
@ -402,8 +423,12 @@ function dfhack.run_command(...)
else
error('Invalid arguments')
end
result = internal.runCommand(command)
output = ""
return internal.runCommand(command)
end
function dfhack.run_command_silent(...)
local result = _run_command(...)
local output = ""
for i, f in pairs(result) do
if type(f) == 'table' then
output = output .. f[2]
@ -412,6 +437,17 @@ function dfhack.run_command(...)
return output, result.status
end
function dfhack.run_command(...)
local output, status = _run_command(...)
for i, fragment in pairs(output) do
if type(fragment) == 'table' then
dfhack.color(fragment[1])
dfhack.print(fragment[2])
end
end
dfhack.color(COLOR_RESET)
end
-- Per-save init file
function dfhack.getSavePath()

@ -0,0 +1,42 @@
-- lua/plugins/repeatUtil.lua
-- author expwnent
-- tools for registering callbacks periodically
-- vaguely based on a script by Putnam
local _ENV = mkmodule("repeat-util")
repeating = repeating or {}
dfhack.onStateChange.repeatUtilStateChange = function(code)
if code == SC_WORLD_UNLOADED then
repeating = {}
end
end
function cancel(name)
if not repeating[name] then
return false
end
dfhack.timeout_active(repeating[name],nil)
repeating[name] = nil
return true
end
function scheduleEvery(name,time,timeUnits,func)
cancel(name)
local function helper()
func()
repeating[name] = dfhack.timeout(time,timeUnits,helper)
end
helper()
end
function scheduleUnlessAlreadyScheduled(name,time,timeUnits,func)
if repeating[name] then
return
end
scheduleEvery(name,time,timeUnits,func)
end
return _ENV

@ -0,0 +1,154 @@
--lua/syndrome-util.lua
--author expwnent
--some utilities for adding syndromes to units
local _ENV = mkmodule("syndrome-util")
local utils = require("utils")
function findUnitSyndrome(unit,syn_id)
for index,syndrome in ipairs(unit.syndromes.active) do
if syndrome['type'] == syn_id then
return syndrome
end
end
return nil
end
--usage: syndromeUtil.ResetPolicy.DoNothing, syndromeUtil.ResetPolicy.ResetDuration, etc
ResetPolicy = ResetPolicy or utils.invert({
"DoNothing",
"ResetDuration",
"AddDuration",
"NewInstance"
})
function eraseSyndrome(unit,syndromeId,oldestFirst)
local i1
local iN
local d
if oldestFirst then
i1 = 0
iN = #unit.syndromes.active-1
d = 1
else
i1 = #unit.syndromes.active-1
iN = 0
d = -1
end
local syndromes = unit.syndromes.active
for i=i1,iN,d do
if syndromes[i]['type'] == syndromeId then
syndromes:erase(i)
return true
end
end
return false
end
function eraseSyndromes(unit,syndromeId)
local count=0
while eraseSyndrome(unit,syndromeId,true) do
count = count+1
end
return count
end
--target is a df.unit, syndrome is a df.syndrome, resetPolicy is one of syndromeUtil.ResetPolicy
--if the target has an instance of the syndrome already, the reset policy takes effect
--returns true if the unit did not have the syndrome before calling and false otherwise
function infectWithSyndrome(target,syndrome,resetPolicy)
local oldSyndrome = findUnitSyndrome(target,syndrome.id)
if oldSyndrome == nil or resetPolicy == nil or resetPolicy == ResetPolicy.NewInstance then
local unitSyndrome = df.unit_syndrome:new()
unitSyndrome.type = syndrome.id
unitSyndrome.year = df.global.cur_year
unitSyndrome.year_time = df.global.cur_year_tick
unitSyndrome.ticks = 0
unitSyndrome.wound_id = -1
for k,v in ipairs(syndrome.ce) do
local symptom = df.unit_syndrome.T_symptoms:new()
symptom.quantity = 0
symptom.delay = 0
symptom.ticks = 0
symptom.flags.active = true
unitSyndrome.symptoms:insert("#",symptom)
end
target.syndromes.active:insert("#",unitSyndrome)
elseif resetPolicy == ResetPolicy.DoNothing then
elseif resetPolicy == ResetPolicy.ResetDuration then
for k,symptom in ipairs(oldSyndrome.symptoms) do
symptom.ticks = 0
end
oldSyndrome.ticks = 0
elseif resetPolicy == ResetPolicy.AddDuration then
for k,symptom in ipairs(oldSyndrome.symptoms) do
--really it's syndrome.ce[k].end, but lua doesn't like that because keywords
if syndrome.ce[k]["end"] ~= -1 then
symptom.ticks = symptom.ticks - syndrome.ce[k]["end"]
end
end
else qerror("Bad reset policy: " .. resetPolicy)
end
return (oldSyndrome == nil)
end
function isValidTarget(unit,syndrome)
--mostly copied from itemsyndrome, which is based on autoSyndrome
if
#syndrome.syn_affected_class==0
and #syndrome.syn_affected_creature==0
and #syndrome.syn_affected_caste==0
and #syndrome.syn_immune_class==0
and #syndrome.syn_immune_creature==0
and #syndrome.syn_immune_caste==0
then
return true
end
local affected = false
local unitRaws = df.creature_raw.find(unit.race)
local casteRaws = unitRaws.caste[unit.caste]
local unitRaceName = unitRaws.creature_id
local unitCasteName = casteRaws.caste_id
local unitClasses = casteRaws.creature_class
for _,unitClass in ipairs(unitClasses) do
for _,syndromeClass in ipairs(syndrome.syn_affected_class) do
if unitClass.value==syndromeClass.value then
affected = true
end
end
end
for caste,creature in ipairs(syndrome.syn_affected_creature) do
local affectedCreature = creature.value
local affectedCaste = syndrome.syn_affectedCaste[caste].value
if affectedCreature == unitRaceName and (affectedCaste == unitCasteName or affectedCaste == "ALL") then
affected = true
end
end
for _,unitClass in ipairs(unitClasses) do
for _,syndromeClass in ipairs(syndrome.syn_immune_class) do
if unitClass.value == syndromeClass.value then
affected = false
end
end
end
for caste,creature in ipairs(syndrome.syn_immune_creature) do
local immuneCreature = creature.value
local immuneCaste = syndrome.syn_immune_caste[caste].value
if immuneCreature == unitRaceName and (immuneCaste == unitCasteName or immuneCaste == "ALL") then
affected = false
end
end
return affected
end
function infectWithSyndromeIfValidTarget(target,syndrome,resetPolicy)
if isValidTarget(target,syndrome) then
infectWithSyndrome(target,syndrome,resetPolicy)
return true
else
return false
end
end
return _ENV

@ -540,4 +540,86 @@ function check_number(text)
return nv ~= nil, nv
end
return _ENV
function invert(tab)
local result = {}
for k,v in pairs(tab) do
result[v]=k
end
return result
end
function processArgs(args, validArgs)
--[[
standardized argument processing for scripts
-argName value
-argName [list of values]
-argName [list of [nested values] -that can be [whatever] format of matched square brackets]
-arg1 \-arg3
escape sequences
--]]
local result = {}
local argName
local bracketDepth = 0
for i,arg in ipairs(args) do
if argName then
if arg == '[' then
if bracketDepth > 0 then
table.insert(result[argName], arg)
end
bracketDepth = bracketDepth+1
elseif arg == ']' then
bracketDepth = bracketDepth-1
if bracketDepth > 0 then
table.insert(result[argName], arg)
else
argName = nil
end
elseif string.sub(arg,1,1) == '\\' then
if bracketDepth == 0 then
result[argName] = string.sub(arg,2)
argName = nil
else
table.insert(result[argName], string.sub(arg,2))
end
else
if bracketDepth == 0 then
result[argName] = arg
argName = nil
else
table.insert(result[argName], arg)
end
end
elseif string.sub(arg,1,1) == '-' then
argName = string.sub(arg,2)
if validArgs and not validArgs[argName] then
error('error: invalid arg: ' .. i .. ': ' .. argName)
end
if result[argName] then
error('duplicate arg: ' .. i .. ': ' .. argName)
end
if i+1 > #args or string.sub(args[i+1],1,1) == '-' then
result[argName] = ''
argName = nil
else
result[argName] = {}
end
else
error('error parsing arg ' .. i .. ': ' .. arg)
end
end
return result
end
function fillTable(table1,table2)
for k,v in pairs(table2) do
table1[k] = v
end
end
function unfillTable(table1,table2)
for k,v in pairs(table2) do
table1[k] = nil
end
end
return _ENV

@ -1,12 +1,15 @@
#include "Core.h"
#include "Console.h"
#include "VTableInterpose.h"
#include "modules/Buildings.h"
#include "modules/Constructions.h"
#include "modules/EventManager.h"
#include "modules/Once.h"
#include "modules/Job.h"
#include "modules/Units.h"
#include "modules/World.h"
#include "df/announcement_type.h"
#include "df/building.h"
#include "df/construction.h"
#include "df/general_ref.h"
@ -14,16 +17,26 @@
#include "df/general_ref_unit_workerst.h"
#include "df/global_objects.h"
#include "df/item.h"
#include "df/item_actual.h"
#include "df/item_constructed.h"
#include "df/item_crafted.h"
#include "df/item_weaponst.h"
#include "df/job.h"
#include "df/job_list_link.h"
#include "df/report.h"
#include "df/ui.h"
#include "df/unit.h"
#include "df/unit_flags1.h"
#include "df/unit_inventory_item.h"
#include "df/unit_report_type.h"
#include "df/unit_syndrome.h"
#include "df/unit_wound.h"
#include "df/world.h"
#include <algorithm>
#include <cstring>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
@ -116,6 +129,10 @@ static void manageConstructionEvent(color_ostream& out);
static void manageSyndromeEvent(color_ostream& out);
static void manageInvasionEvent(color_ostream& out);
static void manageEquipmentEvent(color_ostream& out);
static void manageReportEvent(color_ostream& out);
static void manageUnitAttackEvent(color_ostream& out);
static void manageUnloadEvent(color_ostream& out){};
static void manageInteractionEvent(color_ostream& out);
typedef void (*eventManager_t)(color_ostream&);
@ -130,6 +147,10 @@ static const eventManager_t eventManager[] = {
manageSyndromeEvent,
manageInvasionEvent,
manageEquipmentEvent,
manageReportEvent,
manageUnitAttackEvent,
manageUnloadEvent,
manageInteractionEvent,
};
//job initiated
@ -162,6 +183,17 @@ static int32_t nextInvasion;
//static unordered_map<int32_t, vector<df::unit_inventory_item> > equipmentLog;
static unordered_map<int32_t, vector<InventoryItem> > equipmentLog;
//report
static int32_t lastReport;
//unit attack
static int32_t lastReportUnitAttack;
static std::map<int32_t,std::vector<int32_t> > reportToRelevantUnits;
static int32_t reportToRelevantUnitsTime = -1;
//interaction
static int32_t lastReportInteraction;
void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) {
static bool doOnce = false;
// const string eventNames[] = {"world loaded", "world unloaded", "map loaded", "map unloaded", "viewscreen changed", "core initialized", "begin unload", "paused", "unpaused"};
@ -186,7 +218,14 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event
equipmentLog.clear();
Buildings::clearBuildings(out);
lastReport = -1;
lastReportUnitAttack = -1;
gameLoaded = false;
multimap<Plugin*,EventHandler> copy(handlers[EventType::UNLOAD].begin(), handlers[EventType::UNLOAD].end());
for (auto a = copy.begin(); a != copy.end(); a++ ) {
(*a).second.eventHandler(out, NULL);
}
} else if ( event == DFHack::SC_MAP_LOADED ) {
/*
int32_t tick = df::global::world->frame_counter;
@ -235,6 +274,11 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event
lastSyndromeTime = startTime;
}
}
lastReport = -1;
lastReportUnitAttack = -1;
lastReportInteraction = -1;
reportToRelevantUnitsTime = -1;
reportToRelevantUnits.clear();
for ( size_t a = 0; a < EventType::EVENT_MAX; a++ ) {
eventLastTick[a] = -1;//-1000000;
}
@ -617,7 +661,7 @@ static void manageInvasionEvent(color_ostream& out) {
for ( auto a = copy.begin(); a != copy.end(); a++ ) {
EventHandler handle = (*a).second;
handle.eventHandler(out, (void*)nextInvasion);
handle.eventHandler(out, (void*)(nextInvasion-1));
}
}
@ -635,51 +679,59 @@ static void manageEquipmentEvent(color_ostream& out) {
*/
auto oldEquipment = equipmentLog.find(unit->id);
if ( oldEquipment != equipmentLog.end() ) {
vector<InventoryItem>& v = (*oldEquipment).second;
for ( auto b = v.begin(); b != v.end(); b++ ) {
InventoryItem& i = *b;
itemIdToInventoryItem[i.itemId] = i;
}
for ( size_t b = 0; b < unit->inventory.size(); b++ ) {
df::unit_inventory_item* dfitem_new = unit->inventory[b];
currentlyEquipped.insert(dfitem_new->item->id);
InventoryItem item_new(dfitem_new->item->id, *dfitem_new);
auto c = itemIdToInventoryItem.find(dfitem_new->item->id);
if ( c == itemIdToInventoryItem.end() ) {
//new item equipped (probably just picked up)
InventoryChangeData data(unit->id, NULL, &item_new);
for ( auto h = copy.begin(); h != copy.end(); h++ ) {
EventHandler handle = (*h).second;
handle.eventHandler(out, (void*)&data);
}
continue;
}
InventoryItem item_old = (*c).second;
df::unit_inventory_item& item0 = item_old.item;
df::unit_inventory_item& item1 = item_new.item;
if ( item0.mode == item1.mode && item0.body_part_id == item1.body_part_id && item0.wound_id == item1.wound_id )
continue;
//some sort of change in how it's equipped
InventoryChangeData data(unit->id, &item_old, &item_new);
bool hadEquipment = oldEquipment != equipmentLog.end();
vector<InventoryItem>* temp;
if ( hadEquipment ) {
temp = &((*oldEquipment).second);
} else {
temp = new vector<InventoryItem>;
}
//vector<InventoryItem>& v = (*oldEquipment).second;
vector<InventoryItem>& v = *temp;
for ( auto b = v.begin(); b != v.end(); b++ ) {
InventoryItem& i = *b;
itemIdToInventoryItem[i.itemId] = i;
}
for ( size_t b = 0; b < unit->inventory.size(); b++ ) {
df::unit_inventory_item* dfitem_new = unit->inventory[b];
currentlyEquipped.insert(dfitem_new->item->id);
InventoryItem item_new(dfitem_new->item->id, *dfitem_new);
auto c = itemIdToInventoryItem.find(dfitem_new->item->id);
if ( c == itemIdToInventoryItem.end() ) {
//new item equipped (probably just picked up)
InventoryChangeData data(unit->id, NULL, &item_new);
for ( auto h = copy.begin(); h != copy.end(); h++ ) {
EventHandler handle = (*h).second;
handle.eventHandler(out, (void*)&data);
}
continue;
}
//check for dropped items
for ( auto b = v.begin(); b != v.end(); b++ ) {
InventoryItem i = *b;
if ( currentlyEquipped.find(i.itemId) != currentlyEquipped.end() )
continue;
//TODO: delete ptr if invalid
InventoryChangeData data(unit->id, &i, NULL);
for ( auto h = copy.begin(); h != copy.end(); h++ ) {
EventHandler handle = (*h).second;
handle.eventHandler(out, (void*)&data);
}
InventoryItem item_old = (*c).second;
df::unit_inventory_item& item0 = item_old.item;
df::unit_inventory_item& item1 = item_new.item;
if ( item0.mode == item1.mode && item0.body_part_id == item1.body_part_id && item0.wound_id == item1.wound_id )
continue;
//some sort of change in how it's equipped
InventoryChangeData data(unit->id, &item_old, &item_new);
for ( auto h = copy.begin(); h != copy.end(); h++ ) {
EventHandler handle = (*h).second;
handle.eventHandler(out, (void*)&data);
}
}
if ( !hadEquipment )
delete temp;
//check for dropped items
for ( auto b = v.begin(); b != v.end(); b++ ) {
InventoryItem i = *b;
if ( currentlyEquipped.find(i.itemId) != currentlyEquipped.end() )
continue;
//TODO: delete ptr if invalid
InventoryChangeData data(unit->id, &i, NULL);
for ( auto h = copy.begin(); h != copy.end(); h++ ) {
EventHandler handle = (*h).second;
handle.eventHandler(out, (void*)&data);
}
}
@ -694,3 +746,274 @@ static void manageEquipmentEvent(color_ostream& out) {
}
}
static void updateReportToRelevantUnits() {
if ( df::global::world->frame_counter <= reportToRelevantUnitsTime )
return;
reportToRelevantUnitsTime = df::global::world->frame_counter;
for ( size_t a = 0; a < df::global::world->units.all.size(); a++ ) {
df::unit* unit = df::global::world->units.all[a];
for ( int16_t b = df::enum_traits<df::unit_report_type>::first_item_value; b <= df::enum_traits<df::unit_report_type>::last_item_value; b++ ) {
if ( b == df::unit_report_type::Sparring )
continue;
for ( size_t c = 0; c < unit->reports.log[b].size(); c++ ) {
int32_t report = unit->reports.log[b][c];
if ( std::find(reportToRelevantUnits[report].begin(), reportToRelevantUnits[report].end(), unit->id) != reportToRelevantUnits[report].end() )
continue;
reportToRelevantUnits[unit->reports.log[b][c]].push_back(unit->id);
}
}
}
}
static void manageReportEvent(color_ostream& out) {
multimap<Plugin*,EventHandler> copy(handlers[EventType::REPORT].begin(), handlers[EventType::REPORT].end());
std::vector<df::report*>& reports = df::global::world->status.reports;
size_t a = df::report::binsearch_index(reports, lastReport, false);
//this may or may not be needed: I don't know if binsearch_index goes earlier or later if it can't hit the target exactly
while (a < reports.size() && reports[a]->id <= lastReport) {
a++;
}
for ( ; a < reports.size(); a++ ) {
df::report* report = reports[a];
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)report->id);
}
lastReport = report->id;
}
}
static df::unit_wound* getWound(df::unit* attacker, df::unit* defender) {
for ( size_t a = 0; a < defender->body.wounds.size(); a++ ) {
df::unit_wound* wound = defender->body.wounds[a];
if ( wound->age <= 1 && wound->unit_id == attacker->id ) {
return wound;
}
}
return NULL;
}
static void manageUnitAttackEvent(color_ostream& out) {
multimap<Plugin*,EventHandler> copy(handlers[EventType::UNIT_ATTACK].begin(), handlers[EventType::UNIT_ATTACK].end());
std::vector<df::report*>& reports = df::global::world->status.reports;
size_t a = df::report::binsearch_index(reports, lastReportUnitAttack, false);
//this may or may not be needed: I don't know if binsearch_index goes earlier or later if it can't hit the target exactly
while (a < reports.size() && reports[a]->id <= lastReportUnitAttack) {
a++;
}
std::set<int32_t> strikeReports;
for ( ; a < reports.size(); a++ ) {
df::report* report = reports[a];
lastReportUnitAttack = report->id;
if ( report->flags.bits.continuation )
continue;
df::announcement_type type = report->type;
if ( type == df::announcement_type::COMBAT_STRIKE_DETAILS ) {
strikeReports.insert(report->id);
}
}
if ( strikeReports.empty() )
return;
updateReportToRelevantUnits();
map<int32_t, map<int32_t, int32_t> > alreadyDone;
for ( auto a = strikeReports.begin(); a != strikeReports.end(); a++ ) {
int32_t reportId = *a;
df::report* report = df::report::find(reportId);
if ( !report )
continue; //TODO: error
std::string reportStr = report->text;
for ( int32_t b = reportId+1; ; b++ ) {
df::report* report2 = df::report::find(b);
if ( !report2 )
break;
if ( report2->type != df::announcement_type::COMBAT_STRIKE_DETAILS )
break;
if ( !report2->flags.bits.continuation )
break;
reportStr = reportStr + report2->text;
}
std::vector<int32_t>& relevantUnits = reportToRelevantUnits[report->id];
if ( relevantUnits.size() != 2 ) {
continue;
}
df::unit* unit1 = df::unit::find(relevantUnits[0]);
df::unit* unit2 = df::unit::find(relevantUnits[1]);
df::unit_wound* wound1 = getWound(unit1,unit2);
df::unit_wound* wound2 = getWound(unit2,unit1);
if ( wound1 && !alreadyDone[unit1->id][unit2->id] ) {
UnitAttackData data;
data.attacker = unit1->id;
data.defender = unit2->id;
data.wound = wound1->id;
alreadyDone[data.attacker][data.defender] = 1;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)&data);
}
}
if ( wound2 && !alreadyDone[unit1->id][unit2->id] ) {
UnitAttackData data;
data.attacker = unit2->id;
data.defender = unit1->id;
data.wound = wound2->id;
alreadyDone[data.attacker][data.defender] = 1;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)&data);
}
}
if ( unit1->flags1.bits.dead ) {
UnitAttackData data;
data.attacker = unit2->id;
data.defender = unit1->id;
data.wound = -1;
alreadyDone[data.attacker][data.defender] = 1;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)&data);
}
}
if ( unit2->flags1.bits.dead ) {
UnitAttackData data;
data.attacker = unit1->id;
data.defender = unit2->id;
data.wound = -1;
alreadyDone[data.attacker][data.defender] = 1;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)&data);
}
}
if ( !wound1 && !wound2 ) {
//if ( unit1->flags1.bits.dead || unit2->flags1.bits.dead )
// continue;
if ( reportStr.find("severed part") )
continue;
if ( Once::doOnce("EventManager neither wound") ) {
out.print("%s, %d: neither wound: %s\n", __FILE__, __LINE__, reportStr.c_str());
}
}
}
}
static std::string getVerb(df::unit* unit, std::string reportStr) {
std::string result(reportStr);
std::string name = unit->name.first_name + " ";
bool useName = strncmp(result.c_str(), name.c_str(), name.length()) == 0;
if ( useName ) {
result = result.substr(name.length());
result = result.substr(0,result.length()-1);
return result;
}
//use profession name
std::string profession = "The " + Units::getProfessionName(unit) + " ";
bool match = strncmp(result.c_str(), profession.c_str(), profession.length()) == 0;
if ( !match )
return "";
result = result.substr(profession.length());
result = result.substr(0,result.length()-1);
//TODO: special case for the player in adventure mode
return result;
}
static void manageInteractionEvent(color_ostream& out) {
multimap<Plugin*,EventHandler> copy(handlers[EventType::INTERACTION].begin(), handlers[EventType::INTERACTION].end());
std::vector<df::report*>& reports = df::global::world->status.reports;
size_t a = df::report::binsearch_index(reports, lastReportInteraction, false);
while (a < reports.size() && reports[a]->id <= lastReportInteraction) {
a++;
}
if ( a < reports.size() )
updateReportToRelevantUnits();
int32_t attackerId = -1;
int32_t lastTime = -1;
std::string attackVerb;
int32_t attackReport = -1;
for ( ; a < reports.size(); a++ ) {
df::report* report = reports[a];
lastReportInteraction = report->id;
if ( report->flags.bits.continuation )
continue;
df::announcement_type type = report->type;
if ( type != df::announcement_type::INTERACTION_ACTOR && type != df::announcement_type::INTERACTION_TARGET )
continue;
int32_t unitId = -1;
int32_t validCount = 0;
std::string verb;
//find relevant unit
for ( auto b = reportToRelevantUnits[report->id].begin(); b != reportToRelevantUnits[report->id].end(); b++ ) {
int32_t candidateId = *b;
df::unit* candidate = df::unit::find(candidateId);
if ( !candidate ) {
//TODO: error
continue;
}
if ( candidate->pos != report->pos )
continue;
std::string verbC = getVerb(candidate, report->text);
if ( verbC.length() == 0 )
continue;
verb = verbC;
validCount++;
unitId = candidateId;
}
if ( validCount > 1 ) {
if ( Once::doOnce("EventManager interaction too many actors") ) {
out.print("%s:%d: too many actors for report %d\n", __FILE__, __LINE__, report->id);
out.print("reportStr = \"%s\", pos = %d,%d,%d\n", report->text.c_str(), report->pos.x, report->pos.y, report->pos.z);
}
attackerId = -1;
continue;
}
if ( validCount == 0 ) {
if ( Once::doOnce("EventManager interaction too few actors") ) {
out.print("%s:%d: too few actors for report %d\n", __FILE__, __LINE__, report->id);
out.print("reportStr = \"%s\", pos = %d,%d,%d\n", report->text.c_str(), report->pos.x, report->pos.y, report->pos.z);
}
attackerId = -1;
continue;
}
//int32_t unitId = reportToRelevantUnits[report->id][0];
bool isActor = type == df::announcement_type::INTERACTION_ACTOR;
if ( isActor ) {
attackReport = report->id;
attackerId = unitId;
lastTime = report->year*ticksPerYear + report->time;
attackVerb = verb;
continue;
}
if ( attackerId == -1 )
continue;
if ( report->year*ticksPerYear + report->time != lastTime ) {
attackerId = -1;
continue;
}
InteractionData data;
data.attacker = attackerId;
data.defender = unitId;
data.attackReport = attackReport;
data.defendReport = report->id;
data.attackVerb = attackVerb;
data.defendVerb = verb;
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler handle = (*b).second;
handle.eventHandler(out, (void*)&data);
}
}
}

@ -22,69 +22,72 @@ must not be misrepresented as being the original software.
distribution.
*/
#include "Core.h"
#include "Error.h"
#include "Internal.h"
#include "MemAccess.h"
#include "MiscUtils.h"
#include "Types.h"
#include "VersionInfo.h"
#include <string>
#include <sstream>
#include <vector>
#include <cstdio>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <set>
using namespace std;
#include "Types.h"
#include "VersionInfo.h"
#include "MemAccess.h"
#include "ModuleFactory.h"
#include "modules/MapCache.h"
#include "modules/Materials.h"
#include "modules/Items.h"
#include "modules/Units.h"
#include "modules/MapCache.h"
#include "ModuleFactory.h"
#include "Core.h"
#include "Error.h"
#include "MiscUtils.h"
#include "df/ui.h"
#include "df/world.h"
#include "df/item.h"
#include "df/body_part_raw.h"
#include "df/body_part_template_flags.h"
#include "df/building.h"
#include "df/building_actual.h"
#include "df/tool_uses.h"
#include "df/itemdef_weaponst.h"
#include "df/itemdef_trapcompst.h"
#include "df/itemdef_toyst.h"
#include "df/itemdef_toolst.h"
#include "df/itemdef_instrumentst.h"
#include "df/itemdef_armorst.h"
#include "df/caste_raw.h"
#include "df/creature_raw.h"
#include "df/general_ref.h"
#include "df/general_ref_building_holderst.h"
#include "df/general_ref_contained_in_itemst.h"
#include "df/general_ref_contains_itemst.h"
#include "df/general_ref_projectile.h"
#include "df/general_ref_unit_itemownerst.h"
#include "df/general_ref_unit_holderst.h"
#include "df/historical_entity.h"
#include "df/item.h"
#include "df/item_type.h"
#include "df/itemdef_ammost.h"
#include "df/itemdef_siegeammost.h"
#include "df/itemdef_armorst.h"
#include "df/itemdef_foodst.h"
#include "df/itemdef_glovesst.h"
#include "df/itemdef_shoesst.h"
#include "df/itemdef_shieldst.h"
#include "df/itemdef_helmst.h"
#include "df/itemdef_instrumentst.h"
#include "df/itemdef_pantsst.h"
#include "df/itemdef_foodst.h"
#include "df/trapcomp_flags.h"
#include "df/itemdef_shieldst.h"
#include "df/itemdef_shoesst.h"
#include "df/itemdef_siegeammost.h"
#include "df/itemdef_toolst.h"
#include "df/itemdef_toyst.h"
#include "df/itemdef_trapcompst.h"
#include "df/itemdef_weaponst.h"
#include "df/job_item.h"
#include "df/general_ref.h"
#include "df/general_ref_unit_itemownerst.h"
#include "df/general_ref_contains_itemst.h"
#include "df/general_ref_contained_in_itemst.h"
#include "df/general_ref_building_holderst.h"
#include "df/general_ref_projectile.h"
#include "df/viewscreen_itemst.h"
#include "df/vermin.h"
#include "df/map_block.h"
#include "df/proj_itemst.h"
#include "df/proj_list_link.h"
#include "df/unit_inventory_item.h"
#include "df/body_part_raw.h"
#include "df/reaction_product_itemst.h"
#include "df/tool_uses.h"
#include "df/trapcomp_flags.h"
#include "df/ui.h"
#include "df/unit.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
#include "df/body_part_template_flags.h"
#include "df/general_ref_unit_holderst.h"
#include "df/unit_inventory_item.h"
#include "df/vermin.h"
#include "df/viewscreen_itemst.h"
#include "df/world.h"
#include "df/world_site.h"
using namespace DFHack;
using namespace df::enums;
@ -1329,4 +1332,53 @@ int Items::getValue(df::item *item)
value /= divisor;
}
return value;
}
}
int32_t Items::createItem(df::item_type item_type, int16_t item_subtype, int16_t mat_type, int32_t mat_index, df::unit* unit) {
//based on Quietust's plugins/createitem.cpp
df::map_block* block = Maps::getTileBlock(unit->pos.x, unit->pos.y, unit->pos.z);
CHECK_NULL_POINTER(block);
df::reaction_product_itemst* prod = df::allocate<df::reaction_product_itemst>();
prod->item_type = item_type;
prod->item_subtype = item_subtype;
prod->mat_type = mat_type;
prod->mat_index = mat_index;
prod->probability = 100;
prod->count = 1;
switch(item_type) {
case df::item_type::BAR:
case df::item_type::POWDER_MISC:
case df::item_type::LIQUID_MISC:
case df::item_type::DRINK:
prod->product_dimension = 150;
break;
case df::item_type::THREAD:
prod->product_dimension = 15000;
break;
case df::item_type::CLOTH:
prod->product_dimension = 10000;
break;
default:
prod->product_dimension = 1;
break;
}
//makeItem
vector<df::item*> out_items;
vector<df::reaction_reagent*> in_reag;
vector<df::item*> in_items;
df::enums::game_type::game_type type = *df::global::gametype;
prod->produce(unit, &out_items, &in_reag, &in_items, 1, job_skill::NONE,
df::historical_entity::find(unit->civ_id),
((type == df::enums::game_type::DWARF_MAIN) || (type == df::enums::game_type::DWARF_RECLAIM)) ? df::world_site::find(df::global::ui->site_id) : NULL);
if ( out_items.size() != 1 )
return -1;
for (size_t a = 0; a < out_items.size(); a++ ) {
out_items[a]->moveToGround(unit->pos.x, unit->pos.y, unit->pos.z);
}
return out_items[0]->id;
}

@ -79,98 +79,87 @@ add_custom_target(generate_proto DEPENDS ${PROJECT_PROTO_SRCS} ${PROJECT_PROTO_H
SET_SOURCE_FILES_PROPERTIES( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE )
add_subdirectory(diggingInvaders)
# Plugins
OPTION(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON)
if (BUILD_SUPPORTED)
DFHACK_PLUGIN(reveal reveal.cpp)
DFHACK_PLUGIN(probe probe.cpp)
# this is a plugin which helps detect cursed creatures (vampires, necromancers, werebeasts, ...)
DFHACK_PLUGIN(cursecheck cursecheck.cpp)
# automatically assign labors to dwarves!
DFHACK_PLUGIN(3dveins 3dveins.cpp)
DFHACK_PLUGIN(add-spatter add-spatter.cpp)
DFHACK_PLUGIN(advtools advtools.cpp)
DFHACK_PLUGIN(autodump autodump.cpp)
DFHACK_PLUGIN(autolabor autolabor.cpp)
DFHACK_PLUGIN(dig dig.cpp)
DFHACK_PLUGIN(drybuckets drybuckets.cpp)
DFHACK_PLUGIN(getplants getplants.cpp)
DFHACK_PLUGIN(plants plants.cpp)
DFHACK_PLUGIN(fastdwarf fastdwarf.cpp)
DFHACK_PLUGIN(prospector prospector.cpp)
DFHACK_PLUGIN(automaterial automaterial.cpp)
DFHACK_PLUGIN(autotrade autotrade.cpp)
DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(building-hacks building-hacks.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(buildingplan buildingplan.cpp)
DFHACK_PLUGIN(catsplosion catsplosion.cpp)
DFHACK_PLUGIN(changeitem changeitem.cpp)
DFHACK_PLUGIN(changelayer changelayer.cpp)
DFHACK_PLUGIN(changevein changevein.cpp)
DFHACK_PLUGIN(cleanconst cleanconst.cpp)
DFHACK_PLUGIN(cleaners cleaners.cpp)
DFHACK_PLUGIN(weather weather.cpp)
DFHACK_PLUGIN(colonies colonies.cpp)
DFHACK_PLUGIN(mode mode.cpp)
DFHACK_PLUGIN(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua)
DFHACK_PLUGIN(tiletypes tiletypes.cpp Brushes.h)
DFHACK_PLUGIN(tubefill tubefill.cpp)
DFHACK_PLUGIN(autodump autodump.cpp)
DFHACK_PLUGIN(cleanowned cleanowned.cpp)
DFHACK_PLUGIN(colonies colonies.cpp)
DFHACK_PLUGIN(command-prompt command-prompt.cpp)
DFHACK_PLUGIN(createitem createitem.cpp)
DFHACK_PLUGIN(cursecheck cursecheck.cpp)
DFHACK_PLUGIN(deramp deramp.cpp)
DFHACK_PLUGIN(flows flows.cpp)
DFHACK_PLUGIN(dig dig.cpp)
DFHACK_PLUGIN(digFlood digFlood.cpp)
add_subdirectory(diggingInvaders)
DFHACK_PLUGIN(drybuckets drybuckets.cpp)
DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp)
DFHACK_PLUGIN(embark-tools embark-tools.cpp)
DFHACK_PLUGIN(eventful eventful.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(fastdwarf fastdwarf.cpp)
DFHACK_PLUGIN(feature feature.cpp)
DFHACK_PLUGIN(filltraffic filltraffic.cpp)
DFHACK_PLUGIN(seedwatch seedwatch.cpp)
DFHACK_PLUGIN(initflags initflags.cpp)
DFHACK_PLUGIN(stockpiles stockpiles.cpp)
DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(workflow workflow.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(stockflow stockflow.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(showmood showmood.cpp)
DFHACK_PLUGIN(fixveins fixveins.cpp)
DFHACK_PLUGIN(fix-armory fix-armory.cpp)
DFHACK_PLUGIN(fixpositions fixpositions.cpp)
DFHACK_PLUGIN(fixveins fixveins.cpp)
DFHACK_PLUGIN(flows flows.cpp)
DFHACK_PLUGIN(follow follow.cpp)
DFHACK_PLUGIN(changevein changevein.cpp)
DFHACK_PLUGIN(changelayer changelayer.cpp)
DFHACK_PLUGIN(changeitem changeitem.cpp)
DFHACK_PLUGIN(advtools advtools.cpp)
DFHACK_PLUGIN(tweak tweak.cpp)
DFHACK_PLUGIN(feature feature.cpp)
DFHACK_PLUGIN(lair lair.cpp)
DFHACK_PLUGIN(zone zone.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(catsplosion catsplosion.cpp)
DFHACK_PLUGIN(regrass regrass.cpp)
DFHACK_PLUGIN(forceequip forceequip.cpp)
DFHACK_PLUGIN(getplants getplants.cpp)
DFHACK_PLUGIN(infiniteSky infiniteSky.cpp)
DFHACK_PLUGIN(initflags initflags.cpp)
DFHACK_PLUGIN(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(lair lair.cpp)
DFHACK_PLUGIN(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua)
DFHACK_PLUGIN(manipulator manipulator.cpp)
DFHACK_PLUGIN(mode mode.cpp)
DFHACK_PLUGIN(misery misery.cpp)
DFHACK_PLUGIN(mousequery mousequery.cpp)
DFHACK_PLUGIN(petcapRemover petcapRemover.cpp)
DFHACK_PLUGIN(plants plants.cpp)
DFHACK_PLUGIN(probe probe.cpp)
DFHACK_PLUGIN(prospector prospector.cpp)
DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(regrass regrass.cpp)
DFHACK_PLUGIN(remotefortressreader remotefortressreader.cpp PROTOBUFS RemoteFortressReader)
DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
add_subdirectory(rendermax)
DFHACK_PLUGIN(resume resume.cpp)
DFHACK_PLUGIN(reveal reveal.cpp)
DFHACK_PLUGIN(search search.cpp)
DFHACK_PLUGIN(automaterial automaterial.cpp)
# this one exports functions to lua
DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(seedwatch seedwatch.cpp)
DFHACK_PLUGIN(showmood showmood.cpp)
DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(steam-engine steam-engine.cpp)
DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(eventful eventful.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(add-spatter add-spatter.cpp)
DFHACK_PLUGIN(fix-armory fix-armory.cpp)
# not yet. busy with other crud again...
#DFHACK_PLUGIN(versionosd versionosd.cpp)
DFHACK_PLUGIN(misery misery.cpp)
DFHACK_PLUGIN(workNow workNow.cpp)
#DFHACK_PLUGIN(dfstream dfstream.cpp LINK_LIBRARIES clsocket dfhack-tinythread)
DFHACK_PLUGIN(autoSyndrome autoSyndrome.cpp)
DFHACK_PLUGIN(syndromeTrigger syndromeTrigger.cpp)
DFHACK_PLUGIN(infiniteSky infiniteSky.cpp)
DFHACK_PLUGIN(digFlood digFlood.cpp)
DFHACK_PLUGIN(createitem createitem.cpp)
DFHACK_PLUGIN(outsideOnly outsideOnly.cpp)
DFHACK_PLUGIN(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote)
DFHACK_PLUGIN(buildingplan buildingplan.cpp)
DFHACK_PLUGIN(resume resume.cpp)
DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp)
DFHACK_PLUGIN(mousequery mousequery.cpp)
DFHACK_PLUGIN(autotrade autotrade.cpp)
DFHACK_PLUGIN(stockflow stockflow.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(stockpiles stockpiles.cpp)
DFHACK_PLUGIN(stocks stocks.cpp)
DFHACK_PLUGIN(treefarm treefarm.cpp)
DFHACK_PLUGIN(cleanconst cleanconst.cpp)
DFHACK_PLUGIN(3dveins 3dveins.cpp)
DFHACK_PLUGIN(strangemood strangemood.cpp)
DFHACK_PLUGIN(command-prompt command-prompt.cpp)
DFHACK_PLUGIN(remotefortressreader remotefortressreader.cpp PROTOBUFS RemoteFortressReader)
DFHACK_PLUGIN(building-hacks building-hacks.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(embark-tools embark-tools.cpp)
DFHACK_PLUGIN(petcapRemover petcapRemover.cpp)
add_subdirectory(rendermax)
DFHACK_PLUGIN(tiletypes tiletypes.cpp Brushes.h)
DFHACK_PLUGIN(treefarm treefarm.cpp)
DFHACK_PLUGIN(tubefill tubefill.cpp)
DFHACK_PLUGIN(tweak tweak.cpp)
DFHACK_PLUGIN(weather weather.cpp)
DFHACK_PLUGIN(workflow workflow.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(workNow workNow.cpp)
DFHACK_PLUGIN(zone zone.cpp LINK_LIBRARIES lua)
endif()
# this is the skeleton plugin. If you want to make your own, make a copy and then change it

@ -1,489 +0,0 @@
#include "Core.h"
#include "DataDefs.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/EventManager.h"
#include "modules/Job.h"
#include "modules/Maps.h"
#include "modules/Once.h"
#include "modules/World.h"
#include "df/building.h"
#include "df/caste_raw.h"
#include "df/creature_interaction_effect.h"
#include "df/creature_raw.h"
#include "df/general_ref.h"
#include "df/general_ref_building_holderst.h"
#include "df/general_ref_type.h"
#include "df/general_ref_unit_workerst.h"
#include "df/global_objects.h"
#include "df/item.h"
#include "df/item_boulderst.h"
#include "df/job.h"
#include "df/job_type.h"
#include "df/reaction.h"
#include "df/reaction_product.h"
#include "df/reaction_product_type.h"
#include "df/reaction_product_itemst.h"
#include "df/syndrome.h"
#include "df/unit_syndrome.h"
#include "df/ui.h"
#include "df/unit.h"
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>
using namespace std;
using namespace DFHack;
namespace ResetPolicy {
typedef enum {DoNothing, ResetDuration, AddDuration, NewInstance} ResetPolicy;
}
DFHACK_PLUGIN_IS_ENABLED(enabled);
DFHACK_PLUGIN("autoSyndrome");
command_result autoSyndrome(color_ostream& out, vector<string>& parameters);
void processJob(color_ostream& out, void* jobPtr);
int32_t giveSyndrome(color_ostream& out, int32_t workerId, df::syndrome* syndrome, ResetPolicy::ResetPolicy policy);
DFhackCExport command_result plugin_init(color_ostream& out, vector<PluginCommand> &commands) {
commands.push_back(PluginCommand("autoSyndrome", "Automatically give units syndromes when they complete jobs, as configured in the raw files.\n", &autoSyndrome, false,
"autoSyndrome:\n"
" autoSyndrome 0 //disable\n"
" autoSyndrome 1 //enable\n"
" autoSyndrome disable //disable\n"
" autoSyndrome enable //enable\n"
"\n"
"autoSyndrome looks for recently completed jobs matching certain conditions, and if it finds one, then it will give the unit that finished that job the syndrome specified in the raw files. See Readme.rst for full details.\n"
));
//EventManager::EventHandler handle(processJob, 5);
//EventManager::registerListener(EventManager::EventType::JOB_COMPLETED, handle, plugin_self);
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream& out) {
return CR_OK;
}
/*DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event e) {
return CR_OK;
}*/
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable)
{
if (enabled == enable)
return CR_OK;
enabled = enable;
if ( enabled ) {
EventManager::EventHandler handle(processJob, 0);
EventManager::registerListener(EventManager::EventType::JOB_COMPLETED, handle, plugin_self);
} else {
EventManager::unregisterAll(plugin_self);
}
return CR_OK;
}
command_result autoSyndrome(color_ostream& out, vector<string>& parameters) {
if ( parameters.size() > 1 )
return CR_WRONG_USAGE;
bool enable = false;
if ( parameters.size() == 1 ) {
if ( parameters[0] == "enable" ) {
enable = true;
} else if ( parameters[0] == "disable" ) {
enable = false;
} else {
int32_t a = atoi(parameters[0].c_str());
if ( a < 0 || a > 1 )
return CR_WRONG_USAGE;
enable = (bool)a;
}
}
out.print("autoSyndrome is %s\n", enabled ? "enabled" : "disabled");
return plugin_enable(out, enable);
}
bool maybeApply(color_ostream& out, df::syndrome* syndrome, int32_t workerId, df::unit* unit, ResetPolicy::ResetPolicy policy) {
df::creature_raw* creature = df::global::world->raws.creatures.all[unit->race];
df::caste_raw* caste = creature->caste[unit->caste];
std::string& creature_name = creature->creature_id;
std::string& creature_caste = caste->caste_id;
//check that the syndrome applies to that guy
/*
* If there is no affected class or affected creature, then anybody who isn't immune is fair game.
*
* Otherwise, it works like this:
* add all the affected class creatures
* add all the affected creatures
* remove all the immune class creatures
* remove all the immune creatures
* you're affected if and only if you're in the remaining list after all of that
**/
bool applies = syndrome->syn_affected_class.size() == 0 && syndrome->syn_affected_creature.size() == 0;
for ( size_t c = 0; c < syndrome->syn_affected_class.size(); c++ ) {
if ( applies )
break;
for ( size_t d = 0; d < caste->creature_class.size(); d++ ) {
if ( *syndrome->syn_affected_class[c] == *caste->creature_class[d] ) {
applies = true;
break;
}
}
}
for ( size_t c = 0; c < syndrome->syn_immune_class.size(); c++ ) {
if ( !applies )
break;
for ( size_t d = 0; d < caste->creature_class.size(); d++ ) {
if ( *syndrome->syn_immune_class[c] == *caste->creature_class[d] ) {
applies = false;
return false;
}
}
}
if ( syndrome->syn_affected_creature.size() != syndrome->syn_affected_caste.size() ) {
if ( DFHack::Once::doOnce("autoSyndrome: different affected creature/caste sizes.") ) {
out.print("%s, line %d: different affected creature/caste sizes.\n", __FILE__, __LINE__);
}
return false;
}
for ( size_t c = 0; c < syndrome->syn_affected_creature.size(); c++ ) {
if ( creature_name != *syndrome->syn_affected_creature[c] )
continue;
if ( *syndrome->syn_affected_caste[c] == "ALL" ||
*syndrome->syn_affected_caste[c] == creature_caste ) {
applies = true;
break;
}
}
for ( size_t c = 0; c < syndrome->syn_immune_creature.size(); c++ ) {
if ( creature_name != *syndrome->syn_immune_creature[c] )
continue;
if ( *syndrome->syn_immune_caste[c] == "ALL" ||
*syndrome->syn_immune_caste[c] == creature_caste ) {
applies = false;
return false;
}
}
if ( !applies ) {
return false;
}
if ( giveSyndrome(out, workerId, syndrome, policy) < 0 )
return false;
return true;
}
void processJob(color_ostream& out, void* jobPtr) {
CoreSuspender suspender;
df::job* job = (df::job*)jobPtr;
if ( job == NULL ) {
if ( DFHack::Once::doOnce("autoSyndrome_processJob_null job") )
out.print("Error %s line %d: null job.\n", __FILE__, __LINE__);
return;
}
if ( job->completion_timer > 0 )
return;
if ( job->job_type != df::job_type::CustomReaction )
return;
df::reaction* reaction = NULL;
for ( size_t a = 0; a < df::global::world->raws.reactions.size(); a++ ) {
df::reaction* candidate = df::global::world->raws.reactions[a];
if ( candidate->code != job->reaction_name )
continue;
reaction = candidate;
break;
}
if ( reaction == NULL ) {
if ( DFHack::Once::doOnce("autoSyndrome processJob couldNotFind") )
out.print("%s, line %d: could not find reaction \"%s\".\n", __FILE__, __LINE__, job->reaction_name.c_str() );
return;
}
int32_t workerId = -1;
for ( size_t a = 0; a < job->general_refs.size(); a++ ) {
if ( job->general_refs[a]->getType() != df::enums::general_ref_type::UNIT_WORKER )
continue;
if ( workerId != -1 ) {
if ( DFHack::Once::doOnce("autoSyndrome processJob two workers same job") )
out.print("%s, line %d: Found two workers on the same job.\n", __FILE__, __LINE__);
}
workerId = ((df::general_ref_unit_workerst*)job->general_refs[a])->unit_id;
if (workerId == -1) {
if ( DFHack::Once::doOnce("autoSyndrome processJob invalid worker") )
out.print("%s, line %d: invalid worker.\n", __FILE__, __LINE__);
continue;
}
}
df::unit* worker = df::unit::find(workerId);
if ( worker == NULL ) {
//out.print("%s, line %d: invalid worker.\n", __FILE__, __LINE__);
//this probably means that it finished before EventManager could get a copy of the job while the job was running
//TODO: consider printing a warning once
return;
}
//find the building that made it
int32_t buildingId = -1;
for ( size_t a = 0; a < job->general_refs.size(); a++ ) {
if ( job->general_refs[a]->getType() != df::enums::general_ref_type::BUILDING_HOLDER )
continue;
if ( buildingId != -1 ) {
if ( DFHack::Once::doOnce("autoSyndrome processJob two buildings same job") )
out.print("%s, line %d: Found two buildings for the same job.\n", __FILE__, __LINE__);
}
buildingId = ((df::general_ref_building_holderst*)job->general_refs[a])->building_id;
if (buildingId == -1) {
if ( DFHack::Once::doOnce("autoSyndrome processJob invalid building") )
out.print("%s, line %d: invalid building.\n", __FILE__, __LINE__);
continue;
}
}
df::building* building = df::building::find(buildingId);
if ( building == NULL ) {
if ( DFHack::Once::doOnce("autoSyndrome processJob couldn't find building") )
out.print("%s, line %d: error: couldn't find building %d.\n", __FILE__, __LINE__, buildingId);
return;
}
//find all of the products it makes. Look for a stone.
for ( size_t a = 0; a < reaction->products.size(); a++ ) {
bool appliedSomething = false;
df::reaction_product_type type = reaction->products[a]->getType();
//out.print("type = %d\n", (int32_t)type);
if ( type != df::enums::reaction_product_type::item )
continue;
df::reaction_product_itemst* bob = (df::reaction_product_itemst*)reaction->products[a];
//out.print("item_type = %d\n", (int32_t)bob->item_type);
if ( bob->item_type != df::enums::item_type::BOULDER )
continue;
if ( bob->mat_index < 0 )
continue;
//for now don't worry about subtype
df::inorganic_raw* inorganic = df::global::world->raws.inorganics[bob->mat_index];
//maybe add each syndrome to the guy who did the job, or someone in the building, and maybe execute a command
for ( size_t b = 0; b < inorganic->material.syndrome.size(); b++ ) {
df::syndrome* syndrome = inorganic->material.syndrome[b];
bool workerOnly = true;
bool allowMultipleTargets = false;
bool foundCommand = false;
bool destroyRock = true;
bool foundAutoSyndrome = false;
ResetPolicy::ResetPolicy policy = ResetPolicy::NewInstance;
string commandStr;
vector<string> args;
for ( size_t c = 0; c < syndrome->syn_class.size(); c++ ) {
std::string& clazz = *syndrome->syn_class[c];
//special syn_classes
if ( clazz == "\\AUTO_SYNDROME" ) {
foundAutoSyndrome = true;
continue;
} else if ( clazz == "\\ALLOW_NONWORKER_TARGETS" ) {
workerOnly = false;
continue;
} else if ( clazz == "\\ALLOW_MULTIPLE_TARGETS" ) {
allowMultipleTargets = true;
continue;
} else if ( clazz == "\\PRESERVE_ROCK" ) {
destroyRock = false;
continue;
} else if ( clazz == "\\RESET_POLICY DoNothing" ) {
policy = ResetPolicy::DoNothing;
continue;
} else if ( clazz == "\\RESET_POLICY ResetDuration" ) {
policy = ResetPolicy::ResetDuration;
continue;
} else if ( clazz == "\\RESET_POLICY AddDuration" ) {
policy = ResetPolicy::AddDuration;
continue;
} else if ( clazz == "\\RESET_POLICY NewInstance" ) {
policy = ResetPolicy::NewInstance;
continue;
}
//special arguments for a DFHack console command
if ( foundCommand ) {
if ( commandStr == "" ) {
commandStr = clazz;
} else {
stringstream bob;
if ( clazz == "\\LOCATION" ) {
bob << job->pos.x;
args.push_back(bob.str());
bob.str("");
bob.clear();
bob << job->pos.y;
args.push_back(bob.str());
bob.str("");
bob.clear();
bob << job->pos.z;
args.push_back(bob.str());
bob.str("");
bob.clear();
} else if ( clazz == "\\WORKER_ID" ) {
bob << workerId;
args.push_back(bob.str());
} else if ( clazz == "\\REACTION_INDEX" ) {
bob << reaction->index;
args.push_back(bob.str());
} else {
args.push_back(clazz);
}
}
} else if ( clazz == "\\COMMAND" ) {
foundCommand = true;
}
}
if ( !foundAutoSyndrome ) {
continue;
}
if ( commandStr != "" ) {
Core::getInstance().runCommand(out, commandStr, args);
}
if ( destroyRock ) {
//find the rock and kill it before it can boil and cause problems and ugliness
for ( size_t c = 0; c < df::global::world->items.all.size(); c++ ) {
df::item* item = df::global::world->items.all[c];
if ( item->pos.z != building->z )
continue;
if ( item->pos.x < building->x1 || item->pos.x > building->x2 )
continue;
if ( item->pos.y < building->y1 || item->pos.y > building->y2 )
continue;
if ( item->getType() != df::enums::item_type::BOULDER )
continue;
//make sure it's the right type of boulder
df::item_boulderst* boulder = (df::item_boulderst*)item;
if ( boulder->mat_index != bob->mat_index )
continue;
boulder->flags.bits.hidden = true;
boulder->flags.bits.forbid = true;
boulder->flags.bits.garbage_collect = true;
}
}
if ( maybeApply(out, syndrome, workerId, worker, policy) ) {
appliedSomething = true;
}
if ( workerOnly )
continue;
if ( appliedSomething && !allowMultipleTargets )
continue;
//now try applying it to everybody inside the building
for ( size_t a = 0; a < df::global::world->units.active.size(); a++ ) {
df::unit* unit = df::global::world->units.active[a];
if ( unit == worker )
continue; //we already tried giving it to him, so no doubling up
if ( unit->pos.z != building->z )
continue;
if ( unit->pos.x < building->x1 || unit->pos.x > building->x2 )
continue;
if ( unit->pos.y < building->y1 || unit->pos.y > building->y2 )
continue;
if ( maybeApply(out, syndrome, unit->id, unit, policy) ) {
appliedSomething = true;
if ( !allowMultipleTargets )
break;
}
}
}
}
return;
}
/*
* Heavily based on https://gist.github.com/4061959/
**/
int32_t giveSyndrome(color_ostream& out, int32_t workerId, df::syndrome* syndrome, ResetPolicy::ResetPolicy policy) {
df::unit* unit = df::unit::find(workerId);
if ( !unit ) {
if ( DFHack::Once::doOnce("autoSyndrome giveSyndrome couldn't find unit") )
out.print("%s line %d: Couldn't find unit %d.\n", __FILE__, __LINE__, workerId);
return -1;
}
if ( policy != ResetPolicy::NewInstance ) {
//figure out if already there
for ( size_t a = 0; a < unit->syndromes.active.size(); a++ ) {
df::unit_syndrome* unitSyndrome = unit->syndromes.active[a];
if ( unitSyndrome->type != syndrome->id )
continue;
int32_t most = 0;
switch(policy) {
case ResetPolicy::DoNothing:
return -1;
case ResetPolicy::ResetDuration:
for ( size_t b = 0; b < unitSyndrome->symptoms.size(); b++ ) {
unitSyndrome->symptoms[b]->ticks = 0; //might cause crashes with transformations
}
unitSyndrome->ticks = 0;
break;
case ResetPolicy::AddDuration:
if ( unitSyndrome->symptoms.size() != syndrome->ce.size() ) {
if ( DFHack::Once::doOnce("autoSyndrome giveSyndrome incorrect symptom count") )
out.print("%s, line %d. Incorrect symptom count %d != %d\n", __FILE__, __LINE__, unitSyndrome->symptoms.size(), syndrome->ce.size());
break;
}
for ( size_t b = 0; b < unitSyndrome->symptoms.size(); b++ ) {
if ( syndrome->ce[b]->end == -1 )
continue;
unitSyndrome->symptoms[b]->ticks -= syndrome->ce[b]->end;
if ( syndrome->ce[b]->end > most )
most = syndrome->ce[b]->end;
}
unitSyndrome->ticks -= most;
break;
default:
if ( DFHack::Once::doOnce("autoSyndrome giveSyndrome invalid reset policy") )
out.print("%s, line %d: invalid reset policy %d.\n", __FILE__, __LINE__, policy);
return -1;
}
return 0;
}
}
df::unit_syndrome* unitSyndrome = new df::unit_syndrome();
unitSyndrome->type = syndrome->id;
unitSyndrome->year = DFHack::World::ReadCurrentYear();
unitSyndrome->year_time = DFHack::World::ReadCurrentTick();
unitSyndrome->ticks = 0;
unitSyndrome->wound_id = -1;
unitSyndrome->flags = 0; //TODO: typecast?
for ( size_t a = 0; a < syndrome->ce.size(); a++ ) {
df::unit_syndrome::T_symptoms* symptom = new df::unit_syndrome::T_symptoms();
symptom->quantity = 0;
symptom->delay = 0;
symptom->ticks = 0;
symptom->flags.bits.active = true; //TODO: ???
unitSyndrome->symptoms.push_back(symptom);
}
unit->syndromes.active.push_back(unitSyndrome);
return 0;
}

@ -2,27 +2,24 @@ IF(UNIX)
DFHACK_PLUGIN(vectors vectors.cpp)
endif()
DFHACK_PLUGIN(kittens kittens.cpp)
#DFHACK_PLUGIN(rawdump rawdump.cpp)
#DFHACK_PLUGIN(itemhacks itemhacks.cpp)
DFHACK_PLUGIN(notes notes.cpp)
DFHACK_PLUGIN(memview memview.cpp)
DFHACK_PLUGIN(autolabor2 autolabor2.cpp)
DFHACK_PLUGIN(buildprobe buildprobe.cpp)
DFHACK_PLUGIN(tilesieve tilesieve.cpp)
DFHACK_PLUGIN(frozen frozen.cpp)
DFHACK_PLUGIN(dumpmats dumpmats.cpp)
#DFHACK_PLUGIN(tiles tiles.cpp)
DFHACK_PLUGIN(counters counters.cpp)
DFHACK_PLUGIN(dumpmats dumpmats.cpp)
DFHACK_PLUGIN(eventExample eventExample.cpp)
DFHACK_PLUGIN(frozen frozen.cpp)
DFHACK_PLUGIN(kittens kittens.cpp)
DFHACK_PLUGIN(memview memview.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(notes notes.cpp)
DFHACK_PLUGIN(onceExample onceExample.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(stepBetween stepBetween.cpp)
DFHACK_PLUGIN(stockcheck stockcheck.cpp)
DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(tilesieve tilesieve.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
DFHACK_PLUGIN(autolabor2 autolabor2.cpp)
DFHACK_PLUGIN(eventExample eventExample.cpp)
DFHACK_PLUGIN(printArgs printArgs.cpp)
DFHACK_PLUGIN(onceExample onceExample.cpp)
IF(UNIX)
DFHACK_PLUGIN(ref-index ref-index.cpp)
ENDIF()
DFHACK_PLUGIN(stepBetween stepBetween.cpp)

@ -3,16 +3,24 @@
#include "Core.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/EventManager.h"
#include "DataDefs.h"
#include "VTableInterpose.h"
#include "modules/EventManager.h"
#include "df/body_part_raw.h"
#include "df/caste_body_info.h"
#include "df/construction.h"
#include "df/coord.h"
#include "df/item.h"
#include "df/item_actual.h"
#include "df/job.h"
#include "df/unit.h"
#include "df/unit_wound.h"
#include "df/world.h"
#include <vector>
#include <set>
using namespace DFHack;
using namespace std;
@ -28,6 +36,9 @@ void building(color_ostream& out, void* ptr);
void construction(color_ostream& out, void* ptr);
void syndrome(color_ostream& out, void* ptr);
void invasion(color_ostream& out, void* ptr);
void unitAttack(color_ostream& out, void* ptr);
//bool interposed = false;
command_result eventExample(color_ostream& out, vector<string>& parameters);
@ -36,7 +47,30 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginC
return CR_OK;
}
/*
//df::item::contaminateWound(df::unit*,df::unit_wound*,uint8_t,int16_t);
struct my_contaminate : df::item_actual {
typedef df::item_actual interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, contaminateWound, (df::unit* unit, df::unit_wound* wound, uint8_t unk1, int16_t unk2)) {
INTERPOSE_NEXT(contaminateWound)(unit,wound,unk1,unk2);
CoreSuspendClaimer suspend;
Core::getInstance().print("contaminateWound: item=%d, unit=%d, wound attacker = %d, unk1 = %d, unk2 = %d\n", this->id, unit->id, wound->unit_id, (int32_t)unk1, (int32_t)unk2);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(my_contaminate, contaminateWound);
*/
command_result eventExample(color_ostream& out, vector<string>& parameters) {
/*
if ( !interposed ) {
interposed = true;
if ( !INTERPOSE_HOOK(my_contaminate, contaminateWound).apply() ) {
out.print("Error: could not interpose contaminateWound.");
}
}
*/
EventManager::EventHandler initiateHandler(jobInitiated, 1);
EventManager::EventHandler completeHandler(jobCompleted, 0);
EventManager::EventHandler timeHandler(timePassed, 1);
@ -46,8 +80,9 @@ command_result eventExample(color_ostream& out, vector<string>& parameters) {
EventManager::EventHandler constructionHandler(construction, 100);
EventManager::EventHandler syndromeHandler(syndrome, 1);
EventManager::EventHandler invasionHandler(invasion, 1000);
EventManager::EventHandler unitAttackHandler(unitAttack, 1);
EventManager::unregisterAll(plugin_self);
EventManager::registerListener(EventManager::EventType::JOB_INITIATED, initiateHandler, plugin_self);
EventManager::registerListener(EventManager::EventType::JOB_COMPLETED, completeHandler, plugin_self);
EventManager::registerListener(EventManager::EventType::UNIT_DEATH, deathHandler, plugin_self);
@ -56,6 +91,7 @@ command_result eventExample(color_ostream& out, vector<string>& parameters) {
EventManager::registerListener(EventManager::EventType::CONSTRUCTION, constructionHandler, plugin_self);
EventManager::registerListener(EventManager::EventType::SYNDROME, syndromeHandler, plugin_self);
EventManager::registerListener(EventManager::EventType::INVASION, invasionHandler, plugin_self);
EventManager::registerListener(EventManager::EventType::UNIT_ATTACK, unitAttackHandler, plugin_self);
EventManager::registerTick(timeHandler, 1, plugin_self);
EventManager::registerTick(timeHandler, 2, plugin_self);
EventManager::registerTick(timeHandler, 4, plugin_self);
@ -135,3 +171,20 @@ void invasion(color_ostream& out, void* ptr) {
out.print("New invasion! %d\n", (int32_t)ptr);
}
void unitAttack(color_ostream& out, void* ptr) {
EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr;
out.print("unit %d attacks unit %d\n", data->attacker, data->defender);
df::unit* defender = df::unit::find(data->defender);
int32_t woundIndex = df::unit_wound::binsearch_index(defender->body.wounds, data->wound);
df::unit_wound* wound = defender->body.wounds[woundIndex];
set<int32_t> parts;
for ( auto a = wound->parts.begin(); a != wound->parts.end(); a++ ) {
parts.insert((*a)->body_part_id);
}
for ( auto a = parts.begin(); a != parts.end(); a++ ) {
int32_t body_part_id = (*a);
df::body_part_raw* part = defender->body.body_plan->body_parts[body_part_id];
out.print(" %s\n", part->name_singular[0]->c_str());
}
}

@ -1,32 +0,0 @@
#include "Console.h"
#include "Core.h"
#include "DataDefs.h"
#include "Export.h"
#include "PluginManager.h"
#include <iostream>
using namespace DFHack;
using namespace std;
command_result printArgs (color_ostream &out, std::vector <std::string> & parameters);
DFHACK_PLUGIN("printArgs");
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand(
"printArgs", "Print the arguments given.",
printArgs, false
));
return CR_OK;
}
command_result printArgs (color_ostream &out, std::vector <std::string> & parameters)
{
for ( size_t a = 0; a < parameters.size(); a++ ) {
out << "Argument " << (a+1) << ": \"" << parameters[a] << "\"" << endl;
}
return CR_OK;
}

@ -1,36 +1,32 @@
#include "Core.h"
#include "Error.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <string.h>
#include <stdexcept>
#include <VTableInterpose.h>
#include "Console.h"
#include "Export.h"
#include "LuaTools.h"
#include "MiscUtils.h"
#include "PluginManager.h"
#include "VTableInterpose.h"
#include "df/building.h"
#include "df/building_workshopst.h"
#include "df/unit.h"
#include "df/unit_inventory_item.h"
#include "df/construction.h"
#include "df/item.h"
#include "df/item_actual.h"
#include "df/unit_wound.h"
#include "df/world.h"
#include "df/job.h"
#include "df/proj_itemst.h"
#include "df/proj_unitst.h"
#include "df/reaction.h"
#include "df/reaction_reagent_itemst.h"
#include "df/reaction_product_itemst.h"
#include "df/proj_itemst.h"
#include "df/proj_unitst.h"
#include "MiscUtils.h"
#include "LuaTools.h"
#include "df/unit.h"
#include "df/unit_inventory_item.h"
#include "df/unit_wound.h"
#include "df/world.h"
#include "modules/EventManager.h"
#include "df/job.h"
#include "df/building.h"
#include "df/construction.h"
#include <string.h>
#include <stdexcept>
using std::vector;
using std::string;
@ -124,6 +120,10 @@ static void handle_job_complete(color_ostream &out,df::job*){};
static void handle_constructions(color_ostream &out,df::construction*){};
static void handle_syndrome(color_ostream &out,int32_t,int32_t){};
static void handle_inventory_change(color_ostream& out,int32_t,int32_t,df::unit_inventory_item*,df::unit_inventory_item*){};
static void handle_report(color_ostream& out,int32_t){};
static void handle_unitAttack(color_ostream& out,int32_t,int32_t,int32_t){};
static void handle_unload(color_ostream& out){};
static void handle_interaction(color_ostream& out, std::string, std::string, int32_t, int32_t, int32_t, int32_t){};
DEFINE_LUA_EVENT_1(onBuildingCreatedDestroyed, handle_int32t, int32_t);
DEFINE_LUA_EVENT_1(onJobInitiated,handle_job_init,df::job*);
DEFINE_LUA_EVENT_1(onJobCompleted,handle_job_complete,df::job*);
@ -133,6 +133,10 @@ DEFINE_LUA_EVENT_1(onConstructionCreatedDestroyed, handle_constructions, df::con
DEFINE_LUA_EVENT_2(onSyndrome, handle_syndrome, int32_t,int32_t);
DEFINE_LUA_EVENT_1(onInvasion,handle_int32t,int32_t);
DEFINE_LUA_EVENT_4(onInventoryChange,handle_inventory_change,int32_t,int32_t,df::unit_inventory_item*,df::unit_inventory_item*);
DEFINE_LUA_EVENT_1(onReport,handle_report,int32_t);
DEFINE_LUA_EVENT_3(onUnitAttack,handle_unitAttack,int32_t,int32_t,int32_t);
DEFINE_LUA_EVENT_0(onUnload,handle_unload);
DEFINE_LUA_EVENT_6(onInteraction,handle_interaction, std::string, std::string, int32_t, int32_t, int32_t, int32_t);
DFHACK_PLUGIN_LUA_EVENTS {
DFHACK_LUA_EVENT(onWorkshopFillSidebarMenu),
DFHACK_LUA_EVENT(postWorkshopFillSidebarMenu),
@ -152,6 +156,10 @@ DFHACK_PLUGIN_LUA_EVENTS {
DFHACK_LUA_EVENT(onSyndrome),
DFHACK_LUA_EVENT(onInvasion),
DFHACK_LUA_EVENT(onInventoryChange),
DFHACK_LUA_EVENT(onReport),
DFHACK_LUA_EVENT(onUnitAttack),
DFHACK_LUA_EVENT(onUnload),
DFHACK_LUA_EVENT(onInteraction),
DFHACK_LUA_END
};
@ -212,6 +220,20 @@ static void ev_mng_inventory(color_ostream& out, void* ptr)
}
onInventoryChange(out,unitId,itemId,item_old,item_new);
}
static void ev_mng_report(color_ostream& out, void* ptr) {
onReport(out,(int32_t)ptr);
}
static void ev_mng_unitAttack(color_ostream& out, void* ptr) {
EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr;
onUnitAttack(out,data->attacker,data->defender,data->wound);
}
static void ev_mng_unload(color_ostream& out, void* ptr) {
onUnload(out);
}
static void ev_mng_interaction(color_ostream& out, void* ptr) {
EventManager::InteractionData* data = (EventManager::InteractionData*)ptr;
onInteraction(out, data->attackVerb, data->defendVerb, data->attacker, data->defender, data->attackReport, data->defendReport);
}
std::vector<int> enabledEventManagerEvents(EventManager::EventType::EVENT_MAX,-1);
typedef void (*handler_t) (color_ostream&,void*);
static const handler_t eventHandlers[] = {
@ -225,6 +247,10 @@ static const handler_t eventHandlers[] = {
ev_mng_syndrome,
ev_mng_invasion,
ev_mng_inventory,
ev_mng_report,
ev_mng_unitAttack,
ev_mng_unload,
ev_mng_interaction,
};
static void enableEvent(int evType,int freq)
{

@ -132,6 +132,21 @@ local function invertTable(tbl)
end
return ret
end
eventType=invertTable{[0]="TICK","JOB_INITIATED","JOB_COMPLETED","UNIT_DEATH","ITEM_CREATED",
"BUILDING","CONSTRUCTION","SYNDROME","INVASION","INVENTORY_CHANGE","EVENT_MAX"}
eventType=invertTable{
[0]="TICK",
"JOB_INITIATED",
"JOB_COMPLETED",
"UNIT_DEATH",
"ITEM_CREATED",
"BUILDING",
"CONSTRUCTION",
"SYNDROME",
"INVASION",
"INVENTORY_CHANGE",
"REPORT",
"UNIT_ATTACK",
"UNLOAD",
"INTERACTION",
"EVENT_MAX"
}
return _ENV

@ -1,202 +0,0 @@
#include "Console.h"
#include "Core.h"
#include "DataDefs.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/Buildings.h"
#include "modules/EventManager.h"
#include "modules/Maps.h"
#include "modules/Once.h"
#include "df/coord.h"
#include "df/building.h"
#include "df/building_def.h"
#include "df/map_block.h"
#include "df/tile_designation.h"
#include <map>
#include <string>
//TODO: check if building becomes inside/outside later
using namespace DFHack;
using namespace std;
DFHACK_PLUGIN_IS_ENABLED(enabled);
DFHACK_PLUGIN("outsideOnly");
static map<std::string, int32_t> registeredBuildings;
const int32_t OUTSIDE_ONLY = 1;
const int32_t EITHER = 0;
const int32_t INSIDE_ONLY = -1;
int32_t checkEvery = -1;
void buildingCreated(color_ostream& out, void* data);
void checkBuildings(color_ostream& out, void* data);
command_result outsideOnly(color_ostream& out, vector<string>& parameters);
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand("outsideOnly", "Register buildings as inside/outside only. If the player attempts to construct them in an inapproprate place, the building plan automatically deconstructs.\n", &outsideOnly, false,
"outsideOnly:\n"
" outsideOnly outside [custom building name]\n"
" registers [custom building name] as outside-only\n"
" outsideOnly inside [custom building name]\n"
" registers [custom building name] as inside-only\n"
" outsideOnly either [custom building name]\n"
" unregisters [custom building name]\n"
" outsideOnly checkEvery [n]\n"
" checks for buildings that were previously in appropriate inside/outsideness but are not anymore every [n] ticks. If [n] is negative, disables checking.\n"
" outsideOnly clear\n"
" unregisters all custom buildings\n"
" enable outsideOnly\n"
" enables the plugin. Plugin must be enabled to function!\n"
" disable outsideOnly\n"
" disables the plugin\n"
" outsideOnly clear outside BUILDING_1 BUILDING_2 inside BUILDING_3\n"
" equivalent to:\n"
" outsideOnly clear\n"
" outsideOnly outside BUILDING_1\n"
" outsideOnly outside BUILDING_2\n"
" outsideOnly inside BUILDING_3\n"
));
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_WORLD_UNLOADED:
registeredBuildings.clear();
break;
default:
break;
}
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) {
if ( enabled == enable )
return CR_OK;
enabled = enable;
EventManager::unregisterAll(plugin_self);
if ( enabled ) {
EventManager::EventHandler handler(buildingCreated,1);
EventManager::registerListener(EventManager::EventType::BUILDING, handler, plugin_self);
checkBuildings(out, 0);
}
return CR_OK;
}
void destroy(df::building* building) {
if ( Buildings::deconstruct(building) )
return;
building->flags.bits.almost_deleted = 1;
}
void checkBuildings(color_ostream& out, void* data) {
if ( !enabled )
return;
std::vector<df::building*>& buildings = df::global::world->buildings.all;
for ( size_t a = 0; a < buildings.size(); a++ ) {
df::building* building = buildings[a];
if ( building == NULL )
continue;
if ( building->getCustomType() < 0 )
continue;
df::coord pos(building->centerx,building->centery,building->z);
df::tile_designation* des = Maps::getTileDesignation(pos);
bool outside = des->bits.outside;
df::building_def* def = df::global::world->raws.buildings.all[building->getCustomType()];
int32_t type = registeredBuildings[def->code];
if ( type == EITHER ) {
registeredBuildings.erase(def->code);
} else if ( type == OUTSIDE_ONLY ) {
if ( outside )
continue;
destroy(building);
} else if ( type == INSIDE_ONLY ) {
if ( !outside )
continue;
destroy(building);
} else {
if ( DFHack::Once::doOnce("outsideOnly invalid setting") ) {
out.print("Error: outsideOnly: building has invalid setting: %s %d\n", def->code.c_str(), type);
}
}
}
if ( checkEvery < 0 )
return;
EventManager::EventHandler timeHandler(checkBuildings,-1);
EventManager::registerTick(timeHandler, checkEvery, plugin_self);
}
command_result outsideOnly(color_ostream& out, vector<string>& parameters) {
int32_t status = 2;
for ( size_t a = 0; a < parameters.size(); a++ ) {
if ( parameters[a] == "clear" ) {
registeredBuildings.clear();
} else if ( parameters[a] == "outside" ) {
status = OUTSIDE_ONLY;
} else if ( parameters[a] == "inside" ) {
status = INSIDE_ONLY;
} else if ( parameters[a] == "either" ) {
status = EITHER;
} else if ( parameters[a] == "checkEvery" ) {
if (a+1 >= parameters.size()) {
out.printerr("You must specify how often to check.\n");
return CR_WRONG_USAGE;
}
checkEvery = atoi(parameters[a].c_str());
}
else {
if ( status == 2 ) {
out.printerr("Error: you need to tell outsideOnly whether the building is inside only, outside-only or either.\n");
return CR_WRONG_USAGE;
}
registeredBuildings[parameters[a]] = status;
}
}
out.print("outsideOnly is %s\n", enabled ? "enabled" : "disabled");
if ( enabled ) {
}
return CR_OK;
}
void buildingCreated(color_ostream& out, void* data) {
int32_t id = (int32_t)data;
df::building* building = df::building::find(id);
if ( building == NULL )
return;
if ( building->getCustomType() < 0 )
return;
//check if it was created inside or outside
df::coord pos(building->centerx,building->centery,building->z);
df::tile_designation* des = Maps::getTileDesignation(pos);
bool outside = des->bits.outside;
df::building_def* def = df::global::world->raws.buildings.all[building->getCustomType()];
int32_t type = registeredBuildings[def->code];
if ( type == EITHER ) {
registeredBuildings.erase(def->code);
} else if ( type == OUTSIDE_ONLY ) {
if ( outside )
return;
Buildings::deconstruct(building);
} else if ( type == INSIDE_ONLY ) {
if ( !outside )
return;
Buildings::deconstruct(building);
} else {
if ( DFHack::Once::doOnce("outsideOnly invalid setting") ) {
out.print("Error: outsideOnly: building has invalid setting: %s %d\n", def->code.c_str(), type);
}
}
}

@ -1,198 +0,0 @@
#include "Core.h"
#include "Console.h"
#include "DataDefs.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/EventManager.h"
#include "modules/Once.h"
#include "df/caste_raw.h"
#include "df/creature_raw.h"
#include "df/syndrome.h"
#include "df/unit.h"
#include "df/unit_syndrome.h"
#include "df/world.h"
#include <cstdlib>
using namespace DFHack;
using namespace std;
static bool enabled = false;
DFHACK_PLUGIN("syndromeTrigger");
void syndromeHandler(color_ostream& out, void* ptr);
command_result syndromeTrigger(color_ostream& out, vector<string>& parameters);
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand("syndromeTrigger", "Run commands and enable true transformations, configured by the raw files.\n", &syndromeTrigger, false,
"syndromeTrigger:\n"
" syndromeTrigger 0 //disable\n"
" syndromeTrigger 1 //enable\n"
" syndromeTrigger disable //disable\n"
" syndromeTrigger enable //enable\n"
"\n"
"See Readme.rst for details.\n"
));
return CR_OK;
}
command_result syndromeTrigger(color_ostream& out, vector<string>& parameters) {
if ( parameters.size() > 1 )
return CR_WRONG_USAGE;
bool wasEnabled = enabled;
if ( parameters.size() == 1 ) {
if ( parameters[0] == "enable" ) {
enabled = true;
} else if ( parameters[0] == "disable" ) {
enabled = false;
} else {
int32_t a = atoi(parameters[0].c_str());
if ( a < 0 || a > 1 )
return CR_WRONG_USAGE;
enabled = (bool)a;
}
}
out.print("syndromeTrigger is %s\n", enabled ? "enabled" : "disabled");
if ( enabled == wasEnabled )
return CR_OK;
EventManager::unregisterAll(plugin_self);
if ( enabled ) {
EventManager::EventHandler handle(syndromeHandler, 1);
EventManager::registerListener(EventManager::EventType::SYNDROME, handle, plugin_self);
}
return CR_OK;
}
void syndromeHandler(color_ostream& out, void* ptr) {
EventManager::SyndromeData* data = (EventManager::SyndromeData*)ptr;
if ( !ptr ) {
if ( DFHack::Once::doOnce("syndromeTrigger_null data") ) {
out.print("%s, %d: null pointer from EventManager.\n", __FILE__, __LINE__);
}
return;
}
//out.print("Syndrome started: unit %d, syndrome %d.\n", data->unitId, data->syndromeIndex);
df::unit* unit = df::unit::find(data->unitId);
if (!unit) {
if ( DFHack::Once::doOnce("syndromeTrigger_no find unit" ) )
out.print("%s, line %d: couldn't find unit.\n", __FILE__, __LINE__);
return;
}
df::unit_syndrome* unit_syndrome = unit->syndromes.active[data->syndromeIndex];
//out.print(" syndrome type %d\n", unit_syndrome->type);
df::syndrome* syndrome = df::global::world->raws.syndromes.all[unit_syndrome->type];
bool foundPermanent = false;
bool foundCommand = false;
bool foundAutoSyndrome = false;
string commandStr;
vector<string> args;
int32_t raceId = -1;
df::creature_raw* creatureRaw = NULL;
int32_t casteId = -1;
for ( size_t a = 0; a < syndrome->syn_class.size(); a++ ) {
std::string& clazz = *syndrome->syn_class[a];
//out.print(" clazz %d = %s\n", a, clazz.c_str());
if ( foundCommand ) {
if ( commandStr == "" ) {
commandStr = clazz;
continue;
}
stringstream bob;
if ( clazz == "\\LOCATION" ) {
bob << unit->pos.x;
args.push_back(bob.str());
bob.str("");
bob.clear();
bob << unit->pos.y;
args.push_back(bob.str());
bob.str("");
bob.clear();
bob << unit->pos.z;
args.push_back(bob.str());
bob.str("");
bob.clear();
} else if ( clazz == "\\UNIT_ID" ) {
bob << unit->id;
args.push_back(bob.str());
bob.str("");
bob.clear();
} else if ( clazz == "\\SYNDROME_ID" ) {
bob << unit_syndrome->type;
args.push_back(bob.str());
bob.str("");
bob.clear();
} else {
args.push_back(clazz);
}
continue;
}
if ( clazz == "\\AUTO_SYNDROME" ) {
foundAutoSyndrome = true;
continue;
}
if ( clazz == "\\COMMAND" ) {
foundCommand = true;
continue;
}
if ( clazz == "\\PERMANENT" ) {
foundPermanent = true;
continue;
}
if ( !foundPermanent )
continue;
if ( raceId == -1 ) {
//find the race with the name
string& name = *syndrome->syn_class[a];
for ( size_t b = 0; b < df::global::world->raws.creatures.all.size(); b++ ) {
df::creature_raw* creature = df::global::world->raws.creatures.all[b];
if ( creature->creature_id != name )
continue;
raceId = b;
creatureRaw = creature;
break;
}
continue;
}
if ( raceId != -1 && casteId == -1 ) {
string& name = *syndrome->syn_class[a];
for ( size_t b = 0; b < creatureRaw->caste.size(); b++ ) {
df::caste_raw* caste = creatureRaw->caste[b];
if ( caste->caste_id != name )
continue;
casteId = b;
break;
}
continue;
}
}
if ( !foundAutoSyndrome && commandStr != "" ) {
Core::getInstance().runCommand(out, commandStr, args);
}
if ( !foundPermanent || raceId == -1 || casteId == -1 )
return;
unit->enemy.normal_race = raceId;
unit->enemy.normal_caste = casteId;
//that's it!
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,10 @@
--print-args.lua
--author expwnent
--prints all the arguments on their own line with quotes around them. useful for debugging
local args = {...}
print("print-args")
for _,arg in ipairs(args) do
print("'"..arg.."'")
end

@ -1,62 +0,0 @@
# dig a mineral vein/layer, add tiles as they are discovered
# reuses the dig mode (upstairs etc) of the selected tile
if df.cursor.x < 0
puts "Place the game cursor on a tile to dig"
throw :script_finished
end
tile = df.map_tile_at(df.cursor)
if tile.shape_basic != :Wall or tile.designation.hidden
puts "Place the game cursor on an unmined, discovered tile"
throw :script_finished
end
def digmat_watch(tile, digmode, tilelist)
# watch the tile, expand mining operations when dug out
tilelist << [tile.x, tile.y, tile.z]
if tilelist.length == 1
df.onupdate_register_once("digmat", 10) {
tilelist.dup.each { |x, y, z|
t = df.map_tile_at(x, y, z)
if t.shape_basic != :Wall
digmat_around(t, digmode, tilelist)
tilelist.delete [x, y, z]
end
}
tilelist.empty?
}
end
tilelist.uniq!
end
def digmat_around(tile, digmode=tile.designation.dig, tilelist=[])
digmode = :Default if digmode == :No
[-1, 0, 1].each { |dz|
next if digmode == :Default and dz != 0
next if tile.z+dz < 1 or tile.z+dz > df.world.map.z_count-2
[-1, 0, 1].each { |dy|
next if tile.y+dy < 1 or tile.y+dy > df.world.map.y_count-2
[-1, 0, 1].each { |dx|
next if tile.x+dx < 1 or tile.x+dx > df.world.map.x_count-2
ntile = tile.offset(dx, dy, dz)
next if not ntile
next if ntile.designation.hidden
next if ntile.designation.dig != :No
next if ntile.shape_basic != :Wall
next if not ntile.mat_info === tile.mat_info
# ignore damp/warm stone walls
next if [-1, 0, 1].find { |ddy| [-1, 0, 1].find { |ddx|
t = ntile.offset(ddx, ddy) and t.designation.flow_size > 1
} }
ntile.dig(digmode)
digmat_watch(ntile, digmode, tilelist)
}
}
}
end
digmat_around(tile)

@ -0,0 +1,48 @@
--blood-del.lua
--makes it so that civs won't come with barrels full of blood, ichor, or goo
--author Urist Da Vinci
--edited by expwnent
local my_entity=df.historical_entity.find(df.global.ui.civ_id)
local sText=" "
local k=0
local v=1
for x,y in pairs(df.global.world.entities.all) do
my_entity=y
k=0
while k < #my_entity.resources.misc_mat.extracts.mat_index do
v=my_entity.resources.misc_mat.extracts.mat_type[k]
sText=dfhack.matinfo.decode(v,my_entity.resources.misc_mat.extracts.mat_index[k])
if (sText==nil) then
--LIQUID barrels
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
else
if(sText.material.id=="BLOOD") then
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
end
if(sText.material.id=="ICHOR") then
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
end
if(sText.material.id=="GOO") then
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
end
--VENOM
--POISON
--FLUID
--MILK
--EXTRACT
end
k=k+1
end
end

@ -0,0 +1,37 @@
-- feeding-timers.lua
-- original author: tejón
-- rewritten by expwnent
-- see repeat.lua for how to run this every so often automatically
local args = {...}
if args[1] ~= nil then
print("fix/feeding-timers usage")
print(" fix/feeding-timers")
print(" reset the feeding timers of all units as appropriate")
print(" fix/feeding-timers help")
print(" print this help message")
print(" repeat -time [n] [years/months/ticks/days/etc] -command fix/feeding-timers")
print(" run this script every n time units")
print(" repeat -cancel fix/feeding-timers")
print(" stop automatically running this script")
return
end
local fixcount = 0
for _,unit in ipairs(df.global.world.units.all) do
if dfhack.units.isCitizen(unit) and not (unit.flags1.dead) then
for _,v in pairs(unit.status.misc_traits) do
local didfix = 0
if v.id == 0 then -- I think this should have additional conditions...
v.value = 0 -- GiveWater cooldown set to zero
didfix = 1
end
if v.id == 1 then -- I think this should have additional conditions...
v.value = 0 -- GiveFood cooldown set to zero
didfix = 1
end
fixcount = fixcount + didfix
end
end
end
print("Fixed feeding timers for " .. fixcount .. " citizens.")

@ -0,0 +1,35 @@
--growth-bug.lua: units only grow when the current tick is 0 mod 10, so only 1/10 units will grow naturally. this script periodically sets the birth time of each unit so that it will grow
--to run periodically, use "repeat -time 2 months -command fix/growth-bug -now". see repeat.lua for details
--author expwnent
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'now'
})
local args = utils.processArgs({...}, validArgs)
if args.help or not next(args) then
print("fix/growth-bug usage")
print(" fix/growth-bug")
print(" fix the growth bug for all units on the map")
print(" fix/growth-bug -help")
print(" print this help message")
print(" repeat -time [n] [years/months/ticks/days/etc] -command fix/growth-bug now")
print(" run this script every n time units")
print(" repeat -cancel fix/growth-bug")
print(" stop automatically running this script")
end
local count = 0
for _,unit in ipairs(df.global.world.units.all) do
local offset = unit.relations.birth_time % 10;
if offset ~= 0 then
unit.relations.birth_time = unit.relations.birth_time - offset
count = count+1
end
end
print("Fixed growth bug for "..count.." units.")

@ -0,0 +1,126 @@
-- scripts/forum-dwarves.lua
-- Save a copy of a text screen for the DF forums. Use 'forumdwarves help' for more details.
-- original author: Caldfir
-- edited by expwnent
local args = {...}
if args[1] == 'help' then
print([[
description:
This script will attempt to read the current df-screen, and if it is a
text-viewscreen (such as the dwarf 'thoughts' screen or an item
'description') then append a marked-up version of this text to the
target file. Previous entries in the file are not overwritten, so you
may use the 'forumdwarves' command multiple times to create a single
document containing the text from multiple screens (eg: text screens
from several dwarves, or text screens from multiple artifacts/items,
or some combination).
known screens:
The screens which have been tested and known to function properly with
this script are:
1: dwarf/unit 'thoughts' screen
2: item/art 'description' screen
3: individual 'historical item/figure' screens
There may be other screens to which the script applies. It should be
safe to attempt running the script with any screen active, with an
error message to inform you when the selected screen is not appropriate
for this script.
target file:
The target file's name is 'forumdwarves.txt'. A remider to this effect
will be displayed if the script is successful.
character encoding:
The text will likely be using system-default encoding, and as such
will likely NOT display special characters (eg:é,õ,ç) correctly. To
fix this, you need to modify the character set that you are reading
the document with. 'Notepad++' is a freely available program which
can do this using the following steps:
1: open the document in Notepad++
2: in the menu-bar, select
Encoding->Character Sets->Western European->OEM-US
3: copy the text normally to wherever you want to use it
]])
return
end
local utils = require 'utils'
local gui = require 'gui'
local dialog = require 'gui.dialogs'
local colors_css = {
[0] = 'black',
[1] = 'navy',
[2] = 'green',
[3] = 'teal',
[4] = 'maroon',
[5] = 'purple',
[6] = 'olive',
[7] = 'silver',
[8] = 'gray',
[9] = 'blue',
[10] = 'lime',
[11] = 'cyan',
[12] = 'red',
[13] = 'magenta',
[14] = 'yellow',
[15] = 'white'
}
local scrn = dfhack.gui.getCurViewscreen()
local flerb = dfhack.gui.getFocusString(scrn)
local function format_for_forum(strin)
local strout = strin
local newline_idx = string.find(strout, '[P]', 1, true)
while newline_idx ~= nil do
strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3)
newline_idx = string.find(strout, '[P]', 1, true)
end
newline_idx = string.find(strout, '[B]', 1, true)
while newline_idx ~= nil do
strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3)
newline_idx = string.find(strout, '[B]', 1, true)
end
newline_idx = string.find(strout, '[R]', 1, true)
while newline_idx ~= nil do
strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3)
newline_idx = string.find(strout, '[R]', 1, true)
end
local color_idx = string.find(strout, '[C:', 1, true)
while color_idx ~= nil do
local colormatch = (string.byte(strout, color_idx+3)-48)+((string.byte(strout, color_idx+7)-48)*8)
strout = string.sub(strout,1, color_idx-1)..'[/color][color='..colors_css[colormatch]..']'..string.sub(strout,color_idx+9)
color_idx = string.find(strout, '[C:', 1, true)
end
return strout
end
if flerb == 'textviewer' then
print(scrn)
printall(scrn)
local lines = scrn.formatted_text
local line = ""
if lines ~= nil then
local log = io.open('forumdwarves.txt', 'a')
log:write("[color=silver]")
for n,x in ipairs(lines) do
print(x)
printall(x)
print(x.text)
printall(x.text)
if (x ~= nil) and (x.text ~= nil) then
log:write(format_for_forum(x.text), ' ')
--log:write(x[0],'\n')
end
end
log:write("[/color]\n")
log:close()
end
print 'data prepared for forum in \"forumdwarves.txt\"'
else
print 'this is not a textview screen'
end

@ -0,0 +1,127 @@
--full-heal.lua
--author Kurik Amudnil, Urist DaVinci
--edited by expwnent
-- attempt to fully heal a selected unit, option -r to attempt to resurrect the unit
local args = {...}
local resurrect = false
local i=0
for _,arg in ipairs(args) do
if arg == '-r' or arg == '-R' then
resurrect = true
elseif tonumber(arg) then
unit = df.unit.find(tonumber(arg))
elseif arg == 'help' or arg == '-help' or arg == '-h' then
print('full-heal: heal a unit completely from anything, optionally including death.')
print(' full-heal [unitId]')
print(' heal the unit with the given id')
print(' full-heal -r [unitId]')
print(' heal the unit with the given id and bring them back from death if they are dead')
print(' full-heal')
print(' heal the currently selected unit')
print(' full-heal -r')
print(' heal the currently selected unit and bring them back from death if they are dead')
print(' full-heal help')
print(' print this help message')
return
end
end
unit = unit or dfhack.gui.getSelectedUnit()
if not unit then
qerror('Error: please select a unit or pass its id as an argument.')
end
if unit then
if resurrect then
if unit.flags1.dead then
--print("Resurrecting...")
unit.flags2.slaughter = false
unit.flags3.scuttle = false
end
unit.flags1.dead = false
unit.flags2.killed = false
unit.flags3.ghostly = false
--unit.unk_100 = 3
end
--print("Erasing wounds...")
while #unit.body.wounds > 0 do
unit.body.wounds:erase(#unit.body.wounds-1)
end
unit.body.wound_next_id=1
--print("Refilling blood...")
unit.body.blood_count=unit.body.blood_max
--print("Resetting grasp/stand status...")
unit.status2.limbs_stand_count=unit.status2.limbs_stand_max
unit.status2.limbs_grasp_count=unit.status2.limbs_grasp_max
--print("Resetting status flags...")
unit.flags2.has_breaks=false
unit.flags2.gutted=false
unit.flags2.circulatory_spray=false
unit.flags2.vision_good=true
unit.flags2.vision_damaged=false
unit.flags2.vision_missing=false
unit.flags2.breathing_good=true
unit.flags2.breathing_problem=false
unit.flags2.calculated_nerves=false
unit.flags2.calculated_bodyparts=false
unit.flags2.calculated_insulation=false
unit.flags3.compute_health=true
--print("Resetting counters...")
unit.counters.winded=0
unit.counters.stunned=0
unit.counters.unconscious=0
unit.counters.webbed=0
unit.counters.pain=0
unit.counters.nausea=0
unit.counters.dizziness=0
unit.counters2.paralysis=0
unit.counters2.fever=0
unit.counters2.exhaustion=0
unit.counters2.hunger_timer=0
unit.counters2.thirst_timer=0
unit.counters2.sleepiness_timer=0
unit.counters2.vomit_timeout=0
--print("Resetting body part status...")
v=unit.body.components
for i=0,#v.nonsolid_remaining - 1,1 do
v.nonsolid_remaining[i] = 100 -- percent remaining of fluid layers (Urist Da Vinci)
end
v=unit.body.components
for i=0,#v.layer_wound_area - 1,1 do
v.layer_status[i].whole = 0 -- severed, leaking layers (Urist Da Vinci)
v.layer_wound_area[i] = 0 -- wound contact areas (Urist Da Vinci)
v.layer_cut_fraction[i] = 0 -- 100*surface percentage of cuts/fractures on the body part layer (Urist Da Vinci)
v.layer_dent_fraction[i] = 0 -- 100*surface percentage of dents on the body part layer (Urist Da Vinci)
v.layer_effect_fraction[i] = 0 -- 100*surface percentage of "effects" on the body part layer (Urist Da Vinci)
end
v=unit.body.components.body_part_status
for i=0,#v-1,1 do
v[i].on_fire = false
v[i].missing = false
v[i].organ_loss = false
v[i].organ_damage = false
v[i].muscle_loss = false
v[i].muscle_damage = false
v[i].bone_loss = false
v[i].bone_damage = false
v[i].skin_damage = false
v[i].motor_nerve_severed = false
v[i].sensory_nerve_severed = false
end
if unit.job.current_job and unit.job.current_job.job_type == df.job_type.Rest then
--print("Wake from rest -> clean self...")
unit.job.current_job = df.job_type.CleanSelf
end
end

@ -0,0 +1,252 @@
-- hack-wish.lua
-- Allows for script-based wishing.
-- author Putnam
-- edited by expwnent
function getGenderString(gender)
local genderStr
if gender==0 then
genderStr=string.char(12)
elseif gender==1 then
genderStr=string.char(11)
else
return ""
end
return string.char(40)..genderStr..string.char(41)
end
function getCreatureList()
local crList={}
for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do
for kk,ca in ipairs(cr.caste) do
local str=ca.caste_name[0]
str=str..' '..getGenderString(ca.gender)
table.insert(crList,{str,nil,ca})
end
end
return crList
end
function getMatFilter(itemtype)
local itemTypes={
SEEDS=function(mat,parent,typ,idx)
return mat.flags.SEED_MAT
end,
PLANT=function(mat,parent,typ,idx)
return mat.flags.STRUCTURAL_PLANT_MAT
end,
LEAVES=function(mat,parent,typ,idx)
return mat.flags.LEAF_MAT
end,
MEAT=function(mat,parent,typ,idx)
return mat.flags.MEAT
end,
CHEESE=function(mat,parent,typ,idx)
return (mat.flags.CHEESE_PLANT or mat.flags.CHEESE_CREATURE)
end,
LIQUID_MISC=function(mat,parent,typ,idx)
return (mat.flags.LIQUID_MISC_PLANT or mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER)
end,
POWDER_MISC=function(mat,parent,typ,idx)
return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE)
end,
DRINK=function(mat,parent,typ,idx)
return (mat.flags.ALCOHOL_PLANT or mat.flags.ALCOHOL_CREATURE)
end,
GLOB=function(mat,parent,typ,idx)
return (mat.flags.STOCKPILE_GLOB)
end,
WOOD=function(mat,parent,typ,idx)
return (mat.flags.WOOD)
end,
THREAD=function(mat,parent,typ,idx)
return (mat.flags.THREAD_PLANT)
end,
LEATHER=function(mat,parent,typ,idx)
return (mat.flags.LEATHER)
end
}
return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype)
end
function getRestrictiveMatFilter(itemType)
if not args.veryRestrictive then return nil else
local itemTypes={
WEAPON=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_WEAPON or mat.flags.ITEMS_WEAPON_RANGED)
end,
AMMO=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_AMMO)
end,
ARMOR=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_ARMOR)
end,
SHOES,SHIELD,HELM,GLOVES=ARMOR,ARMOR,ARMOR,ARMOR,
INSTRUMENT=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_HARD)
end,
GOBLET,FLASK,TOY,RING,CROWN,SCEPTER,FIGURINE,TOOL=INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,
AMULET=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_SOFT or mat.flags.ITEMS_HARD)
end,
EARRING,BRACELET=AMULET,AMULET,
ROCK=function(mat,parent,typ,idx)
return (mat.flags.IS_STONE)
end,
BOULDER=ROCK,
BAR=function(mat,parent,typ,idx)
return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id==COAL)
end
}
return itemTypes[df.item_type[itemType]]
end
end
function createItem(mat,itemType,quality,pos,description)
local item=df[df.item_type.attrs[itemType[1]].classname]:new()
item.id=df.global.item_next_id
df.global.world.items.all:insert('#',item)
df.global.item_next_id=df.global.item_next_id+1
item:setSubtype(itemType[2])
item:setMaterial(mat[1])
item:setMaterialIndex(mat[2])
if df.item_type[itemType[1]]=='EGG' then
local creature=df.creature_raw.find(mat[1])
local eggMat={}
eggMat[1]=dfhack.matinfo.find(creature.creature_id..':EGGSHELL')
if eggMat[1] then
eggMat[2]=dfhack.matinfo.find(creature.creature_id..':EGG_WHITE')
eggMat[3]=dfhack.matinfo.find(creature.creature_id..'EGG_YOLK')
for k,v in ipairs(eggMat) do
item.egg_materials.mat_type:insert('#',v.type)
item.egg_materials.mat_index:insert('#',v.index)
end
else
eggMat=dfhack.matinfo.find(creature.creature_id..':MUSCLE')
item.egg_materials.mat_type:insert('#',eggMat.type)
item.egg_materials.mat_index:insert('#',eggMat.index)
end
end
item:categorize(true)
item.flags.removed=true
item:setSharpness(1,0)
item:setQuality(quality-1)
if df.item_type[itemType[1]]=='SLAB' then
item.description=description
end
dfhack.items.moveToGround(item,{x=pos.x,y=pos.y,z=pos.z})
end
--TODO: should this be a function?
function qualityTable()
return {{'None'},
{'-Well-crafted-'},
{'+Finely-crafted+'},
{'*Superior*'},
{string.char(240)..'Exceptional'..string.char(240)},
{string.char(15)..'Masterwork'..string.char(15)}
}
end
local script=require('gui/script')
local guimaterials=require('gui.materials')
function showItemPrompt(text,item_filter,hide_none)
guimaterials.ItemTypeDialog{
prompt=text,
item_filter=item_filter,
hide_none=hide_none,
on_select=script.mkresume(true),
on_cancel=script.mkresume(false),
on_close=script.qresume(nil)
}:show()
return script.wait()
end
function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) --the one included with DFHack doesn't have a filter or the inorganic, creature, plant things available
guimaterials.MaterialDialog{
frame_title = title,
prompt = prompt,
mat_filter = filter,
use_inorganic = inorganic,
use_creature = creature,
use_plant = plant,
on_select = script.mkresume(true),
on_cancel = script.mkresume(false),
on_close = script.qresume(nil)
}:show()
return script.wait()
end
function usesCreature(itemtype)
typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true}
return typesThatUseCreatures[df.item_type[itemtype]]
end
function getCreatureRaceAndCaste(caste)
return df.global.world.raws.creatures.list_creature[caste.index],df.global.world.raws.creatures.list_caste[caste.index]
end
function hackWish(posOrUnit)
local pos = df.unit:is_instance(posOrUnit) and posOrUnit.pos or posOrUnit
script.start(function()
--local amountok, amount
local matok,mattype,matindex,matFilter
local itemok,itemtype,itemsubtype=showItemPrompt('What item do you want?',function(itype) return df.item_type[itype]~='CORPSE' and df.item_type[itype]~='FOOD' end ,true)
if not args.notRestrictive then
matFilter=getMatFilter(itemtype)
end
if not usesCreature(itemtype) then
matok,mattype,matindex=showMaterialPrompt('Wish','And what material should it be made of?',matFilter)
else
local creatureok,useless,creatureTable=script.showListPrompt('Wish','What creature should it be?',COLOR_LIGHTGREEN,getCreatureList())
mattype,matindex=getCreatureRaceAndCaste(creatureTable[3])
end
local qualityok,quality=script.showListPrompt('Wish','What quality should it be?',COLOR_LIGHTGREEN,qualityTable())
local description
if df.item_type[itemtype]=='SLAB' then
local descriptionok
descriptionok,description=script.showInputPrompt('Slab','What should the slab say?',COLOR_WHITE)
end
--repeat amountok,amount=script.showInputPrompt('Wish','How many do you want? (numbers only!)',COLOR_LIGHTGREEN) until tonumber(amount)
if mattype and itemtype then
--for i=1,tonumber(amount) do
createItem({mattype,matindex},{itemtype,itemsubtype},quality,pos,description)
--end
end
end)
end
scriptArgs={...}
args={}
for k,v in ipairs(scriptArgs) do
v=v:lower()
if v=='startup' then args.startup=true end
if v=='all' then args.notRestrictive=true end
if v=='restrictive' then args.veryRestrictive=true end
if v=='unit' then args.unitNum=args[k+1] end
if v=='x' then args.x=args[k+1] end
if v=='y' then args.y=args[k+1] end
if v=='z' then args.z=args[k+1] end
end
eventful=require('plugins.eventful')
function posIsValid(pos)
return pos.x~=-30000 and pos or false
end
if not args.startup then
local posOrUnit=args.x and {x=args.x,y=args.y,z=args.z} or args.unitNum and df.unit.find(args.unitNum) or posIsValid(df.global.cursor) or dfhack.gui.getSelectedUnit(true)
hackWish(posOrUnit)
else
eventful.onReactionComplete.hackWishP=function(reaction,unit,input_items,input_reagents,output_items, call_native)
if not reaction.code:find('DFHACK_WISH') then return nil end
hackWish(unit)
end
end

@ -0,0 +1,793 @@
-- unit-info-viewer.lua
-- Displays age, birth, maxage, shearing, milking, grazing, egg laying, body size, and death info about a unit. Recommended keybinding Alt-I
-- version 1.04
-- original author: Kurik Amudnil
-- edited by expwnent
local gui = require 'gui'
local widgets =require 'gui.widgets'
local utils = require 'utils'
local DEBUG = false
if DEBUG then print('-----') end
local pens = {
BLACK = dfhack.pen.parse{fg=COLOR_BLACK,bg=0},
BLUE = dfhack.pen.parse{fg=COLOR_BLUE,bg=0},
GREEN = dfhack.pen.parse{fg=COLOR_GREEN,bg=0},
CYAN = dfhack.pen.parse{fg=COLOR_CYAN,bg=0},
RED = dfhack.pen.parse{fg=COLOR_RED,bg=0},
MAGENTA = dfhack.pen.parse{fg=COLOR_MAGENTA,bg=0},
BROWN = dfhack.pen.parse{fg=COLOR_BROWN,bg=0},
GREY = dfhack.pen.parse{fg=COLOR_GREY,bg=0},
DARKGREY = dfhack.pen.parse{fg=COLOR_DARKGREY,bg=0},
LIGHTBLUE = dfhack.pen.parse{fg=COLOR_LIGHTBLUE,bg=0},
LIGHTGREEN = dfhack.pen.parse{fg=COLOR_LIGHTGREEN,bg=0},
LIGHTCYAN = dfhack.pen.parse{fg=COLOR_LIGHTCYAN,bg=0},
LIGHTRED = dfhack.pen.parse{fg=COLOR_LIGHTRED,bg=0},
LIGHTMAGENTA = dfhack.pen.parse{fg=COLOR_LIGHTMAGENTA,bg=0},
YELLOW = dfhack.pen.parse{fg=COLOR_YELLOW,bg=0},
WHITE = dfhack.pen.parse{fg=COLOR_WHITE,bg=0},
}
function getUnit_byID(id) -- get unit by id from units.all via binsearch
if type(id) == 'number' then
-- (vector,key,field,cmpfun,min,max) { item/nil , found true/false , idx/insert at }
return utils.binsearch(df.global.world.units.all,id,'id')
end
end
function getUnit_byVS(silent) -- by view screen mode
silent = silent or false
-- if not world loaded, return nil ?
local u,tmp -- u: the unit to return ; tmp: temporary for intermediate tests/return values
local v = dfhack.gui.getCurViewscreen()
u = dfhack.gui.getSelectedUnit(true) -- supports gui scripts/plugin that provide a hook for getSelectedUnit()
if u then
return u
-- else: contexts not currently supported by dfhack.gui.getSelectedUnit()
elseif df.viewscreen_dwarfmodest:is_instance(v) then
tmp = df.global.ui.main.mode
if tmp == 17 or tmp == 42 or tmp == 43 then
-- context: @dwarfmode/QueryBuiding/Some/Cage -- (q)uery cage
-- context: @dwarfmode/ZonesPenInfo/AssignUnit -- i (zone) -> pe(N)
-- context: @dwarfmode/ZonesPitInfo -- i (zone) -> (P)it
u = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
elseif tmp == 49 and df.global.ui.burrows.in_add_units_mode then
-- @dwarfmode/Burrows/AddUnits
u = df.global.ui.burrows.list_units[ df.global.ui.burrows.unit_cursor_pos ]
elseif df.global.ui.follow_unit ~= -1 then
-- context: follow unit mode
u = getUnit_byID(df.global.ui.follow_unit)
end -- end viewscreen_dwarfmodest
elseif df.viewscreen_petst:is_instance(v) then
-- context: @pet/List/Unit -- z (status) -> animals
if v.mode == 0 then -- List
if not v.is_vermin[v.cursor] then
u = v.animal[v.cursor].unit
end
--elseif v.mode = 1 then -- training knowledge (no unit reference)
elseif v.mode == 2 then -- select trainer
u = v.trainer_unit[v.trainer_cursor]
end
elseif df.viewscreen_layer_workshop_profilest:is_instance(v) then
-- context: @layer_workshop_profile/Unit -- (q)uery workshop -> (P)rofile -- df.global.ui.main.mode == 17
u = v.workers[v.layer_objects[0].cursor]
elseif df.viewscreen_layer_overall_healthst:is_instance(v) then
-- context @layer_overall_health/Units -- z -> health
u = v.unit[v.layer_objects[0].cursor]
elseif df.viewscreen_layer_militaryst:is_instance(v) then
local PG_ASSIGNMENTS = 0
local PG_EQUIPMENT = 2
local TB_POSITIONS = 1
local TB_CANDIDATES = 2
-- layer_objects[0: squads list; 1: positions list; 2: candidates list]
-- page 0:positions/assignments 1:alerts 2:equipment 3:uniforms 4:supplies 5:ammunition
if v.page == PG_ASSIGNMENTS and v.layer_objects[TB_CANDIDATES].enabled and v.layer_objects[TB_CANDIDATES].active then
-- context: @layer_military/Positions/Position/Candidates -- m -> Candidates
u = v.positions.candidates[v.layer_objects[TB_CANDIDATES].cursor]
elseif v.page == PG_ASSIGNMENTS and v.layer_objects[TB_POSITIONS].enabled and v.layer_objects[TB_POSITIONS].active then
-- context: @layer_military/Positions/Position -- m -> Positions
u = v.positions.assigned[v.layer_objects[TB_POSITIONS].cursor]
elseif v.page == PG_EQUIPMENT and v.layer_objects[TB_POSITIONS].enabled and v.layer_objects[TB_POSITIONS].active then
-- context: @layer_military/Equip/Customize/View/Position -- m -> (e)quip -> Positions
-- context: @layer_military/Equip/Uniform/Positions -- m -> (e)quip -> assign (U)niforms -> Positions
u = v.equip.units[v.layer_objects[TB_POSITIONS].cursor]
end
elseif df.viewscreen_layer_noblelistst:is_instance(v) then
if v.mode == 0 then
-- context: @layer_noblelist/List -- (n)obles
u = v.info[v.layer_objects[v.mode].cursor].unit
elseif v.mode == 1 then
-- context: @layer_noblelist/Appoint -- (n)obles -> (r)eplace
u = v.candidates[v.layer_objects[v.mode].cursor].unit
end
elseif df.viewscreen_unitst:is_instance(v) then
-- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others
u = v.unit
elseif df.viewscreen_customize_unitst:is_instance(v) then
-- @customize_unit -- @unit -> y
u = v.unit
elseif df.viewscreen_layer_unit_healthst:is_instance(v) then
-- @layer_unit_health -- @unit -> h ; @layer_overall_health/Units -> enter
if df.viewscreen_layer_overall_healthst:is_instance(v.parent) then
-- context @layer_overall_health/Units -- z (status)-> health
u = v.parent.unit[v.parent.layer_objects[0].cursor]
elseif df.viewscreen_unitst:is_instance(v.parent) then
-- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others
u = v.parent.unit
end
elseif df.viewscreen_textviewerst:is_instance(v) then
-- @textviewer -- @unit -> enter (thoughts and preferences)
if df.viewscreen_unitst:is_instance(v.parent) then
-- @unit -- @unit -> enter (thoughts and preferences)
u = v.parent.unit
elseif df.viewscreen_itemst:is_instance(v.parent) then
tmp = v.parent.entry_ref[v.parent.cursor_pos]
if df.general_ref_unit:is_instance(tmp) then -- general_ref_unit and derived ; general_ref_contains_unitst ; others?
u = getUnit_byID(tmp.unit_id)
end
elseif df.viewscreen_dwarfmodest:is_instance(v.parent) then
tmp = df.global.ui.main.mode
if tmp == 24 then -- (v)iew units {g,i,p,w} -> z (thoughts and preferences)
-- context: @dwarfmode/ViewUnits/...
--if df.global.ui_selected_unit > -1 then -- -1 = 'no units nearby'
u = df.global.world.units.active[df.global.ui_selected_unit]
--end
elseif tmp == 25 then -- loo(k) unit -> enter (thoughs and preferences)
-- context: @dwarfmode/LookAround/Unit
tmp = df.global.ui_look_list.items[df.global.ui_look_cursor]
if tmp.type == 2 then -- 0:item 1:terrain >>2: unit<< 3:building 4:colony/vermin 7:spatter
u = tmp.unit
end
end
elseif df.viewscreen_unitlistst:is_instance(v.parent) then -- (u)nit list -> (v)iew unit (not citizen)
-- context: @unitlist/Citizens ; @unitlist/Livestock ; @unitlist/Others ; @unitlist/Dead
u = v.parent.units[v.parent.page][ v.parent.cursor_pos[v.parent.page] ]
end
end -- switch viewscreen
if not u and not silent then
dfhack.printerr('No unit is selected in the UI or context not supported.')
end
return u
end -- getUnit_byVS()
--http://lua-users.org/wiki/StringRecipes ----------
function str2FirstUpper(str)
return str:gsub("^%l", string.upper)
end
--------------------------------------------------
--http://lua-users.org/wiki/StringRecipes ----------
local function tchelper(first, rest)
return first:upper()..rest:lower()
end
-- Add extra characters to the pattern if you need to. _ and ' are
-- found in the middle of identifiers and English words.
-- We must also put %w_' into [%w_'] to make it handle normal stuff
-- and extra stuff the same.
-- This also turns hex numbers into, eg. 0Xa7d4
function str2TitleCase(str)
return str:gsub("(%a)([%w_']*)", tchelper)
end
--------------------------------------------------
--isBlank suggestion by http://stackoverflow.com/a/10330861
function isBlank(x)
x = tostring(x) or ""
-- returns (not not match_begin), _ = match_end => not not true , _ => true
-- returns not not nil => false (no match)
return not not x:find("^%s*$")
end
--http://lua-users.org/wiki/StringRecipes (removed indents since I am not using them)
function wrap(str, limit)--, indent, indent1)
--indent = indent or ""
--indent1 = indent1 or indent
local limit = limit or 72
local here = 1 ---#indent1
return str:gsub("(%s+)()(%S+)()", --indent1..str:gsub(
function(sp, st, word, fi)
if fi-here > limit then
here = st -- - #indent
return "\n"..word --..indent..word
end
end)
end
--------------------------------------------------
---------------------- Time ----------------------
--------------------------------------------------
local TU_PER_DAY = 1200
--[[
if advmode then TU_PER_DAY = 86400 ? or only for cur_year_tick?
advmod_TU / 72 = ticks
--]]
local TU_PER_MONTH = TU_PER_DAY * 28
local TU_PER_YEAR = TU_PER_MONTH * 12
local MONTHS = {
'Granite',
'Slate',
'Felsite',
'Hematite',
'Malachite',
'Galena',
'Limestone',
'Sandstone',
'Timber',
'Moonstone',
'Opal',
'Obsidian',
}
Time = defclass(Time)
function Time:init(args)
self.year = args.year or 0
self.ticks = args.ticks or 0
end
function Time:getDays() -- >>float<< Days as age (including years)
return self.year * 336 + (self.ticks / TU_PER_DAY)
end
function Time:getMonths() -- >>int<< Months as age (not including years)
return math.floor (self.ticks / TU_PER_MONTH)
end
function Time:getMonthStr() -- Month as date
return MONTHS[self:getMonths()+1] or 'error'
end
function Time:getDayStr() -- Day as date
local d = math.floor ( (self.ticks % TU_PER_MONTH) / TU_PER_DAY ) + 1
if d == 11 or d == 12 or d == 13 then
d = tostring(d)..'th'
elseif d % 10 == 1 then
d = tostring(d)..'st'
elseif d % 10 == 2 then
d = tostring(d)..'nd'
elseif d % 10 == 3 then
d = tostring(d)..'rd'
else
d = tostring(d)..'th'
end
return d
end
--function Time:__add()
--end
function Time:__sub(other)
if DEBUG then print(self.year,self.ticks) end
if DEBUG then print(other.year,other.ticks) end
if self.ticks < other.ticks then
return Time{ year = (self.year - other.year - 1) , ticks = (TU_PER_YEAR + self.ticks - other.ticks) }
else
return Time{ year = (self.year - other.year) , ticks = (self.ticks - other.ticks) }
end
end
--------------------------------------------------
--------------------------------------------------
--------------------------------------------------
-------------------- Identity --------------------
--------------------------------------------------
local SINGULAR = 0
local PLURAL = 1
--local POSSESSIVE = 2
local PRONOUNS = {
[0]='She',
[1]='He',
[2]='It',
}
local BABY = 0
local CHILD = 1
local ADULT = 2
local TRAINING_LEVELS = {
[0] = ' (Semi-Wild)', -- Semi-wild
' (Trained)', -- Trained
' (-Trained-)', -- Well-trained
' (+Trained+)', -- Skillfully trained
' (*Trained*)', -- Expertly trained
' ('..string.char(240)..'Trained'..string.char(240)..')', -- Exceptionally trained
' ('..string.char(15)..'Trained'..string.char(15)..')', -- Masterully Trained
' (Tame)', -- Domesticated
'', -- undefined
'', -- wild/untameable
}
local DEATH_TYPES = {
[0] = ' died of old age', -- OLD_AGE
' starved to death', -- HUNGER
' died of dehydration', -- THIRST
' was shot and killed', -- SHOT
' bled to death', -- BLEED
' drowned', -- DROWN
' suffocated', -- SUFFOCATE
' was struck down', -- STRUCK_DOWN
' was scuttled', -- SCUTTLE
" didn't survive a collision", -- COLLISION
' took a magma bath', -- MAGMA
' took a magma shower', -- MAGMA_MIST
' was incinerated by dragon fire', -- DRAGONFIRE
' was killed by fire', -- FIRE
' experienced death by SCALD', -- SCALD
' was crushed by cavein', -- CAVEIN
' was smashed by a drawbridge', -- DRAWBRIDGE
' was killed by falling rocks', -- FALLING_ROCKS
' experienced death by CHASM', -- CHASM
' experienced death by CAGE', -- CAGE
' was murdered', -- MURDER
' was killed by a trap', -- TRAP
' vanished', -- VANISH
' experienced death by QUIT', -- QUIT
' experienced death by ABANDON', -- ABANDON
' suffered heat stroke', -- HEAT
' died of hypothermia', -- COLD
' experienced death by SPIKE', -- SPIKE
' experienced death by ENCASE_LAVA', -- ENCASE_LAVA
' experienced death by ENCASE_MAGMA', -- ENCASE_MAGMA
' was preserved in ice', -- ENCASE_ICE
' became headless', -- BEHEAD
' was crucified', -- CRUCIFY
' experienced death by BURY_ALIVE', -- BURY_ALIVE
' experienced death by DROWN_ALT', -- DROWN_ALT
' experienced death by BURN_ALIVE', -- BURN_ALIVE
' experienced death by FEED_TO_BEASTS', -- FEED_TO_BEASTS
' experienced death by HACK_TO_PIECES', -- HACK_TO_PIECES
' choked on air', -- LEAVE_OUT_IN_AIR
' experienced death by BOIL', -- BOIL
' melted', -- MELT
' experienced death by CONDENSE', -- CONDENSE
' experienced death by SOLIDIFY', -- SOLIDIFY
' succumbed to infection', -- INFECTION
"'s ghost was put to rest with a memorial", -- MEMORIALIZE
' scared to death', -- SCARE
' experienced death by DARKNESS', -- DARKNESS
' experienced death by COLLAPSE', -- COLLAPSE
' was drained of blood', -- DRAIN_BLOOD
' was slaughtered', -- SLAUGHTER
' became roadkill', -- VEHICLE
' killed by a falling object', -- FALLING_OBJECT
}
--GHOST_TYPES[unit.relations.ghost_info.type].." This spirit has not been properly memorialized or buried."
local GHOST_TYPES = {
[0]="A murderous ghost.",
"A sadistic ghost.",
"A secretive ghost.",
"An energetic poltergeist.",
"An angry ghost.",
"A violent ghost.",
"A moaning spirit returned from the dead. It will generally trouble one unfortunate at a time.",
"A howling spirit. The ceaseless noise is making sleep difficult.",
"A troublesome poltergeist.",
"A restless haunt, generally troubling past acquaintances and relatives.",
"A forlorn haunt, seeking out known locations or drifting around the place of death.",
}
Identity = defclass(Identity)
function Identity:init(args)
local u = args.unit
self.ident = dfhack.units.getIdentity(u)
self.unit = u
self.name = dfhack.TranslateName( dfhack.units.getVisibleName(u) )
self.name_en = dfhack.TranslateName( dfhack.units.getVisibleName(u) , true)
self.raw_prof = dfhack.units.getProfessionName(u)
self.pronoun = PRONOUNS[u.sex] or 'It'
if self.ident then
self.birth_date = Time{year = self.ident.birth_year, ticks = self.ident.birth_second}
self.race_id = self.ident.race
self.caste_id = self.ident.caste
if self.ident.histfig_id > -1 then
self.hf_id = self.ident.histfig_id
end
else
self.birth_date = Time{year = self.unit.relations.birth_year, ticks = self.unit.relations.birth_time}
self.race_id = u.race
self.caste_id = u.caste
if u.hist_figure_id > -1 then
self.hf_id = u.hist_figure_id
end
end
self.race = df.global.world.raws.creatures.all[self.race_id]
self.caste = self.race.caste[self.caste_id]
self.isCivCitizen = (df.global.ui.civ_id == u.civ_id)
self.isStray = u.flags1.tame --self.isCivCitizen and not u.flags1.merchant
self.cur_date = Time{year = df.global.cur_year, ticks = df.global.cur_year_tick}
------------ death ------------
self.dead = u.flags1.dead
self.ghostly = u.flags3.ghostly
self.undead = u.enemy.undead
if self.dead and self.hf_id then -- dead-dead not undead-dead
local events = df.global.world.history.events2
local e
for idx = #events - 1,0,-1 do
e = events[idx]
if df.history_event_hist_figure_diedst:is_instance(e) and e.victim_hf == self.hf_id then
self.death_event = e
break
end
end
end
if u.counters.death_id > -1 then -- if undead/ghostly dead or dead-dead
self.death_info = df.global.world.deaths.all[u.counters.death_id]
if not self.death_info.flags.discovered then
self.missing = true
end
end
-- slaughtered?
if self.death_event then
self.death_date = Time{year = self.death_event.year, ticks = self.death_event.seconds}
elseif self.death_info then
self.death_date = Time{year = self.death_info.event_year, ticks = self.death_info.event_time}
end
-- age now or age death?
if self.dead and self.death_date then -- if cursed with no age? -- if hacked a ressurection, such that they aren't dead anymore, don't use the death date
self.age_time = self.death_date - self.birth_date
else
self.age_time = self.cur_date - self.birth_date
end
if DEBUG then print( self.age_time.year,self.age_time.ticks,self.age_time:getMonths() ) end
---------- ---------- ----------
---------- caste_name ----------
self.caste_name = {}
if isBlank(self.caste.caste_name[SINGULAR]) then
self.caste_name[SINGULAR] = self.race.name[SINGULAR]
else
self.caste_name[SINGULAR] = self.caste.caste_name[SINGULAR]
end
if isBlank(self.caste.caste_name[PLURAL]) then
self.caste_name[PLURAL] = self.race.name[PLURAL]
else
self.caste_name[PLURAL] = self.caste.caste_name[PLURAL]
end
---------- ---------- ----------
--------- growth_status ---------
-- 'baby_age' is the age the baby becomes a child
-- 'child_age' is the age the child becomes an adult
if self.age_time.year >= self.caste.misc.child_age then -- has child come of age becoming adult?
self.growth_status = ADULT
elseif self.age_time.year >= self.caste.misc.baby_age then -- has baby come of age becoming child?
self.growth_status = CHILD
else
self.growth_status = BABY
end
---------- ---------- ----------
-------- aged_caste_name --------
local caste_name, race_name
if self.growth_status == ADULT then
caste_name = self.caste.caste_name[SINGULAR]
race_name = self.race.name[SINGULAR]
elseif self.growth_status == CHILD then
caste_name = self.caste.child_name[SINGULAR]
race_name = self.race.general_child_name[SINGULAR]
else --if self.growth_status == BABY then
caste_name = self.caste.baby_name[SINGULAR]
race_name = self.race.general_baby_name[SINGULAR]
end
self.aged_caste_name = {}
if isBlank(caste_name[SINGULAR]) then
self.aged_caste_name[SINGULAR] = race_name[SINGULAR]
else
self.aged_caste_name[SINGULAR] = caste_name[SINGULAR]
end
if isBlank(caste_name[PLURAL]) then
self.aged_caste_name[PLURAL] = race_name[PLURAL]
else
self.aged_caste_name[PLURAL] = caste_name[PLURAL]
end
---------- ---------- ----------
----- Profession adjustment -----
local prof = self.raw_prof
if self.undead then
prof = str2TitleCase( self.caste_name[SINGULAR] )
if isBlank(u.enemy.undead.anon_7) then
prof = prof..' Corpse'
else
prof = u.enemy.undead.anon_7 -- a reanimated body part will use this string instead
end
end
--[[
if self.ghostly then
prof = 'Ghostly '..prof
end
--]]
if u.curse.name_visible and not isBlank(u.curse.name) then
prof = prof..' '..u.curse.name
end
if isBlank(self.name) then
if self.isStray then
prof = 'Stray '..prof --..TRAINING_LEVELS[u.training_level]
end
end
self.prof = prof
---------- ---------- ----------
end
--------------------------------------------------
--------------------------------------------------
--[[
prof_id ?
group_id ?
fort_race_id
fort_civ_id
--fort_group_id?
--]]
UnitInfoViewer = defclass(UnitInfoViewer, gui.FramedScreen)
UnitInfoViewer.focus_path = 'unitinfoviewer' -- -> dfhack/lua/unitinfoviewer
UnitInfoViewer.ATTRS={
frame_style = gui.GREY_LINE_FRAME,
frame_inset = 2, -- used by init
frame_outset = 1,--3, -- new, used by init; 0 = full screen, suggest 0, 1, or 3 or maybe 5
--frame_title , -- not used
--frame_width,frame_height calculated by frame inset and outset in init
}
function UnitInfoViewer:init(args) -- requires args.unit
--if DEBUG then print('-----') end
local x,y = dfhack.screen.getWindowSize()
-- what if inset or outset are defined as {l,r,t,b}?
x = x - 2*(self.frame_inset + 1 + self.frame_outset) -- 1=frame border thickness
y = y - 2*(self.frame_inset + 1 + self.frame_outset) -- 1=frame border thickness
self.frame_width = args.frame_width or x
self.frame_height = args.frame_height or y
self.text = {}
if df.unit:is_instance(args.unit) then
self.ident = Identity{ unit = args.unit }
if not isBlank(self.ident.name_en) then
self.frame_title = 'Unit: '..self.ident.name_en
elseif not isBlank(self.ident.prof) then
self.frame_title = 'Unit: '..self.ident.prof
if self.ident.isStray then
self.frame_title = self.frame_title..TRAINING_LEVELS[self.ident.unit.training_level]
end
end
self:chunk_Name()
self:chunk_Description()
if not (self.ident.dead or self.ident.undead or self.ident.ghostly) then --not self.dead
if self.ident.isCivCitizen then
self:chunk_Age()
self:chunk_MaxAge()
end
if self.ident.isStray then
if self.ident.growth_status == ADULT then
self:chunk_Milkable()
end
self:chunk_Grazer()
if self.ident.growth_status == ADULT then
self:chunk_Shearable()
end
if self.ident.growth_status == ADULT then
self:chunk_EggLayer()
end
end
self:chunk_BodySize()
elseif self.ident.ghostly then
self:chunk_Dead()
self:chunk_Ghostly()
elseif self.ident.undead then
self:chunk_BodySize()
self:chunk_Dead()
else
self:chunk_Dead()
end
else
self:insert_chunk("No unit is selected in the UI or context not supported.",pens.LIGHTRED)
end
self:addviews{ widgets.Label{ frame={yalign=0}, text=self.text } }
end
function UnitInfoViewer:onInput(keys)
if keys.LEAVESCREEN or keys.SELECT then
self:dismiss()
end
end
function UnitInfoViewer:onGetSelectedUnit()
return self.ident.unit
end
function UnitInfoViewer:insert_chunk(str,pen)
local lines = utils.split_string( wrap(str,self.frame_width) , NEWLINE )
for i = 1,#lines do
table.insert(self.text,{text=lines[i],pen=pen})
table.insert(self.text,NEWLINE)
end
table.insert(self.text,NEWLINE)
end
function UnitInfoViewer:chunk_Name()
local i = self.ident
local u = i.unit
local prof = i.prof
local color = dfhack.units.getProfessionColor(u)
local blurb
if i.ghostly then
prof = 'Ghostly '..prof
end
if i.isStray then
prof = prof..TRAINING_LEVELS[u.training_level]
end
if isBlank(i.name) then
if isBlank(prof) then
blurb = 'I am a mystery'
else
blurb = prof
end
else
if isBlank(prof) then
blurb=i.name
else
blurb=i.name..', '..prof
end
end
self:insert_chunk(blurb,dfhack.pen.parse{fg=color,bg=0})
end
function UnitInfoViewer:chunk_Description()
local dsc = self.ident.caste.description
if not isBlank(dsc) then
self:insert_chunk(dsc,pens.WHITE)
end
end
function UnitInfoViewer:chunk_Age()
local i = self.ident
local age_str -- = ''
if i.age_time.year > 1 then
age_str = tostring(i.age_time.year)..' years old'
elseif i.age_time.year > 0 then -- == 1
age_str = '1 year old'
else --if age_time.year == 0 then
local age_m = i.age_time:getMonths() -- math.floor
if age_m > 1 then
age_str = tostring(age_m)..' months old'
elseif age_m > 0 then -- age_m == 1
age_str = '1 month old'
else -- if age_m == 0 then -- and age_m < 0 which would be an error
age_str = 'a newborn'
end
end
local blurb = i.pronoun..' is '..age_str
if i.race_id == df.global.ui.race_id then
blurb = blurb..', born on the '..i.birth_date:getDayStr()..' of '..i.birth_date:getMonthStr()..' in the year '..tostring(i.birth_date.year)..PERIOD
else
blurb = blurb..PERIOD
end
self:insert_chunk(blurb,pens.YELLOW)
end
function UnitInfoViewer:chunk_MaxAge()
local i = self.ident
local maxage = math.floor( (i.caste.misc.maxage_max + i.caste.misc.maxage_min)/2 )
--or i.unit.curse.add_tags1.NO_AGING hidden ident?
if i.caste.misc.maxage_min == -1 then
maxage = ' die of unnatural causes.'
elseif maxage == 0 then
maxage = ' die at a very young age.'
elseif maxage == 1 then
maxage = ' live about '..tostring(maxage)..' year.'
else
maxage = ' live about '..tostring(maxage)..' years.'
end
--' is expected to '..
local blurb = str2FirstUpper(i.caste_name[PLURAL])..maxage
self:insert_chunk(blurb,pens.DARKGREY)
end
function UnitInfoViewer:chunk_Grazer()
if self.ident.caste.flags.GRAZER then
local blurb = 'Grazing satisfies '..tostring(self.ident.caste.misc.grazer)..' units of hunger.'
self:insert_chunk(blurb,pens.LIGHTGREEN)
end
end
function UnitInfoViewer:chunk_EggLayer()
local caste = self.ident.caste
if caste.flags.LAYS_EGGS then
local clutch = math.floor( (caste.misc.clutch_size_max + caste.misc.clutch_size_min)/2 )
local blurb = 'Lays clutches of about '..tostring(clutch)
if clutch > 1 then
blurb = blurb..' eggs.'
else
blurb = blurb..' egg.'
end
self:insert_chunk(blurb,pens.GREEN)
end
end
function UnitInfoViewer:chunk_Milkable()
local i = self.ident
if i.caste.flags.MILKABLE then
local milk = dfhack.matinfo.decode( i.caste.extracts.milkable_mat , i.caste.extracts.milkable_matidx )
if milk then
local days,seconds = math.modf ( i.caste.misc.milkable / TU_PER_DAY )
days = (seconds > 0) and (tostring(days)..' to '..tostring(days + 1)) or tostring(days)
--local blurb = pronoun..' produces '..milk:toString()..' every '..days..' days.'
local blurb = (i.growth_status == ADULT) and (i.pronoun..' secretes ') or str2FirstUpper(i.caste_name[PLURAL])..' secrete '
blurb = blurb..milk:toString()..' every '..days..' days.'
self:insert_chunk(blurb,pens.LIGHTCYAN)
end
end
end
function UnitInfoViewer:chunk_Shearable()
local i = self.ident
local mat_types = i.caste.body_info.materials.mat_type
local mat_idxs = i.caste.body_info.materials.mat_index
local mat_info, blurb
for idx,mat_type in ipairs(mat_types) do
mat_info = dfhack.matinfo.decode(mat_type,mat_idxs[idx])
if mat_info and mat_info.material.flags.YARN then
blurb = (i.growth_status == ADULT) and (i.pronoun..' produces ') or str2FirstUpper(i.caste_name[PLURAL])..' produce '
blurb = blurb..mat_info:toString()..PERIOD
self:insert_chunk(blurb,pens.BROWN)
end
end
end
function UnitInfoViewer:chunk_BodySize()
local i = self.ident
local pat = i.unit.body.physical_attrs
local blurb = i.pronoun..' appears to be about '..pat.STRENGTH.value..':'..pat.AGILITY.value..' cubic decimeters in size.'
self:insert_chunk(blurb,pens.LIGHTBLUE)
end
function UnitInfoViewer:chunk_Ghostly()
local blurb = GHOST_TYPES[self.ident.unit.relations.ghost_info.type].." This spirit has not been properly memorialized or buried."
self:insert_chunk(blurb,pens.LIGHTMAGENTA)
-- Arose in relations.curse_year curse_time
end
function UnitInfoViewer:chunk_Dead()
local i = self.ident
local blurb, str, pen
if i.missing then --dfhack.units.isDead(unit)
str = ' is missing.'
pen = pens.WHITE
elseif i.death_event then
--str = "The Caste_name Unit_Name died in year #{e.year}"
--str << " (cause: #{e.death_cause.to_s.downcase}),"
--str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1
--str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON
--str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.bow_item_subtype].name}" if e.weapon.bow_item_type == :WEAPON
str = DEATH_TYPES[i.death_event.death_cause]..PERIOD
pen = pens.MAGENTA
elseif i.death_info then
--str = "The #{u.race_tg.name[0]}"
--str << " #{u.name}" if u.name.has_name
--str << " died"
--str << " in year #{death_info.event_year}" if death_info
--str << " (cause: #{u.counters.death_cause.to_s.downcase})," if u.counters.death_cause != -1
--str << " killed by the #{killer.race_tg.name[0]} #{killer.name}" if killer
str = DEATH_TYPES[i.death_info.death_cause]..PERIOD
pen = pens.MAGENTA
elseif i.unit.flags2.slaughter and i.unit.flags2.killed then
str = ' was slaughtered.'
pen = pens.MAGENTA
else
str = ' is dead.'
pen = pens.MAGENTA
end
if i.undead or i.ghostly then
str = ' is undead.'
pen = pens.GREY
end
blurb = 'The '..i.prof -- assume prof is not blank
if not isBlank(i.name) then
blurb = blurb..', '..i.name
end
blurb = blurb..str
self:insert_chunk(blurb,pen)
end
-- only show if UnitInfoViewer isn't the current focus
if dfhack.gui.getCurFocus() ~= 'dfhack/lua/'..UnitInfoViewer.focus_path then
local gui_no_unit = false -- show if not found?
local unit = getUnit_byVS(gui_no_unit) -- silent? or let the gui display
if unit or gui_no_unit then
local kan_viewscreen = UnitInfoViewer{unit = unit}
kan_viewscreen:show()
end
end

@ -1,29 +0,0 @@
-- invasion-now civName start end : schedules an invasion in the near future, or several
args = {...}
civName = args[1]
if ( civName ~= nil ) then
--find the civ with that name
evilEntity = nil;
for _,candidate in ipairs(df.global.world.entities.all) do
if candidate.entity_raw.code == civName then
evilEntity = candidate
break
end
end
if ( evilEntity == nil ) then
do return end
end
time = tonumber(args[2]) or 1
time2 = tonumber(args[3]) or time
for i = time,time2 do
df.global.timed_events:insert('#',{
new = df.timed_event,
type = df.timed_event_type.CivAttack,
season = df.global.cur_season,
season_ticks = df.global.cur_season_tick+i,
entity = evilEntity
})
end
end

@ -0,0 +1,100 @@
--modtools/add-syndrome.lua
--author expwnent
--add syndromes to a target, or remove them
local syndromeUtil = require 'syndrome-util'
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'syndrome',
'resetPolicy',
'erase',
'eraseOldest',
'eraseAll',
'target',
'skipImmunities'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/add-syndrome usage:
arguments:
-help
print this help message
-syndrome name
the name of the syndrome to operate on
examples:
"gila monster bite"
-resetPolicy policy
specify a policy of what to do if the unit already has an instance of the syndrome
examples:
NewInstance
default behavior: create a new instance of the syndrome
DoNothing
ResetDuration
AddDuration
-erase
instead of adding an instance of the syndrome, erase one
-eraseAll
erase every instance of the syndrome
-target id
the unit id of the target unit
examples:
0
28
-skipImmunities
add the syndrome to the target regardless of whether it is immune to the syndrome
]])
return
end
if args.resetPolicy then
args.resetPolicy = syndromeUtil.ResetPolicy[args.resetPolicy]
if not args.resetPolicy then
error ('Invalid reset policy.')
end
end
if not args.syndrome then
error 'Specify a syndrome name.'
end
local syndrome
for _,syn in ipairs(df.global.world.raws.syndromes.all) do
if syn.syn_name == args.syndrome then
syndrome = syn
break
end
end
if not syndrome then
error ('Invalid syndrome: ' .. args.syndrome)
end
args.syndrome = syndrome
if not args.target then
error 'Specify a target.'
end
local targ = df.unit.find(tonumber(args.target))
if not targ then
error ('Could not find target: ' .. args.target)
end
args.target = targ
if args.erase then
syndromeUtil.eraseSyndrome(args.target,args.syndrome.id,args.eraseOldest)
return
end
if args.eraseAll then
syndromeUtil.eraseSyndromes(args.target,args.syndrome.id)
return
end
if skipImmunities then
syndromeUtil.infectWithSyndrome(args.target,args.syndrome,args.resetPolicy)
else
syndromeUtil.infectWithSyndromeIfValidTarget(args.target,args.syndrome,args.resetPolicy)
end

@ -0,0 +1,17 @@
--scripts/modtools/anonymous-script.lua
--author expwnent
--a tool for invoking simple lua scripts without putting them in a file first
--anonymous-script "print(args[1])" arg1 arg2
--# prints "arg1"
local args = {...}
--automatically collect arguments to make the anonymous script more succinct
local f,err = load('local args = {...}; ' .. args[1], '=(anonymous lua script)') --,'=(lua command)', 't')
if err then
error(err)
end
--we don't care about the result even if they return something for some reason: we just want to ensure its side-effects happen and print appropriate error messages
dfhack.safecall(f,table.unpack(args,2))

@ -0,0 +1,110 @@
--scripts/modtools/create-item.lua
--author expwnent
--creates an item of a given type and material
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'creator',
'material',
'item',
-- 'creature',
-- 'caste',
'matchingGloves',
'matchingShoes'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print(
[[scripts/modtools/create-item.lua
arguments:
-help
print this help message
-creator id
specify the id of the unit who will create the item
examples:
0
2
-material matstring
specify the material of the item to be created
examples:
INORGANIC:IRON
CREATURE_MAT:DWARF:BRAIN
PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK
-item itemstr
specify the itemdef of the item to be created
examples:
WEAPON:ITEM_WEAPON_PICK
-matchingShoes
create two of this item
-matchingGloves
create two of this item, and set handedness appropriately
]])
return
end
if not args.creator or not tonumber(args.creator) or not df.unit.find(tonumber(args.creator)) then
error 'Invalid creator.'
end
args.creator = df.unit.find(tonumber(args.creator))
if not args.creator then
error 'Invalid creator.'
end
if not args.item then
error 'Invalid item.'
end
local itemType = dfhack.items.findType(args.item)
if itemType == -1 then
error 'Invalid item.'
end
local itemSubtype = dfhack.items.findSubtype(args.item)
organicTypes = organicTypes or utils.invert({
df.item_type.REMAINS,
df.item_type.FISH,
df.item_type.FISH_RAW,
df.item_type.VERMIN,
df.item_type.PET,
df.item_type.EGG,
})
if organicTypes[itemType] then
--TODO: look up creature and caste
error 'Not yet supported.'
end
badTypes = badTypes or utils.invert({
df.item_type.CORPSE,
df.item_type.CORPSEPIECE,
df.item_type.FOOD,
})
if badTypes[itemType] then
error 'Not supported.'
end
if not args.material then
error 'Invalid material.'
end
args.material = dfhack.matinfo.find(args.material)
if not args.material then
error 'Invalid material.'
end
local item1 = dfhack.items.createItem(itemType, itemSubtype, args.material['type'], args.material.index, args.creator)
if args.matchingGloves or args.matchingShoes then
if args.matchingGloves then
item1 = df.item.find(item1)
item1:setGloveHandedness(1);
end
local item2 = dfhack.items.createItem(itemType, itemSubtype, args.material['type'], args.material.index, args.creator)
if args.matchingGloves then
item2 = df.item.find(item2)
item2:setGloveHandedness(2);
end
end

@ -0,0 +1,80 @@
-- scripts/modtools/force.lua
-- author Putnam
-- edited by expwnent
-- Forces an event.
local utils = require 'utils'
local function findCiv(arg)
local entities = df.global.world.entities.all
if tonumber(arg) then return arg end
if arg then
for eid,entity in ipairs(entities) do
if entity.entity_raw.code == arg then return entity end
end
end
return nil
end
validArgs = validArgs or utils.invert({
'eventType',
'help',
'civ'
})
local args = utils.processArgs({...}, validArgs)
if next(args) == nil or args.help then
print([[force usage
arguments:
-help
print this help message
-eventType event
specify the type of the event to trigger
examples:
MegaBeast
Migrants
Caravan
Diplomat
WildlifeCurious
WildlifeMischievous
WildlifeFlier
CivAttack
NightCreature
-civ entity
specify the civ of the event, if applicable
examples:
player
MOUNTAIN
EVIL
28
]])
print('force: -eventType [Megabeast, Migrants, Caravan, Diplomat, WildlifeCurious, WildlifeMischievous, WildlifeFlier, CivAttack, NightCreature] -civ [player,ENTITY_ID]')
return
end
if not args.eventType then
error 'Specify an eventType.'
elseif not df.timed_event_type[args.eventType] then
error('Invalid eventType: ' .. args.eventType)
end
if args.civ == 'player' then
args.civ = df.historical_entity.find(df.global.ui.civ_id)
elseif args.civ then
args.civ = findCiv(args.civ)
end
if args.eventType == 'Migrants' then
args.civ = df.historical_entity.find(df.global.ui.civ_id)
end
local timedEvent = df.timed_event:new()
timedEvent['type'] = df.timed_event_type[args.eventType]
timedEvent.season = df.global.cur_season
timedEvent.season_ticks = df.global.cur_season_tick
if args.civ then
timedEvent.entity = args.civ
end
df.global.timed_events:insert('#', timedEvent)

@ -0,0 +1,157 @@
--scripts/modtools/interaction-trigger.lua
--author expwnent
--triggers scripts when a unit does a unit interaction on another
local eventful = require 'plugins.eventful'
local utils = require 'utils'
attackStrTriggers = attackStrTriggers or {}
defendStrTriggers = defendStrTriggers or {}
eventful.enableEvent(eventful.eventType.INTERACTION,1) --cheap, so every tick is fine
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.interactionTrigger = function()
attackStrTriggers = {}
defendStrTriggers = {}
end
local function processTrigger(args)
local command = {}
for _,arg in ipairs(args.command) do
if arg == '\\ATTACK_VERB' then
table.insert(command,args.attackVerb)
elseif arg == '\\DEFEND_VERB' then
table.insert(command,args.defendVerb)
elseif arg == '\\ATTACKER_ID' then
table.insert(command,args.attackerId)
elseif arg == '\\DEFENDER_ID' then
table.insert(command,args.defenderId)
elseif arg == '\\ATTACK_REPORT' then
table.insert(command,args.attackReport)
elseif arg == '\\DEFEND_REPORT' then
table.insert(command,args.defendReport)
elseif string.sub(arg,1,1) == '\\' then
table.insert(command,string.sub(arg,2))
else
table.insert(command,arg)
end
end
dfhack.run_command(table.unpack(command))
end
eventful.onInteraction.interactionTrigger = function(attackVerb, defendVerb, attacker, defender, attackReport, defendReport)
local extras = {}
extras.attackVerb = attackVerb
extras.defendVerb = defendVerb
extras.attackReport = attackReport
extras.defendReport = defendReport
extras.attackerId = attacker
extras.defenderId = defender
local suppressAttack = false
local suppressDefend = false
for _,trigger in ipairs(attackStrTriggers[attackVerb] or {}) do
suppressAttack = suppressAttack or trigger.suppressAttack
suppressDefend = suppressDefend or trigger.suppressDefend
utils.fillTable(trigger,extras)
processTrigger(trigger)
utils.unfillTable(trigger,extras)
end
for _,trigger in ipairs(defendStrTriggers[defendVerb] or {}) do
suppressAttack = suppressAttack or trigger.suppressAttack
suppressDefend = suppressDefend or trigger.suppressDefend
utils.fillTable(trigger,extras)
processTrigger(trigger)
utils.unfillTable(trigger,extras)
end
local eraseReport = function(unit,report)
for i,v in ipairs(unit.reports.log.Combat) do
if v == report then
unit.reports.log.Combat:erase(i)
break
end
end
end
if suppressAttack or suppressDefend then
attacker = df.unit.find(tonumber(attacker))
defender = df.unit.find(tonumber(defender))
end
if suppressAttack then
eraseReport(attacker,attackReport)
eraseReport(defender,attackReport)
end
if suppressDefend then
eraseReport(attacker,defendReport)
eraseReport(defender,defendReport)
end
end
----------------------------------------------------
--argument processing
validArgs = validArgs or utils.invert({
'clear',
'help',
'onAttackStr',
'onDefendStr',
'command',
'suppressAttack',
'suppressDefend',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/interaction-trigger.lua
arguments:
-help
print this help message
-clear
unregisters all triggers
-onAttackStr str
trigger the command when the attack verb is "str"
-onDefendStr str
trigger the command when the defend verb is "str"
-suppressAttack
delete the attack announcement from the combat logs
-suppressDefend
delete the defend announcement from the combat logs
-command [ commandStrs ]
specify the command to be executed
commandStrs
\\ATTACK_VERB
\\DEFEND_VERB
\\ATTACKER_ID
\\DEFENDER_ID
\\ATTACK_REPORT
\\DEFEND_REPORT
\\anything -> anything
anything -> anything
]])
return
end
if args.clear then
attackStrTriggers = {}
defendStrTriggers = {}
end
if not args.command then
return
end
if args.onAttackStr then
if not attackStrTriggers[args.onAttackStr] then
attackStrTriggers[args.onAttackStr] = {}
end
table.insert(attackStrTriggers[args.onAttackStr], args)
end
if args.onDefendStr then
if not defendStrTriggers[args.onDefendStr] then
defendStrTriggers[args.onDefendStr] = {}
end
table.insert(defendStrTriggers[args.onDefendStr], args)
end

@ -0,0 +1,178 @@
--scripts/modtools/invader-item-destroyer.lua
--author expwnent
--configurably deletes invader items when they die
local eventful = require 'plugins.eventful'
local utils = require 'utils'
--invaders = invaders or {}
entities = entities or {}
items = items or {}
allEntities = allEntities or false
allItems = allitems or true
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.invaderItemDestroyer = function()
entities = {}
items = {}
allEntities = false
allItems = true
end
eventful.enableEvent(eventful.eventType.UNIT_DEATH, 1) --requires iterating through all units
eventful.onUnitDeath.invaderItemDestroyer = function(unitId)
local unit = df.unit.find(unitId)
if not unit then
return
end
local entity = df.historical_entity.find(unit.civ_id)
if not allEntities and not entity then
return
end
if not allEntities and not entities[entity.entity_raw.code] then
return
end
if dfhack.units.isCitizen(unit) then
return
end
local function forEach(item)
if not allItems and not items[dfhack.items.getSubtypeDef(item:getType(),item:getSubtype()).id] then
return
end
if not (item.flags.foreign and item.flags.forbid) then
return
end
if item.pos.x ~= unit.pos.x then
return
end
if item.pos.y ~= unit.pos.y then
return
end
if item.pos.z ~= unit.pos.z then
return
end
item.flags.garbage_collect = true
item.flags.forbid = true
item.flags.hidden = true
end
for _,item in ipairs(unit.inventory) do
local item2 = df.item.find(item.item)
forEach(item2)
end
--for each item on the ground
local block = dfhack.maps.getTileBlock(unit.pos.x, unit.pos.y, unit.pos.z)
for _,item in ipairs(block.items) do
local item2 = df.item.find(item)
forEach(item2)
end
end
--[[eventful.onUnitDeath.invaderItemDestroyer = function(unit)
if invaders[unit] then
print ('Invader ' .. unit .. ' dies.')
end
for _,item in ipairs(invaders[unit] or {}) do
local item2 = df.item.find(item)
if item2 then
print ('deleting item ' .. item)
item2.flags.garbage_collect = true
item2.flags.forbid = true
item2.flags.hidden = true
item2.flags.encased = true
end
end
invaders[unit] = nil
--TODO: delete corpses?
end]]
validArgs = validArgs or utils.invert({
'clear',
'help',
'allRaces',
'allEntities',
'allItems',
'item',
'entity',
'race',
})
local args = utils.processArgs({...}, validArgs)
if args.clear then
entities = {}
items = {}
allEntities = false
allItems = true
end
if args.help then
print([[scripts/modtools/invader-item-destroyer.lua usage
arguments:
-help
print this help message
-clear
reset all registered data
-allEntities [true/false]
set whether it should delete items from invaders from any civ
-allItems [true/false]
set whether it should delete all invader items regardless of type when an appropriate invader dies
-item itemdef
set a particular itemdef to be destroyed when an invader from an appropriate civ dies
examples:
ITEM_WEAPON_PICK
-entity entityName
set a particular entity up so that its invaders destroy their items shortly after death
examples:
MOUNTAIN
EVIL
]])
return
end
if args.allEntities then
if args.allEntities == 'true' then
allEntities = true
else
allEntities = false
end
end
if args.allItems then
if args.allItems == 'true' then
allItems = true
else
allItems = false
end
end
if args.item then
local itemType
for _,itemdef in ipairs(df.global.world.raws.itemdefs.all) do
if itemdef.id == args.item then
itemType = itemdef.id
break
end
end
if not itemType then
error ('Invalid item type: ' .. args.item)
end
items[itemType] = true
end
if args.entity then
local success
for _,entity in ipairs(df.global.world.entities.all) do
if entity.entity_raw.code == args.entity then
success = true
break
end
end
if not success then
error 'Invalid entity'
end
entities[args.entity] = true
end

@ -0,0 +1,274 @@
--scripts/modtools/attack-trigger.lua
--author expwnent
--based on itemsyndrome by Putnam
--triggers scripts when a unit attacks another with a weapon type, a weapon of a particular material, or a weapon contaminated with a particular material, or when a unit equips/unequips a particular item type, an item of a particular material, or an item contaminated with a particular material
local eventful = require 'plugins.eventful'
local utils = require 'utils'
itemTriggers = itemTriggers or {}
materialTriggers = materialTriggers or {}
contaminantTriggers = contaminantTriggers or {}
eventful.enableEvent(eventful.eventType.UNIT_ATTACK,1) -- this event type is cheap, so checking every tick is fine
eventful.enableEvent(eventful.eventType.INVENTORY_CHANGE,5) --this is expensive, but you might still want to set it lower
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.itemTrigger = function()
itemTriggers = {}
materialTriggers = {}
contaminantTriggers = {}
end
function processTrigger(command)
local command2 = {}
for i,arg in ipairs(command.command) do
if arg == '\\ATTACKER_ID' then
command2[i] = '' .. command.attacker.id
elseif arg == '\\DEFENDER_ID' then
command2[i] = '' .. command.defender.id
elseif arg == '\\ITEM_MATERIAL' then
command2[i] = command.itemMat:getToken()
elseif arg == '\\ITEM_MATERIAL_TYPE' then
command2[i] = command.itemMat['type']
elseif arg == '\\ITEM_MATERIAL_INDEX' then
command2[i] = command.itemMat.index
elseif arg == '\\ITEM_ID' then
command2[i] = '' .. command.item.id
elseif arg == '\\ITEM_TYPE' then
command2[i] = command.itemType
elseif arg == '\\CONTAMINANT_MATERIAL' then
command2[i] = command.contaminantMat:getToken()
elseif arg == '\\CONTAMINANT_MATERIAL_TYPE' then
command2[i] = command.contaminantMat['type']
elseif arg == '\\CONTAMINANT_MATERIAL_INDEX' then
command2[i] = command.contaminantMat.index
elseif arg == '\\MODE' then
command2[i] = command.mode
elseif arg == '\\UNIT_ID' then
command2[i] = command.unit.id
elseif string.sub(arg,1,1) == '\\' then
command2[i] = string.sub(arg,2)
else
command2[i] = arg
end
end
dfhack.run_command(table.unpack(command2))
end
function handler(table)
local itemMat = dfhack.matinfo.decode(table.item)
local itemMatStr = itemMat:getToken()
local itemType = dfhack.items.getSubtypeDef(table.item:getType(),table.item:getSubtype()).id
table.itemMat = itemMat
table.itemType = itemType
for _,command in ipairs(itemTriggers[itemType] or {}) do
if command[table.mode] then
utils.fillTable(command,table)
processTrigger(command)
utils.unfillTable(command,table)
end
end
for _,command in ipairs(materialTriggers[itemMatStr] or {}) do
if command[table.mode] then
utils.fillTable(command,table)
processTrigger(command)
utils.unfillTable(command,table)
end
end
for _,contaminant in ipairs(table.item.contaminants or {}) do
local contaminantMat = dfhack.matinfo.decode(contaminant.mat_type, contaminant.mat_index)
local contaminantStr = contaminantMat:getToken()
table.contaminantMat = contaminantMat
for _,command in ipairs(contaminantTriggers[contaminantStr] or {}) do
utils.fillTable(command,table)
processTrigger(command)
utils.unfillTable(command,table)
end
table.contaminantMat = nil
end
end
function equipHandler(unit, item, isEquip)
local mode = (isEquip and 'onEquip') or (not isEquip and 'onUnequip')
local table = {}
table.mode = mode
table.item = df.item.find(item)
table.unit = unit
handler(table)
end
eventful.onInventoryChange.equipmentTrigger = function(unit, item, item_old, item_new)
if item_old and item_new then
return
end
local isEquip = item_new and not item_old
equipHandler(unit,item,isEquip)
end
eventful.onUnitAttack.attackTrigger = function(attacker,defender,wound)
attacker = df.unit.find(attacker)
defender = df.unit.find(defender)
if not attacker then
return
end
local attackerWeapon
for _,item in ipairs(attacker.inventory) do
if item.mode == df.unit_inventory_item.T_mode.Weapon then
attackerWeapon = item.item
break
end
end
if not attackerWeapon then
return
end
local table = {}
table.attacker = attacker
table.defender = defender
table.item = attackerWeapon
table.mode = 'onStrike'
handler(table)
end
validArgs = validArgs or utils.invert({
'clear',
'help',
'checkAttackEvery',
'checkInventoryEvery',
'command',
'itemType',
'onStrike',
'onEquip',
'onUnequip',
'material',
'contaminant',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/item-trigger.lua usage
arguments:
-help
print this help message
-clear
clear all registered triggers
-checkAttackEvery n
check the attack event at least every n ticks
-checkInventoryEvery n
check inventory event at least every n ticks
-itemType type
trigger the command for items of this type
examples:
ITEM_WEAPON_PICK
-onStrike
trigger the command when someone strikes someone with an appropriate weapon
-onEquip
trigger the command when someone equips an appropriate item
-onUnequip
trigger the command when someone unequips an appropriate item
-material mat
trigger the commmand on items with the given material
examples
INORGANIC:IRON
CREATURE_MAT:DWARF:BRAIN
PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK
-contaminant mat
trigger the command on items with a given material contaminant
examples
INORGANIC:IRON
CREATURE_MAT:DWARF:BRAIN
PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK
-command [ commandStrs ]
specify the command to be executed
commandStrs
\\ATTACKER_ID
\\DEFENDER_ID
\\ITEM_MATERIAL
\\ITEM_MATERIAL_TYPE
\\ITEM_ID
\\ITEM_TYPE
\\CONTAMINANT_MATERIAL
\\CONTAMINANT_MATERIAL_TYPE
\\CONTAMINANT_MATERIAL_INDEX
\\MODE
\\UNIT_ID
\\anything -> anything
anything -> anything
]])
return
end
if args.clear then
itemTriggers = {}
materialTriggers = {}
contaminantTriggers = {}
end
if args.checkAttackEvery then
if not tonumber(args.checkAttackEvery) then
error('checkAttackEvery must be a number')
end
eventful.enableEvent(eventful.eventType.UNIT_ATTACK,tonumber(args.checkAttackEvery))
end
if args.checkInventoryEvery then
if not tonumber(args.checkInventoryEvery) then
error('checkInventoryEvery must be a number')
end
eventful.enableEvent(eventful.eventType.INVENTORY_CHANGE,tonumber(args.checkInventoryEvery))
end
if not args.command then
if not args.clear then
error 'specify a command'
end
return
end
if args.itemType then
local temp
for _,itemdef in ipairs(df.global.world.raws.itemdefs.all) do
if itemdef.id == args.itemType then
temp = itemdef.subtype
break
end
end
if not temp then
error 'Could not find item type.'
end
args.itemType = temp
end
local numConditions = (args.material and 1 or 0) + (args.itemType and 1 or 0) + (args.contaminant and 1 or 0)
if numConditions > 1 then
error 'too many conditions defined: not (yet) supported (pester expwnent if you want it)'
elseif numConditions == 0 then
error 'specify a material, weaponType, or contaminant'
end
if args.material then
if not materialTriggers[args.material] then
materialTriggers[args.material] = {}
end
table.insert(materialTriggers[args.material],args)
elseif args.itemType then
if not itemTriggers[args.itemType] then
itemTriggers[args.itemType] = {}
end
table.insert(itemTriggers[args.itemType],args)
elseif args.contaminant then
if not contaminantTriggers[args.contaminant] then
contaminantTriggers[args.contaminant] = {}
end
table.insert(contaminantTriggers[args.contaminant],args)
end

@ -0,0 +1,95 @@
--scripts/modtools/moddable-gods.lua
--based on moddableGods by Putnam
--edited by expwnent
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'name',
'spheres',
'gender',
'depictedAs',
'domain',
'description',
-- 'entities',
})
local args = utils.processArgs({...})
if args.help then
print([[scripts/modtools/moddable-gods.lua
arguments:
-help
print this help message
-name godName
sets the name of the god to godName
if there's already a god of that name, the script halts
-spheres [ sphereList ]
define a space-separated list of spheres of influence of the god
-depictedAs str
often depicted as a str
-domain str
set the domain of the god
-description str
set the description of the god
]])
return
end
if not args.name or not args.depictedAs or not args.domain or not args.description or not args.spheres or not args.gender then
error('All arguments must be specified.')
end
local templateGod
for _,fig in ipairs(df.global.world.history.figures) do
if fig.flags.deity then
templateGod = fig
break
end
end
if not templateGod then
error 'Could not find template god.'
end
if args.gender == 'male' then
args.gender = 1
elseif args.gender == 'female' then
args.gender = 0
else
error 'invalid gender'
end
for _,fig in ipairs(df.global.world.history.figures) do
if fig.name.first_name == args.name then
print('god ' .. args.name .. ' already exists. Skipping')
return
end
end
local godFig = df.historical_figure:new()
godFig.appeared_year = -1
godFig.born_year = -1
godFig.born_seconds = -1
godFig.curse_year = -1
godFig.curse_seconds = -1
godFig.old_year = -1
godFig.old_seconds = -1
godFig.died_year = -1
godFig.died_seconds = -1
godFig.name.has_name = true
godFig.breed_id = -1
godFig.flags:assign(templateGod.flags)
godFig.id = df.global.hist_figure_next_id
df.global.hist_figure_next_id = 1+df.global.hist_figure_next_id
godFig.info = df.historical_figure_info:new()
godFig.info.spheres = {new=true}
godFig.info.secret = df.historical_figure_info.T_secret:new()
godFig.sex = args.gender
godFig.name.first_name = args.name
for _,sphere in ipairs(args.spheres) do
godFig.info.spheres:insert('#',df.sphere_type[sphere])
end
df.global.world.history.figures:insert('#',godFig)

@ -0,0 +1,134 @@
--scripts/modtools/outsideOnly.lua
--author expwnent
--enables outside only and inside only buildings
local eventful = require 'plugins.eventful'
local utils = require 'utils'
buildingType = buildingType or utils.invert({'EITHER','OUTSIDE_ONLY','INSIDE_ONLY'})
registeredBuildings = registeredBuildings or {}
checkEvery = checkEvery or 100
timeoutId = timeoutId or nil
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.outsideOnly = function()
registeredBuildings = {}
checkEvery = 100
timeoutId = nil
end
local function destroy(building)
if #building.jobs > 0 and building.jobs[0] and building.jobs[0].job_type == df.job_type.DestroyBuilding then
return
end
local b = dfhack.buildings.deconstruct(building)
if b then
--TODO: print an error message to the user so they know
return
end
-- building.flags.almost_deleted = 1
end
local function checkBuildings()
local toDestroy = {}
local function forEach(building)
if building:getCustomType() < 0 then
--TODO: support builtin building types if someone wants
return
end
local pos = df.coord:new()
pos.x = building.centerx
pos.y = building.centery
pos.z = building.z
local outside = dfhack.maps.getTileBlock(pos).designation[pos.x%16][pos.y%16].outside
local def = df.global.world.raws.buildings.all[building:getCustomType()]
local btype = registeredBuildings[def.code]
if btype then
-- print('outside: ' .. outside==true .. ', type: ' .. btype)
end
if not btype or btype == buildingType.EITHER then
registeredBuildings[def.code] = nil
return
elseif btype == buildingType.OUTSIDE_ONLY then
if outside then
return
end
else
if not outside then
return
end
end
table.insert(toDestroy,building)
end
for _,building in ipairs(df.global.world.buildings.all) do
forEach(building)
end
for _,building in ipairs(toDestroy) do
destroy(building)
end
if timeoutId then
dfhack.timeout_active(timeoutId,nil)
timeoutId = nil
end
timeoutId = dfhack.timeout(checkEvery, 'ticks', checkBuildings)
end
eventful.enableEvent(eventful.eventType.BUILDING, 100)
eventful.onBuildingCreatedDestroyed.outsideOnly = function(buildingId)
checkBuildings()
end
validArgs = validArgs or utils.invert({
'help',
'clear',
'checkEvery',
'building',
'type'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/outside-only
arguments
-help
print this help message
-clear
clears the list of registered buildings
-checkEvery n
set how often existing buildings are checked for whether they are in the appropriate location to n ticks
-type [EITHER, OUTSIDE_ONLY, INSIDE_ONLY]
specify what sort of restriction to put on the building
-building name
specify the id of the building
]])
return
end
if args.clear then
registeredBuildings = {}
end
if args.checkEvery then
if not tonumber(args.checkEvery) then
error('Invalid checkEvery.')
end
checkEvery = tonumber(args.checkEvery)
end
if not args.building then
return
end
if not args['type'] then
print 'outside-only: please specify type'
return
end
if not buildingType[args['type']] then
error('Invalid building type: ' .. args['type'])
end
registeredBuildings[args.building] = buildingType[args['type']]
checkBuildings()

@ -0,0 +1,102 @@
--scripts/modtools/projectile-trigger.lua
--author expwnent
--based on Putnam's projectileExpansion
--TODO: trigger based on contaminants
local eventful = require 'plugins.eventful'
local utils = require 'utils'
materialTriggers = materialTriggers or {}
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.projectileTrigger = function()
materialTriggers = {}
end
function processTrigger(args)
local command2 = {}
for _,arg in ipairs(args.command) do
if arg == '\\LOCATION' then
table.insert(command2,args.pos.x)
table.insert(command2,args.pos.y)
table.insert(command2,args.pos.z)
elseif arg == '\\PROJECTILE_ID' then
table.insert(command2,args.projectile.id)
elseif arg == '\\FIRER_ID' then
table.insert(command2,args.projectile.firer.id)
elseif string.sub(arg,1,1) == '\\' then
table.insert(command2,string.sub(arg,2))
else
table.insert(command2,arg)
end
end
dfhack.run_command(table.unpack(command2))
end
eventful.onProjItemCheckImpact.expansion = function(projectile)
local matStr = dfhack.matinfo.decode(projectile.item):getToken()
local table = {}
table.pos = projectile.cur_pos
table.projectile = projectile
table.item = projectile.item
for _,args in ipairs(materialTriggers[matStr] or {}) do
utils.fillTable(args,table)
processTrigger(args)
utils.unfillTable(args,table)
end
end
validArgs = validArgs or utils.invert({
'help',
'clear',
'command',
'material',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/projectile-trigger.lua
arguments
-help
print this help message
-clear
unregister all triggers
-material
specify a material for projectiles that will trigger the command
examples:
INORGANIC:IRON
CREATURE_MAT:DWARF:BRAIN
PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK
-command [ commandList ]
\\LOCATION
\\PROJECTILE_ID
\\FIRER_ID
\\anything -> anything
anything -> anything
]])
return
end
if args.clear then
materialTriggers = {}
end
if not args.command then
return
end
if not args.material then
error 'specify a material'
end
if not dfhack.matinfo.find(args.material) then
error ('invalid material: ' .. args.material)
end
if not materialTriggers[args.material] then
materialTriggers[args.material] = {}
end
table.insert(materialTriggers[args.material], args)

@ -0,0 +1,171 @@
--scripts/modtools/random-trigger.lua
--triggers random scripts
--register a few scripts, then tell it to "go" and it will pick a random one based on the probability weights you specified. outcomes are mutually exclusive. To make independent random events, call the script multiple times.
local utils = require 'utils'
local eventful = require 'plugins.eventful'
outcomeLists = outcomeLists or {}
randomGen = randomGen or dfhack.random.new()
eventful.enableEvent(eventful.eventType.UNLOAD, 1)
eventful.onUnload.randomTrigger = function()
outcomeLists = {}
end
validArgs = validArgs or utils.invert({
'help',
'command',
'outcomeListName',
'weight',
'seed',
'trigger',
'preserveList',
'withProbability',
'listOutcomes',
'clear',
})
local function triggerEvent(outcomeListName)
local outcomeList = outcomeLists[outcomeListName]
local r = randomGen:random(outcomeList.total)
local sum = 0
--print ('r = ' .. r)
for i,outcome in ipairs(outcomeList.outcomes) do
sum = sum + outcome.weight
if sum > r then
local temp = outcome.command
--print('triggering outcome ' .. i .. ': "' .. table.concat(temp, ' ') .. '"')
--dfhack.run_command(table.unpack(temp))
dfhack.run_script(table.unpack(temp))
break
else
--print ('sum = ' .. sum .. ' <= r = ' .. r)
end
end
--print('Done.')
--dfhack.print('\n')
end
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/random-trigger.lua
Allows mutually-exclusive random events. Register a list of scripts along with positive integer relative weights, then tell the script to select one of them with the specified probabilities and run it.
The weights must be positive integers, but they do NOT have to sum to 100 or any other particular number.
The outcomes are mutually exclusive: only one will be triggered.
If you want multiple independent random events, call the script multiple times.
99% of the time, you won't need to worry about this, but just in case, you can specify a name of a list of outcomes to prevent interference from other scripts that call this one.
That also permits situations where you don't know until runtime what outcomes you want.
For example, you could make a reaction-trigger that registers the worker as a mayor candidate, then run this script to choose a random mayor from the list of units that did the mayor reaction.
arguments:
-help
print this help message
-outcomeListName name
specify the name of this list of outcomes to prevent interference if two scripts are registering outcomes at the same time
if none is specified, the default outcome list is selected automatically
-command [ commandStrs ]
specify the command to be run if this outcome is selected
must be specified unless the -trigger argument is given
-weight n
the relative probability weight of this outcome
n must be a non-negative integer
if not specified, n=1 is used by default
-trigger
selects a random script based on the specified outcomeList (or the default one if none is specified)
-preserveList
when combined with trigger, preserves the list of outcomes so you don't have to register them again
it is extremely highly recommended that you always specify the outcome list name when you give this command to prevent almost certain interference
if you want to trigger one of 5 outcomes three times, you might want this option even without -outcomeListName
most of the time, you won't want this
will NOT be preserved after the user saves/loads (ask expwnent if you want this: it's not that hard but if nobody wants it I won't bother)
performance will be slightly faster if you preserve the outcome lists when possible and trigger them multiple times instead of reregistering each time, but the effect should be small
-withProbability p
p is a real number between 0 and 1 inclusive
triggers the command immediately with this probability
-seed s
sets the random seed (guarantees the same sequence of random numbers will be produced internally)
use for debugging purposes
-listOutcomes
lists the currently registered list of outcomes of the outcomeList along with their probability weights
use for debugging purposes
-clear
unregister everything
]])
return
end
if args.clear then
outcomeLists = {}
end
if args.weight and not tonumber(args.weight) then
error ('Invalid weight: ' .. args.weight)
end
args.weight = (args.weight and tonumber(args.weight)) or 1
if args.weight ~= math.floor(args.weight) then
error 'Noninteger weight.'
end
if args.weight < 0 then
error 'invalid weight: must be non-negative'
end
if args.seed then
randomGen:init(tonumber(args.seed), 37) --37 is probably excessive and definitely arbitrary
end
args.outcomeListName = args.outcomeListName or ''
args.outcomeListName = 'outcomeList ' .. args.outcomeListName
if args.withProbability then
args.withProbability = tonumber(args.withProbability)
if not args.withProbability or args.withProbability < 0 or args.withProbability > 1 then
error('Invalid withProbability: ' .. (args.withProbability or 'nil'))
end
if randomGen:drandom() < args.withProbability then
dfhack.run_command(table.unpack(args.command))
end
end
if args.trigger then
triggerEvent(args.outcomeListName)
if not args.preserveList then
outcomeLists[args.outcomeListName] = nil
end
return
end
if args.listOutcomes then
local outcomeList = outcomeLists[args.outcomeListName]
if not outcomeList then
print ('No outcomes registered.')
return
end
print ('Total weight: ' .. outcomeList.total)
for _,outcome in ipairs(outcomeList.outcomes) do
print(' outcome weight ' .. outcome.weight .. ': ' .. table.concat(outcome.command, ' '))
end
print('\n')
return
end
if not args.command then
return
end
--actually register
local outcomeList = outcomeLists[args.outcomeListName]
if not outcomeList then
outcomeLists[args.outcomeListName] = {}
outcomeList = outcomeLists[args.outcomeListName]
end
outcomeList.total = args.weight + (outcomeList.total or 0)
local outcome = {}
outcome.weight = args.weight
outcome.command = args.command
outcomeList.outcomes = outcomeList.outcomes or {}
table.insert(outcomeList.outcomes, outcome)

@ -0,0 +1,137 @@
-- scripts/modtools/reaction-trigger-transition.lua
-- author expwnent
-- prints useful things to the console and a file to help modders transition from autoSyndrome to reaction-trigger
-- this script is basically an apology for breaking backward compatibiility
local function maybeQuote(str)
if str == '' or string.find(str,' ') then
return ('"' .. str .. '"')
else
return str
end
end
warnings = ''
output = ''
for _,reaction in ipairs(df.global.world.raws.reactions) do
local function foreachProduct(product)
local prodType = product:getType()
if prodType ~= df.reaction_product_type.item then
return
end
if product.item_type ~= df.item_type.BOULDER then
return
end
if product.mat_index < 0 then
return
end
local inorganic = df.global.world.raws.inorganics[product.mat_index]
local didInorganicName
for _,syndrome in ipairs(inorganic.material.syndrome) do
local workerOnly = true
local allowMultipleTargets = false;
local command
local commandStr
local destroyRock = true;
local foundAutoSyndrome = false;
local resetPolicy;
for i,synclass in ipairs(syndrome.syn_class) do
synclass = synclass.value
if false then
elseif synclass == '\\AUTO_SYNDROME' then
foundAutoSyndrome = true
elseif synclass == '\\ALLOW_NONWORKER_TARGETS' then
workerOnly = false
elseif synclass == '\\ALLOW_MULTIPLE_TARGETS' then
allowMultipleTargets = true
elseif synclass == '\\PRESERVE_ROCK' then
destroyRock = false
elseif synclass == '\\RESET_POLICY DoNothing' then
resetPolicy = 'DoNothing'
elseif synclass == '\\RESET_POLICY ResetDuration' then
resetPolicy = 'ResetDuration'
elseif synclass == '\\RESET_POLICY AddDuration' then
resetPolicy = 'AddDuration'
elseif synclass == '\\RESET_POLICY NewInstance' then
resetPolicy = 'NewInstance'
elseif synclass == '\\COMMAND' then
command = ''
elseif command then
if synclass == '\\LOCATION' then
command = command .. '\\LOCATION '
elseif synclass == '\\WORKER_ID' then
command = command .. '\\WORKER_ID '
elseif synclass == '\\REACTION_INDEX' then
warnings = warnings .. ('Warning: \\REACTION_INDEX is deprecated. Use \\REACTION_NAME instead.\n')
command = command .. '\\REACTION_NAME '
else
commandStr = true
command = command .. maybeQuote(synclass) .. ' '
end
end
end
if foundAutoSyndrome then
if destroyRock then
warnings = warnings .. ('Warning: instead of destroying the rock, do not produce it in the first place.\n')
end
if workerOnly then
workerOnly = 'true'
else
workerOnly = 'false'
end
if allowMultipleTargets then
allowMultipleTargets = 'true'
else
allowMultipleTargets = 'false'
end
local reactionTriggerStr = 'modtools/reaction-trigger -reactionName ' .. maybeQuote(reaction.code) --.. '"'
if workerOnly ~= 'true' then
reactionTriggerStr = reactionTriggerStr .. ' -workerOnly ' .. workerOnly
end
if allowMultipleTargets ~= 'false' then
reactionTriggerStr = reactionTriggerStr .. ' -allowMultipleTargets ' .. allowMultipleTargets
end
if resetPolicy and resetPolicy ~= 'NewInstance' then
reactionTriggerStr = reactionTriggerStr .. ' -resetPolicy ' .. resetPolicy
end
if #syndrome.ce > 0 then
if syndrome.syn_name == '' then
warnings = warnings .. ('Warning: give this syndrome a name!\n')
end
reactionTriggerStr = reactionTriggerStr .. ' -syndrome ' .. maybeQuote(syndrome.syn_name) .. ''
end
if command and commandStr then
reactionTriggerStr = reactionTriggerStr .. ' -command ' .. command
end
if (not command or command == '') and (not syndrome.syn_name or syndrome.syn_name == '') then
--output = output .. '#'
else
if not didInorganicName then
-- output = output .. '# ' .. (inorganic.id) .. '\n'
didInorganicName = true
end
output = output .. (reactionTriggerStr) .. '\n'
end
end
end
end
for _,product in ipairs(reaction.products) do
foreachProduct(product)
end
end
print(warnings)
print('\n\n\n\n')
print(output)
local file = io.open('reaction-trigger-transition.txt', 'w+')
--io.output(file)
--file:write(warnings)
--file:write('\n\n\n\n')
file:write(output)
file:flush()
--io.flush(file)
io.close(file)
--io.output()
print('transition information written to reaction-trigger-transition.txt')

@ -0,0 +1,228 @@
-- scripts/modtools/reaction-trigger.lua
-- author expwnent
-- replaces autoSyndrome: trigger commands when custom reactions are completed
local eventful = require 'plugins.eventful'
local syndromeUtil = require 'syndromeUtil'
local utils = require 'utils'
reactionHooks = reactionHooks or {}
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.reactionTrigger = function()
reactionHooks = {}
end
local function getWorkerAndBuilding(job)
local workerId = -1
local buildingId = -1
for _,generalRef in ipairs(job.general_refs) do
if generalRef:getType() == df.general_ref_type.UNIT_WORKER then
if workerId ~= -1 then
print(job)
printall(job)
error('reaction-trigger: two workers on same job: ' .. workerId .. ', ' .. generalRef.unit_id)
else
workerId = generalRef.unit_id
if workerId == -1 then
print(job)
printall(job)
error('reaction-trigger: invalid worker')
end
end
elseif generalRef:getType() == df.general_ref_type.BUILDING_HOLDER then
if buildingId ~= -1 then
print(job)
printall(job)
error('reaction-trigger: two buildings same job: ' .. buildingId .. ', ' .. generalRef.building_id)
else
buildingId = generalRef.building_id
if buildingId == -1 then
print(job)
printall(job)
error('reaction-trigger: invalid building')
end
end
end
end
return workerId,buildingId
end
local function processCommand(job, worker, target, building, command)
local result = {}
for _,arg in ipairs(command) do
if arg == '\\WORKER_ID' then
table.insert(result,''..worker.id)
elseif arg == '\\TARGET_ID' then
table.insert(restul,''..target.id)
elseif arg == '\\BUILDING_ID' then
table.insert(result,''..building.id)
elseif arg == '\\LOCATION' then
table.insert(result,''..job.pos.x)
table.insert(result,''..job.pos.y)
table.insert(result,''..job.pos.z)
elseif arg == '\\REACTION_NAME' then
table.insert(result,''..job.reaction_name)
elseif string.sub(arg,1,1) == '\\' then
table.insert(result,string.sub(arg,2))
else
table.insert(result,arg)
end
end
return result
end
eventful.onJobCompleted.reactionTrigger = function(job)
if job.completion_timer > 0 then
return
end
-- if job.job_type ~= df.job_type.CustomReaction then
-- --TODO: support builtin reaction triggers if someone asks
-- return
-- end
if not job.reaction_name or job.reaction_name == '' then
return
end
-- print('reaction name: ' .. job.reaction_name)
if not job.reaction_name or not reactionHooks[job.reaction_name] then
return
end
local worker,building = getWorkerAndBuilding(job)
worker = df.unit.find(worker)
building = df.building.find(building)
if not worker or not building then
--this probably means that it finished before EventManager could get a copy of the job while the job was running
--TODO: consider printing a warning once
return
end
local function doAction(action)
local didSomething
if action.command then
local processed = processCommand(job, worker, worker, building, action.command)
dfhack.run_command(table.unpack(processed))
end
if action.syndrome then
didSomething = syndromeUtil.infectWithSyndromeIfValidTarget(worker, action.syndrome, action.resetPolicy) or didSomething
end
if action.workerOnly then
return
end
if didSomething and not action.allowMultipleTargets then
return
end
local function foreach(unit)
if unit == worker then
return false
elseif unit.pos.z ~= building.z then
return false
elseif unit.pos.x < building.x1 or unit.pos.x > building.x2 then
return false
elseif unit.pos.y < building.y1 or unit.pos.y > building.y2 then
return false
else
if action.command then
processCommand(job, worker, unit, building, action.command)
end
if action.syndrome then
didSomething = syndrome.infectWithSyndromeIfValidTarget(unit,action.syndrome,action.resetPolicy) or didSomething
end
if didSomething and not action.allowMultipleTargets then
return true
end
return false
end
end
for _,unit in ipairs(df.global.world.units.all) do
if foreach(unit) then
break
end
end
end
for _,action in ipairs(reactionHooks[job.reaction_name]) do
doAction(action)
end
end
eventful.enableEvent(eventful.eventType.JOB_COMPLETED,0) --0 is necessary to catch cancelled jobs and not trigger them
validArgs = validArgs or utils.invert({
'help',
'clear',
'reactionName',
'syndrome',
'command',
'allowNonworkerTargets',
'allowMultipleTargets'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/reaction-trigger.lua
arguments:
-help
print this help message
-clear
unregister all reaction hooks
-reactionName name
specify the name of the reaction
-syndrome name
specify the name of the syndrome to be applied to the targets
-allowNonworkerTargets
allow other units in the same building to be targetted by either the script or the syndrome
-allowMultipleTargets
allow multiple targets to the script or syndrome
if absent:
if running a script, only one target will be used
if applying a syndrome, then only one target will be infected
-resetPolicy policy
set the reset policy in the case that the syndrome is already present
policy
NewInstance (default)
DoNothing
ResetDuration
AddDuration
-command [ commandStrs ]
specify the command to be run on the target(s)
special args
\\WORKER_ID
\\TARGET_ID
\\BUILDING_ID
\\LOCATION
\\REACTION_NAME
\\anything -> anything
anything -> anything
]])
return
end
if args.clear then
reactionHooks = {}
end
if not args.reactionName then
return
end
if not reactionHooks[args.reactionName] then
reactionHooks[args.reactionName] = {}
end
if args.syndrome then
local foundIt
for _,syndrome in ipairs(df.global.world.raws.syndromes.all) do
if syndrome.syn_name == args.syndrome then
args.syndrome = syndrome
foundIt = true
break
end
end
if not foundIt then
error('Could not find syndrome ' .. args.syndrome)
end
end
table.insert(reactionHooks[args.reactionName], args)

@ -0,0 +1,101 @@
--scripts/modtools/skill-change.lua
--author expwnent
--based on skillChange.lua by Putnam
--TODO: update skill level once experience increases/decreases
--TODO: skill rust?
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'skill',
'mode',
'value',
'granularity',
'unit'
})
mode = mode or utils.invert({
'add',
'set',
})
granularity = granularity or utils.invert({
'experience',
'level',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/skill-change.lua
arguments
-help
print this help message
-skill skillName
set the skill that we're talking about
-mode (add/set)
are we adding experience/levels or setting them?
-granularity (experience/levels)
direct experience, or experience levels?
-unit id
id of the target unit
-value amount
how much to set/add
]])
return
end
if not args.unit or not tonumber(args.unit) or not df.unit.find(tonumber(args.unit)) then
error 'Invalid unit.'
end
args.unit = df.unit.find(tonumber(args.unit))
args.skill = df.job_skill[args.skill]
args.mode = mode[args.mode or 'set']
args.granularity = granularity[args.granularity or 'level']
args.value = tonumber(args.value)
if not args.skill then
error('invalid skill')
end
if not args.value then
error('invalid value')
end
local skill
for _,skill_c in ipairs(args.unit.status.current_soul.skills) do
if skill_c.id == args.skill then
skill = skill_c
end
end
if not skill then
skill = df.unit_skill:new()
skill.id = args.skill
args.unit.status.current_soul.skills:insert('#', skill)
end
print('old: ' .. skill.rating .. ': ' .. skill.experience)
if args.granularity == granularity.experience then
if args.mode == mode.set then
skill.experience = args.value
elseif args.mode == mode.add then
skill.experience = skill.experience + args.value
else
error 'bad mode'
end
elseif args.granularity == granularity.level then
if args.mode == mode.set then
skill.rating = df.skill_rating[args.value]
elseif args.mode == mode.add then
skill.rating = df.skill_rating[args.value + df.skill_rating[skill.rating]]
else
error 'bad mode'
end
else
error 'bad granularity'
end
print('new: ' .. skill.rating .. ': ' .. skill.experience)

@ -0,0 +1,86 @@
--scripts/modtools/spawn-flow.lua
--author expwnent
--spawns flows at locations
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'material',
'flowType',
'location',
'flowSize',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/spawn-flow.lua
arguments:
-help
print this help message
-material mat
specify the material of the flow, if applicable
examples:
INORGANIC:IRON
CREATURE_MAT:DWARF:BRAIN
PLANT_MAT:MUSHROOM_HELMET_PLUMP:DRINK
-location [ x y z]
the location to spawn the flow
-flowType type
specify the flow type
examples:
Miasma
Steam
Mist
MaterialDust
MagmaMist
Smoke
Dragonfire
Fire
Web
MaterialGas
MaterialVapor
OceanWave
SeaFoam
-flowSize size
specify how big the flow is
]])
return
end
local mat_index = -1;
local mat_type = -1;
if args.material then
local mat = dfhack.matinfo.find(args.material)
if not mat then
error ('Invalid material: ' .. mat)
end
mat_index = mat.index
mat_type = mat['type']
end
if args.flowSize and not tonumber(args.flowSize) then
error ('Invalid flow size: ' .. args.flowSize)
end
args.flowSize = tonumber(args.flowSize or 'z') or 100
if not args.flowType or not df.flow_type[args.flowType] then
error ('Invalid flow type: ' .. (args.flowType or 'none specified'))
end
args.flowType = df.flow_type[args.flowType]
if not args.location then
error 'Specify a location.'
end
local pos = df.coord:new();
pos.x = tonumber(args.location[1] or 'a')
pos.y = tonumber(args.location[2] or 'a')
pos.z = tonumber(args.location[3] or 'a')
if not pos.x or not pos.y or not pos.z then
error ('Invalid pos.')
end
dfhack.maps.spawnFlow(pos, args.flowType, mat_type, mat_index, args.flowSize)

@ -0,0 +1,117 @@
--scripts/modtools/syndrome-trigger.lua
--author expwnent
--triggers scripts when units are infected with syndromes
local eventful = require 'plugins.eventful'
local utils = require 'utils'
onInfection = onInfection or {}
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.syndromeTrigger = function()
onInfection = {}
end
eventful.enableEvent(eventful.eventType.SYNDROME,5) --requires iterating through every unit, so not cheap, but not slow either
local function processTrigger(args)
local command = {}
for i,arg in ipairs(args.command) do
if arg == '\\SYNDROME_ID' then
table.insert(command, '' .. args.syndrome.id)
elseif arg == '\\UNIT_ID' then
table.insert(command, '' .. args.unit.id)
elseif arg == '\\LOCATION' then
table.insert(command, '' .. args.unit.pos.x)
table.insert(command, '' .. args.unit.pos.y)
table.insert(command, '' .. args.unit.pos.z)
elseif string.sub(arg,1,1) == '\\' then
table.insert(command, string.sub(arg,2))
else
table.insert(command, arg)
end
end
dfhack.run_command(table.unpack(command))
end
eventful.onSyndrome.syndromeTrigger = function(unitId, syndromeIndex)
local unit = df.unit.find(unitId)
local unit_syndrome = unit.syndromes.active[syndromeIndex]
local syn_id = unit_syndrome['type']
if not onInfection[syn_id] then
return
end
local syndrome = df.syndrome.find(syn_id)
local table = {}
table.unit = unit
table.unit_syndrome = unit_syndrome
table.syndrome = syndrome
for _,args in ipairs(onInfection[syn_id] or {}) do
utils.fillTable(args,table)
processTrigger(args)
utils.unfillTable(args,table)
end
end
------------------------------
--argument processing
validArgs = validArgs or utils.invert({
'clear',
'help',
'command',
'syndrome'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/syndrome-trigger.lua
arguments
-help
print this help message
-clear
clear all triggers
-syndrome name
specify the name of a syndrome
-command [ commandStrs ]
specify the command to be executed after infection
args
\\SYNDROME_ID
\\UNIT_ID
\\LOCATION
\anything -> anything
anything -> anything
]])
return
end
if args.clear then
onInfection = {}
end
if not args.command then
return
end
if not args.syndrome then
error 'Select a syndrome.'
end
local syndrome
for _,syn in ipairs(df.global.world.raws.syndromes.all) do
if syn.syn_name == args.syndrome then
if syndrome then
error ('Multiple syndromes with same name: ' .. syn.syn_name)
end
syndrome = syn.id
end
end
if not syndrome then
error ('Could not find syndrome named ' .. syndrome)
end
onInfection[syndrome] = onInfection[syndrome] or {}
table.insert(onInfection[syndrome], args)

@ -0,0 +1,158 @@
--scripts/modtools/transform-unit.lua
--author expwnent
--based on shapechange by Putnam
--warning: will crash arena mode if you view the unit on the same tick that it transforms
--if you wait until later, it will be fine
local utils = require 'utils'
normalRace = normalRace or {}
local function transform(unit,race,caste)
unit.enemy.normal_race = race
unit.enemy.normal_caste = caste
unit.enemy.were_race = race
unit.enemy.were_caste = caste
end
validArgs = validArgs or utils.invert({
'clear',
'help',
'unit',
'duration',
'setPrevRace',
'keepInventory',
'race',
'caste',
'suppressAnnouncement',
'untransform',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/modtools/transform-unit.lua
arguments
-help
print this help message
-clear
clear records of normal races
-unit id
set the target unit
-duration ticks
how long it should last, or "forever"
-setPrevRace
make a record of the previous race so that you can change it back with -untransform
-keepInventory
move items back into inventory after transformation
-race raceName
-caste casteName
-suppressAnnouncement
don't show the Unit has transformed into a Blah! event
-untransform
turn the unit back into what it was before
]])
return
end
if args.clear then
normalRace = {}
end
if not args.unit then
error 'Specify a unit.'
end
if not args.duration then
args.duration = 'forever'
end
local raceIndex
local race
local caste
if args.untransform then
local unit = df.unit.find(tonumber(args.unit))
raceIndex = normalRace[args.unit].race
race = df.creature_raw.find(raceIndex)
caste = normalRace[args.unit].caste
normalRace[args.unit] = nil
else
if not args.race or not args.caste then
error 'Specficy a target form.'
end
--find race
for i,v in ipairs(df.global.world.raws.creatures.all) do
if v.creature_id == args.race then
raceIndex = i
race = v
break
end
end
if not race then
error 'Invalid race.'
end
for i,v in ipairs(race.caste) do
if v.caste_id == args.caste then
caste = i
break
end
end
if not caste then
error 'Invalid caste.'
end
end
local unit = df.unit.find(tonumber(args.unit))
local oldRace = unit.enemy.normal_race
local oldCaste = unit.enemy.normal_caste
if args.setPrevRace then
normalRace[args.unit] = {}
normalRace[args.unit].race = oldRace
normalRace[args.unit].caste = oldCaste
end
transform(unit,raceIndex,caste,args.setPrevRace)
local inventoryItems = {}
local function getInventory()
local result = {}
for _,item in ipairs(unit.inventory) do
table.insert(result, item:new());
end
return result
end
local function restoreInventory()
dfhack.timeout(1, 'ticks', function()
for _,item in ipairs(inventoryItems) do
dfhack.items.moveToInventory(item.item, unit, item.mode, item.body_part_id)
item:delete()
end
inventoryItems = {}
end)
end
if args.keepInventory then
inventoryItems = getInventory()
end
if args.keepInventory then
restoreInventory()
end
if args.duration and args.duration ~= 'forever' then
--when the timeout ticks down, transform them back
dfhack.timeout(tonumber(args.duration), 'ticks', function()
if args.keepInventory then
inventoryItems = getInventory()
end
transform(unit,oldRace,oldCaste)
if args.keepInventory then
restoreInventory()
end
end)
end

@ -0,0 +1,57 @@
-- scripts/remove-wear.lua
-- Resets all items in your fort to 0 wear
-- original author: Laggy
-- edited by expwnent
local args = {...}
if args[1] == 'help' then
print([[remove-wear - this script removes wear from all items, or from individual ones
remove-wear all
remove wear from all items
remove-wear n1 n2 n3 ...
remove wear from items with the given ids. order does not matter
repeat -time 2 months -command remove-wear all
remove wear from all items every 2 months. see repeat.lua for details
]])
do return end
elseif args[1] == 'all' then
local count = 0;
for _,item in ipairs(df.global.world.items.all) do
if (item.wear > 0) then
item:setWear(0)
count = count+1
end
end
print('remove-wear removed wear from '..count..' objects')
else
local argIndex = 1
local isCompleted = {}
for i,x in ipairs(args) do
args[i] = tonumber(x)
end
table.sort(args)
for _,item in ipairs(df.global.world.items.all) do
local function loop()
if argIndex > #args then
return
elseif item.id > args[argIndex] then
argIndex = argIndex+1
loop()
return
elseif item.id == args[argIndex] then
--print('removing wear from item with id ' .. args[argIndex])
item:setWear(0)
isCompleted[args[argIndex]] = true
argIndex = argIndex+1
end
end
loop()
end
for _,arg in ipairs(args) do
if isCompleted[arg] ~= true then
print('failed to remove wear from item ' .. arg .. ': could not find item with that id')
end
end
end

@ -0,0 +1,61 @@
-- scripts/repeat.lua
-- repeatedly calls a lua script, eg "repeat -time 1 months -command cleanowned"; to disable "repeat -cancel cleanowned"
-- repeat -help for details
-- author expwnent
-- vaguely based on a script by Putnam
local repeatUtil = require 'repeat-util'
local utils = require 'utils'
validArgs = validArgs or utils.invert({
'help',
'cancel',
'name',
'time',
'timeUnits',
'command'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[repeat.lua
repeat -help
print this help message
repeat -cancel bob
cancels the repetition with the name bob
repeat -name jim -time delay -timeUnits units -printResult true -command printArgs 3 1 2
-name sets the name for the purposes of cancelling and making sure you don't schedule the same repeating event twice
if not specified, it's set to the first argument after -command
-time delay -timeUnits units
delay is some positive integer
units is some valid time unit for dfhack.timeout(delay,timeUnits,function)
-command ...
specify the command to be run
]])
return
end
if args.cancel then
repeatUtil.cancel(args.cancel)
if args.name then
repeatUtil.cancel(args.name)
end
return
end
args.time = tonumber(args.time)
if not args.name then
args.name = args.command[1]
end
if not args.timeUnits then
args.timeUnits = 'ticks'
end
local callCommand = function()
dfhack.run_command(table.unpack(args.command))
end
repeatUtil.scheduleEvery(args.name,args.time,args.timeUnits,callCommand)

@ -0,0 +1,36 @@
-- teleport.lua
-- teleports a unit to a location
-- author Putnam
-- edited by expwnent
local function teleport(unit,pos)
local unitoccupancy = dfhack.maps.getTileBlock(unit.pos).occupancy[unit.pos.x%16][unit.pos.y%16]
unit.pos.x = pos.x
unit.pos.y = pos.y
unit.pos.z = pos.z
if not unit.flags1.on_ground then unitoccupancy.unit = false else unitoccupancy.unit_grounded = false end
end
local function getArgsTogether(args)
local settings={pos={}}
for k,v in ipairs(args) do
v=string.lower(v)
if v=="unit" then settings.unitID=tonumber(args[k+1]) end
if v=="x" then settings.pos['x']=tonumber(args[k+1]) end
if v=="y" then settings.pos['y']=tonumber(args[k+1]) end
if v=="z" then settings.pos['z']=tonumber(args[k+1]) end
if v=="showunitid" then print(dfhack.gui.getSelectedUnit(true).id) end
if v=="showpos" then printall(df.global.cursor) end
end
if not settings.pos.x or not settings.pos.y or not settings.pos.z then settings.pos=nil end
if not settings.unitID and not settings.pos.x then qerror("Needs a position, a unit ID or both, but not neither!") end
return settings
end
local args = {...}
local teleportSettings=getArgsTogether(args)
local unit = teleportSettings.unitID and df.unit.find(teleportSettings.unitID) or dfhack.gui.getSelectedUnit(true)
local pos = teleportSettings.pos and teleportSettings.pos or df.global.cursor
teleport(unit,pos)