forked from minetest-mods/simplecrafting_lib
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpostprocessing.lua
More file actions
357 lines (319 loc) · 12.3 KB
/
Copy pathpostprocessing.lua
File metadata and controls
357 lines (319 loc) · 12.3 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
local registered_callbacks = {}
simplecrafting_lib.register_postprocessing_callback = function(callback)
table.insert(registered_callbacks, callback)
end
----------------------------------------------------------------------------------------
-- Run-once code, post server initialization, that purges all uncraftable recipes from the
-- crafting system data.
-- 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
local group_examples = {}
local function input_exists(input_item)
if minetest.registered_items[input_item] then
return true
end
if group_examples[input_item] then
return true
end
if not string.match(input_item, ",") then
return false
end
local target_groups = split(input_item, ",")
for item, def in pairs(minetest.registered_items) do
local overall_found = true
for _, target_group in pairs(target_groups) do
local one_group_found = false
for group, _ in pairs(def.groups) do
if group == target_group then
one_group_found = true
break
end
end
if not one_group_found then
overall_found = false
break
end
end
if overall_found then
group_examples[input_item] = item
return true
end
end
return false
end
local function validate_inputs_and_outputs(recipe)
for item, count in pairs(recipe.input) do
if not input_exists(item) then
return item
end
end
if recipe.output then
local output_name = ItemStack(recipe.output):get_name()
if not minetest.registered_items[output_name] then
return output_name
end
end
if recipe.returns then
for item, count in pairs(recipe.returns) do
if not minetest.registered_items[item] then
return item
end
end
end
return true
end
local log_removals = minetest.settings:get_bool("simplecrafting_lib_log_invalid_recipe_removal")
local recipe_to_string = function(recipe)
local out = "{\n"
for k, v in pairs(recipe) do
if k == "output" then
out = out .. "output = \"" .. ItemStack(v):to_string() .."\",\n"
else
out = out .. k .. " = " .. dump(v) .. ",\n"
end
end
out = out .. "}"
return out
end
local merge_item_count = function(list, alias, item_to_merge)
local existing_count = list[alias] or 0 -- in case a recipe mixes various items that all alias to the same item
list[alias] = item_to_merge + existing_count
end
local merge_sublists = function(list, alias, item_to_merge)
local existing_list = list[alias] or {}
for _, recipe in ipairs(item_to_merge) do
table.insert(existing_list, recipe)
end
list[alias] = existing_list
end
local resolve_aliases_in_list = function(aliases, list, action_to_perform)
if list == nil then return end
local items_to_remove = {}
for item, value in pairs(list) do
if item:find(":") then -- only apply this test to non-group items
local alias = aliases[item]
if alias then
table.insert(items_to_remove, item)
action_to_perform(list, alias, value)
end
end
end
for _, item in ipairs(items_to_remove) do
list[item] = nil
end
end
local resolve_aliases = function()
local aliases = minetest.registered_aliases
for craft_type, recs in pairs(simplecrafting_lib.type) do
for _, recipe in ipairs(recs.recipes) do
resolve_aliases_in_list(aliases, recipe.input, merge_item_count)
resolve_aliases_in_list(aliases, recipe.returns, merge_item_count)
local output = ItemStack(recipe.output)
local output_alias = aliases[output:get_name()]
if output_alias then
output:set_name(output_alias)
recipe.output = output
end
end
resolve_aliases_in_list(aliases, recs.recipes_by_in, merge_sublists)
resolve_aliases_in_list(aliases, recs.recipes_by_out, merge_sublists)
end
end
local purge_uncraftable_recipes = function()
for item, def in pairs(minetest.registered_items) do
for group, _ in pairs(def.groups) do
group_examples[group] = item
end
end
for craft_type, _ in pairs(simplecrafting_lib.type) do
local i = 1
local recs = simplecrafting_lib.type[craft_type].recipes
while i <= #simplecrafting_lib.type[craft_type].recipes do
local validation_result = validate_inputs_and_outputs(recs[i])
if validation_result == true then
i = i + 1
else
if log_removals then
if string.match(validation_result, ":") then
minetest.log("error", "[simplecrafting_lib] Uncraftable recipe purged due to the nonexistent item " .. validation_result .. "\n"..recipe_to_string(recs[i]) .. "\nThis could be due to an error in the mod that defined this recipe, rather than an error in simplecrafting_lib itself.")
else
minetest.log("error", "[simplecrafting_lib] Uncraftable recipe purged due to no registered items matching the group requirement " .. validation_result .. "\n"..recipe_to_string(recs[i]) .. "\nThis could be due to an error in the mod that defined this recipe, rather than an error in simplecrafting_lib itself.")
end
end
table.remove(recs, i)
end
end
for output, outs in pairs(simplecrafting_lib.type[craft_type].recipes_by_out) do
i = 1
while i <= #outs do
if validate_inputs_and_outputs(outs[i]) == true then
i = i + 1
else
table.remove(outs, i)
end
end
if #outs == 0 then
if log_removals then
minetest.log("error", "[simplecrafting_lib] All recipes that had an output of " .. output
.. " have been purged as uncraftable, this item can not be made by the player.")
end
simplecrafting_lib.type[craft_type].recipes_by_out[output] = nil
end
end
for input, ins in pairs(simplecrafting_lib.type[craft_type].recipes_by_in) do
i = 1
while i <= #ins do
if validate_inputs_and_outputs(ins[i]) == true then
i = i + 1
else
table.remove(ins, i)
end
end
if #ins == 0 then
simplecrafting_lib.type[craft_type].recipes_by_in[input] = nil
end
end
end
group_examples = nil -- don't need this any more.
end
-- Tests for recipies that don't actually do anything (A => A)
-- and for recipes that pointlessly consume input without giving new output
local operative_recipe = function(recipe)
local has_an_effect = false
for in_item, in_count in pairs(recipe.input) do
local out_count = recipe.output[in_item] or 0
local returns_count = 0
if recipe.returns then returns_count = recipe.returns[in_item] or 0 end
if out_count + returns_count ~= in_count then
has_an_effect = true
break
end
end
if not has_an_effect then
return false
end
local new_output
local out_item = recipe.output:get_name()
local out_count = recipe.output:get_count()
if not recipe.input[out_item] or recipe.input[out_item] < out_count then
-- produces something that's not in the input, or produces more of the input item than there was intially.
return true
end
return false
end
-- This method goes through all of the recipes in a craft type and finds ones that "feed into" each other, creating new recipes that skip those unnecessary intermediate steps.
-- So for example if there's a recipe that goes A + B => C, and a recipe that goes C + D => E, this method will detect that and create an additional recipe that goes A + B + D => E.
local disintermediate = function(craft_type, contents)
local disintermediating_recipes = {}
for _, recipe in pairs(contents.recipes) do
if not recipe.do_not_disintermediate then
for in_item, in_count in pairs(recipe.input) do
-- check if there's recipes in this crafting type that produces the input item
if contents.recipes_by_out[in_item] then
-- find a recipe whose output divides evenly into the input
for _, recipe_producing_in_item in pairs(contents.recipes_by_out[in_item]) do
if not recipe_producing_in_item.do_not_use_for_disintermediation and
(in_count % recipe_producing_in_item.output:get_count() == 0 or recipe_producing_in_item.output:get_count() % in_count == 0) then
local multiplier = in_count / recipe_producing_in_item.output:get_count()
local working_recipe = simplecrafting_lib.deep_copy(recipe)
working_recipe.input[in_item] = nil -- clear the input from the working recipe (soon to be our newly created disintermediated recipe)
if multiplier < 1 then
-- the recipe_producing_in_item produces more than the working recipe requires by a whole multiplier.
-- eg, 1A + 1B => 2C, 1C + 1D => 1E.
-- Multiply working recipe by the inverse of multiplier and then set multiplier to 1.
-- That will get us 1A + 1B + 2D => 2E
local inverse = 1/multiplier
multiplier = 1
for item, count in pairs(working_recipe.input) do
working_recipe.input[item] = count * inverse
end
working_recipe.output:set_count(working_recipe.output:get_count() * inverse)
if working_recipe.returns then
for item, count in pairs(working_recipe.returns) do
working_recipe.returns[item] = count * inverse
end
end
end
-- add the inputs and outputs of the disintermediating recipe
for new_in_item, new_in_count in pairs(recipe_producing_in_item.input) do
if not working_recipe.input[new_in_item] then
working_recipe.input[new_in_item] = new_in_count * multiplier
else
working_recipe.input[new_in_item] = working_recipe.input[new_in_item] + new_in_count * multiplier
end
end
local new_out_item = recipe_producing_in_item.output:get_name()
local new_out_count = recipe_producing_in_item.output:get_count()
if new_out_item ~= in_item then -- this output is what's replacing the input we deleted, so don't add it.
if not working_recipe.output:get_name() == new_out_item then
working_recipe.output = ItemStack(new_out_item .. " " .. tostring(new_out_count * multiplier))
else
working_recipe.output:set_count(working_recipe.output:get_count() + new_out_count * multiplier)
end
end
if recipe_producing_in_item.returns then
for new_returns_item, new_returns_count in pairs(recipe_producing_in_item.returns) do
working_recipe.returns = working_recipe.returns or {}
if not working_recipe.returns[new_returns_item] then
working_recipe.returns[new_returns_item] = new_returns_count * multiplier
else
working_recipe.returns[new_returns_item] = working_recipe.returns[new_returns_item] + new_returns_count * multiplier
end
end
end
if operative_recipe(working_recipe) then
table.insert(disintermediating_recipes, working_recipe)
end
end
end
end
end
end
end
local count = 0
for _, new_recipe in pairs(disintermediating_recipes) do
if simplecrafting_lib.register(craft_type, new_recipe) then
count = count + 1
end
end
return count
end
local postprocess = function()
resolve_aliases()
purge_uncraftable_recipes()
for craft_type, contents in pairs(simplecrafting_lib.type) do
local cycles = contents.disintermediation_cycles or 0
local previous_count = 0
while cycles > 0 do
local new_count = disintermediate(craft_type, contents)
if new_count == 0 then
minetest.log("info", "[simplecrafting_lib] disintermediation loop for crafting type " .. craft_type
.. " exited early due to no need for additional disintermediation recipes.")
cycles = 0
break
elseif previous_count > 0 and new_count > previous_count then
minetest.log("error", "[simplecrafting_lib] potential disintermediation problem: crafting type \""
.. craft_type .. "\" added " .. previous_count .. " disintermediation recipes on the previous cycle "
.. "and " .. new_count .. " disintermediation recipes on the current cycle. This growing recipe "
.. "addition rate could indicate that there's a unbalanced \"loop\" in the recipes defined for "
.. "this craft type, resulting in an ever-increasing number of ways of producing a particular "
.. "output. Examine the recipes for this craft type to see if you can identify the culprit, "
.. "or reduce the value of this craft type's disintermediation_cycles property to prevent "
.. "the recipe growth from getting too bad.")
end
previous_count = new_count
cycles = cycles - 1
end
end
for _, callback in ipairs(registered_callbacks) do
callback()
end
registered_callbacks = nil
end
minetest.after(0, postprocess)