Add 3d quiver and new inventory slots

This commit is contained in:
Juraj Vajda 2022-10-28 17:49:43 +00:00
commit 6b44d29533
31 changed files with 1469 additions and 121 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
.DS_Store
docs/build
*.blend1
*.blend2
*.old

View File

@ -60,5 +60,11 @@ read_globals = {
"hb",
"mesecon",
"armor",
"default"
"default",
"i3",
"unified_inventory",
"player_api",
"u_skins",
"wardrobe",
"3d_armor"
}

View File

@ -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

569
api.lua
View File

@ -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,20 +480,39 @@ 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
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 self.registered_arrows[arrow_stack:get_name()] and is_allowed_ammunition then
table.insert(itemstack_arrows, {stack = arrow_stack, idx = 1})
end
---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)
local _is_allowed_ammunition = self:is_allowed_ammunition(bow_name, st_name)
if self.registered_arrows[st_name] and is_allowed_ammunition then
if self.registered_arrows[st_name] and _is_allowed_ammunition then
table.insert(itemstack_arrows, {stack = st, idx = i})
end
end
end
end
-- take 1st found arrow in the list
itemstack_arrow = #itemstack_arrows > 0 and itemstack_arrows[1].stack or nil
@ -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:remove_hud(user)
XBowsQuiver:show_3d_quiver(user)
end
else
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,6 +1492,51 @@ function XBowsQuiver.get_itemstack_arrow_from_quiver(self, player)
local quiver_id
local quiver_name
---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')
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 = quiver_stack: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 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')
@ -1496,6 +1587,7 @@ function XBowsQuiver.get_itemstack_arrow_from_quiver(self, player)
end
end
end
end
return {
found_arrow_stack = found_arrow_stack,
@ -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)
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
if player_inv and player_inv:contains_item('main', quiver_item_name) then
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

476
i18n.py Executable file
View File

@ -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()

256
init.lua
View File

@ -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

View File

@ -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'},

21
locale/template.txt Normal file
View File

@ -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=

21
locale/x_bows.sk.tr Normal file
View File

@ -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č

View File

@ -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

Binary file not shown.

Binary file not shown.

BIN
models/x_bows_arrow.blend Normal file

Binary file not shown.

Binary file not shown.

BIN
models/x_bows_character.b3d Normal file

Binary file not shown.

Binary file not shown.

View File

@ -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},

View File

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 124 B

View File

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

View File

@ -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

View File

@ -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<string, boolean> 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<string, MtgPlayerApiAnimationDef>
---@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)

25
types/mtg-sfinv.lua Normal file
View File

@ -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)

View File

@ -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

View File

@ -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<string, ItemDef|BowItemDefCustom>
---@field registered_arrows table<string, ItemDef|ArrowItemDefCustom>
---@field registered_quivers table<string, ItemDef|QuiverItemDefCustom>
@ -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