123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- -- ZCG mod for minetest
- -- See README for more information
- -- Released by Zeg9 under WTFPL
- if not minetest.global_exists("zcg") then zcg = {} end
- zcg.modpath = minetest.get_modpath("zcg")
- zcg.users = zcg.users or {}
- zcg.crafts = zcg.crafts or {}
- zcg.itemlist = zcg.itemlist or {}
- -- Localize for performance.
- local math_floor = math.floor
- zcg.items_in_group = function(group)
- local items = {}
- local ok = true
- for name, item in pairs(minetest.registered_items) do
- -- the node should be in all groups
- ok = true
- for _, g in ipairs(group:split(',')) do
- if not item.groups[g] then
- ok = false
- end
- end
- if ok then table.insert(items,name) end
- end
- return items
- end
- local table_copy = function(table)
- local out = {}
- for k,v in pairs(table) do
- out[k] = v
- end
- return out
- end
- zcg.add_craft = function(input, realout, output, groups)
- if minetest.get_item_group(output, "not_in_craft_guide") > 0 then
- return
- end
- if not groups then groups = {} end
- local c = {}
- c.width = input.width
- c.type = input.type
- c.items = input.items
- if type(realout) == "string" then
- c.result = realout
- elseif type(realout) == "table" then
- --minetest.log(dump(realout))
- if type(realout.output) == "string" then
- c.result = realout.output
- elseif type(realout.output) == "table" then
- -- Recipe output should be two items (separating recipe).
- assert(type(realout.output[1]) == "string")
- assert(type(realout.output[2]) == "string")
- c.result = table.copy(realout.output)
- end
- end
- assert(type(c.result) == "string" or type(c.result) == "table")
- if c.items == nil then return end
- for i, item in pairs(c.items) do
- if item:sub(0,6) == "group:" then
- -- The recipe item name may contain a count value.
- -- We must extract just the name after the "group:".
- local strpart = item:sub(7)
- local parts = string.split(strpart, " ")
- assert(type(parts[1]) == "string")
- local groupname = parts[1]
- local groupcount = parts[2] or 1
- if groups[groupname] ~= nil then
- c.items[i] = groups[groupname] .. " " .. groupcount
- else
- for _, gi in ipairs(zcg.items_in_group(groupname)) do
- local g2 = groups
- g2[groupname] = gi
- zcg.add_craft({
- width = c.width,
- type = c.type,
- items = table_copy(c.items)
- }, realout, output, g2) -- it is needed to copy the table, else groups won't work right
- end
- return
- end
- end
- end
- if c.width == 0 then c.width = 3 end
- table.insert(zcg.crafts[output],c)
- end
- zcg.load_crafts = function(name)
- zcg.crafts[name] = {}
- local _recipes = minetest.get_all_craft_recipes(name)
- if _recipes then
- for i, recipe in ipairs(_recipes) do
- if (recipe and recipe.items and recipe.type) then
- assert(type(recipe.output) ~= "nil")
- zcg.add_craft(recipe, recipe.output, name)
- end
- end
- end
- if zcg.crafts[name] == nil or #zcg.crafts[name] == 0 then
- zcg.crafts[name] = nil
- else
- table.insert(zcg.itemlist,name)
- end
- end
- zcg.formspec = function(pn)
- local page = zcg.users[pn].page
- local alt = zcg.users[pn].alt
- local current_item = zcg.users[pn].current_item
- local formspec = "size[8,8.5]" ..
- default.gui_bg ..
- default.gui_bg_img ..
- default.gui_slots ..
- "button[0,0.5;2,.5;main;Back]"
- if zcg.users[pn].history.index > 1 then
- formspec = formspec .. "image_button[0,1.5;1,1;zcg_previous.png;zcg_previous;;false;false;zcg_previous_press.png]"
- else
- formspec = formspec .. "image[0,1.5;1,1;zcg_previous_inactive.png]"
- end
- if zcg.users[pn].history.index < #zcg.users[pn].history.list then
- formspec = formspec .. "image_button[1,1.5;1,1;zcg_next.png;zcg_next;;false;false;zcg_next_press.png]"
- else
- formspec = formspec .. "image[1,1.5;1,1;zcg_next_inactive.png]"
- end
- -- Show craft recipe
- if current_item ~= "" then
- if zcg.crafts[current_item] then
- if alt > #zcg.crafts[current_item] then
- alt = #zcg.crafts[current_item]
- end
- if alt > 1 then
- formspec = formspec .. "button[7,0.5;1,1;zcg_alt:"..(alt-1)..";^]"
- end
- if alt < #zcg.crafts[current_item] then
- formspec = formspec .. "button[7,2.5;1,1;zcg_alt:"..(alt+1)..";v]"
- end
- local c = zcg.crafts[current_item][alt]
- if c then
- local x = 3
- local y = 0
- -- Crafting recipe generated here.
- for i, item in pairs(c.items) do
- local stack = ItemStack(item)
- local itemname = stack:get_name()
- formspec = formspec .. "item_image_button["..((i-1)%c.width+x)..","..(math_floor((i-1)/c.width+y)+0.5)..";1,1;"..item..";zcg:"..itemname..";]"
- end
- if c.type == "normal" or
- c.type == "cooking" or
- c.type == "grinding" or
- c.type == "cutting" or
- c.type == "extracting" or
- c.type == "alloying" or
- c.type == "separating" or
- c.type == "compressing" or
- c.type == "anvil" or
- c.type == "crushing" then
- formspec = formspec .. "image[6,2.5;1,1;zcg_method_"..c.type..".png]"
- else -- we don't have an image for other types of crafting
- formspec = formspec .. "label[0,2.5;Method: "..c.type.."]"
- end
- if c.type == "normal" then
- formspec = formspec .. "label[0,2.5;Method: Crafting]"
- elseif c.type == "cooking" then
- formspec = formspec .. "label[0,2.5;Method: Cooking/Smelting]"
- elseif c.type == "grinding" then
- formspec = formspec .. "label[0,2.5;Method: Grinding]"
- elseif c.type == "crushing" then
- formspec = formspec .. "label[0,2.5;Method: Crushing]"
- elseif c.type == "cutting" then
- formspec = formspec .. "label[0,2.5;Method: Cutting]"
- elseif c.type == "extracting" then
- formspec = formspec .. "label[0,2.5;Method: Extracting]"
- elseif c.type == "compressing" then
- formspec = formspec .. "label[0,2.5;Method: Compressing]"
- elseif c.type == "alloying" then
- formspec = formspec .. "label[0,2.5;Method: Alloying]"
- elseif c.type == "separating" then
- formspec = formspec .. "label[0,2.5;Method: Separating]"
- elseif c.type == "anvil" then
- formspec = formspec .. "label[0,2.5;Method: Hammering]"
- end
- if type(c.result) == "string" then
- formspec = formspec .. "image[6,1.5;1,1;zcg_craft_arrow.png]"
- formspec = formspec .. "item_image_button[7,1.5;1,1;".. c.result ..";;]"
- elseif type(c.result) == "table" then
- -- Separating recipes have two outputs.
- formspec = formspec .. "item_image_button[6,1.5;1,1;".. c.result[1] ..";;]"
- formspec = formspec .. "item_image_button[7,1.5;1,1;".. c.result[2] ..";;]"
- end
- --minetest.chat_send_all(dump(c))
- end
- end
- end
- -- Node list
- local npp = 8*3 -- nodes per page
- local i = 0 -- for positionning buttons
- local s = 0 -- for skipping pages
- local whichlist = zcg.itemlist
- local listname = " total items."
- if zcg.users[pn].searchtext ~= "" then
- whichlist = zcg.users[pn].searchlist
- page = zcg.users[pn].spage
- listname = " result(s)."
- end
- if #whichlist > 0 then
- formspec = formspec ..
- "label[0,4.0;" .. #whichlist .. " " .. listname .. "]"
- for _, name in ipairs(whichlist) do
- if s < page*npp then s = s+1 else
- if i >= npp then break end
- formspec = formspec .. "item_image_button["..(i%8)..","..(math_floor(i/8)+4.5)..";1,1;"..name..";zcg:"..name..";]"
- i = i+1
- end
- end
- else
- formspec = formspec ..
- "label[0,4.0;No results.]"
- end
- -- Page buttons.
- local maxpage = (math.ceil(#whichlist/npp))
- if maxpage < 1 then maxpage = 1 end -- In case no results, must have 1 page.
- local curpage = page+1
- if page > 0 then
- formspec = formspec .. "button[0,8;1,.5;zcg_page:"..(page-1)..";<<]"
- else
- formspec = formspec .. "button[0,8;1,.5;zcg_page:"..(maxpage-1)..";<<]"
- end
- if curpage < maxpage then
- formspec = formspec .. "button[1,8;1,.5;zcg_page:"..(page+1)..";>>]"
- elseif curpage >= maxpage then
- formspec = formspec .. "button[1,8;1,.5;zcg_page:".. 0 ..";>>]"
- end
- -- The Y is approximatively the good one to have it centered vertically...
- formspec = formspec .. "label[2,7.85;Page " .. curpage .."/".. maxpage .."]"
- -- Search field.
- formspec = formspec ..
- "button[6,8;1,0.5;zcg_search;?]" ..
- "button[7,8;1,0.5;zcg_clear;X]"
- local text = zcg.users[pn].searchtext or ""
- formspec = formspec ..
- "field[4,8.1;2.3,1;zcg_sbox;;" .. minetest.formspec_escape(text) .. "]" ..
- "field_close_on_enter[zcg_sbox;false]"
- return formspec
- end
- function zcg.update_search(pn, tsearch)
- minetest.log("action", "<" .. rename.gpn(pn) .. "> executes craftguide search for \"" .. tsearch .. "\".")
- zcg.users[pn].searchlist = {}
- if tsearch == "" or tsearch == "<INVAL>" then
- return
- end
- -- Let user search multiple tokens at once.
- local texts = string.split(tsearch)
- if not texts or #texts == 0 then
- return
- end
- local list = {}
- local find = string.find
- local ipairs = ipairs
- local pairs = pairs
- local type = type
- local items = minetest.registered_items
- -- Returns true only if all tokens in list are found in the search string.
- local function find_all(search, combined)
- local count = 0
- for i=1, #combined do
- if find(search, combined[i], 1, true) then
- count = count + 1
- else
- -- Early finish.
- return false
- end
- end
- return (count == #combined)
- end
- for i=1, #texts, 1 do
- local text = string.trim(texts[i]):lower()
- local combined = string.split(text, " ")
- for k=1, #combined do
- combined[k] = string.trim(combined[k]):lower()
- end
- for _, name in ipairs(zcg.itemlist) do
- if find_all(name:lower(), combined) then
- list[name] = true
- else
- local ndef = items[name]
- if ndef then
- -- Search description for a match.
- if ndef.description then
- if find_all(ndef.description:lower(), combined) then
- list[name] = true
- end
- end
- end -- if ndef.
- end
- end
- end -- for all texts.
- -- Duplicate results are removed.
- local flat = {}
- for k, v in pairs(list) do
- flat[#flat + 1] = k
- end
- zcg.users[pn].searchlist = flat
- end
- -- Sound volumes.
- local click_gain = 0.4
- local page_gain = 0.7
- local sound_range = 16
- -- It seems the player's main inventory formspec does not have a formname.
- local singleplayer = minetest.is_singleplayer()
- zcg.on_receive_fields = function(player, formname, fields)
- local played_sound = false
- local pn = player:get_player_name()
- afk.reset_timeout(pn)
- if zcg.users[pn] == nil then
- zcg.users[pn] = {
- current_item = "",
- alt = 1,
- page = 0,
- history = {index=0, list={}},
- searchlist = {},
- searchtext = "",
- spage = 0, -- Keep search page # seperate from main page #.
- }
- end
- if fields.zcg then
- inventory_plus.set_inventory_formspec(player, zcg.formspec(pn))
- if not played_sound and not fields.quit then
- ambiance.sound_play("button_click", player:get_pos(), click_gain, sound_range)
- end
- return
- elseif fields.zcg_previous then
- if zcg.users[pn].history.index > 1 then
- zcg.users[pn].history.index = zcg.users[pn].history.index - 1
- zcg.users[pn].current_item = zcg.users[pn].history.list[zcg.users[pn].history.index]
- zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- end
- elseif fields.zcg_next then
- if zcg.users[pn].history.index < #zcg.users[pn].history.list then
- zcg.users[pn].history.index = zcg.users[pn].history.index + 1
- zcg.users[pn].current_item = zcg.users[pn].history.list[zcg.users[pn].history.index]
- zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- end
- end
- for k, v in pairs(fields) do
- if (k:sub(0,4)=="zcg:") then
- local ni = k:sub(5)
- zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
- if zcg.crafts[ni] then
- local previtem = zcg.users[pn].current_item
- zcg.users[pn].current_item = ni
- table.insert(zcg.users[pn].history.list, ni)
- zcg.users[pn].history.index = #zcg.users[pn].history.list
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- -- Add item to inventory if creative access is enabled.
- if gdac.player_is_admin(pn) or singleplayer then
- -- If player clicked twice.
- if previtem == ni then
- local inv = player:get_inventory()
- local stack = ItemStack(ni)
- stack:set_count(stack:get_stack_max())
- local leftover = inv:add_item("main", stack)
- -- Notify if a mapping kit was added.
- if map.is_mapping_kit(stack:get_name()) then
- map.update_inventory_info(pn)
- end
- if not leftover or leftover:get_count() == 0 then
- local desc = utility.get_short_desc(stack:get_definition().description or "Undescribed Item")
- minetest.chat_send_player(pn, "# Server: Added '" .. desc .. "' to inventory!")
- else
- minetest.chat_send_player(pn, "# Server: Not enough room in inventory!")
- end
- end
- end
- end
- elseif (k:sub(0,9)=="zcg_page:") then
- if zcg.users[pn].searchtext == "" then
- zcg.users[pn].page = tonumber(k:sub(10))
- else
- zcg.users[pn].spage = tonumber(k:sub(10))
- end
- ambiance.sound_play("pageflip", player:get_pos(), page_gain, sound_range)
- played_sound = true
- zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- elseif (k:sub(0,8)=="zcg_alt:") then
- zcg.users[pn].alt = tonumber(k:sub(9))
- zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- elseif (k == "zcg_clear") then
- zcg.users[pn].searchlist = {}
- zcg.users[pn].searchtext = ""
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- elseif (fields.key_enter_field == "zcg_sbox" or k == "zcg_search") then
- local newtext = fields.zcg_sbox or "<INVAL>"
- if newtext ~= zcg.users[pn].searchtext then -- Don't update if same.
- zcg.users[pn].searchlist = {}
- zcg.users[pn].searchtext = newtext
- if string.len(zcg.users[pn].searchtext) > 128 then
- zcg.users[pn].searchtext = "<INVAL>"
- else
- local res, err = pcall(function() zcg.update_search(pn, zcg.users[pn].searchtext) end)
- if not res and type(err) == "string" then
- minetest.log("error", err)
- end
- zcg.users[pn].spage = 0 -- Reset user's search page.
- end
- inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
- ambiance.sound_play("pageflip", player:get_pos(), page_gain, sound_range)
- played_sound = true
- end
- end
- end
- if not played_sound and not fields.quit then
- ambiance.sound_play("button_click", player:get_pos(), click_gain, sound_range)
- end
- if not played_sound and (fields.close or fields.exit or fields.done) then
- ambiance.sound_play("button_click", player:get_pos(), click_gain, sound_range)
- end
- -- This works because the 'quit' field is sent whenever the player tabs or ESC's out of a formspec,
- -- but not when changing to show a different formspec.
- if fields.quit then
- if passport.open_keys[pn] then
- passport.open_keys[pn] = nil
- ambiance.sound_play("fancy_chime2", player:get_pos(), 1.0, 20, "", false)
- end
- end
- end
- if not zcg.registered then
- -- Load all crafts directly after server-init time.
- -- We can't do this at craft-register time because the logic needs access to
- -- the groups of the recipe output items, which may not be known by the engine
- -- until after recipes for the items are registered.
- if not minetest.is_singleplayer() then
- minetest.register_on_mods_loaded(function()
- local t1 = os.clock()
- -- Must search through ALL registered items! Cannot use shortcut tables.
- for name, item in pairs(minetest.registered_items) do
- if name and name ~= "" then
- -- Ignore stairs nodes. They do have generic/standard recipes, but we
- -- wouldn't show them anway -- WAY too much CG spam.
- if not name:find("^%:?stairs:") then
- zcg.load_crafts(name)
- end
- end
- end
- table.sort(zcg.itemlist)
- local t2 = os.clock()
- minetest.log("action", "Loading craft recipes took " .. (t2 - t1) .. " seconds.")
- end)
- end -- Speed up singleplayer game testing.
- -- Register button once.
- inventory_plus.register_button("zcg", "Craft Journal")
- -- Per Lua docs, newest functions are called first.
- -- Therefore resister inside minetest.after() to ensure this function is
- -- registered AFTER all other mods have registered theirs.
- minetest.after(0, function()
- minetest.register_on_player_receive_fields(function(...)
- return zcg.on_receive_fields(...)
- end)
- end)
- local c = "zcg:core"
- local f = zcg.modpath .. "/init.lua"
- reload.register_file(c, f, false)
- zcg.registered = true
- end
|