diff --git a/.gitignore b/.gitignore index 4f8931c..d22ff94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .DS_Store docs/build +*.blend1 +*.blend2 +*.old diff --git a/.luacheckrc b/.luacheckrc index 7612294..6d21f31 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -60,5 +60,11 @@ read_globals = { "hb", "mesecon", "armor", - "default" + "default", + "i3", + "unified_inventory", + "player_api", + "u_skins", + "wardrobe", + "3d_armor" } diff --git a/README.md b/README.md index 5be07c1..aa3f7a4 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,17 @@ Video: https://youtu.be/pItpltmUoa8 * quiver shows temporarily its inventory in HUD overlay when loading or shooting (quickview) * quiver item shows its content in infotext (hover over the item) * X Bows API for creating custom shooters and projectiles +* 3d quiver shown in 3rd person view (compatible with 3d armor) ## How To ### Bow With the bow selected in hotbar and in your hand, press right click on mouse (PC) or the same action as when placing blocks, to load the bow. -For bow to be loaded you have to have arrows in the main invetory. Charging bow will have slight sound effect and can be fired at any time with left click (PC) +For bow to be loaded you have to have arrows in the arrow/quiver inventory - there should be extra tab in your inventory MOD to show arrow and quiver inventory slots. +Arrows and quivers in the players main inventory don't count and will not be used. +You have to have arrows and/or quiver in dedicated arrow/quiver inventory slots in order to charge the bow. +Charging bow will have slight sound effect and can be fired at any time with left click (PC) or the same action as when you are digging a block. Waiting for full charge of the bow is recommended as it will give the arrow full speed (maximum shooting distance) and chance for critical arrow (double damage). @@ -56,8 +60,8 @@ If you have `playerphysics` or `player_monoids` mod installed, charged bow will ### Quiver -Quiver item can hold inventory of arrows. When player has quiver in his/hers inventory, bow can take arrows from quiver, otherwise arrows outside of the quiver are used to load the bow. -Though, if arrow from quivers are used to laod the bow, the arrows have additional speed and damage. +Quiver item can hold inventory of arrows. When player has quiver in his/hers quiver inventory slot - there should be extra tab in your inventory MOD to show arrow and quiver inventory slots, bow can take arrows from quiver, otherwise arrows outside of the quiver are used to load the bow. +Though, if arrows from quiver are used to load the bow, the arrows have additional speed and damage. If we are loading/shooting arrows from quiver, there is temporary quickview HUD overlay shown, peeking in to the quivers inventory from which the arrow was taken. Arrows used from quiver will be faster only when the bow is fully charged - see "How To - Bow" for more information on how to know when bow is fully charged. There are few indications on how to know when the bow shot arrow from quiver: @@ -78,6 +82,12 @@ There are few indications on how to know when the bow shot arrow from quiver: - playerphysics (force sneak when holding charged bow) - player_monoids (force sneak when holding charged bow) - wool (quiver recipe) +- i3 +- unified_inventory +- simple_skins +- u_skins +- wardrobe +- sfinv ## License: @@ -106,8 +116,9 @@ Modified by SaKeL: **CC-BY-SA-3.0, by paramat** -- x_bows_quiver_hotbar_selected.png +- x_bows_hotbar_selected.png - x_bows_quiver_hotbar.png +- x_bows_single_hotbar.png **LGPL-2.1-or-later, by SaKeL** @@ -115,6 +126,10 @@ Modified by SaKeL: - x_bows_quiver_open.png - x_bows_arrow_slot.png - x_bows_arrow_mesh.png +- x_bows_quiver_mesh.png +- x_bows_quiver_empty_mesh.png +- x_bows_quiver_blank_mesh.png +- x_bows_quiver_slot.png ### Sounds @@ -160,9 +175,20 @@ Modified by SaKeL: ### Models -****LGPL-2.1-or-later, by SaKeL** +**LGPL-2.1-or-later, by SaKeL** - x_bows_arrow.obj +- x_bows_arrow.blend + +**Original model by MirceaKitsune (CC BY-SA 3.0).** +**Various alterations and fixes by kilbith, sofar, xunto, Rogier-5, TeTpaAka, Desour, stujones11, An0n3m0us (CC BY-SA 3.0):** + +Modified by SaKeL (added quiver): + +- x_bows_3d_armor_character.b3d +- x_bows_3d_armor_character.blend +- x_bows_character.b3d +- x_bows_character.blend ## Installation diff --git a/api.lua b/api.lua index a92ecb9..ae47f68 100644 --- a/api.lua +++ b/api.lua @@ -1,3 +1,7 @@ +local S = minetest.get_translator(minetest.get_current_modname()) + +sfinv = sfinv--[[@as Sfinv]] + ---Check if table contains value ---@param table table ---@param value string|number @@ -21,18 +25,6 @@ local function mergeTables(t1, t2) return t1 end ----create UUID ----@return string -local function uuid() - local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - - ---@diagnostic disable-next-line: redundant-return-value - return string.gsub(template, '[xy]', function (c) - local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) - return string.format('%x', v) - end) -end - ---@type XBows XBows = { pvp = minetest.settings:get_bool('enable_pvp') or false, @@ -40,6 +32,11 @@ XBows = { mesecons = minetest.get_modpath('mesecons'), playerphysics = minetest.get_modpath('playerphysics'), player_monoids = minetest.get_modpath('player_monoids'), + i3 = minetest.get_modpath('i3'), + unified_inventory = minetest.get_modpath('unified_inventory'), + u_skins = minetest.get_modpath('u_skins'), + wardrobe = minetest.get_modpath('wardrobe'), + _3d_armor = minetest.get_modpath('3d_armor'), registered_bows = {}, registered_arrows = {}, registered_quivers = {}, @@ -49,7 +46,8 @@ XBows = { settings = { x_bows_attach_arrows_to_entities = minetest.settings:get_bool('x_bows_attach_arrows_to_entities', false) }, - charge_sound_after_job = {} + charge_sound_after_job = {}, + fallback_quiver = not minetest.global_exists('sfinv') and not minetest.global_exists('unified_inventory') and not minetest.global_exists('i3') } XBows.__index = XBows @@ -57,7 +55,8 @@ XBows.__index = XBows ---@type XBowsQuiver XBowsQuiver = { hud_item_ids = {}, - after_job = {} + after_job = {}, + quiver_empty_state = {} } XBowsQuiver.__index = XBowsQuiver setmetatable(XBowsQuiver, XBows) @@ -68,6 +67,18 @@ local XBowsEntityDef = {} XBowsEntityDef.__index = XBowsEntityDef setmetatable(XBowsEntityDef, XBows) +---create UUID +---@return string +function XBows.uuid() + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + + ---@diagnostic disable-next-line: redundant-return-value + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) +end + ---Check if creative is enabled or if player has creative priv ---@param self XBows ---@param name string @@ -171,20 +182,20 @@ function XBows.register_bow(self, name, def, override) def.custom.gravity = def.custom.gravity or -10 if def.custom.crit_chance then - def.description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Critical Arrow Chance: ' + def.description = def.description .. '\n' .. minetest.colorize('#00FF00', S('Critical Arrow Chance') ..': ' .. (1 / def.custom.crit_chance) * 100 .. '%') end - def.description = def.description .. '\n' .. minetest.colorize('#00BFFF', 'Strength: ' + def.description = def.description .. '\n' .. minetest.colorize('#00BFFF', S('Strength') .. ': ' .. def.custom.strength) if def.custom.allowed_ammunition then local allowed_amm_desc = table.concat(def.custom.allowed_ammunition, '\n') if allowed_amm_desc ~= '' then - def.description = def.description .. '\nAllowed ammunition:\n' .. allowed_amm_desc + def.description = def.description .. '\n' .. S('Allowed ammunition') .. ':\n' .. allowed_amm_desc else - def.description = def.description .. '\nAllowed ammunition: none' + def.description = def.description .. '\n' .. S('Allowed ammunition') .. ': ' .. S('none') end end @@ -286,11 +297,15 @@ function XBows.register_arrow(self, name, def) local mod_name = def.custom.mod_name or 'x_bows' def.custom.name = mod_name .. ':' .. name def.description = def.description or name + def.short_description = def.short_description or name def.custom.tool_capabilities = def.custom.tool_capabilities or { full_punch_interval = 1, max_drop_level = 0, damage_groups = {fleshy=2} } + def.custom.description_abilities = minetest.colorize('#00FF00', S('Damage') .. ': ' + .. def.custom.tool_capabilities.damage_groups.fleshy) .. '\n' .. minetest.colorize('#00BFFF', S('Charge Time') .. ': ' + .. def.custom.tool_capabilities.full_punch_interval .. 's') def.groups = mergeTables({arrow = 1, flammable = 1}, def.groups or {}) def.custom.particle_effect = def.custom.particle_effect or 'arrow' def.custom.particle_effect_crit = def.custom.particle_effect_crit or 'arrow_crit' @@ -304,10 +319,8 @@ function XBows.register_arrow(self, name, def) self.registered_arrows[def.custom.name] = def minetest.register_craftitem(def.custom.name, { - description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Damage: ' - .. def.custom.tool_capabilities.damage_groups.fleshy) .. '\n' .. minetest.colorize('#00BFFF', 'Charge Time: ' - .. def.custom.tool_capabilities.full_punch_interval .. 's'), - short_description = def.description, + description = def.description .. '\n' .. def.custom.description_abilities, + short_description = def.short_description, inventory_image = def.inventory_image, groups = def.groups }) @@ -348,13 +361,13 @@ function XBows.register_quiver(self, name, def) def.custom.groups_charged = mergeTables({quiver = 1, quiver_open = 1, flammable = 1, not_in_creative_inventory = 1}, def.groups or {}) if def.custom.faster_arrows then - def.description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.custom.faster_arrows) * 100 .. '%') - def.short_description = def.short_description .. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.custom.faster_arrows) * 100 .. '%') + def.description = def.description .. '\n' .. minetest.colorize('#00FF00', S('Faster Arrows') .. ': ' .. (1 / def.custom.faster_arrows) * 100 .. '%') + def.short_description = def.short_description .. '\n' .. minetest.colorize('#00FF00', S('Faster Arrows') .. ': ' .. (1 / def.custom.faster_arrows) * 100 .. '%') end if def.custom.add_damage then - def.description = def.description .. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.custom.add_damage) - def.short_description = def.short_description .. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.custom.add_damage) + def.description = def.description .. '\n' .. minetest.colorize('#FF8080', S('Arrow Damage') .. ': +' .. def.custom.add_damage) + def.short_description = def.short_description .. '\n' .. minetest.colorize('#FF8080', S('Arrow Damage') .. ': +' .. def.custom.add_damage) end self.registered_quivers[def.custom.name] = def @@ -438,7 +451,6 @@ end function XBows.load(self, itemstack, user, pointed_thing) local player_name = user:get_player_name() local inv = user:get_inventory()--[[@as InvRef]] - local inv_list = inv:get_list('main') local bow_name = itemstack:get_name() local bow_def = self.registered_bows[bow_name] ---@alias ItemStackArrows {["stack"]: ItemStack, ["idx"]: number|integer}[] @@ -460,6 +472,7 @@ function XBows.load(self, itemstack, user, pointed_thing) local itemstack_arrow = quiver_result.found_arrow_stack if itemstack_arrow then + ---we got arrow from quiver local itemstack_arrow_meta = itemstack_arrow:get_meta() itemstack_arrow_meta:set_int('is_arrow_from_quiver', 1) @@ -467,17 +480,36 @@ function XBows.load(self, itemstack, user, pointed_thing) itemstack_arrow_meta:set_string('quiver_name', quiver_result.quiver_name) itemstack_arrow_meta:set_string('quiver_id', quiver_result.quiver_id) else - XBowsQuiver:remove_hud(user) + if not inv:is_empty('x_bows:arrow_inv') then + XBowsQuiver:udate_or_create_hud(user, inv:get_list('x_bows:arrow_inv')) + else + ---no ammo (fake stack) + XBowsQuiver:udate_or_create_hud(user, {ItemStack({ + name = 'x_bows:no_ammo' + })}) + end ---find itemstack arrow in players inventory - for i, st in ipairs(inv_list) do - local st_name = st:get_name() + local arrow_stack = inv:get_stack('x_bows:arrow_inv', 1) + local is_allowed_ammunition = self:is_allowed_ammunition(bow_name, arrow_stack:get_name()) - if not st:is_empty() and self.registered_arrows[st_name] then - local is_allowed_ammunition = self:is_allowed_ammunition(bow_name, st_name) + if self.registered_arrows[arrow_stack:get_name()] and is_allowed_ammunition then + table.insert(itemstack_arrows, {stack = arrow_stack, idx = 1}) + end - if self.registered_arrows[st_name] and is_allowed_ammunition then - table.insert(itemstack_arrows, {stack = st, idx = i}) + ---if everything else fails + if self.fallback_quiver then + local inv_list = inv:get_list('main') + + for i, st in ipairs(inv_list) do + local st_name = st:get_name() + + if not st:is_empty() and self.registered_arrows[st_name] then + local _is_allowed_ammunition = self:is_allowed_ammunition(bow_name, st_name) + + if self.registered_arrows[st_name] and _is_allowed_ammunition then + table.insert(itemstack_arrows, {stack = st, idx = i}) + end end end end @@ -509,7 +541,7 @@ function XBows.load(self, itemstack, user, pointed_thing) if not self:is_creative(v_user:get_player_name()) and v_itemstack_arrow_meta:get_int('is_arrow_from_quiver') ~= 1 then v_itemstack_arrow:take_item() - v_inv:set_stack('main', v_itemstack_arrows[1].idx, v_itemstack_arrow) + v_inv:set_stack('x_bows:arrow_inv', v_itemstack_arrows[1].idx, v_itemstack_arrow) end end end, user, bow_name, itemstack_arrow, inv, itemstack_arrows) @@ -575,8 +607,22 @@ function XBows.shoot(self, itemstack, user, pointed_thing) if is_arrow_from_quiver == 1 then XBowsQuiver:udate_or_create_hud(user, detached_inv:get_list('main'), found_arrow_stack_idx) + + if detached_inv:is_empty('main') then + XBowsQuiver:show_3d_quiver(user, {is_empty = true}) + else + XBowsQuiver:show_3d_quiver(user) + end else - XBowsQuiver:remove_hud(user) + local inv = user:get_inventory()--[[@as InvRef]] + if not inv:is_empty('x_bows:arrow_inv') then + XBowsQuiver:udate_or_create_hud(user, inv:get_list('x_bows:arrow_inv')) + else + ---no ammo (fake stack) + XBowsQuiver:udate_or_create_hud(user, {ItemStack({ + name = 'x_bows:no_ammo' + })}) + end end local x_bows_registered_arrow_def = self.registered_arrows[arrow_name] @@ -1446,53 +1492,99 @@ function XBowsQuiver.get_itemstack_arrow_from_quiver(self, player) local quiver_id local quiver_name - ---find matching quiver item in players inventory with the open formspec name - if player_inv and player_inv:contains_item('main', 'x_bows:quiver') then - local inv_list = player_inv:get_list('main') + ---check quiver inventory slot + if player_inv and player_inv:contains_item('x_bows:quiver_inv', 'x_bows:quiver') then + local player_name = player:get_player_name() + local quiver_stack = player_inv:get_stack('x_bows:quiver_inv', 1) + local st_meta = quiver_stack:get_meta() + quiver_id = st_meta:get_string('quiver_id') - for i, st in ipairs(inv_list) do - if not st:is_empty() and st:get_name() == 'x_bows:quiver' then - local st_meta = st:get_meta() - local player_name = player:get_player_name() - quiver_id = st_meta:get_string('quiver_id') + local detached_inv = self:get_or_create_detached_inv( + quiver_id, + player_name, + st_meta:get_string('quiver_items') + ) - local detached_inv = self:get_or_create_detached_inv( - quiver_id, - player_name, - st_meta:get_string('quiver_items') - ) + if not detached_inv:is_empty('main') then + local detached_inv_list = detached_inv:get_list('main') - if not detached_inv:is_empty('main') then - local detached_inv_list = detached_inv:get_list('main') + ---find arrows inside quiver inventory + for j, qst in ipairs(detached_inv_list) do + ---save copy of inv list before we take the item + table.insert(prev_detached_inv_list, detached_inv:get_stack('main', j)) - ---find arrows inside quiver inventory - for j, qst in ipairs(detached_inv_list) do - ---save copy of inv list before we take the item - table.insert(prev_detached_inv_list, detached_inv:get_stack('main', j)) + if not qst:is_empty() and not found_arrow_stack then + local is_allowed_ammunition = self:is_allowed_ammunition(wielded_stack:get_name(), qst:get_name()) - if not qst:is_empty() and not found_arrow_stack then - local is_allowed_ammunition = self:is_allowed_ammunition(wielded_stack:get_name(), qst:get_name()) + if is_allowed_ammunition then + quiver_name = quiver_stack:get_name() + found_arrow_stack = qst:take_item() + found_arrow_stack_idx = j - if is_allowed_ammunition then - quiver_name = st:get_name() - found_arrow_stack = qst:take_item() - found_arrow_stack_idx = j + if not self:is_creative(player_name) then + detached_inv:set_list('main', detached_inv_list) + self:save(detached_inv, player, true) + end + end + end + end + end - if not self:is_creative(player_name) then - detached_inv:set_list('main', detached_inv_list) - self:save(detached_inv, player, true) + if found_arrow_stack then + ---show HUD - quiver inventory + self:udate_or_create_hud(player, prev_detached_inv_list, found_arrow_stack_idx) + end + end + + if self.fallback_quiver then + ---find matching quiver item in players inventory with the open formspec name + if player_inv and player_inv:contains_item('main', 'x_bows:quiver') then + local inv_list = player_inv:get_list('main') + + for i, st in ipairs(inv_list) do + if not st:is_empty() and st:get_name() == 'x_bows:quiver' then + local st_meta = st:get_meta() + local player_name = player:get_player_name() + quiver_id = st_meta:get_string('quiver_id') + + local detached_inv = self:get_or_create_detached_inv( + quiver_id, + player_name, + st_meta:get_string('quiver_items') + ) + + if not detached_inv:is_empty('main') then + local detached_inv_list = detached_inv:get_list('main') + + ---find arrows inside quiver inventory + for j, qst in ipairs(detached_inv_list) do + ---save copy of inv list before we take the item + table.insert(prev_detached_inv_list, detached_inv:get_stack('main', j)) + + if not qst:is_empty() and not found_arrow_stack then + local is_allowed_ammunition = self:is_allowed_ammunition(wielded_stack:get_name(), qst:get_name()) + + if is_allowed_ammunition then + quiver_name = st:get_name() + found_arrow_stack = qst:take_item() + found_arrow_stack_idx = j + + if not self:is_creative(player_name) then + detached_inv:set_list('main', detached_inv_list) + self:save(detached_inv, player, true) + end end end end end end - end - if found_arrow_stack then - ---show HUD - quiver inventory - self:udate_or_create_hud(player, prev_detached_inv_list, found_arrow_stack_idx) + if found_arrow_stack then + ---show HUD - quiver inventory + self:udate_or_create_hud(player, prev_detached_inv_list, found_arrow_stack_idx) - break + break + end end end end @@ -1546,7 +1638,27 @@ function XBowsQuiver.udate_or_create_hud(self, player, inv_list, idx) local _idx = idx or 1 local player_name = player:get_player_name() local selected_bg_added = false + local is_arrow = #inv_list == 1 + local item_def = minetest.registered_items['x_bows:quiver'] + local is_no_ammo = false + if is_arrow then + item_def = minetest.registered_items[inv_list[1]:get_name()] + is_no_ammo = inv_list[1]:get_name() == 'x_bows:no_ammo' + end + + if is_no_ammo then + item_def = { + inventory_image = 'x_bows_arrow_slot.png', + short_description = S('No Ammo') .. '!' + } + end + + if not item_def then + return + end + + ---cancel previous timeouts and reset if self.after_job[player_name] then for _, v in pairs(self.after_job[player_name]) do v:cancel() @@ -1564,18 +1676,17 @@ function XBowsQuiver.udate_or_create_hud(self, player, inv_list, idx) hud_elem_type = 'image', position = {x = 1, y = 0.5}, offset = {x = -120, y = -140}, - text = 'x_bows_quiver.png', + text = item_def.inventory_image, scale = {x = 4, y = 4}, alignment = 0, }) ---title copy - local quiver_def = minetest.registered_items['x_bows:quiver'] self.hud_item_ids[player_name].title_copy = player:hud_add({ hud_elem_type = 'text', position = {x = 1, y = 0.5}, offset = {x = -120, y = -75}, - text = quiver_def.short_description, + text = item_def.short_description, alignment = 0, scale = {x = 100, y = 30}, number = 0xFFFFFF, @@ -1586,7 +1697,7 @@ function XBowsQuiver.udate_or_create_hud(self, player, inv_list, idx) hud_elem_type = 'image', position = {x = 1, y = 0.5}, offset = {x = -238, y = 0}, - text = 'x_bows_quiver_hotbar.png', + text = is_arrow and 'x_bows_single_hotbar.png' or 'x_bows_quiver_hotbar.png', scale = {x = 1, y = 1}, alignment = {x = 1, y = 0 }, }) @@ -1595,6 +1706,10 @@ function XBowsQuiver.udate_or_create_hud(self, player, inv_list, idx) if not qst:is_empty() then local found_arrow_stack_def = minetest.registered_items[qst:get_name()] + if is_no_ammo then + found_arrow_stack_def = item_def + end + if not selected_bg_added and j == _idx then selected_bg_added = true @@ -1603,7 +1718,7 @@ function XBowsQuiver.udate_or_create_hud(self, player, inv_list, idx) hud_elem_type = 'image', position = {x = 1, y = 0.5}, offset = {x = -308 + (j * 74), y = 2}, - text = 'x_bows_quiver_hotbar_selected.png', + text = 'x_bows_hotbar_selected.png', scale = {x = 1, y = 1}, alignment = {x = 1, y = 0 }, }) @@ -1625,7 +1740,7 @@ function XBowsQuiver.udate_or_create_hud(self, player, inv_list, idx) hud_elem_type = 'text', position = {x = 1, y = 0.5}, offset = {x = -244 + (j * 74), y = 23}, - text = qst:get_count(), + text = is_no_ammo and 0 or qst:get_count(), alignment = -1, scale = {x = 50, y = 10}, number = 0xFFFFFF, @@ -1709,6 +1824,16 @@ function XBowsQuiver.get_or_create_detached_inv(self, quiver_id, player_name, qu ---@param stack ItemStack stack of item what was put ---@param player ObjectRef on_put = function(inv, listname, index, stack, player) + local quiver_inv_st = player:get_inventory():get_stack('x_bows:quiver_inv', 1) + + if quiver_inv_st and quiver_inv_st:get_meta():get_string('quiver_id') == inv:get_location().name then + if inv:is_empty('main') then + self:show_3d_quiver(player, {is_empty = true}) + else + self:show_3d_quiver(player) + end + end + self:save(inv, player) end, ---@param inv InvRef detached inventory @@ -1717,6 +1842,16 @@ function XBowsQuiver.get_or_create_detached_inv(self, quiver_id, player_name, qu ---@param stack ItemStack ---@param player ObjectRef on_take = function(inv, listname, index, stack, player) + local quiver_inv_st = player:get_inventory():get_stack('x_bows:quiver_inv', 1) + + if quiver_inv_st and quiver_inv_st:get_meta():get_string('quiver_id') == inv:get_location().name then + if inv:is_empty('main') then + self:show_3d_quiver(player, {is_empty = true}) + else + self:show_3d_quiver(player) + end + end + self:save(inv, player) end, }, player_name) @@ -1742,14 +1877,18 @@ function XBowsQuiver.get_formspec(self, name) local list_w = 8 local list_pos_x = (list_w - width) / 2 - local formspec = - 'size['..list_w..',6]' .. - 'list[detached:'..name..';main;'..list_pos_x..',0.3;'..width..',1;]'.. - 'list[current_player;main;0,'..(height + 0.85)..';'..list_w..',1;]'.. - 'list[current_player;main;0,'..(height + 2.08)..';'..list_w..',3;8]'.. - 'listring[detached:'..name..';main]'.. - 'listring[current_player;main]'.. - default.get_hotbar_bg(0, height + 0.85) + local formspec = { + 'size['..list_w..',6]' , + 'list[detached:'..name..';main;'..list_pos_x..',0.3;'..width..',1;]', + 'list[current_player;main;0,'..(height + 0.85)..';'..list_w..',1;]', + 'list[current_player;main;0,'..(height + 2.08)..';'..list_w..',3;8]', + 'listring[detached:'..name..';main]', + 'listring[current_player;main]' + } + + if minetest.global_exists('default') then + formspec[#formspec + 1] = default.get_hotbar_bg(0, height + 0.85) + end --update formspec local inv = minetest.get_inventory({type='detached', name=name}) @@ -1760,13 +1899,14 @@ function XBowsQuiver.get_formspec(self, name) for i = 1, 3 do if not invlist or invlist[i]:is_empty() then - formspec = formspec .. - 'image[' .. px .. ',' .. py .. ';1,1;x_bows_arrow_slot.png]' + formspec[#formspec + 1] = 'image[' .. px .. ',' .. py .. ';1,1;x_bows_arrow_slot.png]' end px = px + 1 end + formspec = table.concat(formspec, '') + return formspec end @@ -1790,7 +1930,7 @@ function XBowsQuiver.get_string_from_inv(self, inv) return { inv_string = minetest.serialize(t), - content_description = content_description == '' and '\nEmpty' or content_description + content_description = content_description == '' and '\n' .. S('Empty') or content_description } end @@ -1816,12 +1956,25 @@ end ---@param quiver_is_closed? boolean ---@return nil function XBowsQuiver.save(self, inv, player, quiver_is_closed) - local player_inv = player:get_inventory() + local player_inv = player:get_inventory()--[[@as InvRef]] local inv_loc = inv:get_location() local quiver_item_name = quiver_is_closed and 'x_bows:quiver' or 'x_bows:quiver_open' + local player_quiver_inv_stack = player_inv:get_stack('x_bows:quiver_inv', 1) - ---find matching quiver item in players inventory with the open formspec name - if player_inv and player_inv:contains_item('main', quiver_item_name) then + if not player_quiver_inv_stack:is_empty() and player_quiver_inv_stack:get_meta():get_string('quiver_id') == inv_loc.name then + local st_meta = player_quiver_inv_stack:get_meta() + ---save inventory items in quiver item meta + local string_from_inventory_result = self:get_string_from_inv(inv) + + st_meta:set_string('quiver_items', string_from_inventory_result.inv_string) + + ---update description + local new_description = player_quiver_inv_stack:get_short_description()..'\n'..string_from_inventory_result.content_description..'\n' + + st_meta:set_string('description', new_description) + player_inv:set_stack('x_bows:quiver_inv', 1, player_quiver_inv_stack) + elseif player_inv and player_inv:contains_item('main', quiver_item_name) then + ---find matching quiver item in players inventory with the open formspec name local inv_list = player_inv:get_list('main') for i, st in ipairs(inv_list) do @@ -1851,11 +2004,16 @@ end ---@param player ObjectRef ---@return boolean function XBowsQuiver.quiver_can_allow(self, inv, player) - local player_inv = player:get_inventory() + local player_inv = player:get_inventory()--[[@as InvRef]] local inv_loc = inv:get_location() + local player_quiver_inv_stack = player_inv:get_stack('x_bows:quiver_inv', 1) - ---find matching quiver item in players inventory with the open formspec name - if player_inv and player_inv:contains_item('main', 'x_bows:quiver_open') then + if not player_quiver_inv_stack:is_empty() and player_quiver_inv_stack:get_meta():get_string('quiver_id') == inv_loc.name then + ---find quiver in player `quiver_inv` inv list + return true + elseif player_inv and player_inv:contains_item('main', 'x_bows:quiver_open') then + ---find quiver in player `main` inv list + ---matching quiver item in players inventory with the open formspec name local inv_list = player_inv:get_list('main') for i, st in ipairs(inv_list) do @@ -1882,7 +2040,7 @@ function XBows.open_quiver(self, itemstack, user) ---create inventory id and save it if quiver_id == '' then - quiver_id = itemstack:get_name()..'_'..uuid() + quiver_id = itemstack:get_name()..'_'..self.uuid() itemstack_meta:set_string('quiver_id', quiver_id) end @@ -1903,3 +2061,304 @@ function XBows.open_quiver(self, itemstack, user) minetest.show_formspec(pname, quiver_id, XBowsQuiver:get_formspec(quiver_id)) return itemstack end + +---Register sfinv page +---@param self XBowsQuiver +function XBowsQuiver.sfinv_register_page(self) + sfinv.register_page('x_bows:quiver_page', { + title = 'X Bows', + get = function(this, player, context) + local formspec = { + ---arrow + 'label[0,0;' .. minetest.formspec_escape(S('Arrows')) .. ':]', + 'list[current_player;x_bows:arrow_inv;0,0.5;1,1;]', + 'image[0,0.5;1,1;x_bows_arrow_slot.png;]', + 'listring[current_player;x_bows:arrow_inv]', + 'listring[current_player;main]', + ---quiver + 'label[3.5,0;' .. minetest.formspec_escape(S('Quiver')) .. ':]', + 'list[current_player;x_bows:quiver_inv;3.5,0.5;1,1;]', + 'image[3.5,0.5;1,1;x_bows_quiver_slot.png]', + 'listring[current_player;x_bows:quiver_inv]', + 'listring[current_player;main]', + } + + local player_inv = player:get_inventory() + context._itemstack_arrow = player_inv:get_stack('x_bows:arrow_inv', 1) + context._itemstack_quiver = player_inv:get_stack('x_bows:quiver_inv', 1) + + if context._itemstack_arrow and not context._itemstack_arrow:is_empty() then + local x_bows_registered_arrow_def = self.registered_arrows[context._itemstack_arrow:get_name()] + + if x_bows_registered_arrow_def then + formspec[#formspec + 1] = 'label[0,1.5;' .. minetest.formspec_escape(context._itemstack_arrow:get_short_description()) .. '\n'.. minetest.formspec_escape(x_bows_registered_arrow_def.custom.description_abilities) ..']' + end + end + + + if context._itemstack_quiver and not context._itemstack_quiver:is_empty() then + local st_meta = context._itemstack_quiver:get_meta() + local quiver_id = st_meta:get_string('quiver_id') + + ---description + formspec[#formspec + 1] = 'label[3.5,1.5;' .. minetest.formspec_escape(context._itemstack_quiver:get_short_description()) .. ']' + formspec[#formspec + 1] = 'list[detached:'..quiver_id..';main;4.5,0.5;3,1;]' + formspec[#formspec + 1] = 'listring[detached:'..quiver_id..';main]' + formspec[#formspec + 1] = 'listring[current_player;main]' + end + + return sfinv.make_formspec(player, context, table.concat(formspec, ''), true) + end + }) +end + +---Register i3 page +function XBowsQuiver.i3_register_page(self) + i3.new_tab('x_bows:quiver_page', { + description = 'X Bows', + formspec = function(player, data, fs) + local formspec = { + ---arrow + 'label[0.5,1;' .. minetest.formspec_escape(S('Arrows')) .. ':]', + 'list[current_player;x_bows:arrow_inv;0.5,1.5;1,1;]', + 'listring[current_player;x_bows:arrow_inv]', + 'listring[current_player;main]', + ---quiver + 'label[5,1;' .. minetest.formspec_escape(S('Quiver')) .. ':]', + 'list[current_player;x_bows:quiver_inv;5,1.5;1,1;]', + 'listring[current_player;x_bows:quiver_inv]', + 'listring[current_player;main]', + ---main + 'background9[0,0;10.23,12;i3_bg_full.png;false;12]', + 'listcolors[#bababa50;#bababa99]', + 'style_type[box;colors=#77777710,#77777710,#777,#777]', + 'box[0.22,6.9;1,1;]', + 'box[1.32,6.9;1,1;]', + 'box[2.42,6.9;1,1;]', + 'box[3.52,6.9;1,1;]', + 'box[4.62,6.9;1,1;]', + 'box[5.72,6.9;1,1;]', + 'box[6.82,6.9;1,1;]', + 'box[7.92,6.9;1,1;]', + 'box[9.02,6.9;1,1;]', + 'style_type[list;size=1;spacing=0.1]', + 'list[current_player;main;0.22,6.9;9,1;]', + 'style_type[list;size=1;spacing=0.1,0.1]', + 'list[current_player;main;0.22,8.05;9,4;9]', + 'style_type[list;size=1;spacing=0.15]', + 'listring[current_player;craft]listring[current_player;main]' + } + + local context = {} + local player_inv = player:get_inventory() + context._itemstack_arrow = player_inv:get_stack('x_bows:arrow_inv', 1) + context._itemstack_quiver = player_inv:get_stack('x_bows:quiver_inv', 1) + + if context._itemstack_arrow and not context._itemstack_arrow:is_empty() then + local x_bows_registered_arrow_def = self.registered_arrows[context._itemstack_arrow:get_name()] + + if x_bows_registered_arrow_def then + formspec[#formspec + 1] = 'label[0.5,3;' .. minetest.formspec_escape(context._itemstack_arrow:get_short_description()) .. '\n'.. minetest.formspec_escape(x_bows_registered_arrow_def.custom.description_abilities) ..']' + end + end + + + if context._itemstack_quiver and not context._itemstack_quiver:is_empty() then + local st_meta = context._itemstack_quiver:get_meta() + local quiver_id = st_meta:get_string('quiver_id') + + ---description + formspec[#formspec + 1] = 'label[5,3;' .. minetest.formspec_escape(context._itemstack_quiver:get_short_description()) .. ']' + formspec[#formspec + 1] = 'list[detached:'..quiver_id..';main;6.3,1.5;3,1;]' + formspec[#formspec + 1] = 'listring[detached:'..quiver_id..';main]' + formspec[#formspec + 1] = 'listring[current_player;main]' + end + + formspec = table.concat(formspec, '') + + fs(formspec) + end + }) +end + +---Register i3 page +function XBowsQuiver.ui_register_page(self) + unified_inventory.register_page('x_bows:quiver_page', { + get_formspec = function(player, data, fs) + local formspec = { + unified_inventory.style_full.standard_inv_bg, + 'listcolors[#00000000;#00000000]', + ---arrow + 'label[0.5,0.5;' .. minetest.formspec_escape(S('Arrows')) .. ':]', + unified_inventory.single_slot(0.4,0.9), + 'list[current_player;x_bows:arrow_inv;0.5,1;1,1;]', + 'listring[current_player;x_bows:arrow_inv]', + 'listring[current_player;main]', + ---quiver + 'label[5,0.5;' .. minetest.formspec_escape(S('Quiver')) .. ':]', + unified_inventory.single_slot(4.9,0.9), + 'list[current_player;x_bows:quiver_inv;5,1;1,1;]', + 'listring[current_player;x_bows:quiver_inv]', + 'listring[current_player;main]', + } + + local context = {} + context._itemstack_arrow = player:get_inventory():get_stack('x_bows:arrow_inv', 1) + context._itemstack_quiver = player:get_inventory():get_stack('x_bows:quiver_inv', 1) + + if context._itemstack_arrow and not context._itemstack_arrow:is_empty() then + local x_bows_registered_arrow_def = self.registered_arrows[context._itemstack_arrow:get_name()] + + if x_bows_registered_arrow_def then + formspec[#formspec + 1] = 'label[0.5,2.5;' .. minetest.formspec_escape(context._itemstack_arrow:get_short_description()) .. '\n'.. minetest.formspec_escape(x_bows_registered_arrow_def.custom.description_abilities) ..']' + end + end + + + if context._itemstack_quiver and not context._itemstack_quiver:is_empty() then + local st_meta = context._itemstack_quiver:get_meta() + local quiver_id = st_meta:get_string('quiver_id') + + ---description + formspec[#formspec + 1] = 'label[5,2.5;' .. minetest.formspec_escape(context._itemstack_quiver:get_short_description()) .. ']' + formspec[#formspec + 1] = unified_inventory.single_slot(6.4,0.9) + formspec[#formspec + 1] = unified_inventory.single_slot(7.65,0.9) + formspec[#formspec + 1] = unified_inventory.single_slot(8.9,0.9) + formspec[#formspec + 1] = 'list[detached:'..quiver_id..';main;6.5,1;3,1;]' + formspec[#formspec + 1] = 'listring[detached:'..quiver_id..';main]' + formspec[#formspec + 1] = 'listring[current_player;main]' + end + + return { + formspec = table.concat(formspec, '') + } + end + }) + + unified_inventory.register_button('x_bows:quiver_page', { + type = 'image', + image = "x_bows_bow_wood_charged.png", + tooltip = 'X Bows', + }) +end + +function XBowsQuiver.show_3d_quiver(self, player, props) + local _props = props or {} + local p_name = player:get_player_name() + local quiver_texture = 'x_bows_quiver_mesh.png' + local player_textures + + if _props.is_empty then + quiver_texture = 'x_bows_quiver_empty_mesh.png' + end + + if self._3d_armor then + minetest.after(0.1, function() + player_textures = { + armor.textures[p_name].skin, + armor.textures[p_name].armor, + armor.textures[p_name].wielditem, + quiver_texture + } + + if player_textures then + if _props.is_empty and not self.quiver_empty_state[player:get_player_name()] then + self.quiver_empty_state[player:get_player_name()] = true + player_api.set_textures(player, player_textures) + elseif not _props.is_empty and self.quiver_empty_state[player:get_player_name()] then + self.quiver_empty_state[player:get_player_name()] = false + player_api.set_textures(player, player_textures) + end + end + end) + + return + elseif self.u_skins then + local u_skin_texture = u_skins.u_skins[p_name] + + player_textures = { + u_skin_texture .. '.png', + quiver_texture + } + elseif self.wardrobe and wardrobe.playerSkins and wardrobe.playerSkins[p_name] then + player_textures = { + wardrobe.playerSkins[p_name], + quiver_texture + } + else + local textures = player_api.get_textures(player) + + ---cleanup + for index, value in ipairs(textures) do + if value == 'x_bows_quiver_blank_mesh.png' or value == 'x_bows_quiver_mesh.png' or value == 'x_bows_quiver_empty_mesh.png' then + table.remove(textures, index) + end + end + + table.insert(textures, quiver_texture) + + player_textures = textures + end + + if player_textures then + if _props.is_empty and not self.quiver_empty_state[player:get_player_name()] then + self.quiver_empty_state[player:get_player_name()] = true + player_api.set_textures(player, player_textures) + elseif not _props.is_empty and self.quiver_empty_state[player:get_player_name()] then + self.quiver_empty_state[player:get_player_name()] = false + player_api.set_textures(player, player_textures) + end + end +end + +function XBowsQuiver.hide_3d_quiver(self, player) + local p_name = player:get_player_name() + local player_textures + + if self._3d_armor then + minetest.after(0.1, function() + player_textures = { + armor.textures[p_name].skin, + armor.textures[p_name].armor, + armor.textures[p_name].wielditem, + 'x_bows_quiver_blank_mesh.png' + } + + if player_textures then + player_api.set_textures(player, player_textures) + end + + end) + + return + elseif self.u_skins then + local u_skin_texture = u_skins.u_skins[p_name] + + player_textures = { + u_skin_texture .. '.png', + 'x_bows_quiver_blank_mesh.png' + } + elseif self.wardrobe and wardrobe.playerSkins and wardrobe.playerSkins[p_name] then + player_textures = { + wardrobe.playerSkins[p_name], + 'x_bows_quiver_blank_mesh.png' + } + else + local textures = player_api.get_textures(player) + + ---cleanup + for index, value in ipairs(textures) do + if value == 'x_bows_quiver_mesh.png' or value == 'x_bows_quiver_blank_mesh.png' or value == 'x_bows_quiver_empty_mesh.png' then + table.remove(textures, index) + end + end + + table.insert(textures, 'x_bows_quiver_blank_mesh.png') + + player_textures = textures + end + + if player_textures then + player_api.set_textures(player, player_textures) + end +end diff --git a/i18n.py b/i18n.py new file mode 100755 index 0000000..da1c825 --- /dev/null +++ b/i18n.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Script to generate the template file and update the translation files. +# Copy the script into the mod or modpack root folder and run it there. +# +# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer +# LGPLv2.1+ +# +# See https://github.com/minetest-tools/update_translations for +# potential future updates to this script. + +from __future__ import print_function +import os, fnmatch, re, shutil, errno +from sys import argv as _argv +from sys import stderr as _stderr + +# Running params +params = {"recursive": False, + "help": False, + "mods": False, + "verbose": False, + "folders": [], + "no-old-file": False, + "break-long-lines": False, + "sort": False, + "print-source": False, + "truncate-unused": False, +} +# Available CLI options +options = {"recursive": ['--recursive', '-r'], + "help": ['--help', '-h'], + "mods": ['--installed-mods', '-m'], + "verbose": ['--verbose', '-v'], + "no-old-file": ['--no-old-file', '-O'], + "break-long-lines": ['--break-long-lines', '-b'], + "sort": ['--sort', '-s'], + "print-source": ['--print-source', '-p'], + "truncate-unused": ['--truncate-unused', '-t'], +} + +# Strings longer than this will have extra space added between +# them in the translation files to make it easier to distinguish their +# beginnings and endings at a glance +doublespace_threshold = 80 + +def set_params_folders(tab: list): + '''Initialize params["folders"] from CLI arguments.''' + # Discarding argument 0 (tool name) + for param in tab[1:]: + stop_param = False + for option in options: + if param in options[option]: + stop_param = True + break + if not stop_param: + params["folders"].append(os.path.abspath(param)) + +def set_params(tab: list): + '''Initialize params from CLI arguments.''' + for option in options: + for option_name in options[option]: + if option_name in tab: + params[option] = True + break + +def print_help(name): + '''Prints some help message.''' + print(f'''SYNOPSIS + {name} [OPTIONS] [PATHS...] +DESCRIPTION + {', '.join(options["help"])} + prints this help message + {', '.join(options["recursive"])} + run on all subfolders of paths given + {', '.join(options["mods"])} + run on locally installed modules + {', '.join(options["no-old-file"])} + do not create *.old files + {', '.join(options["sort"])} + sort output strings alphabetically + {', '.join(options["break-long-lines"])} + add extra line breaks before and after long strings + {', '.join(options["print-source"])} + add comments denoting the source file + {', '.join(options["verbose"])} + add output information + {', '.join(options["truncate-unused"])} + delete unused strings from files +''') + + +def main(): + '''Main function''' + set_params(_argv) + set_params_folders(_argv) + if params["help"]: + print_help(_argv[0]) + elif params["recursive"] and params["mods"]: + print("Option --installed-mods is incompatible with --recursive") + else: + # Add recursivity message + print("Running ", end='') + if params["recursive"]: + print("recursively ", end='') + # Running + if params["mods"]: + print(f"on all locally installed modules in {os.path.expanduser('~/.minetest/mods/')}") + run_all_subfolders(os.path.expanduser("~/.minetest/mods")) + elif len(params["folders"]) >= 2: + print("on folder list:", params["folders"]) + for f in params["folders"]: + if params["recursive"]: + run_all_subfolders(f) + else: + update_folder(f) + elif len(params["folders"]) == 1: + print("on folder", params["folders"][0]) + if params["recursive"]: + run_all_subfolders(params["folders"][0]) + else: + update_folder(params["folders"][0]) + else: + print("on folder", os.path.abspath("./")) + if params["recursive"]: + run_all_subfolders(os.path.abspath("./")) + else: + update_folder(os.path.abspath("./")) + +#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ') +#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote +pattern_lua_s = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL) +pattern_lua_fs = re.compile(r'[\.=^\t,{\(\s]N?FS\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL) +pattern_lua_bracketed_s = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL) +pattern_lua_bracketed_fs = re.compile(r'[\.=^\t,{\(\s]N?FS\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL) + +# Handles "concatenation" .. " of strings" +pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL) + +pattern_tr = re.compile(r'(.*?[^@])=(.*)') +pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)') +pattern_tr_filename = re.compile(r'\.tr$') +pattern_po_language_code = re.compile(r'(.*)\.po$') + +#attempt to read the mod's name from the mod.conf file or folder name. Returns None on failure +def get_modname(folder): + try: + with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf: + for line in mod_conf: + match = pattern_name.match(line) + if match: + return match.group(1) + except FileNotFoundError: + if not os.path.isfile(os.path.join(folder, "modpack.txt")): + folder_name = os.path.basename(folder) + # Special case when run in Minetest's builtin directory + if folder_name == "builtin": + return "__builtin" + else: + return folder_name + else: + return None + return None + +#If there are already .tr files in /locale, returns a list of their names +def get_existing_tr_files(folder): + out = [] + for root, dirs, files in os.walk(os.path.join(folder, 'locale/')): + for name in files: + if pattern_tr_filename.search(name): + out.append(name) + return out + +# A series of search and replaces that massage a .po file's contents into +# a .tr file's equivalent +def process_po_file(text): + # The first three items are for unused matches + text = re.sub(r'#~ msgid "', "", text) + text = re.sub(r'"\n#~ msgstr ""\n"', "=", text) + text = re.sub(r'"\n#~ msgstr "', "=", text) + # comment lines + text = re.sub(r'#.*\n', "", text) + # converting msg pairs into "=" pairs + text = re.sub(r'msgid "', "", text) + text = re.sub(r'"\nmsgstr ""\n"', "=", text) + text = re.sub(r'"\nmsgstr "', "=", text) + # various line breaks and escape codes + text = re.sub(r'"\n"', "", text) + text = re.sub(r'"\n', "\n", text) + text = re.sub(r'\\"', '"', text) + text = re.sub(r'\\n', '@n', text) + # remove header text + text = re.sub(r'=Project-Id-Version:.*\n', "", text) + # remove double-spaced lines + text = re.sub(r'\n\n', '\n', text) + return text + +# Go through existing .po files and, if a .tr file for that language +# *doesn't* exist, convert it and create it. +# The .tr file that results will subsequently be reprocessed so +# any "no longer used" strings will be preserved. +# Note that "fuzzy" tags will be lost in this process. +def process_po_files(folder, modname): + for root, dirs, files in os.walk(os.path.join(folder, 'locale/')): + for name in files: + code_match = pattern_po_language_code.match(name) + if code_match == None: + continue + language_code = code_match.group(1) + tr_name = f'{modname}.{language_code}.tr' + tr_file = os.path.join(root, tr_name) + if os.path.exists(tr_file): + if params["verbose"]: + print(f"{tr_name} already exists, ignoring {name}") + continue + fname = os.path.join(root, name) + with open(fname, "r", encoding='utf-8') as po_file: + if params["verbose"]: + print(f"Importing translations from {name}") + text = process_po_file(po_file.read()) + with open(tr_file, "wt", encoding='utf-8') as tr_out: + tr_out.write(text) + +# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612 +# Creates a directory if it doesn't exist, silently does +# nothing if it already exists +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: raise + +# Converts the template dictionary to a text to be written as a file +# dKeyStrings is a dictionary of localized string to source file sets +# dOld is a dictionary of existing translations and comments from +# the previous version of this text +def strings_to_text(dkeyStrings, dOld, mod_name, header_comments): + lOut = [f"# textdomain: {mod_name}"] + if header_comments is not None: + lOut.append(header_comments) + + dGroupedBySource = {} + + for key in dkeyStrings: + sourceList = list(dkeyStrings[key]) + if params["sort"]: + sourceList.sort() + sourceString = "\n".join(sourceList) + listForSource = dGroupedBySource.get(sourceString, []) + listForSource.append(key) + dGroupedBySource[sourceString] = listForSource + + lSourceKeys = list(dGroupedBySource.keys()) + lSourceKeys.sort() + for source in lSourceKeys: + localizedStrings = dGroupedBySource[source] + if params["sort"]: + localizedStrings.sort() + if params["print-source"]: + if lOut[-1] != "": + lOut.append("") + lOut.append(source) + for localizedString in localizedStrings: + val = dOld.get(localizedString, {}) + translation = val.get("translation", "") + comment = val.get("comment") + if params["break-long-lines"] and len(localizedString) > doublespace_threshold and not lOut[-1] == "": + lOut.append("") + if comment != None and comment != "" and not comment.startswith("# textdomain:"): + lOut.append(comment) + lOut.append(f"{localizedString}={translation}") + if params["break-long-lines"] and len(localizedString) > doublespace_threshold: + lOut.append("") + + + unusedExist = False + if not params["truncate-unused"]: + for key in dOld: + if key not in dkeyStrings: + val = dOld[key] + translation = val.get("translation") + comment = val.get("comment") + # only keep an unused translation if there was translated + # text or a comment associated with it + if translation != None and (translation != "" or comment): + if not unusedExist: + unusedExist = True + lOut.append("\n\n##### not used anymore #####\n") + if params["break-long-lines"] and len(key) > doublespace_threshold and not lOut[-1] == "": + lOut.append("") + if comment != None: + lOut.append(comment) + lOut.append(f"{key}={translation}") + if params["break-long-lines"] and len(key) > doublespace_threshold: + lOut.append("") + return "\n".join(lOut) + '\n' + +# Writes a template.txt file +# dkeyStrings is the dictionary returned by generate_template +def write_template(templ_file, dkeyStrings, mod_name): + # read existing template file to preserve comments + existing_template = import_tr_file(templ_file) + + text = strings_to_text(dkeyStrings, existing_template[0], mod_name, existing_template[2]) + mkdir_p(os.path.dirname(templ_file)) + with open(templ_file, "wt", encoding='utf-8') as template_file: + template_file.write(text) + + +# Gets all translatable strings from a lua file +def read_lua_file_strings(lua_file): + lOut = [] + with open(lua_file, encoding='utf-8') as text_file: + text = text_file.read() + #TODO remove comments here + + text = re.sub(pattern_concat, "", text) + + strings = [] + for s in pattern_lua_s.findall(text): + strings.append(s[1]) + for s in pattern_lua_bracketed_s.findall(text): + strings.append(s) + for s in pattern_lua_fs.findall(text): + strings.append(s[1]) + for s in pattern_lua_bracketed_fs.findall(text): + strings.append(s) + + for s in strings: + s = re.sub(r'"\.\.\s+"', "", s) + s = re.sub("@[^@=0-9]", "@@", s) + s = s.replace('\\"', '"') + s = s.replace("\\'", "'") + s = s.replace("\n", "@n") + s = s.replace("\\n", "@n") + s = s.replace("=", "@=") + lOut.append(s) + return lOut + +# Gets strings from an existing translation file +# returns both a dictionary of translations +# and the full original source text so that the new text +# can be compared to it for changes. +# Returns also header comments in the third return value. +def import_tr_file(tr_file): + dOut = {} + text = None + header_comment = None + if os.path.exists(tr_file): + with open(tr_file, "r", encoding='utf-8') as existing_file : + # save the full text to allow for comparison + # of the old version with the new output + text = existing_file.read() + existing_file.seek(0) + # a running record of the current comment block + # we're inside, to allow preceeding multi-line comments + # to be retained for a translation line + latest_comment_block = None + for line in existing_file.readlines(): + line = line.rstrip('\n') + if line.startswith("###"): + if header_comment is None and not latest_comment_block is None: + # Save header comments + header_comment = latest_comment_block + # Strip textdomain line + tmp_h_c = "" + for l in header_comment.split('\n'): + if not l.startswith("# textdomain:"): + tmp_h_c += l + '\n' + header_comment = tmp_h_c + + # Reset comment block if we hit a header + latest_comment_block = None + continue + elif line.startswith("#"): + # Save the comment we're inside + if not latest_comment_block: + latest_comment_block = line + else: + latest_comment_block = latest_comment_block + "\n" + line + continue + match = pattern_tr.match(line) + if match: + # this line is a translated line + outval = {} + outval["translation"] = match.group(2) + if latest_comment_block: + # if there was a comment, record that. + outval["comment"] = latest_comment_block + latest_comment_block = None + dOut[match.group(1)] = outval + return (dOut, text, header_comment) + +# Walks all lua files in the mod folder, collects translatable strings, +# and writes it to a template.txt file +# Returns a dictionary of localized strings to source file sets +# that can be used with the strings_to_text function. +def generate_template(folder, mod_name): + dOut = {} + for root, dirs, files in os.walk(folder): + for name in files: + if fnmatch.fnmatch(name, "*.lua"): + fname = os.path.join(root, name) + found = read_lua_file_strings(fname) + if params["verbose"]: + print(f"{fname}: {str(len(found))} translatable strings") + + for s in found: + sources = dOut.get(s, set()) + sources.add(f"### {os.path.basename(fname)} ###") + dOut[s] = sources + + if len(dOut) == 0: + return None + templ_file = os.path.join(folder, "locale/template.txt") + write_template(templ_file, dOut, mod_name) + return dOut + +# Updates an existing .tr file, copying the old one to a ".old" file +# if any changes have happened +# dNew is the data used to generate the template, it has all the +# currently-existing localized strings +def update_tr_file(dNew, mod_name, tr_file): + if params["verbose"]: + print(f"updating {tr_file}") + + tr_import = import_tr_file(tr_file) + dOld = tr_import[0] + textOld = tr_import[1] + + textNew = strings_to_text(dNew, dOld, mod_name, tr_import[2]) + + if textOld and textOld != textNew: + print(f"{tr_file} has changed.") + if not params["no-old-file"]: + shutil.copyfile(tr_file, f"{tr_file}.old") + + with open(tr_file, "w", encoding='utf-8') as new_tr_file: + new_tr_file.write(textNew) + +# Updates translation files for the mod in the given folder +def update_mod(folder): + modname = get_modname(folder) + if modname is not None: + process_po_files(folder, modname) + print(f"Updating translations for {modname}") + data = generate_template(folder, modname) + if data == None: + print(f"No translatable strings found in {modname}") + else: + for tr_file in get_existing_tr_files(folder): + update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file)) + else: + print(f"\033[31mUnable to find modname in folder {folder}.\033[0m", file=_stderr) + exit(1) + +# Determines if the folder being pointed to is a mod or a mod pack +# and then runs update_mod accordingly +def update_folder(folder): + is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf")) + if is_modpack: + subfolders = [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')] + for subfolder in subfolders: + update_mod(subfolder) + else: + update_mod(folder) + print("Done.") + +def run_all_subfolders(folder): + for modfolder in [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]: + update_folder(modfolder) + + +main() diff --git a/init.lua b/init.lua index e8f2335..344542e 100644 --- a/init.lua +++ b/init.lua @@ -5,6 +5,9 @@ minetest = minetest--[[@as Minetest]] ItemStack = ItemStack--[[@as ItemStack]] vector = vector--[[@as Vector]] default = default--[[@as MtgDefault]] +sfinv = sfinv--[[@as Sfinv]] +unified_inventory = unified_inventory--[[@as UnifiedInventory]] +player_api = player_api--[[@as MtgPlayerApi]] math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 9))--[[@as number]]) @@ -17,16 +20,263 @@ dofile(path .. '/particle_effects.lua') dofile(path .. '/nodes.lua') dofile(path .. '/arrow.lua') dofile(path .. '/items.lua') -dofile(path .. '/quiver.lua') ----backwards compatibility -minetest.register_alias('x_bows:arrow_diamond_tipped_poison', 'x_bows:arrow_diamond') +if XBows.i3 then + XBowsQuiver:i3_register_page() +elseif XBows.unified_inventory then + XBowsQuiver:ui_register_page() +else + XBowsQuiver:sfinv_register_page() +end minetest.register_on_joinplayer(function(player) + local inv_quiver = player:get_inventory()--[[@as InvRef]] + local inv_arrow = player:get_inventory()--[[@as InvRef]] + + if XBows._3d_armor then + player_api.set_model(player, 'x_bows_3d_armor_character.b3d') + else + player_api.set_model(player, 'x_bows_character.b3d') + end + + inv_quiver:set_size('x_bows:quiver_inv', 1 * 1) + inv_arrow:set_size('x_bows:arrow_inv', 1 * 1) + + local quiver_stack = player:get_inventory():get_stack('x_bows:quiver_inv', 1) + + if quiver_stack and not quiver_stack:is_empty() then + local st_meta = quiver_stack:get_meta() + local quiver_id = st_meta:get_string('quiver_id') + + ---create detached inventory + local detached_inv = XBowsQuiver:get_or_create_detached_inv( + quiver_id, + player:get_player_name(), + st_meta:get_string('quiver_items') + ) + + ---set model textures + if detached_inv:is_empty('main') then + XBowsQuiver.quiver_empty_state[player:get_player_name()] = false + XBowsQuiver:show_3d_quiver(player, {is_empty = true}) + else + XBowsQuiver.quiver_empty_state[player:get_player_name()] = true + XBowsQuiver:show_3d_quiver(player) + end + else + ---set model textures + XBowsQuiver:hide_3d_quiver(player) + end + XBows:reset_charged_bow(player, true) XBowsQuiver:close_quiver(player) end) +local model_name = 'x_bows_character.b3d' +if XBows._3d_armor then + ---3d armor + model_name = 'x_bows_3d_armor_character.b3d' +end + +player_api.register_model(model_name, { + animation_speed = 30, + textures = {'character.png'}, + animations = { + -- Standard animations. + stand = {x = 0, y = 79}, + lay = {x = 162, y = 166, eye_height = 0.3, override_local = true, + collisionbox = {-0.6, 0.0, -0.6, 0.6, 0.3, 0.6}}, + walk = {x = 168, y = 187}, + mine = {x = 189, y = 198}, + walk_mine = {x = 200, y = 219}, + sit = {x = 81, y = 160, eye_height = 0.8, override_local = true, + collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.0, 0.3}} + }, + collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.7, 0.3}, + stepheight = 0.6, + eye_height = 1.47 +}) + +---formspec callbacks +minetest.register_allow_player_inventory_action(function(player, action, inventory, inventory_info) + ---arrow inventory + if action == 'move' and inventory_info.to_list == 'x_bows:arrow_inv' then + local stack = inventory:get_stack(inventory_info.from_list, inventory_info.from_index) + + if minetest.get_item_group(stack:get_name(), 'arrow') ~= 0 then + return inventory_info.count + else + return 0 + end + elseif action == 'move' and inventory_info.from_list == 'x_bows:arrow_inv' then + local stack = inventory:get_stack(inventory_info.from_list, inventory_info.from_index) + + if minetest.get_item_group(stack:get_name(), 'arrow') ~= 0 then + return inventory_info.count + else + return 0 + end + elseif action == 'put' and inventory_info.listname == 'x_bows:arrow_inv' then + if minetest.get_item_group(inventory_info.stack:get_name(), 'arrow') ~= 0 then + return inventory_info.stack:get_count() + else + return 0 + end + elseif action == 'take' and inventory_info.listname == 'x_bows:arrow_inv' then + if minetest.get_item_group(inventory_info.stack:get_name(), 'arrow') ~= 0 then + return inventory_info.stack:get_count() + else + return 0 + end + end + + ---quiver inventory + if action == 'move' and inventory_info.to_list == 'x_bows:quiver_inv' then + local stack = inventory:get_stack(inventory_info.from_list, inventory_info.from_index) + if minetest.get_item_group(stack:get_name(), 'quiver') ~= 0 then + return inventory_info.count + else + return 0 + end + elseif action == 'move' and inventory_info.from_list == 'x_bows:quiver_inv' then + local stack = inventory:get_stack(inventory_info.from_list, inventory_info.from_index) + if minetest.get_item_group(stack:get_name(), 'quiver') ~= 0 then + return inventory_info.count + else + return 0 + end + elseif action == 'put' and inventory_info.listname == 'x_bows:quiver_inv' then + if minetest.get_item_group(inventory_info.stack:get_name(), 'quiver') ~= 0 then + return inventory_info.stack:get_count() + else + return 0 + end + elseif action == 'take' and inventory_info.listname == 'x_bows:quiver_inv' then + if minetest.get_item_group(inventory_info.stack:get_name(), 'quiver') ~= 0 then + return inventory_info.stack:get_count() + else + return 0 + end + end + + return inventory_info.count or inventory_info.stack:get_count() +end) + +minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info) + ---arrow + if action == 'move' and inventory_info.to_list == 'x_bows:arrow_inv' then + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + elseif action == 'move' and inventory_info.from_list == 'x_bows:arrow_inv' then + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + elseif action == 'put' and inventory_info.listname == 'x_bows:arrow_inv' then + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + elseif action == 'take' and inventory_info.listname == 'x_bows:arrow_inv' then + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + end + + ---quiver + if action == 'move' and inventory_info.to_list == 'x_bows:quiver_inv' then + local stack = inventory:get_stack(inventory_info.to_list, inventory_info.to_index) + + ---init detached inventory if not already + local st_meta = stack:get_meta() + local quiver_id = st_meta:get_string('quiver_id') + + if quiver_id == '' then + quiver_id = stack:get_name()..'_'..XBows.uuid() + st_meta:set_string('quiver_id', quiver_id) + inventory:set_stack(inventory_info.to_list, inventory_info.to_index, stack) + end + + local detached_inv = XBowsQuiver:get_or_create_detached_inv( + quiver_id, + player:get_player_name(), + st_meta:get_string('quiver_items') + ) + + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + + ---set player visual + if detached_inv:is_empty('main') then + XBowsQuiver.quiver_empty_state[player:get_player_name()] = false + XBowsQuiver:show_3d_quiver(player, {is_empty = true}) + else + XBowsQuiver.quiver_empty_state[player:get_player_name()] = true + XBowsQuiver:show_3d_quiver(player) + end + elseif action == 'move' and inventory_info.from_list == 'x_bows:quiver_inv' then + local stack = inventory:get_stack(inventory_info.from_list, inventory_info.from_index) + + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + + ---set player visual + if stack:is_empty() then + XBowsQuiver:hide_3d_quiver(player) + end + elseif action == 'put' and inventory_info.listname == 'x_bows:quiver_inv' then + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + elseif action == 'take' and inventory_info.listname == 'x_bows:quiver_inv' then + if XBows.i3 then + i3.set_fs(player) + elseif XBows.unified_inventory then + unified_inventory.set_inventory_formspec(player, 'x_bows:quiver_page') + else + sfinv.set_player_inventory_formspec(player) + end + end +end) + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if player and fields.quit then + XBowsQuiver:close_quiver(player, formname) + end +end) + +---backwards compatibility +minetest.register_alias('x_bows:arrow_diamond_tipped_poison', 'x_bows:arrow_diamond') + -- sneak, fov adjustments when bow is charged minetest.register_globalstep(function(dtime) bow_charged_timer = bow_charged_timer + dtime diff --git a/items.lua b/items.lua index ab42f2f..04fb0fc 100644 --- a/items.lua +++ b/items.lua @@ -1,6 +1,8 @@ +local S = minetest.get_translator(minetest.get_current_modname()) + XBows:register_bow('bow_wood', { - description = 'Wooden Bow', - short_description = 'Wooden Bow', + description = S('Wooden Bow'), + short_description = S('Wooden Bow'), custom = { uses = 385, crit_chance = 10, @@ -22,7 +24,8 @@ XBows:register_bow('bow_wood', { }) XBows:register_arrow('arrow_wood', { - description = 'Arrow Wood', + description = S('Arrow Wood'), + short_description = S('Arrow Wood'), inventory_image = 'x_bows_arrow_wood.png', custom = { recipe = { @@ -40,7 +43,8 @@ XBows:register_arrow('arrow_wood', { }) XBows:register_arrow('arrow_stone', { - description = 'Arrow Stone', + description = S('Arrow Stone'), + short_description = S('Arrow Stone'), inventory_image = 'x_bows_arrow_stone.png', custom = { recipe = { @@ -57,7 +61,8 @@ XBows:register_arrow('arrow_stone', { }) XBows:register_arrow('arrow_bronze', { - description = 'Arrow Bronze', + description = S('Arrow Bronze'), + short_description = S('Arrow Bronze'), inventory_image = 'x_bows_arrow_bronze.png', custom = { recipe = { @@ -74,7 +79,8 @@ XBows:register_arrow('arrow_bronze', { }) XBows:register_arrow('arrow_steel', { - description = 'Arrow Steel', + description = S('Arrow Steel'), + short_description = S('Arrow Steel'), inventory_image = 'x_bows_arrow_steel.png', custom = { recipe = { @@ -91,7 +97,8 @@ XBows:register_arrow('arrow_steel', { }) XBows:register_arrow('arrow_mese', { - description = 'Arrow Mese', + description = S('Arrow Mese'), + short_description = S('Arrow Mese'), inventory_image = 'x_bows_arrow_mese.png', custom = { recipe = { @@ -108,7 +115,8 @@ XBows:register_arrow('arrow_mese', { }) XBows:register_arrow('arrow_diamond', { - description = 'Arrow Diamond', + description = S('Arrow Diamond'), + short_description = S('Arrow Diamond'), inventory_image = 'x_bows_arrow_diamond.png', custom = { recipe = { @@ -125,11 +133,11 @@ XBows:register_arrow('arrow_diamond', { }) XBows:register_quiver('quiver', { - description = 'Quiver \n\n Empty\n', - short_description = 'Quiver', + description = S('Quiver') .. '\n\n' .. S('Empty') ..'\n', + short_description = S('Quiver'), custom = { - description = 'Quiver \n\n Empty\n', - short_description = 'Quiver', + description = S('Quiver') .. '\n\n' .. S('Empty') ..'\n', + short_description = S('Quiver'), recipe = { {'group:arrow', 'group:arrow', 'group:arrow'}, {'group:arrow', 'wool:brown', 'group:arrow'}, diff --git a/locale/template.txt b/locale/template.txt new file mode 100644 index 0000000..109876a --- /dev/null +++ b/locale/template.txt @@ -0,0 +1,21 @@ +# textdomain: x_bows +Critical Arrow Chance= +Strength= +Allowed ammunition= +none= +Damage= +Charge Time= +Faster Arrows= +Arrow Damage= +No Ammo= +Arrows= +Quiver= +Empty= +Wooden Bow= +Arrow Wood= +Arrow Stone= +Arrow Bronze= +Arrow Steel= +Arrow Mese= +Arrow Diamond= +Target= diff --git a/locale/x_bows.sk.tr b/locale/x_bows.sk.tr new file mode 100644 index 0000000..be7c233 --- /dev/null +++ b/locale/x_bows.sk.tr @@ -0,0 +1,21 @@ +# textdomain: x_bows +Critical Arrow Chance=Šanca kritického šípu +Strength=Sila +Allowed ammunition=Povolené strelivo +none=Žiaden +Damage=Poškodenie +Charge Time=Doba nabíjania +Faster Arrows=Rýchlejšie šípy +Arrow Damage=Poškodenie šípom +No Ammo=Žiadne strelivo +Arrows=Šípy +Quiver=Púzdro +Empty=Prázdne +Wooden Bow=Drevený luk +Arrow Wood=Drevený šíp +Arrow Stone=Kamenný šíp +Arrow Bronze=Bronzový šíp +Arrow Steel=Oceľový šíp +Arrow Mese=Mese šíp +Arrow Diamond=Diamantový šíp +Target=Terč diff --git a/mod.conf b/mod.conf index 5f177b6..337a062 100644 --- a/mod.conf +++ b/mod.conf @@ -1,6 +1,6 @@ name = x_bows description = Adds bow and arrows to Minetest. depends = -optional_depends = default, farming, 3d_armor, mesecons, playerphysics, player_monoids, wool +optional_depends = default, farming, 3d_armor, mesecons, playerphysics, player_monoids, wool, i3, unified_inventory, simple_skins, u_skins, wardrobe, sfinv supported_games = minetest_game min_minetest_version = 5.4 diff --git a/models/x_bows_3d_armor_character.b3d b/models/x_bows_3d_armor_character.b3d new file mode 100644 index 0000000..ee6d07d Binary files /dev/null and b/models/x_bows_3d_armor_character.b3d differ diff --git a/models/x_bows_3d_armor_character.blend b/models/x_bows_3d_armor_character.blend new file mode 100644 index 0000000..fb7d4ef Binary files /dev/null and b/models/x_bows_3d_armor_character.blend differ diff --git a/models/x_bows_arrow.blend b/models/x_bows_arrow.blend new file mode 100644 index 0000000..01acca7 Binary files /dev/null and b/models/x_bows_arrow.blend differ diff --git a/models/x_bows_arrow_anim_bones.blend b/models/x_bows_arrow_anim_bones.blend deleted file mode 100644 index 8397347..0000000 Binary files a/models/x_bows_arrow_anim_bones.blend and /dev/null differ diff --git a/models/x_bows_character.b3d b/models/x_bows_character.b3d new file mode 100644 index 0000000..aea1342 Binary files /dev/null and b/models/x_bows_character.b3d differ diff --git a/models/x_bows_character.blend b/models/x_bows_character.blend new file mode 100644 index 0000000..da9824a Binary files /dev/null and b/models/x_bows_character.blend differ diff --git a/nodes.lua b/nodes.lua index 3ab9071..e5cac46 100644 --- a/nodes.lua +++ b/nodes.lua @@ -1,5 +1,8 @@ +local S = minetest.get_translator(minetest.get_current_modname()) + minetest.register_node('x_bows:target', { - description = 'Target', + description = S('Target'), + short_description = S('Target'), tiles = {'x_bows_target.png'}, is_ground_content = false, groups = {snappy=3, flammable=4, fall_damage_add_percent = -30}, diff --git a/quiver.lua b/quiver.lua deleted file mode 100644 index 064b96e..0000000 --- a/quiver.lua +++ /dev/null @@ -1,5 +0,0 @@ -minetest.register_on_player_receive_fields(function(player, formname, fields) - if player and fields.quit then - XBowsQuiver:close_quiver(player, formname) - end -end) diff --git a/textures/x_bows_arrow_mesh.png b/textures/x_bows_arrow_mesh.png index 93f05e2..21aae46 100644 Binary files a/textures/x_bows_arrow_mesh.png and b/textures/x_bows_arrow_mesh.png differ diff --git a/textures/x_bows_arrow_slot.png b/textures/x_bows_arrow_slot.png index 579b6f3..045d9d7 100644 Binary files a/textures/x_bows_arrow_slot.png and b/textures/x_bows_arrow_slot.png differ diff --git a/textures/x_bows_quiver_hotbar_selected.png b/textures/x_bows_hotbar_selected.png similarity index 100% rename from textures/x_bows_quiver_hotbar_selected.png rename to textures/x_bows_hotbar_selected.png diff --git a/textures/x_bows_quiver_blank_mesh.png b/textures/x_bows_quiver_blank_mesh.png new file mode 100644 index 0000000..9e7ec19 Binary files /dev/null and b/textures/x_bows_quiver_blank_mesh.png differ diff --git a/textures/x_bows_quiver_empty_mesh.png b/textures/x_bows_quiver_empty_mesh.png new file mode 100644 index 0000000..65fb025 Binary files /dev/null and b/textures/x_bows_quiver_empty_mesh.png differ diff --git a/textures/x_bows_quiver_mesh.png b/textures/x_bows_quiver_mesh.png new file mode 100644 index 0000000..d128775 Binary files /dev/null and b/textures/x_bows_quiver_mesh.png differ diff --git a/textures/x_bows_quiver_slot.png b/textures/x_bows_quiver_slot.png new file mode 100644 index 0000000..1acc67a Binary files /dev/null and b/textures/x_bows_quiver_slot.png differ diff --git a/textures/x_bows_single_hotbar.png b/textures/x_bows_single_hotbar.png new file mode 100644 index 0000000..4aa7104 Binary files /dev/null and b/textures/x_bows_single_hotbar.png differ diff --git a/types/minetest.type.lua b/types/minetest.type.lua index ba78ff0..e193ca1 100644 --- a/types/minetest.type.lua +++ b/types/minetest.type.lua @@ -70,6 +70,11 @@ ---@field dir_to_wallmounted fun(dir: Vector): number Convert a vector to a wallmounted value, used for `paramtype2="wallmounted"` ---@field item_place_node fun(itemstack: ItemStack, placer: ObjectRef, pointed_thing: PointedThingDef, param2?: , prevent_after_place?: boolean): Vector|nil Place item as a node, `param2` overrides `facedir` and wallmounted `param2`, `prevent_after_place`: if set to `true`, `after_place_node` is not called or the newly placed node to prevent a callback and placement loop. returns `itemstack, position`, `position`: the location the node was placed to. `nil` if nothing was placed. ---@field unregister_item fun(name: string): nil Unregisters the item from the engine, and deletes the entry with key `name` from `minetest.registered_items` and from the associated item table according to its nature: `minetest.registered_nodes`, etc. +---@field register_allow_player_inventory_action fun(func: fun(player: ObjectRef, action: string, inventory: InvRef, inventory_info: {["from_list"]: string, ["to_list"]: string, ["from_index"]: number, ["to_index"]: number, ["count"]: number} | {["listname"]: string, ["index"]: number, ["stack"]: ItemStack}): number): nil Determines how much of a stack may be taken, put or moved to a player inventory. `player` (type `ObjectRef`) is the player who modified the inventory, `inventory` (type `InvRef`). List of possible `action` (string) values and their `inventory_info` (table) contents: `move`: `{from_list=string, to_list=string, from_index=number, to_index=number, count=number}`, `put`: `{listname=string, index=number, stack=ItemStack}`, `take`: Same as `put`. Return a numeric value to limit the amount of items to be taken, put or moved. A value of `-1` for `take` will make the source stack infinite. +---@field register_on_player_inventory_action fun(func: fun(player: ObjectRef, action: string, inventory: InvRef, inventory_info: {["from_list"]: string, ["to_list"]: string, ["from_index"]: number, ["to_index"]: number, ["count"]: number} | {["listname"]: string, ["index"]: number, ["stack"]: ItemStack}): nil): nil Called after a take, put or move event from/to/in a player inventory. Function arguments: see `minetest.register_allow_player_inventory_action`. Does not accept or handle any return value. +---@field formspec_escape fun(str: string): string returns a string, escapes the characters "[", "]", "\", "," and ";", which can not be used in formspecs. +---@field get_translator fun(textdomain: string): any +---@field get_current_modname fun(): string returns the currently loading mod's name, when loading a mod. ---Minetest settings ---@class MinetestSettings diff --git a/types/mtg-player-api.type.lua b/types/mtg-player-api.type.lua new file mode 100644 index 0000000..c0d2b76 --- /dev/null +++ b/types/mtg-player-api.type.lua @@ -0,0 +1,32 @@ +---The player API can register player models and update the player's appearance. +---@class MtgPlayerApi +---@field globalstep fun(dtime: number, ...): nil The function called by the globalstep that controls player animations. You can override this to replace the globalstep with your own implementation. Receives all args that minetest.register_globalstep() passes +---@field register_model fun(name: string, def: MtgPlayerApiModelDef): nil Register a new model to be used by players, `name`: model filename such as "character.x", "foo.b3d", etc., `def`: see [#Model definition] Saved to player_api.registered_models +---@field registered_models string[] Get a model's definition, `name`: model filename See [#Model definition] +---@field set_model fun(player: ObjectRef, model_name: string): nil Change a player's model, `player`: PlayerRef, `model_name`: model registered with `player_api.register_model` +---@field set_animation fun(player: ObjectRef, anim_name: string, speed: number): nil Applies an animation to a player if speed or anim_name differ from the currently playing animation, `player`: PlayerRef, `anim_name`: name of the animation, `speed`: keyframes per second. If nil, the default from the model def is used +---@field set_textures fun(player: ObjectRef, textures: string[]): nil Sets player textures `player`: PlayerRef, `textures`: array of textures. If nil, the default from the model def is used +---@field set_texture fun(player: ObjectRef, index: number, texture: string): nil Sets one of the player textures, `player`: PlayerRef, `index`: Index into array of all textures, `texture`: the texture string +---@field get_textures fun(player: ObjectRef): string[] Returns player textures table +---@field get_animation fun(player: ObjectRef): {["model"]: string | nil, ["textures"]: string[] | nil, ["animation"]: table | nil} Returns a table containing fields `model`, `textures` and `animation`. Any of the fields of the returned table may be nil, `player`: PlayerRef +---@field player_attached table A table that maps a player name to a boolean. If the value for a given player is set to true, the default player animations (walking, digging, ...) will no longer be updated, and knockback from damage is prevented for that player. Example of usage: A mod sets a player's value to true when attached to a vehicle. + + +---Model Definition +---@class MtgPlayerApiModelDef +---@field animation_speed number Default: 30, animation speed, in keyframes per second +---@field textures string[] Default `{"character.png"}`, array of textures +---@field animations table +---@field visual_size {["x"]: number, ["y"]: number} +---@field collisionbox number[] +---@field stepheight number +---@field eye_height number + + +---Model Animation definition +---@class MtgPlayerApiAnimationDef +---@field x number start frame +---@field y number end frame +---@field collisionbox number[] | nil +---@field eye_height number | nil model eye height +---@field override_local boolean | nil suspend client side animations while this one is active (optional) diff --git a/types/mtg-sfinv.lua b/types/mtg-sfinv.lua new file mode 100644 index 0000000..7d0070b --- /dev/null +++ b/types/mtg-sfinv.lua @@ -0,0 +1,25 @@ +---Sfinv API +---@class Sfinv +---@field register_page fun(name: string, def: SfinvDef): nil Register a page +---@field make_formspec fun(player: ObjectRef, contex: SfinvContext, content: string, show_inv?: boolean, size?: string): nil Adds a theme to a formspec show_inv, defaults to false. Whether to show the player's main inventory size, defaults to `size[8,8.6]` if not specified +---@field get_or_create_context fun(player: ObjectRef): SfinvContext Gets the player's context +---@field set_context fun(player: ObjectRef, context: SfinvContext): nil +---@field get_formspec fun(player: ObjectRef, context: SfinvContext): string Builds current page's formspec +---@field set_player_inventory_formspec fun(player: ObjectRef): string (re)builds page formspec and calls set_inventory_formspec(). + + +---Sfinv Definition +---@class SfinvDef +---@field title string Human readable page name (required) +---@field get fun(self: Sfinv, player: ObjectRef, context: SfinvContext): string Returns a formspec string. See formspec variables. (required) +---@field is_in_nav fun(self: Sfinv, player: ObjectRef, context: SfinvContext): boolean Return true to show in the navigation (the tab header, by default) +---@field on_player_receive_fields fun(self: Sfinv, player: ObjectRef, context: SfinvContext, fields: table): nil On formspec submit. +---@field on_enter fun(self: Sfinv, player: ObjectRef, context: SfinvContext): nil Called when the player changes pages, usually using the tabs. +---@field on_leave fun(self: Sfinv, player: ObjectRef, context: SfinvContext): nil When leaving this page to go to another, called before other's on_enter + +---Sfinv Context, including: any thing you want to store, sfinv will clear the stored data on log out / log in +---@class SfinvContext +---@field page string Current page name +---@field nav string[] A list of page names +---@field nav_titles string[] A list of page titles +---@field nav_idx number Current nav index (in nav and nav_titles) diff --git a/types/unified_inventory.type.lua b/types/unified_inventory.type.lua new file mode 100644 index 0000000..1f1bfd0 --- /dev/null +++ b/types/unified_inventory.type.lua @@ -0,0 +1,7 @@ +---Base class Unified Inventory +---@class UnifiedInventory +---@field set_inventory_formspec fun(player: ObjectRef, formspecname: string): nil +---@field register_button fun(name: string, def: table): nil +---@field single_slot fun(x: number, y: number): nil +---@field register_page fun(name: string, def: table): nil +---@field style_full table diff --git a/types/xbows.type.lua b/types/xbows.type.lua index 8cf36b7..101f244 100644 --- a/types/xbows.type.lua +++ b/types/xbows.type.lua @@ -5,6 +5,9 @@ ---@field mesecons string|nil ---@field playerphysics string|nil ---@field player_monoids string|nil +---@field u_skins string|nil +---@field wardrobe string|nil +---@field _3d_armor string|nil ---@field registered_bows table ---@field registered_arrows table ---@field registered_quivers table @@ -27,6 +30,8 @@ ---@field shoot fun(self: XBows, itemstack: ItemStack, user: ObjectRef, pointed_thing?: PointedThingDef): ItemStack Shoot bow ---@field register_particle_effect fun(self: XBows, name: string, def: ParticlespawnerDef|ParticlespawnerDefCustom): nil Add new particle to XBow registration ---@field open_quiver fun(self: XBowsQuiver, itemstack: ItemStack, user: ObjectRef): ItemStack Open quiver +---@field uuid fun(): string Creates UUID +---@field fallback_quiver boolean If no invenotory mod is detected then fallback solution will be used ---XBowsQuiver class extended from XBows @@ -45,6 +50,11 @@ ---@field get_string_from_inv fun(self: XBowsQuiver, inv: InvRef): {['inv_string']: string, ['content_description']: string} Convert inventory of itemstacks to serialized string ---@field set_string_to_inv fun(self: XBowsQuiver, inv: InvRef, str: string): nil Set items from serialized string to inventory ---@field quiver_can_allow fun(self: XBowsQuiver, inv: InvRef, player: ObjectRef): boolean Check if we are allowing actions in the correct quiver inventory +---@field show_3d_quiver fun(self: XBowsQuiver, player: ObjectRef, props?: {["is_empty"]: boolean|nil}): nil Applies full/empty quiver textures +---@field hide_3d_quiver fun(self: XBowsQuiver, player: ObjectRef): nil Applies blank quiver textures - hiding the quiver +---@field sfinv_register_page fun(): nil register inventoy mod page +---@field i3_register_page fun(): nil register inventoy mod page +---@field ui_register_page fun(): nil register inventoy mod page ---Custom field in ParticlespawnerDef @@ -107,6 +117,7 @@ ---@field on_hit_entity fun(self: table, pointed_thing_ref: table) ---@field on_hit_player fun(self: table, pointed_thing_ref: table) ---@field on_after_activate fun(self: table) +---@field description_abilities string ---Custom field in ItemDef