forked from minetest-mods/simplecrafting_lib
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutil.lua
More file actions
443 lines (406 loc) · 15.5 KB
/
Copy pathutil.lua
File metadata and controls
443 lines (406 loc) · 15.5 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
--------------------------------------------------------------------------------------------------------------------
-- Local functions
-- Turns an item list (as returned by inv:get_list) into a form more easily used by crafting functions
local function itemlist_to_countlist(itemlist)
local count_list = {}
for _, stack in ipairs(itemlist) do
if not stack:is_empty() then
local name = stack:get_name()
count_list[name] = (count_list[name] or 0) + stack:get_count()
-- alias its groups to the item
if minetest.registered_items[name] then
for group, _ in pairs(minetest.registered_items[name].groups or {}) do
if not count_list[group] then count_list[group] = {} end
count_list[group][name] = true -- using names as keys makes this act as a set
end
end
end
end
return count_list
end
simplecrafting_lib.itemstack_to_countlist = function(stack)
if not stack then
return {}
end
if type(stack) == "string" then
stack = ItemStack(stack)
end
if stack and stack:is_empty() then
return {}
end
return { [stack:get_name()] = stack:get_count() }
end
simplecrafting_lib.itemstacks_to_countlist = function(stacklist)
local out_list = {}
for i, stack in ipairs(stacklist) do
out_list = simplecrafting_lib.count_list_add(
out_list, simplecrafting_lib.itemstack_to_countlist(stack)
)
end
return out_list
end
-- splits a string into an array of substrings based on a delimiter
local function split(str, delimiter)
local result = {}
for match in (str..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match)
end
return result
end
-- I apologise for this function.
-- From the items in groupset, checks input_list to find the item
-- with the highest count and adds it to required_input
local function get_highest_count_item_for_group(groupset, input_list, required_input, count)
local highest_item_name
local highest_item_count = 0
for group_item, _ in pairs(groupset) do
if input_list[group_item] > highest_item_count then
highest_item_count = input_list[group_item]
highest_item_name = group_item
end
end
if highest_item_count == 0 then
return false
end
required_input[highest_item_name] = (required_input[highest_item_name] or 0) + count
return true
end
-- returns the number of times the recipe can be crafted from the given input_list,
-- and also a copy of the recipe with groups substituted for the most common item
-- in the input_list that matches them
local function get_craft_count(input_list, recipe)
-- Recipe without groups (most common node in group instead)
local work_recipe = simplecrafting_lib.deep_copy(recipe)
if recipe.output then
work_recipe.output = ItemStack(recipe.output)
end
work_recipe.input = {}
local required_input = work_recipe.input
for item, count in pairs(recipe.input) do
if string.find(item, ",") then -- special syntax used to require an item that belongs to multiple groups
local groups = split(item, ",")
-- This unfortunate block of code builds up an intersection
-- of the items belonging to each group in the list of groups
-- that this recipe item slot requires.
local multigroup_itemset
for _, group in pairs(groups) do
if not input_list[group] then
return 0
end
if not multigroup_itemset then
multigroup_itemset = {}
for multigroup_item, _ in pairs(input_list[group]) do
multigroup_itemset[multigroup_item] = true
end
else
local intersect = {}
for multigroup_item, _ in pairs(input_list[group]) do
if multigroup_itemset[multigroup_item] then
intersect[multigroup_item] = true
end
end
multigroup_itemset = intersect
end
end
if not get_highest_count_item_for_group(multigroup_itemset, input_list, required_input, count) then
return 0
end
else
if not input_list[item] then
return 0
end
-- Groups are a string alias to most common member item
if type(input_list[item]) == "table" then
-- find group item with highest count
if not get_highest_count_item_for_group(input_list[item], input_list, required_input, count) then
return 0
end
else
required_input[item] = (required_input[item] or 0) + count
end
end
end
local number = math.huge
for ingredient, count in pairs(required_input) do
local max = input_list[ingredient] / count
if max < 1 then
return 0
elseif max < number then
number = max
end
end
-- Return number of possible crafts as integer
return math.floor(number), work_recipe
end
-- Used for alphabetizing an array of itemstacks by description
local function compare_stacks_by_desc(stack1, stack2)
local item1 = stack1:get_name()
local item2 = stack2:get_name()
local def1 = minetest.registered_items[item1]
local def2 = minetest.registered_items[item2]
return def1.description < def2.description
end
--------------------------------------------------------------------------------------------------------------------
-- Public API
-- Note that a circular table reference will result in a crash, TODO: guard against that.
-- Unlikely to be needed, though - it'd take a lot of work for users to get into this bit of trouble.
simplecrafting_lib.deep_copy = function(recipe_in)
local recipe_out = {}
for index, value in pairs(recipe_in) do
if type(value) == "table" then
recipe_out[index] = simplecrafting_lib.deep_copy(value)
elseif type(value) == "userdata" and index == "output" then
recipe_out[index] = ItemStack(value)
else
recipe_out[index] = value
end
end
return recipe_out
end
simplecrafting_lib.get_crafting_info = function(craft_type)
-- ensure the destination tables exist
simplecrafting_lib.type[craft_type] = simplecrafting_lib.type[craft_type] or {}
simplecrafting_lib.type[craft_type].recipes = simplecrafting_lib.type[craft_type].recipes or {}
simplecrafting_lib.type[craft_type].recipes_by_out = simplecrafting_lib.type[craft_type].recipes_by_out or {}
simplecrafting_lib.type[craft_type].recipes_by_in = simplecrafting_lib.type[craft_type].recipes_by_in or {}
return simplecrafting_lib.type[craft_type]
end
simplecrafting_lib.set_description = function(craft_type, desc)
simplecrafting_lib.get_crafting_info(craft_type).description = desc
end
simplecrafting_lib.get_description = function(craft_type)
return simplecrafting_lib.get_crafting_info(craft_type).description
end
simplecrafting_lib.set_disintermediation_cycles = function(craft_type, cycle_count)
simplecrafting_lib.get_crafting_info(craft_type).disintermediation_cycles = tonumber(cycle_count)
end
-- returns a fuel definition for the item if it is fuel, nil otherwise
-- note: will always return the last-registered definition for a particular item
-- or group.
simplecrafting_lib.is_fuel = function(craft_type, item)
local fuels = simplecrafting_lib.get_crafting_info(craft_type).recipes_by_in
-- First check if the item has been explicitly registered as fuel
if fuels[item] then
return fuels[item][#fuels[item]]
end
-- Failing that, check its groups.
local def = minetest.registered_items[item]
if def and def.groups then
local max = -1
local fuel_group
for group, _ in pairs(def.groups) do
if fuels[group] then
local last_fuel_def = fuels[group][#fuels[group]]
local last_fuel_burntime
if last_fuel_def.output and last_fuel_def.output:get_name() == "simplecrafting_lib:heat" then
last_fuel_burntime = last_fuel_def.output:get_count()
else
last_fuel_burntime = 0
end
if last_fuel_burntime > max then
fuel_group = last_fuel_def -- track whichever is the longest-burning group
max = last_fuel_burntime
end
end
end
if fuel_group then
return fuel_group
end
end
return nil
end
-- Returns a list of all fuel recipes whose ingredients can be satisfied by the item_list
simplecrafting_lib.get_fuels = function(craft_type, item_list)
local count_list = itemlist_to_countlist(item_list)
local burnable = {}
for item, count in pairs(count_list) do
local recipe = simplecrafting_lib.is_fuel(craft_type, item)
if recipe then
table.insert(burnable, recipe)
end
end
return burnable
end
-- Returns a list of all recipes whose ingredients can be satisfied by the item_list
simplecrafting_lib.get_craftable_recipes = function(craft_type, item_list)
local count_list = itemlist_to_countlist(item_list)
local craftable = {}
local recipes = simplecrafting_lib.type[craft_type].recipes
for i = 1, #recipes do
local number, recipe = get_craft_count(count_list, recipes[i])
if number > 0 then
table.insert(craftable, recipe)
end
end
return craftable
end
-- Returns a list of all the possible item stacks that could be crafted from the provided item list
-- if max_craftable is true the returned stacks will have as many items in them as possible to craft,
-- if max_craftable is false or nil the returned stacks will have only the minimum output
-- if alphabetize is true then the items will be sorted alphabetically by description
-- if alphabetize is false or nil the items will be left in default order
simplecrafting_lib.get_craftable_items = function(craft_type, item_list, max_craftable, alphabetize)
local count_list = itemlist_to_countlist(item_list)
local craftable_count_list = {}
local craftable_stacks = {}
local chosen_recipe = {}
local recipes = simplecrafting_lib.type[craft_type].recipes
for i = 1, #recipes do
local number, recipe = get_craft_count(count_list, recipes[i])
if number > 0 then
if not max_craftable then number = 1 end
local output_name = recipe.output:get_name()
local output_count = recipe.output:get_count()
if craftable_count_list[output_name] and output_count*number > craftable_count_list[output_name] then
craftable_count_list[output_name] = output_count*number
chosen_recipe[output_name] = recipe
elseif not craftable_count_list[output_name] and output_count*number > 0 then
craftable_count_list[output_name] = output_count*number
chosen_recipe[output_name] = recipe
end
end
end
-- Limit stacks to stack limit
for item, count in pairs(craftable_count_list) do
local stack = ItemStack(item)
local max = stack:get_stack_max()
if count > max then
count = max - max % chosen_recipe[item].output:get_count()
end
stack:set_count(count)
simplecrafting_lib.execute_pre_craft(craft_type, chosen_recipe[item], stack, item_list)
table.insert(craftable_stacks, stack)
end
if alphabetize then
table.sort(craftable_stacks, compare_stacks_by_desc)
end
return craftable_stacks
end
-- Returns true if the item name is an input for at least one
-- recipe belonging to the given craft type
simplecrafting_lib.is_possible_input = function(craft_type, item_name)
local info = simplecrafting_lib.get_crafting_info(craft_type)
if info.recipes_by_in[item_name] then
return true -- that was easy.
end
-- Now for the group checks. :(
local item_def = minetest.registered_items[item_name]
if not item_def then return false end -- undefined item
local groups = item_def.groups or {}
for _, recipe in pairs(info.recipes) do
--the multi-group flower/dye type inputs are complicated to check for
for input_item, _ in pairs(recipe.input) do
if not string.match(input_item, ":") then
local input_grouplist = split(input_item, ",") -- most of the time we'll have a list of one.
local input_matches = true
for _, needed_group in pairs(input_grouplist) do -- for each group in that list of input groups
if not groups[needed_group] then -- if the test item doesn't have that group
input_matches = false -- it's not a match.
break
end
end
if input_matches then return true end
end
end
end
return false
end
-- Returns true if the item is a possible output for at least
-- one recipe belonging to the given craft type
simplecrafting_lib.is_possible_output = function(craft_type, item_name)
return simplecrafting_lib.type[craft_type].recipes_by_out[item_name] ~= nil
end
simplecrafting_lib.random_returns_to_countlist = function(random_returns)
local out_stacks = {}
local last_rarity = 0
for i, spec in ipairs(random_returns) do
local spec_rarity = spec.rarity
if math.random(spec_rarity) == spec_rarity then
if spec_rarity > last_rarity then
out_stacks = spec.items
last_rarity = spec_rarity
end
end
end
return simplecrafting_lib.itemstacks_to_countlist(out_stacks)
end
-- adds two count lists together, returns a new count list with the sum of the parameters' contents
simplecrafting_lib.count_list_add = function(list1, list2)
local out_list = {}
for item, count in pairs(list1) do
out_list[item] = count
end
if list2 == nil then return out_list end
for item, count in pairs(list2) do
if type(count) == "table" then
-- item is actually a group name, it has a set of items associated with it.
-- Perform a union with existing set.
out_list[item] = out_list[item] or {}
for group_item, _ in pairs(count) do
out_list[item][group_item] = true
end
else
out_list[item] = (out_list[item] or 0) + count
end
end
return out_list
end
-- Returns a recipe with the inputs and outputs multiplied to match the requested
-- quantity of ouput items in the crafted stack. Note that the output could
-- actually be larger than crafted_stack if an exactly matching recipe can't be found.
-- returns nil if crafting is impossible with the given source inventory
simplecrafting_lib.get_crafting_result = function(craft_type, input_list, request_stack)
local input_count = itemlist_to_countlist(input_list)
local request_name = request_stack:get_name()
local request_count = request_stack:get_count()
local recipes = simplecrafting_lib.type[craft_type].recipes_by_out[request_name]
local smallest_remainder = math.huge
local smallest_remainder_output_count = 0
local smallest_remainder_recipe = nil
if not recipes then
return
end
for i = 1, #recipes do
local number, recipe = get_craft_count(input_count, recipes[i])
if number > 0 then
local output_count = recipe.output:get_count()
if (request_count % output_count) <= smallest_remainder and output_count > smallest_remainder_output_count then
smallest_remainder = request_count % output_count
smallest_remainder_output_count = output_count
smallest_remainder_recipe = recipe
end
end
end
if smallest_remainder_recipe then
local multiple = math.ceil(request_count / smallest_remainder_recipe.output:get_count())
for input_item, quantity in pairs(smallest_remainder_recipe.input) do
smallest_remainder_recipe.input[input_item] = multiple * quantity
end
smallest_remainder_recipe.output:set_count(smallest_remainder_recipe.output:get_count() * multiple)
if smallest_remainder_recipe.returns then
for returned_item, quantity in pairs(smallest_remainder_recipe.returns) do
smallest_remainder_recipe.returns[returned_item] = multiple * quantity
end
end
return smallest_remainder_recipe
end
return nil
end
local pre_craft = {}
local post_craft = {}
simplecrafting_lib.register_pre_craft = function(callback)
table.insert(pre_craft, callback)
end
simplecrafting_lib.register_post_craft = function(callback)
table.insert(post_craft, callback)
end
simplecrafting_lib.execute_pre_craft = function(craft_type, recipe, output_stack, source_item_list)
for k, callback in ipairs(pre_craft) do
callback(craft_type, recipe, output_stack, source_item_list)
end
end
simplecrafting_lib.execute_post_craft = function(craft_type, recipe, output_stack, source_inv, source_listname, destination_inv, destination_listname)
for k, callback in ipairs(post_craft) do
callback(craft_type, recipe, output_stack, source_inv, source_listname, destination_inv, destination_listname)
end
end