Thanks to everyone's really hard work, I got DFHack working, and even ported over a version of the old spawn-unit command which seems to function. But I'm having two issues with it.
For now I just commented that out since it probably doesn't even apply to animals... just thought it might be useful to any of the authors working on a fork of that thing.
The other error is specific to my application, where I wanted to extract the name of a unit.
showed me), since I'm guessing that would be passed in the interaction trigger I'll be using eventually.
-- create Awakened Stone creature
-- modtools/interaction-trigger -onAttackStr "awakens living andesite" -suppressAttack -suppressDefend -command [ tesb-spawn -caste ANDESITE -miner \\ATTACKER_ID -location [ \\LOCATION ] -friendly ]
-- modtools/interaction-trigger -onAttackStr "angers living andesite" -suppressAttack -suppressDefend -command [ tesb-spawn -caste ANDESITE -miner \\ATTACKER_ID -location [ \\LOCATION ] ]
--[=[
Only the caste ID is required, all others have defaults (though default x,y,z exists only when cursor is visible)
Made by warmist, but edited by Putnam for the dragon ball mod to be used in reactions, then customized by Dirst for The Earth Strikes Back mod
Dirst also included some parts from Rubble's announce.lua and expwent's unit-info-viewer.lua and spawn-flow.lua
modtools/interaction-trigger intercepts the interaction verb and executes this script instead. The interactions are in interaction_tesb.txt with names of the pattern AWAKEN_ANDESITE and ANGER_ANDESITE
--]=]
-- args={...}
local utils=require 'utils'
validArgs = validArgs or utils.invert({
'help',
'caste',
'miner',
'location',
'friendly',
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print([[scripts/tesb-spawn.lua
arguments:
-help
Print this help message
-caste
Specify the type of awakened stone
This parameter is required
-location [ x y z]
The location to spawn the awakened stone
Will default to the cursor location if it is displayed
-miner
Unit ID of creature causing the spawn
Optional, only affects in-game announcement
-friendly
If flag is present, the awakened stone will be "Tame"
If flag is absent, the awakened stone will be a "Wild Animal"
]])
return
end
function getCaste(race_id,caste_id)
local cr=df.creature_raw.find(race_id)
return cr.caste[caste_id]
end
function genBodyModifier(body_app_mod)
local a=math.random(0,#body_app_mod.ranges-2)
return math.random(body_app_mod.ranges[a],body_app_mod.ranges[a+1])
end
function getBodySize(caste,time)
--TODO: real body size...
return caste.body_size_1[#caste.body_size_1-1] --returns last body size
end
function genAttribute(array)
local a=math.random(0,#array-2)
return math.random(array[a],array[a+1])
end
function norm()
return math.sqrt((-2)*math.log(math.random()))*math.cos(2*math.pi*math.random())
end
function normalDistributed(mean,sigma)
return mean+sigma*norm()
end
function clampedNormal(min,median,max)
local val=normalDistributed(median,math.sqrt(max-min))
if val<min then return min end
if val>max then return max end
return val
end
function makeSoul(unit,caste)
local tmp_soul=df.unit_soul:new()
tmp_soul.unit_id=unit.id
tmp_soul.name:assign(unit.name)
tmp_soul.race=unit.race
tmp_soul.sex=unit.sex
tmp_soul.caste=unit.caste
--todo: preferences,traits.
local attrs=caste.attributes
for k,v in pairs(attrs.ment_att_range) do
local max_percent=attrs.ment_att_cap_perc[k]/100
local cvalue=genAttribute(v)
tmp_soul.mental_attrs[k]={value=cvalue,max_value=cvalue*max_percent}
end
--[=[ Throws an error, so skipping for now.
for k,v in pairs(tmp_soul.traits) do
local min,mean,max
min=caste.personality.a[k]
mean=caste.personality.b[k]
max=caste.personality.c[k]
tmp_soul.traits[k]=clampedNormal(min,mean,max)
end
--]=]
--[[natural skill fix]]
for k, skill in ipairs(caste.natural_skill_id) do
local rating = caste.natural_skill_lvl[k]
utils.insert_or_update(tmp_soul.skills,
{new=true,id=skill,experience=caste.natural_skill_exp[k],rating=rating}, 'id')
end
unit.status.souls:insert("#",tmp_soul)
unit.status.current_soul=tmp_soul
end
function CreateUnit(race_id,caste_id)
local race=df.creature_raw.find(race_id)
if race==nil then error("Invalid race_id") end
local caste=getCaste(race_id,caste_id)
local unit=df.unit:new()
unit:assign{
race=race_id,
caste=caste_id,
sex=caste.gender,
}
unit.relations.birth_year=df.global.cur_year-0 --AGE is set here, Awakened Stones are mature at age 0
if caste.misc.maxage_max==-1 then
unit.relations.old_year=-1
else
unit.relations.old_year=df.global.cur_year+math.random(caste.misc.maxage_min,caste.misc.maxage_max)
end
--unit.relations.birth_time=??
--unit.relations.old_time=?? --TODO add normal age
--[[ interataction stuff, probably timers ]]--
local num_inter=#caste.body_info.interactions -- new for interactions
unit.curse.own_interaction:resize(num_inter) -- changed from anon_4
unit.curse.own_interaction_delay:resize(num_inter) -- changed from anon_5
--[[ body stuff ]]
local body=unit.body
body.body_plan=caste.body_info
local body_part_count=#body.body_plan.body_parts
local layer_count=#body.body_plan.layer_part
--[[ body components ]]
local cp=body.components
cp.body_part_status:resize(body_part_count)
cp.numbered_masks:resize(#body.body_plan.numbered_masks)
for num,v in ipairs(body.body_plan.numbered_masks) do
cp.numbered_masks[num]=v
end
cp.layer_status:resize(layer_count)
cp.layer_wound_area:resize(layer_count)
cp.layer_cut_fraction:resize(layer_count)
cp.layer_dent_fraction:resize(layer_count)
cp.layer_effect_fraction:resize(layer_count)
local attrs=caste.attributes
for k,v in pairs(attrs.phys_att_range) do
local max_percent=attrs.phys_att_cap_perc[k]/100
local cvalue=genAttribute(v)
unit.body.physical_attrs[k]={value=cvalue,max_value=cvalue*max_percent}
end
body.blood_max=getBodySize(caste,0) --TODO normal values
body.blood_count=body.blood_max
body.infection_level=0
unit.status2.body_part_temperature:resize(body_part_count)
for k,v in pairs(unit.status2.body_part_temperature) do
unit.status2.body_part_temperature[k]={new=true,whole=10067,fraction=0}
end
--[[ largely unknown stuff ]]
local stuff=unit.enemy
stuff.body_part_878:resize(body_part_count) -- all = 3
stuff.body_part_888:resize(body_part_count) -- all = 3
stuff.body_part_relsize:resize(body_part_count) -- all =0
stuff.were_race=race_id
stuff.were_caste=caste_id
stuff.normal_race=race_id
stuff.normal_caste=caste_id
stuff.body_part_8a8:resize(body_part_count) -- all = 1
stuff.body_part_base_ins:resize(body_part_count)
stuff.body_part_clothing_ins:resize(body_part_count)
stuff.body_part_8d8:resize(body_part_count)
--TODO add correct sizes. (calculate from age)
local size=caste.body_size_2[#caste.body_size_2-1]
body.size_info.size_cur=size
body.size_info.size_base=size
body.size_info.area_cur=math.pow(size,0.666)
body.size_info.area_base=math.pow(size,0.666)
body.size_info.area_cur=math.pow(size*10000,0.333)
body.size_info.area_base=math.pow(size*10000,0.333)
unit.recuperation.healing_rate:resize(layer_count)
--appearance
local app=unit.appearance
app.body_modifiers:resize(#caste.body_appearance_modifiers) --3
for k,v in pairs(app.body_modifiers) do
app.body_modifiers[k]=genBodyModifier(caste.body_appearance_modifiers[k])
end
app.bp_modifiers:resize(#caste.bp_appearance.modifier_idx) --0
for k,v in pairs(app.bp_modifiers) do
app.bp_modifiers[k]=genBodyModifier(caste.bp_appearance.modifiers[caste.bp_appearance.modifier_idx[k]])
end
--app.unk_4c8:resize(33)--33
app.tissue_style:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_civ_id:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_id:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_type:resize(#caste.bp_appearance.style_part_idx)
app.tissue_length:resize(#caste.bp_appearance.style_part_idx)
app.genes.appearance:resize(#caste.body_appearance_modifiers+#caste.bp_appearance.modifiers) --3
app.genes.colors:resize(#caste.color_modifiers*2) --???
app.colors:resize(#caste.color_modifiers)--3
makeSoul(unit,caste)
--finally set the id
unit.id=df.global.unit_next_id
df.global.unit_next_id=df.global.unit_next_id+1
df.global.world.units.all:insert("#",unit)
df.global.world.units.active:insert("#",unit)
return unit
end
function findRace(name)
for k,v in pairs(df.global.world.raws.creatures.all) do
if v.creature_id==name then
return k
end
end
qerror("Race:"..name.." not found!")
end
function createFigure(trgunit,he,he_group)
local hf=df.historical_figure:new()
hf.id=df.global.hist_figure_next_id
hf.race=trgunit.race
hf.caste=trgunit.caste
hf.profession = trgunit.profession
hf.sex = trgunit.sex
df.global.hist_figure_next_id=df.global.hist_figure_next_id+1
hf.appeared_year = df.global.cur_year
hf.born_year = trgunit.relations.birth_year
hf.born_seconds = trgunit.relations.birth_time
hf.curse_year = trgunit.relations.curse_year
hf.curse_seconds = trgunit.relations.curse_time
hf.birth_year_bias = trgunit.relations.birth_year_bias
hf.birth_time_bias = trgunit.relations.birth_time_bias
hf.old_year = trgunit.relations.old_year
hf.old_seconds = trgunit.relations.old_time
hf.died_year = -1
hf.died_seconds = -1
hf.name:assign(trgunit.name)
hf.civ_id = trgunit.civ_id
hf.population_id = trgunit.population_id
hf.breed_id = -1
hf.unit_id = trgunit.id
df.global.world.history.figures:insert("#",hf)
hf.info = df.historical_figure_info:new()
hf.info.unk_14 = df.historical_figure_info.T_unk_14:new() -- hf state?
--unk_14.region_id = -1; unk_14.beast_id = -1; unk_14.unk_14 = 0
hf.info.unk_14.unk_18 = -1; hf.info.unk_14.unk_1c = -1
-- set values that seem related to state and do event
--change_state(hf, dfg.ui.site_id, region_pos)
--lets skip skills for now
--local skills = df.historical_figure_info.T_skills:new() -- skills snap shot
-- ...
hf.info.skills = {new=true}
he.histfig_ids:insert('#', hf.id)
he.hist_figures:insert('#', hf)
if he_group then
he_group.histfig_ids:insert('#', hf.id)
he_group.hist_figures:insert('#', hf)
hf.entity_links:insert("#",{new=df.histfig_entity_link_memberst,entity_id=he_group.id,link_strength=100})
end
trgunit.flags1.important_historical_figure = true
trgunit.flags2.important_historical_figure = true
trgunit.hist_figure_id = hf.id
trgunit.hist_figure_id2 = hf.id
hf.entity_links:insert("#",{new=df.histfig_entity_link_memberst,entity_id=trgunit.civ_id,link_strength=100})
--add entity event
local hf_event_id=df.global.hist_event_next_id
df.global.hist_event_next_id=df.global.hist_event_next_id+1
df.global.world.history.events:insert("#",{new=df.history_event_add_hf_entity_linkst,year=trgunit.relations.birth_year,
seconds=trgunit.relations.birth_time,id=hf_event_id,civ=hf.civ_id,histfig=hf.id,link_type=0})
return hf
end
function allocateNewChunk(hist_entity)
hist_entity.save_file_id=df.global.unit_chunk_next_id
df.global.unit_chunk_next_id=df.global.unit_chunk_next_id+1
hist_entity.next_member_idx=0
print("allocating chunk:",hist_entity.save_file_id)
end
function PlaceAwakenedStone(caste,position,is_friendly) -- Stripped-down version of the original PlaceUnit
local pos=position or copyall(df.global.cursor)
print("at " .. pos.x .. ", " .. pos.y .. ", " .. pos.z .. ".")
if pos.x == "cursor" then -- x of "cursor" allows DFHack to place at the cursor if the script is run interactively, still needs a y and z even though they are ignored
print("Placing awakened stone at DF cursor")
pos=copyall(df.global.cursor)
end
if pos.x==-30000 then
qerror("Point your pointy thing somewhere")
end
local race=findRace("AWAKENED_STONE") -- Hardcoded race
local u=CreateUnit(race,tonumber(caste) or 0)
u.pos:assign(pos)
u.name.has_name=false -- These creatures do not inherently have names
local civ_id
if not is_friendly then
civ_id = -1
end
local group_id
if df.global.gamemode==df.game_mode.ADVENTURE then
u.civ_id=civ_id or df.global.world.units.active[0].civ_id
group_id=-1
else
u.civ_id=civ_id or df.global.ui.civ_id
end
if civ_id==-1 then
group_id=group_id or -1
else
group_id=group_id or df.global.ui.group_id
end
local desig,ocupan=dfhack.maps.getTileFlags(pos)
if ocupan.unit then
ocupan.unit_grounded=true
u.flags1.on_ground=true
else
ocupan.unit=true
end
-- Removed nemesis code
end
function Announce(caste_id,unit_id,is_friendly) -- Create an in-game announcement that a stone has awakened
-- Rubble's Announcement command, customized by Dirst for The Earth Strikes Back mod
--[[
Rubble Announcement DFHack Command
Copyright 2013-2014 Milo Christiansen
This software is provided 'as-is', without any express or implied warranty. In
no event will the authors be held liable for any damages arising from the use of
this software.
Permission is granted to anyone to use this software for any purpose, including
commercial applications, and to alter it and redistribute it freely, subject to
the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim
that you wrote the original software. If you use this software in a product, an
acknowledgment in the product documentation would be appreciated but is not
required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
]]
if not dfhack.isMapLoaded() then
dfhack.printerr('Error: Map is not loaded.')
return
end
local caste_name = string.upper(string.sub(caste_id,1,1))..string.lower(string.sub(caste_id,2,-1))
if caste_name == "Rocksalt" then -- Special handling for two-word stone
caste_name = "Rock Salt"
end
local unit_name
if not unit_id then
unit_name = "Something"
else
--unit_name = dfhack.TranslateName(dfhack.units.getVisibleName(unit_id))
-- The above throws an error.
unit_name = "Someone"
end
if is_friendly then
text = unit_name.." has awakened a creature of Living "..caste_name
color_id = "COLOR_WHITE"
else
text = unit_name.." has incurred the wrath of an Awakened "..caste_name
color_id = "COLOR_RED"
end
local color = _G[color_id]
dfhack.gui.showAnnouncement(text, color)
local log = io.open('gamelog.txt', 'a')
log:write(text.."\n")
log:close()
print(text)
end
-- Conversion table of CASTE_IDs to caste indices
local caste_table = {
["ANDESITE"] = 0,
["BASALT"] = 1,
["CHALK"] = 2,
["CHERT"] = 3,
["CLAYSTONE"] = 4,
["CONGLOMERATE"] = 5,
["DACITE"] = 6,
["DIORITE"] = 7,
["DOLOMITE"] = 8,
["GABBRO"] = 9,
["GNEISS"] = 10,
["GRANITE"] = 11,
["LIMESTONE"] = 12,
["MARBLE"] = 13,
["MUDSTONE"] = 14,
["PHYLLITE"] = 15,
["QUARTZITE"] = 16,
["RHYOLITE"] = 17,
["ROCKSALT"] = 18,
["SANDSTONE"] = 19,
["SCHIST"] = 20,
["SHALE"] = 21,
["SILTSTONE"] = 22,
["SLATE"] = 23
}
local argPos
if args.location then
argPos={}
argPos.x=args.location[1]
argPos.y=args.location[2]
argPos.z=args.location[3]
end
Announce(args.caste,args.miner,args.friendly)
PlaceAwakenedStone(caste_table[args.caste],argPos,args.friendly)