mods that hopefully work for a TechAge-based pack

This commit is contained in:
2026-09-09 06:27:51 -06:00
parent 4e6b08d194
commit c27db2a537
6234 changed files with 375578 additions and 0 deletions
@@ -0,0 +1,528 @@
local S = minetest.get_translator("unified_inventory")
local F = minetest.formspec_escape
local ui = unified_inventory
local recipes_initialized = false
--- @return boolean. Returns `true` when all ingredients exist.
local function is_recipe_craftable(recipe)
-- Ensure the ingedients exist
for _, itemname in pairs(recipe.items) do
local groups = string.find(itemname, "group:")
if groups then
local items = ui.get_matching_items(itemname)
if not next(items) then
--print("No items for group item: ", itemname)
return false
end
else
-- Possibly an item. Clean its name (could be "default:stick 4")
itemname = ItemStack(itemname):get_name()
local def = core.registered_items[itemname]
if not def then
return false
end
-- inlined core.get_item_group
if (def.groups.not_in_craft_guide or 0) ~= 0 then
return false
end
end
end
return true
end
local function register_normal_craft_recipes()
local rev_aliases = {}
for original, newname in pairs(minetest.registered_aliases) do
if not rev_aliases[newname] then
rev_aliases[newname] = {}
end
table.insert(rev_aliases[newname], original)
end
for _, name in ipairs(ui.items_list) do
-- Alias processing: Find recipes that belong to the current item name
local all_names = rev_aliases[name] or {}
table.insert(all_names, name)
for _, itemname in ipairs(all_names) do
local recipes = minetest.get_all_craft_recipes(itemname)
for _, recipe in ipairs(recipes or {}) do
if is_recipe_craftable(recipe) then
ui.register_craft(recipe)
end
end
end
end
end
local function register_dig_drops(name)
-- Analyse dropped items -> custom "digging" recipes
local def = minetest.registered_items[name]
-- Simple drops
if type(def.drop) == "string" then
local dstack = ItemStack(def.drop)
if not dstack:is_empty() and dstack:get_name() ~= name then
ui.register_craft({
type = "digging",
items = {name},
output = def.drop,
width = 0,
})
end
-- Complex drops. Yes, it's really complex!
elseif type(def.drop) == "table" then
--[[ Extract single items from the table and save them into dedicated tables
to register them later, in order to avoid duplicates. These tables counts
the total number of guaranteed drops and drops by chance (“maybes”) for each item.
For “maybes”, the final count is the theoretical maximum number of items, not
neccessarily the actual drop count. ]]
local drop_guaranteed = {}
local drop_maybe = {}
-- This is for catching an obscure corner case: If the top items table has
-- only items with rarity = 1, but max_items is set, then only the first
-- max_items will be part of the drop, any later entries are logically
-- impossible, so this variable is for keeping track of this
local max_items_left = def.drop.max_items
-- For checking whether we still encountered only guaranteed only so far;
-- for the first “maybe” item it will become false which will cause ALL
-- later items to be considered “maybes”.
-- A common idiom is:
-- { max_items 1, { items = {
-- { items={"example:1"}, rarity = 5 },
-- { items={"example:2"}, rarity = 1 }, }}}
-- example:2 must be considered a “maybe” because max_items is set and it
-- appears after a “maybe”
local max_start = true
-- Let's iterate through the items madness!
-- Handle invalid drop entries gracefully.
local drop_items = def.drop.items or { }
for i=1,#drop_items do
if max_items_left ~= nil and max_items_left <= 0 then break end
local itit = drop_items[i]
for j=1,#itit.items do
local dstack = ItemStack(itit.items[j])
if not dstack:is_empty() and dstack:get_name() ~= name then
local dname = dstack:get_name()
local dcount = dstack:get_count()
-- Guaranteed drops AND we are not yet in “maybe mode”
if #itit.items == 1 and itit.rarity == 1 and max_start then
if drop_guaranteed[dname] == nil then
drop_guaranteed[dname] = 0
end
drop_guaranteed[dname] = drop_guaranteed[dname] + dcount
if max_items_left ~= nil then
max_items_left = max_items_left - 1
if max_items_left <= 0 then break end
end
-- Drop was a “maybe”
else
if max_items_left ~= nil then max_start = false end
if drop_maybe[dname] == nil then
drop_maybe[dname] = 0
end
drop_maybe[dname] = drop_maybe[dname] + dcount
end
end
end
end
for itemstring, count in pairs(drop_guaranteed) do
ui.register_craft({
type = "digging",
items = {name},
output = itemstring .. " " .. count,
width = 0,
})
end
for itemstring, count in pairs(drop_maybe) do
ui.register_craft({
type = "digging_chance",
items = {name},
output = itemstring .. " " .. count,
width = 0,
})
end
end
end
-- Create detached creative inventory after loading all mods
minetest.after(0.01, function()
-- Filtered item list
ui.items_list = {}
for name, def in pairs(minetest.registered_items) do
if ui.is_itemdef_listable(def) then
table.insert(ui.items_list, name)
end
end
table.sort(ui.items_list)
ui.items_list_size = #ui.items_list
print("Unified Inventory. Inventory size: "..ui.items_list_size)
-- Step 1: Initialize cache for looking up groups
unified_inventory.init_matching_cache()
-- Index all craftable craft recipes. This depends on 'init_matching_cache'
register_normal_craft_recipes()
for _, name in ipairs(ui.items_list) do
register_dig_drops(name)
end
-- Step 2: Find all matching items for the given spec (groups)
local get_matching_spec_items = unified_inventory.get_matching_items
for outputitemname, recipes in pairs(ui.crafts_for.recipe) do
-- List of crafts that return this item string (variable "_")
-- Problem: The group cache must be initialized after all mods finished loading
-- thus, invalid recipes might be indexed. Hence perform filtering with `new_recipe_list`
local new_recipe_list = {}
for _, recipe in ipairs(recipes) do
local ingredient_items = {}
for _, spec in pairs(recipe.items) do
-- Get items that fit into this spec (group or item name)
local specname = ItemStack(spec):get_name()
for item_name, _ in pairs(get_matching_spec_items(specname)) do
ingredient_items[item_name] = true
end
end
for name, _ in pairs(ingredient_items) do
if not ui.crafts_for.usage[name] then
ui.crafts_for.usage[name] = {}
end
table.insert(ui.crafts_for.usage[name], recipe)
end
if next(ingredient_items) then
-- There's at least one known ingredient: mark as good recipe
-- PS: What whatll be done about partially incomplete recipes?
table.insert(new_recipe_list, recipe)
end
end
ui.crafts_for.recipe[outputitemname] = new_recipe_list
end
recipes_initialized = true
-- Remove unknown items from all categories
local total_removed = 0
for cat_name, cat_def in pairs(ui.registered_category_items) do
for itemname, _ in pairs(cat_def) do
local idef = minetest.registered_items[itemname]
if not idef then
total_removed = total_removed + 1
--[[
-- For analysis
minetest.log("warning", "[unified_inventory] Removed item '"
.. itemname .. "' from category '" .. cat_name
.. "'. Reason: item not registered")
]]
cat_def[itemname] = nil
elseif not ui.is_itemdef_listable(idef) then
total_removed = total_removed + 1
--[[
-- For analysis
minetest.log("warning", "[unified_inventory] Removed item '"
.. itemname .. "' from category '" .. cat_name
.. "'. Reason: item is in 'not_in_creative_inventory' group")
]]
cat_def[itemname] = nil
end
end
end
if total_removed > 0 then
minetest.log("info", "[unified_inventory] Removed " .. total_removed ..
" items from the categories.")
end
for _, callback in ipairs(ui.initialized_callbacks) do
callback()
end
end)
---------------- Home API ----------------
local function load_home()
local input = io.open(ui.home_filename, "r")
if not input then
ui.home_pos = {}
return
end
while true do
local x = input:read("*n")
if not x then break end
local y = input:read("*n")
local z = input:read("*n")
local name = input:read("*l")
ui.home_pos[name:sub(2)] = {x = x, y = y, z = z}
end
io.close(input)
end
load_home()
function ui.set_home(player, pos)
local player_name = player:get_player_name()
ui.home_pos[player_name] = vector.round(pos)
-- save the home data from the table to the file
local output = io.open(ui.home_filename, "w")
if not output then
minetest.log("warning", "[unified_inventory] Failed to save file: "
.. ui.home_filename)
return
end
for k, v in pairs(ui.home_pos) do
output:write(v.x.." "..v.y.." "..v.z.." "..k.."\n")
end
io.close(output)
end
function ui.go_home(player)
local pos = ui.home_pos[player:get_player_name()]
if pos then
player:set_pos(pos)
return true
end
return false
end
---------------- Crafting API ----------------
function ui.register_craft(options)
if not options.output then
return
end
if recipes_initialized then
-- If you're getting this warning, do use either:
-- a) unified_inventory.register_craft(...)
-- b) core.register_on_mods_loaded(...)
core.log("warning", "[unified_inventory] Recipe registered too late: " ..
dump(options.output):gsub("[\t\n]", " ") ..
". It might not show up correctly.")
end
if options.type == "normal" and options.width == 0 then
options = {
type = "shapeless",
items = options.items,
output = options.output,
width = 0
}
end
do
-- Convert the the 'output' field into a table for convenience
local outputs = options.output
if type(outputs) == "string" then
-- Most common
outputs = { outputs }
elseif type(outputs) == "userdata" and outputs.add_item then
-- Rare: ItemStack
outputs = { outputs }
end
assert(type(outputs) == "table", "Invalid 'output' field: " .. type(options.output))
options.output = {}
for _, itemstack in ipairs(outputs) do
itemstack = ItemStack(itemstack)
if not itemstack:is_empty() then
table.insert(options.output, itemstack)
end
end
end
-- Pre-filter: discard empty outputs
for _, itemstack in ipairs(options.output) do
local item_name = itemstack:get_name()
if not ui.crafts_for.recipe[item_name] then
ui.crafts_for.recipe[item_name] = {}
end
table.insert(ui.crafts_for.recipe[item_name], options)
end
-- Run callbacks
for _, itemstack in ipairs(options.output) do
local item_name = itemstack:get_name()
for _, callback in ipairs(ui.craft_registered_callbacks) do
callback(item_name, options)
end
end
end
local craft_type_defaults = {
width = 3,
height = 3,
uses_crafting_grid = false,
}
function ui.craft_type_defaults(name, options)
if not options.description then
options.description = name
end
setmetatable(options, {__index = craft_type_defaults})
return options
end
function ui.register_craft_type(name, options)
ui.registered_craft_types[name] = ui.craft_type_defaults(name, options)
end
ui.register_craft_type("normal", {
description = F(S("Crafting")),
icon = "ui_craftgrid_icon.png",
width = 3,
height = 3,
get_shaped_craft_width = function (craft) return craft.width end,
dynamic_display_size = function (craft)
local w = craft.width
local h = math.ceil(table.maxn(craft.items) / craft.width)
local g = w < h and h or w
return { width = g, height = g }
end,
uses_crafting_grid = true,
})
ui.register_craft_type("shapeless", {
description = F(S("Mixing")),
icon = "ui_craftgrid_icon.png",
width = 3,
height = 3,
dynamic_display_size = function (craft)
local maxn = table.maxn(craft.items)
local g = 1
while g*g < maxn do g = g + 1 end
return { width = g, height = g }
end,
uses_crafting_grid = true,
})
ui.register_craft_type("cooking", {
description = F(S("Cooking")),
icon = "default_furnace_front.png",
width = 1,
height = 1,
})
ui.register_craft_type("digging", {
description = F(S("Digging")),
icon = "default_tool_steelpick.png",
width = 1,
height = 1,
})
ui.register_craft_type("digging_chance", {
description = "Digging (by chance)",
icon = "default_tool_steelpick.png^[transformFY.png",
width = 1,
height = 1,
})
---------------- GUI registrations ----------------
function ui.register_page(name, def)
ui.pages[name] = def
end
function ui.register_button(name, def)
if not def.action then
def.action = function(player)
ui.set_inventory_formspec(player, name)
end
end
def.name = name
table.insert(ui.buttons, def)
end
---------------- Callback registrations ----------------
function ui.register_on_initialized(callback)
assert(type(callback) == "function")
table.insert(ui.initialized_callbacks, callback)
end
function ui.register_on_craft_registered(callback)
assert(type(callback) == "function")
table.insert(ui.craft_registered_callbacks, callback)
end
---------------- List getters ----------------
local warned_get_recipe_list = false
function ui.get_recipe_list(output)
if not warned_get_recipe_list then
warned_get_recipe_list = true
core.log("warning", debug.traceback("Deprecated call to 'get_recipe_list'. " ..
"Please use 'get_recipe_list2' instead."))
end
local recipes = ui.crafts_for.recipe[output]
if not recipes then
return nil
end
-- Slow backwards compat
local list = {}
for _, recipe in ipairs(recipes) do
if #recipe.output == 1 then
recipe = table.copy(recipe)
recipe.output = recipe.output[1]:to_string()
table.insert(list, recipe)
end
end
return list
end
function ui.get_recipe_list2(output)
assert(type(output) == "string")
return ui.crafts_for.recipe[output]
end
function ui.get_registered_outputs()
local outputs = {}
for item_name, _ in pairs(ui.crafts_for.recipe) do
table.insert(outputs, item_name)
end
return outputs
end
---------------- Player utilities ----------------
function ui.is_creative(playername)
return minetest.check_player_privs(playername, {creative=true})
or minetest.settings:get_bool("creative_mode")
end
---------------- Formspec helpers ----------------
function ui.single_slot(xpos, ypos, bright)
return string.format("background9[%f,%f;%f,%f;ui_single_slot%s.png;false;16]",
xpos, ypos, ui.imgscale, ui.imgscale, (bright and "_bright" or "") )
end
function ui.make_trash_slot(xpos, ypos)
return
ui.single_slot(xpos, ypos)..
"image["..xpos..","..ypos..";1.25,1.25;ui_trash_slot_icon.png]"..
"list[detached:trash;main;"..(xpos + ui.list_img_offset)..","..(ypos + ui.list_img_offset)..";1,1;]"
end
function ui.make_inv_img_grid(xpos, ypos, width, height, bright)
local tiled = {}
local n=1
for y = 0, (height - 1) do
for x = 0, (width -1) do
tiled[n] = ui.single_slot(xpos + (ui.imgscale * x), ypos + (ui.imgscale * y), bright)
n = n + 1
end
end
return table.concat(tiled)
end