6 Commits

Author SHA1 Message Date
247bcdd058 Fix client-side out-of-bounds '[combine' warnings 2025-03-16 18:49:54 +01:00
a0ed992121 Update player_api model registration
Ensures correct eye position and collision box.
Values taken from the player_api 'character.b3d' model.
2025-03-16 18:38:45 +01:00
3cf80c9272 skinlist: Allow textures containing '-' characters
Fixes issue #110
Thanks to Bastrabun for the code suggestion
2024-09-28 11:12:31 +02:00
df62f2042d skinlist: avoid 'Too many open files' errors
Due to application-specific file descriptor limits,
the garbage collector cannot close the descriptors in time,
resulting in unexpected file open errors.
2024-07-22 18:41:15 +02:00
b7cd514cea API: make skins.register_skin public (#106) 2024-07-07 18:51:48 +02:00
11bb5bad0e Add migration code to player skins using '.' delimiters (#105)
Previously, the players would have their selected skin reset after renaming
the skin textures to the dot separator. This commit implements skin name
migration to ease the transition for server owners.

See 'skins.__fuzzy_match_skin_name' for a detailed explanation.
2024-06-15 15:16:52 +02:00
7 changed files with 172 additions and 29 deletions

25
API.md
View File

@ -27,6 +27,31 @@ Get all allowed skins for player. All public and all player's private skins. If
Get all skins with metadata key is set to value. Example: Get all skins with metadata key is set to value. Example:
skins.get_skinlist_with_meta("playername", playername) - Get all private skins (w.o. public) for playername skins.get_skinlist_with_meta("playername", playername) - Get all private skins (w.o. public) for playername
## skins.register_skin(path, filename)
Registers a new skin based on the texture file path specified by `path` and `filename`.
* `path` (string): points to the parent directory of the texture `filename`.
Generally, this should be in the format `mymod.modpath .. "/textures"`.
* `filename` (string): full file name, without any path specifications.
The file name must adhere to [one of the accepted naming formats](textures/readme.txt).
Note: this function takes the following files into consideration:
1. `<path>/<filename>` (required)
* Main skin texture
2. `<path>/<filenamestem><separator>preview.png` (optional)
* Pre-generated preview image
3. `<path>/../meta/<filenamestem>.txt` (optional)
* Metadata regarding the skin
Return values:
* On failure: `false, reason`
* `reason` (string): human readable reason string (similar to `io.open` errors)
* On success: `true, key`
* `key`: unique skins key for use with e.g. `skins.get(key)` for subsequent
fine-tuning of the skin registration.
## skins.new(key, object) ## skins.new(key, object)
Create and register a new skin object for given key Create and register a new skin object for given key

18
api.lua
View File

@ -2,14 +2,26 @@
local storage = minetest.get_mod_storage() local storage = minetest.get_mod_storage()
function skins.get_player_skin(player) function skins.get_player_skin(player)
local player_name = player:get_player_name()
local meta = player:get_meta() local meta = player:get_meta()
if meta:get("skinsdb:skin_key") then if meta:get("skinsdb:skin_key") then
-- Move player data prior July 2018 to mod storage -- Move player data prior July 2018 to mod storage
storage:set_string(player:get_player_name(), meta:get_string("skinsdb:skin_key")) storage:set_string(player_name, meta:get_string("skinsdb:skin_key"))
meta:set_string("skinsdb:skin_key", "") meta:set_string("skinsdb:skin_key", "")
end end
local skin = storage:get_string(player:get_player_name())
return skins.get(skin) or skins.get(skins.default) local skin_name = storage:get_string(player_name)
local skin = skins.get(skin_name)
if #skin_name > 0 and not skin then
-- Migration step to convert `_`-delimited skins to `.` (if possible)
skin = skins.__fuzzy_match_skin_name(player_name, skin_name, true)
if skin then
storage:set_string(player_name, skin:get_key())
else
storage:set_string(player_name, "")
end
end
return skin or skins.get(skins.default)
end end
-- Assign skin to player -- Assign skin to player

View File

@ -72,6 +72,7 @@ minetest.register_on_shutdown(function()
end end
end) end)
-- See also: 3d_armor/init.lua
player_api.register_model("skinsdb_3d_armor_character_5.b3d", { player_api.register_model("skinsdb_3d_armor_character_5.b3d", {
animation_speed = 30, animation_speed = 30,
textures = { textures = {
@ -82,16 +83,21 @@ player_api.register_model("skinsdb_3d_armor_character_5.b3d", {
}, },
animations = { animations = {
stand = {x=0, y=79}, stand = {x=0, y=79},
lay = {x=162, y=166}, 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}, walk = {x=168, y=187},
mine = {x=189, y=198}, mine = {x=189, y=198},
walk_mine = {x=200, y=219}, walk_mine = {x=200, y=219},
sit = {x=81, y=160}, 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}},
-- compatibility w/ the emote mod -- compatibility w/ the emote mod
wave = {x = 192, y = 196, override_local = true}, wave = {x = 192, y = 196, override_local = true},
point = {x = 196, y = 196, override_local = true}, point = {x = 196, y = 196, override_local = true},
freeze = {x = 205, y = 205, override_local = true}, freeze = {x = 205, y = 205, override_local = true},
}, },
collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.7, 0.3},
-- stepheight: use default
eye_height = 1.47,
}) })
-- Register default character.png if not part of this mod -- Register default character.png if not part of this mod
@ -111,3 +117,6 @@ minetest.register_allow_player_inventory_action(function(player, action, inv, da
return 0 return 0
end end
end) end)
--dofile(skins.modpath.."/unittest.lua")

View File

@ -109,7 +109,7 @@ function skin_class:get_preview()
--Right Leg --Right Leg
skin = skin .. "([combine:16x32:0,0=" .. player_skin .. "^[mask:skindb_mask_rleg.png)^" skin = skin .. "([combine:16x32:0,0=" .. player_skin .. "^[mask:skindb_mask_rleg.png)^"
-- 64x skins have non-mirrored arms and legs -- 64x64 skins have non-mirrored arms and legs
local left_arm local left_arm
local left_leg local left_leg
@ -126,17 +126,21 @@ function skin_class:get_preview()
--Left Leg --Left Leg
skin = skin .. left_leg skin = skin .. left_leg
-- Add overlays for 64x skins. these wont appear if skin is 32x because it will be cropped out if self:get_meta("format") == "1.8" then
--Chest Overlay -- Add overlays for 64x64 skins. This check is needed to avoid
skin = skin .. "([combine:16x32:-16,-28=" .. player_skin .. "^[mask:skindb_mask_chest.png)^" -- client-side out-of-bounds "[combine" warnings.
--Right Arm Overlay
skin = skin .. "([combine:16x32:-44,-28=" .. player_skin .. "^[mask:skindb_mask_rarm.png)^" --Chest Overlay
--Right Leg Overlay skin = skin .. "([combine:16x32:-16,-28=" .. player_skin .. "^[mask:skindb_mask_chest.png)^"
skin = skin .. "([combine:16x32:0,-16=" .. player_skin .. "^[mask:skindb_mask_rleg.png)^" --Right Arm Overlay
--Left Arm Overlay skin = skin .. "([combine:16x32:-44,-28=" .. player_skin .. "^[mask:skindb_mask_rarm.png)^"
skin = skin .. "([combine:16x32:-40,-44=" .. player_skin .. "^[mask:(skindb_mask_rarm.png^[transformFX))^" --Right Leg Overlay
--Left Leg Overlay skin = skin .. "([combine:16x32:0,-16=" .. player_skin .. "^[mask:skindb_mask_rleg.png)^"
skin = skin .. "([combine:16x32:4,-32=" .. player_skin .. "^[mask:(skindb_mask_rleg.png^[transformFX))" --Left Arm Overlay
skin = skin .. "([combine:16x32:-40,-44=" .. player_skin .. "^[mask:(skindb_mask_rarm.png^[transformFX))^"
--Left Leg Overlay
skin = skin .. "([combine:16x32:4,-32=" .. player_skin .. "^[mask:(skindb_mask_rleg.png^[transformFX))"
end
-- Full Preview -- Full Preview
skin = "(((" .. skin .. ")^[resize:64x128)^[mask:skindb_transform.png)" skin = "(((" .. skin .. ")^[resize:64x128)^[mask:skindb_transform.png)"

View File

@ -2,10 +2,11 @@ local dbgprint = false and print or function() end
--- @param path Path to the "textures" directory, without tailing slash. --- @param path Path to the "textures" directory, without tailing slash.
--- @param filename Current file name, such as "player.groot.17.png". --- @param filename Current file name, such as "player.groot.17.png".
local function process_skin_texture(path, filename) --- @return On error: false, error message. On success: true, skin key
function skins.register_skin(path, filename)
-- See "textures/readme.txt" for allowed formats -- See "textures/readme.txt" for allowed formats
local prefix, sep, identifier, extension = filename:match("^(%a+)([_.])([%w_.]+)%.(%a+)$") local prefix, sep, identifier, extension = filename:match("^(%a+)([_.])([%w_.-]+)%.(%a+)$")
--[[ --[[
prefix: "character" or "player" prefix: "character" or "player"
sep: "." (new) or "_" (legacy) sep: "." (new) or "_" (legacy)
@ -16,17 +17,21 @@ local function process_skin_texture(path, filename)
-- Filter out files that do not match the allowed patterns -- Filter out files that do not match the allowed patterns
if not extension or extension:lower() ~= "png" then if not extension or extension:lower() ~= "png" then
return -- Not a skin texture return false, "invalid skin name"
end end
if prefix ~= "player" and prefix ~= "character" then if prefix ~= "player" and prefix ~= "character" then
return -- Unknown type return false, "unknown type"
end end
local preview_suffix = sep .. "preview" local preview_suffix = sep .. "preview"
if identifier:sub(-#preview_suffix) == preview_suffix then if identifier:sub(-#preview_suffix) == preview_suffix then
-- skip preview textures -- The preview texture is added by the main skin texture (if exists)
-- This is added by the main skin texture (if exists) return false, "preview texture"
return end
assert(path)
if path == ":UNITTEST:" then
path = nil
end end
dbgprint("Found skin", prefix, identifier, extension) dbgprint("Found skin", prefix, identifier, extension)
@ -58,12 +63,16 @@ local function process_skin_texture(path, filename)
local skin_obj = skins.get(filename_noext) or skins.new(filename_noext) local skin_obj = skins.get(filename_noext) or skins.new(filename_noext)
skin_obj:set_texture(filename) skin_obj:set_texture(filename)
skin_obj:set_meta("_sort_id", sort_id) skin_obj:set_meta("_sort_id", sort_id)
if sep ~= "_" then
skin_obj._legacy_name = filename_noext:gsub("[._]+", "_")
end
if playername then if playername then
skin_obj:set_meta("assignment", "player:"..playername) skin_obj:set_meta("assignment", "player:"..playername)
skin_obj:set_meta("playername", playername) skin_obj:set_meta("playername", playername)
end end
do if path then
-- Get type of skin based on dimensions -- Get type of skin based on dimensions
local file = io.open(path .. "/" .. filename, "r") local file = io.open(path .. "/" .. filename, "r")
local skin_format = skins.get_skin_format(file) local skin_format = skins.get_skin_format(file)
@ -74,7 +83,7 @@ local function process_skin_texture(path, filename)
skin_obj:set_hand_from_texture() skin_obj:set_hand_from_texture()
skin_obj:set_meta("name", identifier) skin_obj:set_meta("name", identifier)
do if path then
-- Optional skin information -- Optional skin information
local file = io.open(path .. "/../meta/" .. filename_noext .. ".txt", "r") local file = io.open(path .. "/../meta/" .. filename_noext .. ".txt", "r")
if file then if file then
@ -83,18 +92,50 @@ local function process_skin_texture(path, filename)
skin_obj:set_meta("name", data[1]) skin_obj:set_meta("name", data[1])
skin_obj:set_meta("author", data[2]) skin_obj:set_meta("author", data[2])
skin_obj:set_meta("license", data[3]) skin_obj:set_meta("license", data[3])
file:close() -- do not rely on delayed GC
end end
end end
do if path then
-- Optional preview texture -- Optional preview texture
local preview_name = filename_noext .. sep .. "preview.png" local preview_name = filename_noext .. sep .. "preview.png"
local fh = io.open(path .. "/" .. preview_name) local fh = io.open(path .. "/" .. preview_name)
if fh then if fh then
dbgprint("Found preview", preview_name) dbgprint("Found preview", preview_name)
skin_obj:set_preview(preview_name) skin_obj:set_preview(preview_name)
fh:close() -- do not rely on delayed GC
end end
end end
return true, skin_obj:get_key()
end
--- Internal function. Fallback/migration code for `.`-delimited skin names that
--- were equipped between d3c7fa7 and 312780c (master branch).
--- During this period, `.`-delimited skin names were internally registered with
--- `_` delimiters. This function tries to find a matching skin.
--- @param player_name (string)
--- @param skin_name (string) e.g. `player_foo_mc_bar`
--- @param be_noisy (boolean) whether to print a warning in case of mismatches`
--- @return On match, the new skin (skins.skin_class) or `nil` if nothing matched.
function skins.__fuzzy_match_skin_name(player_name, skin_name, be_noisy)
if select(2, skin_name:gsub("%.", "")) > 0 then
-- Not affected by ambiguity
return
end
for _, skin in pairs(skins.meta) do
if skin._legacy_name == skin_name then
dbgprint("Match", skin_name, skin:get_key())
return skin
end
--dbgprint("Try match", skin_name, skin:get_key(), skin._legacy_name)
end
if be_noisy then
minetest.log("warning", "skinsdb: cannot find matching skin '" ..
skin_name .. "' for player '" .. player_name .. "'.")
end
end end
do do
@ -103,7 +144,7 @@ do
local skins_dir_list = minetest.get_dir_list(skins_path) local skins_dir_list = minetest.get_dir_list(skins_path)
for _, fn in pairs(skins_dir_list) do for _, fn in pairs(skins_dir_list) do
process_skin_texture(skins_path, fn) skins.register_skin(skins_path, fn)
end end
end end

View File

@ -6,6 +6,7 @@ List of accepted texture names
Public skin available for all users: Public skin available for all users:
character.[number or name].png character.[number or name].png
^ The allowed characters in "[number or name]" are "[A-z0-9_.-]+".
One or multiple private skins for player "[nick]": One or multiple private skins for player "[nick]":
player.[nick].png player.[nick].png
@ -26,7 +27,7 @@ The character `_` is accepted in player names, thus it is not recommended to
use such file names. For compatibility reasons, they are still recognized. use such file names. For compatibility reasons, they are still recognized.
character_[number or name].png character_[number or name].png
player_[nick]_png player_[nick].png
player_[nick]_[number or name].png player_[nick]_[number or name].png
... and corresponding previews that end in `_preview.png`. ... and corresponding previews that end in `_preview.png`.

51
unittest.lua Normal file
View File

@ -0,0 +1,51 @@
local function get_skin(skin_name)
local skin = skins.get(skin_name)
or skins.__fuzzy_match_skin_name("(unittest)", skin_name, true)
return skin and skin:get_key() or nil
end
local function run_unittest()
local PATH = ":UNITTEST:"
-- -----
-- `.`: Simple register + retrieve operations
assert(skins.register_skin(PATH, "player.DotSep.png"))
assert(skins.register_skin(PATH, "player._DotSep_666_.1.png"))
assert(skins.register_skin(PATH, "character._DotSep_With-Dash-.png"))
assert(get_skin("player.DotSep"))
assert(get_skin("player._DotSep_666_.1"))
assert(get_skin("player.DotSep.1") == nil)
assert(get_skin("character._DotSep_With-Dash-"))
-- -----
-- Ambiguous skin names (filenames without extension). Register + retrieve
skins.new("player_AmbSki")
skins.new("player_AmbSki_1")
skins.new("player_AmbSki_666_1")
assert(get_skin("player_AmbSki"))
assert(get_skin("player_AmbSki_") == nil)
assert(get_skin("player_AmbSki_1"))
assert(get_skin("player_AmbSki_666_1"))
-- There are no `__` patterns as they were silently removed by string.split
-- -----
-- Mod Storage backwards compatibility
-- Match the old `_` notation to `.`-separated skins
skins.register_skin(PATH, "player.ComPat42.png")
skins.register_skin(PATH, "player.ComPat42.5.png")
skins.register_skin(PATH, "player._Com_Pat_42.png")
skins.register_skin(PATH, "player._Com_Pat_42.1.png")
assert(get_skin("player_ComPat42") == "player.ComPat42")
assert(get_skin("player_ComPat42_5") == "player.ComPat42.5")
assert(get_skin("player_Com_Pat_42") == "player._Com_Pat_42")
assert(get_skin("player_Com_Pat_42_1") == "player._Com_Pat_42.1")
error("Unittest passed! Please disable them now.")
end
run_unittest()