-
Notifications
You must be signed in to change notification settings - Fork 226
Expand file tree
/
Copy pathcreate-item.lua
More file actions
453 lines (434 loc) · 19.2 KB
/
create-item.lua
File metadata and controls
453 lines (434 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
-- creates an item of a given type and material
--author expwnent
--@module=true
local argparse = require('argparse')
local utils = require('utils')
local no_quality_item_types = utils.invert{
'BAR',
'SMALLGEM',
'BLOCKS',
'ROUGH',
'BOULDER',
'WOOD',
'CORPSE',
'CORPSEPIECE',
'REMAINS',
'MEAT',
'FISH',
'FISH_RAW',
'VERMIN',
'PET',
'SEEDS',
'PLANT',
'SKIN_TANNED',
'PLANT_GROWTH',
'THREAD',
'DRINK',
'POWDER_MISC',
'CHEESE',
'FOOD',
'LIQUID_MISC',
'COIN',
'GLOB',
'ROCK',
'EGG',
'BRANCH',
}
local typesThatUseCreaturesExceptCorpses = utils.invert {
'REMAINS',
'FISH',
'FISH_RAW',
'VERMIN',
'PET',
'EGG',
}
local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE', 'NAIL', 'HORN', 'HOOF', 'CHITIN',
'SHELL', 'IVORY', 'SCALE'}
local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS'}
local LIQUID_PIECES = utils.invert{'BLOOD', 'PUS', 'VENOM', 'SWEAT', 'TEARS', 'SPIT', 'MILK'}
local function moveToContainer(item, creator, container_type)
local containerMat = dfhack.matinfo.find('PLANT_MAT:NETHER_CAP:WOOD')
if not containerMat then
for i,n in ipairs(df.global.world.raws.plants.all) do
if n.flags.TREE then
containerMat = dfhack.matinfo.find('PLANT_MAT:' .. n.id .. ':WOOD')
end
if containerMat then break end
end
end
local bucketType = dfhack.items.findType(container_type .. ':NONE')
local buckets = dfhack.items.createItem(creator, bucketType, -1, containerMat.type, containerMat.index)
local bucket = buckets[1]
dfhack.items.moveToContainer(item, bucket)
return bucket
end
-- this part was written by four rabbits in a trenchcoat (ppaawwll)
local function createCorpsePiece(creator, bodypart, partlayer, creatureID, casteID, generic)
-- (partlayer is also used to determine the material if we're spawning a "generic" body part (i'm just lazy lol))
creatureID = tonumber(creatureID)
-- get the actual raws of the target creature
local creatorRaceRaw = df.creature_raw.find(creatureID)
local wholePart = false
casteID = tonumber(casteID)
bodypart = tonumber(bodypart)
partlayer = tonumber(partlayer)
-- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part.
-- we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway.
-- if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and
-- there there's lines that reference the part layer
if partlayer == -1 and not generic then
partlayer = 0
wholePart = true
end
-- get body info for easy reference
local creatorBody = creatorRaceRaw.caste[casteID].body_info
local layerName
local layerMat = 'BONE'
local tissueID
local liquid = false
-- in the hackWish function, the bodypart variable is initialized to -1, which isn't changed if the spawned item is a corpse
local isCorpse = bodypart == -1 and not generic
if not generic and not isCorpse then -- if we have a specified body part and layer, figure all the stuff out about that
-- store the tissue id of the specific layer we selected
tissueID = tonumber(creatorBody.body_parts[bodypart].layers[partlayer].tissue_id)
local mats = {}
-- get the material name from the material itself
for i in string.gmatch(dfhack.matinfo.getToken(creatorRaceRaw.tissue[tissueID].mat_type, creatureID), '([^:]+)') do
table.insert(mats, i)
end
layerMat = mats[3]
layerName = creatorBody.body_parts[bodypart].layers[partlayer].layer_name
elseif not isCorpse then -- otherwise, figure out the mat name from the dual-use partlayer argument
layerMat = creatorRaceRaw.material[partlayer].id
layerName = layerMat
end
-- default is MEAT, so if anything else fails to change it to something else, we know that the body layer is a meat item
local item_type = 'MEAT'
-- get race name and layer name, both for finding the item material, and the latter for determining the corpsepiece flags to set
local raceName = string.upper(creatorRaceRaw.creature_id)
-- every key is a valid non-hair corpsepiece, so if we try to index a key that's not on the table, we don't have a non-hair corpsepiece
-- we do the same as above but with hair
-- if the layer is fat, spawn a glob of fat and DON'T check for other layer types
if layerName == 'FAT' then
item_type = 'GLOB'
elseif CORPSE_PIECES[layerName] or HAIR_PIECES[layerName] then -- check if hair
item_type = 'CORPSEPIECE'
elseif LIQUID_PIECES[layerName] then
item_type = 'LIQUID_MISC'
liquid = true
end
if isCorpse then
item_type = 'CORPSE'
generic = true
end
local itemType = dfhack.items.findType(item_type .. ':NONE')
local itemSubtype = dfhack.items.findSubtype(item_type .. ':NONE')
local material = 'CREATURE_MAT:' .. raceName .. ':' .. layerMat
local materialInfo = dfhack.matinfo.find(material)
local items = dfhack.items.createItem(creator, itemType, itemSubtype, materialInfo['type'], materialInfo.index)
local item = items[1]
-- if the item type is a corpsepiece, we know we have one, and then go on to set the appropriate flags
if item_type == 'CORPSEPIECE' then
if layerName == 'BONE' then -- check if bones
item.corpse_flags.bone = true
item.material_amount.Bone = 1
elseif layerName == 'SKIN' then -- check if skin/leather
item.corpse_flags.leather = true
item.material_amount.Leather = 1
-- elseif layerName == "CARTILAGE" then -- check if cartilage (NO SPECIAL FLAGS)
elseif layerName == 'HAIR' then -- check if hair (simplified from before)
item.corpse_flags.hair_wool = true
item.material_amount.HairWool = 1
if materialInfo.material.flags.YARN then
item.corpse_flags.yarn = true
item.material_amount.Yarn = 1
end
elseif layerName == 'TOOTH' or layerName == 'IVORY' then -- check if tooth
item.corpse_flags.tooth = true
item.material_amount.Tooth = 1
elseif layerName == 'NERVE' then -- check if nervous tissue
item.corpse_flags.rottable = true
item.corpse_flags.use_blood_color = true
-- elseif layerName == "NAIL" then -- check if nail (NO SPECIAL FLAGS)
elseif layerName == 'HORN' or layerName == 'HOOF' then -- check if nail
item.corpse_flags.horn = true
item.material_amount.Horn = 1
elseif layerName == 'SHELL' then
item.corpse_flags.shell = true
item.material_amount.Shell = 1
end
-- checking for skull
if not generic and not isCorpse and creatorBody.body_parts[bodypart].token == 'SKULL' then
item.corpse_flags.skull = true
end
end
local matType
-- figure out which material type the material is (probably a better way of doing this but whatever)
for i in pairs(creatorRaceRaw.tissue) do
if creatorRaceRaw.tissue[i].tissue_material_str[1] == layerMat then
matType = creatorRaceRaw.tissue[i].mat_type
end
end
if item_type == 'CORPSEPIECE' or item_type == 'CORPSE' then
--referencing the source unit for, material, relation purposes???
item.race = creatureID
item.normal_race = creatureID
item.normal_caste = casteID
-- usually the first two castes are for the creature's sex, so we set the item's sex to
-- the caste if both the creature has one and it's a valid sex id (0 or 1)
if casteID < 2 and #(creatorRaceRaw.caste) > 1 then
item.sex = casteID
else
item.sex = -1 -- it
end
-- on a dwarf tissue index 3 (bone) is 22, but this is not always the case for all creatures, so we get the mat_type of index 3 instead
-- here we also set the actual referenced creature material of the corpsepiece
item.largest_tissue.mat_type = matType
item.largest_tissue.mat_index = creatureID
item.largest_unrottable_tissue.mat_type = matType
item.largest_unrottable_tissue.mat_index = creatureID
-- skin (and presumably other parts) use body part modifiers for size or amount
for i = 0,200 do -- fuck it this works
-- inserts
item.body.bp_modifiers:insert('#', 1) --jus,t, set a lot of it to one who cares
end
-- copy target creature's relsizes to the item's's body relsizes thing
for i,n in pairs(creatorBody.body_parts) do
-- inserts
item.body.body_part_relsize:insert('#', n.relsize)
--copy the status of the creator's first part to every body_part_status of the desired creature
item.body.components.body_part_status:insert(i, creator.body.components.body_part_status[0])
item.body.components.body_part_status[i].missing = true
end
for i in pairs(creatorBody.layer_part) do
-- inserts
-- copy the layer status of the creator's first layer to every layer_status of the desired creature
item.body.components.layer_status:insert(i, creator.body.components.layer_status[0])
item.body.components.layer_status[i].gone = true
end
if item_type == 'CORPSE' then
item.corpse_flags.unbutchered = true
end
if not generic then
-- keeps the body part that the user selected to spawn the item from
item.body.components.body_part_status[bodypart].missing = false
-- restores the selected layer of the selected body part
item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[partlayer].layer_id].gone = false
elseif generic then
for i in pairs(creatorBody.body_parts) do
for n in pairs(creatorBody.body_parts[i].layers) do
if item_type == 'CORPSE' then
item.body.components.body_part_status[i].missing = false
item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false
else
-- search through the target creature's body parts and bring back every one which has the desired material
if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and
creatorBody.body_parts[i].token ~= 'SKULL' and not creatorBody.body_parts[i].flags.SMALL then
item.body.components.body_part_status[i].missing = false
item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false
-- save the index of the bone layer to a variable
end
end
end
end
end
-- brings back every tissue layer associated with a body part if we're spawning the entire thing
if wholePart then
for i in pairs(creatorBody.body_parts[bodypart].layers) do
item.body.components.layer_status[creatorBody.body_parts[bodypart].layers[i].layer_id].gone = false
item.corpse_flags.use_blood_color = true
item.corpse_flags.unbutchered = true
end
end
-- DO THIS LAST or else the game crashes for some reason
item.caste = casteID
end
if liquid then
return moveToContainer(item, creator, 'BUCKET')
end
return item
end
local function createItem(mat, itemType, quality, creator, description, amount)
-- The "reaction-gloves" tweak can cause this to create multiple gloves
local items = dfhack.items.createItem(creator, itemType[1], itemType[2], mat[1], mat[2])
assert(#items > 0, ('failed to create item: item_type: %s, item_subtype: %s, mat_type: %s, mat_index: %s, unit: %s'):format(
itemType[1], itemType[2], mat[1], mat[2], creator and creator.id or 'nil'))
local item = items[1]
local mat_token = dfhack.matinfo.decode(item):getToken()
quality = math.max(0, math.min(5, quality - 1))
-- If we got multiple gloves, set quality on all of them
for _, it in pairs(items) do
it:setQuality(quality)
end
local item_type = df.item_type[itemType[1]]
if item_type == 'SLAB' then
item.description = description
elseif item_type == 'GLOVES' then
--create matching gloves, if necessary
if item:getGloveHandedness() == 0 then
for _, it in pairs(items) do
it:setGloveHandedness(1)
end
local items2 = dfhack.items.createItem(creator, itemType[1], itemType[2], mat[1], mat[2])
assert(#items2 > 0, 'failed to create second gloves')
for _, it2 in pairs(items2) do
it2:setQuality(quality)
it2:setGloveHandedness(2)
table.insert(items, it2)
end
end
elseif item_type == 'SHOES' then
--create matching shoes
local items2 = dfhack.items.createItem(creator, itemType[1], itemType[2], mat[1], mat[2])
assert(#items2 > 0, 'failed to create second shoes')
for _, it2 in pairs(items2) do
it2:setQuality(quality)
table.insert(items, it2)
end
end
if tonumber(amount) > 1 then
item:setStackSize(amount)
end
if item_type == 'DRINK' then
return moveToContainer(item, creator, 'BARREL')
elseif mat_token == 'WATER' or mat_token == 'LYE' then
return moveToContainer(item, creator, 'BUCKET')
end
return items
end
local function get_default_unit()
local citizens = dfhack.units.getCitizens(true)
if citizens and citizens[1] then
return citizens[1]
end
local adventurer = dfhack.world.getAdventurer()
if adventurer then
return adventurer
end
qerror('Could not choose a creator unit. Please select one in the UI')
end
-- returns the list of created items, or nil on error
function hackWish(accessors, opts)
local unit = accessors.get_unit(opts) or get_default_unit()
local qualityok, quality = false, df.item_quality.Ordinary
local itemok, itemtype, itemsubtype = accessors.get_item_type()
if not itemok then return end
local matok, mattype, matindex, casteId, bodypart, partlayerID, corpsepieceGeneric = accessors.get_mat(itemtype, opts)
if not matok then return end
if not no_quality_item_types[df.item_type[itemtype]] then
qualityok, quality = accessors.get_quality()
if not qualityok then return end
end
local description
if df.item_type[itemtype] == 'SLAB' then
local descriptionok
descriptionok, description = accessors.get_description()
if not descriptionok then return end
end
local count = opts.count
if not count then
repeat
local amountok, amount = accessors.get_count()
if not amountok then return end
count = tonumber(amount)
until count
end
if not mattype or not itemtype then return end
if df.item_type.attrs[itemtype].is_stackable then
local mat = typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and {matindex, casteId} or {mattype, matindex}
return createItem(mat, {itemtype, itemsubtype}, quality, unit, description, count)
end
local items = {}
for _ = 1,count do
if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then
table.insert(items, createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric))
else
local mat = typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and {matindex, casteId} or {mattype, matindex}
for _,item in ipairs(createItem(mat, {itemtype, itemsubtype}, quality, unit, description, 1)) do
table.insert(items, item)
end
end
end
if opts.pos then
for _,item in ipairs(items) do
dfhack.items.moveToGround(item, opts.pos)
end
end
return items
end
if dfhack_flags.module then
return
end
if not dfhack.isMapLoaded() then
qerror('modtools/create-item needs a loaded map to work')
end
local opts = {}
local positionals = argparse.processArgsGetopt({...}, {
{'h', 'help', handler = function() opts.help = true end},
{'u', 'unit', hasArg = true, handler = function(arg) opts.unit = arg end},
{'i', 'item', hasArg = true, handler = function(arg) opts.item = arg end},
{'m', 'material', hasArg = true, handler = function(arg) opts.mat = arg end},
{'t', 'caste', hasArg = true, handler = function(arg) opts.caste = arg end},
{'q', 'quality', hasArg = true, handler = function(arg) opts.quality = arg end},
{'d', 'description', hasArg = true, handler = function(arg) opts.description = arg end},
{
'c',
'count',
hasArg = true,
handler = function(arg) opts.count = argparse.nonnegativeInt(arg, 'count') end,
},
{'p', 'pos', hasArg = true, handler = function(arg) opts.pos = argparse.coords(arg) end},
})
if positionals[1] == 'help' then opts.help = true end
if opts.help then
print(dfhack.script_help())
return
end
if opts.unit == '\\LAST' then
opts.unit = tostring(df.global.unit_next_id - 1)
end
local function get_caste(race_id, caste)
if not caste then return 0 end
if tonumber(caste) then return tonumber(caste) end
caste = caste:lower()
for i, c in ipairs(df.creature_raw.find(race_id).caste) do
if caste == tostring(c.caste_id):lower() then return i end
end
return 0
end
local accessors = {
get_unit = function()
return tonumber(opts.unit) and df.unit.find(tonumber(opts.unit)) or nil
end,
get_item_type = function()
local item_type = dfhack.items.findType(opts.item)
if item_type == -1 then
error('invalid item: ' .. tostring(opts.item))
end
return true, item_type, dfhack.items.findSubtype(opts.item)
end,
get_mat = function(itype)
local mat_info = dfhack.matinfo.find(opts.mat)
if not mat_info then
error('invalid material: ' .. tostring(opts.mat))
end
local caste = get_caste(mat_info.index, opts.caste)
if df.item_type[itype] ~= 'CORPSEPIECE' then
return true, mat_info['type'], mat_info.index, caste, -1
end
-- support only generic corpse pieces for now. this can be extended to support
-- everything that gui/create-item can produce, but we need more commandline
-- arguments to provide the info
return true, -1, mat_info.index, caste, 1, 0, true
end,
get_quality = function()
return true, (tonumber(opts.quality) or df.item_quality.Ordinary) + 1
end,
get_description = function()
return true, opts.description or error('description not specified')
end,
get_count = function()
return true, opts.count or 1
end,
}
hackWish(accessors, {pos=opts.pos})