diff --git a/.luacheckrc b/.luacheckrc index cc54a3ce74..094a2d93c2 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -16,7 +16,7 @@ read_globals = { "Settings", "unpack", -- Silence errors about custom table methods. - table = { fields = { "copy", "indexof" } }, + table = { fields = { "copy", "indexof", "insert_all" } }, -- Silence warnings about accessing undefined fields of global 'math' math = { fields = { "sign" } } } diff --git a/game_api.txt b/game_api.txt index e46ea6bd53..70654c2bb9 100644 --- a/game_api.txt +++ b/game_api.txt @@ -83,6 +83,17 @@ in `bones.player_inventory_lists`. e.g. `table.insert(bones.player_inventory_lists, "backpack")` +Additionally, callbacks can be registered to transfer items to the bones on death: + +`bones.register_collect_items(callback)` + +Functions registered this way won't be called if `bones_mode` is `keep` or in case of the death of a creative player. + +`callback` is a `function(player)` which `return`s a table of items to be transferred. + +Please note that this does not remove the items from the inventory they were taken of. +Disposing of the items in this inventory is the `callback`'s responsibility. + Creative API ------------ diff --git a/minetest.conf.example b/minetest.conf.example index bb6eb6444b..76bb7ce967 100644 --- a/minetest.conf.example +++ b/minetest.conf.example @@ -13,6 +13,9 @@ # keep: Player keeps items. #bones_mode = bones +# Sets the maximum item slots inside a bones node. Anything above this limit will be dropped +#bones_max_slots = 150 + # The time in seconds after which the bones of a dead player can be looted by # everyone. # 0 to disable. diff --git a/mods/bones/init.lua b/mods/bones/init.lua index 725f662218..ddf966534a 100644 --- a/mods/bones/init.lua +++ b/mods/bones/init.lua @@ -6,8 +6,17 @@ -- Load support for MT game translation. local S = minetest.get_translator("bones") +local bones_max_slots = tonumber(minetest.settings:get("bones_max_slots")) or 15 * 10 +local min_inv_size = 4 * 8 -- display and provide at least this many slots + bones = {} + +local function NS(s) + return s +end + + local function is_owner(pos, name) local owner = minetest.get_meta(pos):get_string("owner") if owner == "" or owner == name or minetest.check_player_privs(name, "protection_bypass") then @@ -16,18 +25,93 @@ local function is_owner(pos, name) return false end -local bones_formspec = - "size[8,9]" .. - "list[current_name;main;0,0.3;8,4;]" .. - "list[current_player;main;0,4.85;8,1;]" .. - "list[current_player;main;0,6.08;8,3;8]" .. - "listring[current_name;main]" .. - "listring[current_player;main]" .. - default.get_hotbar_bg(0,4.85) + +local function appendmulti(tbl, ...) + for _, v in pairs({...}) do + table.insert(tbl, v) + end +end + + +local function get_bones_formspec_for_size(numitems) + local cols, rows + local scroll=false + if numitems <= min_inv_size then + cols, rows = 8, 4 + else + cols, rows = 8, math.ceil(numitems / 8) + scroll=true + end + local output={} + appendmulti(output, "size[", 8.5, ", ", 9, "]") + if scroll then + local row_05 = 13.2 + local row_15 = 128 + local multiplier = (row_15 - row_05) / 10 + local scrollmax = (rows - 5) * multiplier + row_05 + appendmulti(output, "scrollbaroptions[max=", scrollmax, "]") + appendmulti(output, "scrollbar[8,0;0.3,4.5;vertical;bones_scroll;0]", + "scroll_container[0,0.3;10.3,4.95;bones_scroll;vertical;0.1]") + end + appendmulti(output, "list[current_name;main;0,0;", cols, ",", rows, ";]") + if scroll then + appendmulti(output, "scroll_container_end[]") + end + appendmulti(output, "list[current_player;main;", 0, ",", 4.75, ";8,1;]") + appendmulti(output, "list[current_player;main;", 0, ",", 5.98, ";8,3;8]") + appendmulti(output, "listring[current_name;main]") + appendmulti(output, "listring[current_player;main]") + appendmulti(output, default.get_hotbar_bg(0, 4.85)) + return table.concat(output) +end + local share_bones_time = tonumber(minetest.settings:get("share_bones_time")) or 1200 local share_bones_time_early = tonumber(minetest.settings:get("share_bones_time_early")) or share_bones_time / 4 + +local function find_next_empty(inv, listname, start) + while start <= inv:get_size(listname) do + if inv:get_stack(listname, start):get_count() == 0 then + return start + end + start = start + 1 + end + return -1 +end + + +local function find_next_populated(inv, listname, start) + while start <= inv:get_size(listname) do + if inv:get_stack(listname, start):get_count() > 0 then + return start + end + start = start + 1 + end + return -1 +end + + +-- slot reordering to make sure the first rows of the bone are always populated +local function bones_inv_reorder(meta) + local next_empty = 1 -- there are no empty slots inside the bones before this + local next_populated -- there are no populated slots preceded by unpopulated slots before this + local inv = meta:get_inventory() + next_empty = find_next_empty(inv, "main", next_empty) + if next_empty < 0 then + return + end + next_populated = find_next_populated(inv, "main", next_empty + 1) + while next_populated > 0 do + local stack = inv:get_stack("main", next_populated) + inv:set_stack("main", next_populated, ItemStack()) + inv:set_stack("main", next_empty, stack) + next_empty = find_next_empty(inv, "main", next_empty + 1) + next_populated = find_next_populated(inv, "main", next_populated + 1) + end +end + + local bones_def = { description = S("Bones"), tiles = { @@ -79,6 +163,8 @@ local bones_def = { minetest.add_item(pos, "bones:bones") end minetest.remove_node(pos) + else + bones_inv_reorder(meta) end end, @@ -91,7 +177,8 @@ local bones_def = { return end - local inv = minetest.get_meta(pos):get_inventory() + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() local player_inv = player:get_inventory() local has_space = true @@ -106,14 +193,17 @@ local bones_def = { end end - -- remove bones if player emptied them if has_space then + -- remove bones if player emptied them if player_inv:room_for_item("main", {name = "bones:bones"}) then player_inv:add_item("main", {name = "bones:bones"}) else - minetest.add_item(pos,"bones:bones") + minetest.add_item(pos, "bones:bones") end minetest.remove_node(pos) + else + -- reorder items if player haven't emptied the bones + bones_inv_reorder(meta) end end, @@ -132,10 +222,12 @@ local bones_def = { end, } + default.set_inventory_action_loggers(bones_def, "bones") minetest.register_node("bones:bones", bones_def) + local function may_replace(pos, player) local node_name = minetest.get_node(pos).name local node_definition = minetest.registered_nodes[node_name] @@ -171,7 +263,8 @@ local function may_replace(pos, player) return node_definition.buildable_to end -local drop = function(pos, itemstack) + +local function drop(pos, itemstack) local obj = minetest.add_item(pos, itemstack:take_item(itemstack:get_count())) if obj then obj:set_velocity({ @@ -182,41 +275,118 @@ local drop = function(pos, itemstack) end end -local player_inventory_lists = { "main", "craft" } -bones.player_inventory_lists = player_inventory_lists -local function is_all_empty(player_inv) - for _, list_name in ipairs(player_inventory_lists) do - if not player_inv:is_empty(list_name) then - return false +bones.player_inventory_lists = { "main", "craft" } + +local collect_items_callbacks = {} + + +function bones.register_collect_items(func) + table.insert(collect_items_callbacks, func) +end + + +bones.register_collect_items(function(player) + local items = {} + local player_inv = player:get_inventory() + for _, list_name in ipairs(bones.player_inventory_lists) do + local inv_list=player_inv:get_list(list_name) or {} + for _, inv_slot in ipairs(inv_list) do + if inv_slot:get_count() > 0 then + table.insert(items, inv_slot) + end + end + + player_inv:set_list(list_name, {}) + end + return items +end) + + +local function collect_items(player, player_name) + local items = {} + for _, callback in ipairs(collect_items_callbacks) do + table.insert_all(items, callback(player)) + end + return items +end + + +-- Try to find the closest space near the player to place bones +local function find_bones_pos(player) + local rounded_player_pos = vector.round(player:get_pos()) + local bones_pos + if may_replace(rounded_player_pos, player) then + bones_pos = rounded_player_pos + else + bones_pos = minetest.find_node_near(rounded_player_pos, 1, {"air"}) + end + return bones_pos +end + + +local function place_bones(player, bones_pos, items) + local param2 = minetest.dir_to_facedir(player:get_look_dir()) + minetest.set_node(bones_pos, {name = "bones:bones", param2 = param2}) + local bones_meta = minetest.get_meta(bones_pos) + local bones_inv = bones_meta:get_inventory() + -- Make it big enough that anything reasonable will fit + bones_inv:set_size("main", bones_max_slots) + local leftover_items = {} + for _, item in ipairs(items) do + if bones_inv:room_for_item("main", item) then + bones_inv:add_item("main", item) + else + table.insert(leftover_items, item) + end + end + local inv_size = bones_max_slots + for i = 1, bones_max_slots do + if bones_inv:get_stack("main", i):get_count() == 0 then + inv_size = i - 1 + break end end - return true + bones_inv:set_size("main", math.max(inv_size, min_inv_size)) + bones_meta:set_string("formspec", get_bones_formspec_for_size(inv_size)) + -- "Ownership" + local player_name = player:get_player_name() + bones_meta:set_string("owner", player_name) + if share_bones_time ~= 0 then + bones_meta:set_string("infotext", S("@1's fresh bones", player_name)) + if share_bones_time_early == 0 or + not minetest.is_protected(bones_pos, player_name) then + bones_meta:set_int("time", 0) + else + bones_meta:set_int("time", share_bones_time - share_bones_time_early) + end + minetest.get_node_timer(bones_pos):start(10) + else + bones_meta:set_string("infotext", S("@1's bones", player_name)) + end + return leftover_items end + minetest.register_on_dieplayer(function(player) local bones_mode = minetest.settings:get("bones_mode") or "bones" if bones_mode ~= "bones" and bones_mode ~= "drop" and bones_mode ~= "keep" then bones_mode = "bones" end + local player_name = player:get_player_name() local bones_position_message = minetest.settings:get_bool("bones_position_message") == true - local player_name = player:get_player_name() - local pos = vector.round(player:get_pos()) - local pos_string = minetest.pos_to_string(pos) + local pos_string = minetest.pos_to_string(player:get_pos()) - -- return if keep inventory set or in creative mode - if bones_mode == "keep" or minetest.is_creative_enabled(player_name) then - minetest.log("action", player_name .. " dies at " .. pos_string .. - ". No bones placed") - if bones_position_message then - minetest.chat_send_player(player_name, S("@1 died at @2.", player_name, pos_string)) - end - return + local items = {} + + if not minetest.is_creative_enabled(player_name) and + bones_mode ~= "keep" + then + items = collect_items(player, player_name) end - local player_inv = player:get_inventory() - if is_all_empty(player_inv) then + if #items == 0 then minetest.log("action", player_name .. " dies at " .. pos_string .. ". No bones placed") if bones_position_message then @@ -225,71 +395,52 @@ minetest.register_on_dieplayer(function(player) return end - -- check if it's possible to place bones, if not find space near player - if bones_mode == "bones" and not may_replace(pos, player) then - local air = minetest.find_node_near(pos, 1, {"air"}) - if air then - pos = air + local bones_placed, drop_bones = false, false + if bones_mode == "bones" then + local bones_pos = find_bones_pos(player) + if bones_pos then + items = place_bones(player, bones_pos, items) + bones_placed, drop_bones = true, #items ~= 0 else - bones_mode = "drop" + drop_bones = true end + elseif bones_mode == "drop" then + drop_bones = true end - - if bones_mode == "drop" then - for _, list_name in ipairs(player_inventory_lists) do - for i = 1, player_inv:get_size(list_name) do - drop(pos, player_inv:get_stack(list_name, i)) - end - player_inv:set_list(list_name, {}) + if drop_bones then + if not bones_placed then + table.insert(items, ItemStack("bones:bones")) end - drop(pos, ItemStack("bones:bones")) - minetest.log("action", player_name .. " dies at " .. pos_string .. - ". Inventory dropped") - if bones_position_message then - minetest.chat_send_player(player_name, S("@1 died at @2, and dropped their inventory.", player_name, pos_string)) + for _, item in ipairs(items) do + drop(player:get_pos(), item) end - return end - local param2 = minetest.dir_to_facedir(player:get_look_dir()) - minetest.set_node(pos, {name = "bones:bones", param2 = param2}) + local log_message + local chat_message - minetest.log("action", player_name .. " dies at " .. pos_string .. - ". Bones placed") - if bones_position_message then - minetest.chat_send_player(player_name, S("@1 died at @2, and bones were placed.", player_name, pos_string)) - end - - local meta = minetest.get_meta(pos) - local inv = meta:get_inventory() - inv:set_size("main", 8 * 4) - - for _, list_name in ipairs(player_inventory_lists) do - for i = 1, player_inv:get_size(list_name) do - local stack = player_inv:get_stack(list_name, i) - if inv:room_for_item("main", stack) then - inv:add_item("main", stack) - else -- no space left - drop(pos, stack) - end + if bones_placed then + if drop_bones then + log_message = "Inventory partially dropped" + chat_message = NS("@1 died at @2, and partially dropped their inventory.") + else + log_message = "Bones placed" + chat_message = NS("@1 died at @2, and bones were placed.") end - player_inv:set_list(list_name, {}) - end - - meta:set_string("formspec", bones_formspec) - meta:set_string("owner", player_name) - - if share_bones_time ~= 0 then - meta:set_string("infotext", S("@1's fresh bones", player_name)) - - if share_bones_time_early == 0 or not minetest.is_protected(pos, player_name) then - meta:set_int("time", 0) + else + if drop_bones then + log_message = "Inventory dropped" + chat_message = NS("@1 died at @2, and dropped their inventory.") else - meta:set_int("time", (share_bones_time - share_bones_time_early)) + log_message = "No bones placed" + chat_message = NS("@1 died at @2.") end + end - minetest.get_node_timer(pos):start(10) - else - meta:set_string("infotext", S("@1's bones", player_name)) + if bones_position_message then + chat_message = S(chat_message, player_name, pos_string) + minetest.chat_send_player(player_name, chat_message) end + + minetest.log("action", player_name .. " dies at " .. pos_string .. ". " .. log_message) end)