Initial working release with code based from skinsdb

master
Richard Qian 2020-01-01 17:59:23 -06:00
parent cdb61eae24
commit f91f6296f1
25 changed files with 1709 additions and 50 deletions

88
API.md Normal file
View File

@ -0,0 +1,88 @@
# Change Player Model (chchar) Application Programming Interface (API)
## chchar.get_player_model(player)
Return the model object assigned to the player. Returns default if nothing assigned.
## chchar.assign_player_model(player, model)
Check if allowed and assign the model for the player without visual updates. The "model" parameter could be the model key or the model object.
Returns false if model is not valid or applicable to player.
## chchar.update_player_model(player)
Update selected model visuals on player.
## chchar.set_player_model(player, model)
Function for external usage on model selection. This function assigns the model, calls the model:set_model(player) hook to update dynamic models, then update the visuals.
## chchar.get_modellist_for_player(playername)
Get all allowed models for player. All public and all player's private models. If playername not given only public models returned.
## chchar.get_modellist_with_meta(key, value)
Get all models with metadata key is set to value. Example:
chchar.get_modellist_with_meta("playername", playername) - Get all private models (w.o. public) for playername.
## chchar.new(key, object)
Create and register a new model object for given key.
- key: Unique models key, like "character_1"
- object: Optional. Could be a prepared object with redefinitions
## chchar.get(key)
Get existing model object.
HINT: During build-up phase maybe the next statement is useful.
```
local model = chchar.get(name) or chchar.new(name)
```
# Model object
## model:get_key()
Get the unique model key.
## model:set_texture(texture)
Set the list of textures used by the 3D model - usually at the init time only.
## model:get_texture()
Get the list of textures used by the 3D model for any reason. Note to apply them the model:set_model() should be used.
## model:set_icon(texture)
Set the model icon - usually at the init time only.
## model:get_icon()
Get the model icon.
It is currently not possible to draw arbitrary 3D models in formspecs, except for when drawing nodes in inventory slots. An icon is the best alternative to this.
## model:set_model(player)
Hook for dynamic models updates on select. Is called in chchar.set_player_model()
In chchar the default implementation for this function is empty.
model:apply_model_to_player(player)
Apply the model to the player. Called in chchar.update_player_model() to update visuals.
## model:set_meta(key, value)
Add a meta information to the model object.
Note: the information is not stored, therefore should be filled each time during models registration.
## model:get_meta(key)
The next metadata keys are filled or/and used interally in chchar framework, some of which are optional. These optional items are denoted with '*'.
- name - A name for the 3D model
- author* - The model author
- license* - The model license
- animation_speed* - How many frames per second the animations play. Default: 30
- textures - A list of all textures that the model uses. It is possible to leave this field blank, but the model will then not have an optimal appearance.
- animations* - A list of animations that the model will use on certain actions.
- collisionbox* - A list of 2 XYZ coordinates (6 points) in meters relative from the origin of the model.
- stepheight* - How fast this model moves on the ground. Default: 0.6
- eye_height* - How far up the Y-axis in meters the camera will be, in 1st-person view. Default: 1.47
- playername* - Player assignment for private model. Set false for models not usable by all players (like NPC-models), true or nothing for all player models
- in_inventory_list* - If set to false the model is not visible in inventory models selection but can be still applied to the player
- _sort_id - The model lists are sorted by this field for output (internal key)
## model:get_meta_string(key)
Same as get_meta() but does return "" instead of nil if the meta key does not exists
## model:is_applicable_for_player(playername)
Returns whether this model is applicable for player "playername" or not, like private models

22
CHANGELOG.md Normal file
View File

@ -0,0 +1,22 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Pre-release] - Starting from 2019-12-28
* Initial working version based on code from the [skinsdb](https://github.com/minetest-mods/skinsdb) mod. This includes the code and directory structure. All previous code is gone with this change.
* License changed to GPLv3 from LGPLv3.
* Out-of-the-box support for the inventory mods *sfinv* and *unified_inventory*, with the former already shipping with Minetest.
* Other mods that alter the player model may come into conflict with *chchar*, and should be disabled before using this mod.
* Assigning models to specific players is currently untested. All models loaded are assumed to be public for all players.
* Translation support with *intllib* included, but there are no translations yet.
* Additional chat command `/chchar reset` allows to quickly go back to the default model.
### Changes from *skinsdb*
* Drop support for Minetest 0.4 versions.
* Drop optional support for the *3d_armor* and *clothing* mods. This support can be reintroduced at a later time.
* Disable the online downloader, as there are currently no places to download pre-made 3D models for use with this mod. If such a place appears, it can be re-enabled.
* Rename some files to be more relevant for 3D models and not for skins.
* Alter the meta file format to include more fields, including those that can changed with *player_api*.
* Add more information to be displayed in formspecs, and make images and model buttons square sizes.

View File

@ -1,45 +1,34 @@
# Change Player Model, a mod for Minetest
# Change Player Model (chchar), a mod for Minetest
Allows players to change the default character model for another one, optionally with different animations. While using this mod, skins for the default player model cannot be used unless it is switched back to. Basically a frontend to the built-in player_api mod shipping with Minetest Game.
Right now, this mod is in the planning and drafting stage. No code has been implemented yet.
Minetest 5.0.0 or later is required in order to use this mod. No backwards compatibility with older Minetest versions 0.4.16 and 0.4.17 is currently planned.
As a side bonus, it is possible to create models with collision boxes that are small enough to fit in one node wide tunnels or crevices, which the default player model cannot do.
## Directory structure of this mod
Note that the media formats represented here can be replaced by any format supported by the Irrlicht engine.
## Installing models
chchar
```
|-- init.lua (required) - Runs when the game loads.
|-- mod.conf (recommended) - Contains description and dependencies.
|-- models
| |-- <model name> - code name of a model.
| |-- model.ini - Information about this model, including name, costumes available, where to play animations, etc.
| `-- costumes - Different versions of a model. Can contain alternate versions with different models, textures, and/or sounds.
| |-- default - The default costume to load if no alternate versions of a model are available.
| |-- textures (optional)
| | `-- ... any textures or images for the default model.
| |-- sounds (optional)
| | `-- ... any sounds for the default model.
| |-- model.b3d/model.obj - Model file that will be rendered in-game by default.
| `-- icon.png - Image representing this model overall.
| |-- <costume name> - Name of an optional costume. It can also be represented by a number, where the ordering is based alphabetically \(a - 0 to z - 25\).
| |-- textures (optional)
| | `-- ... any textures or images for the alternate version of this model.
| |-- sounds (optional)
| | `-- ... any sounds for the alternate version of this model.
| |-- model.b3d/model.obj - Model file that will be rendered in-game for this particular version.
| `-- icon.png - Image representing the alternate version of this model.
| |-- ... any other models can be added here
|-- textures - other images used in places such as formspecs. Not part of any model.
`-- ... any other files or directories
```
## Command Help
* /chchar \[model name\] \[costume name\] - If "default" is specified in place of <model name>, or /chchar is used by itself, it will load the default player model. If nothing is specified for <costume name>, the default costume for the specified model will load.
What happens during errors:
* If no models are found in the models/ directory: Prints a message in its GUI and chat commands about this.
* If an invalid model name or costume name is specified (chat command only): Prints an error telling the user about this.
### Manual addition
* /chchargui - This command brings up the GUI. Useful in case if an inventory mod is not supported.
* /help chchar - Pulls up usage information on how to use /chchar.
1) Copy your 3D model files to `models`. Only four formats will be accepted by Minetest:
* Blitz3D (.b3d)
* Quake2 models (.md2)
* Maya/Wavefront (.obj)
* Microsoft DirectX (.x)
2) Copy all the textures that each 3D model uses to `textures`.
3) Create `meta/<name>.txt` with the minimum following fields (separated by new lines):
* Model Name
* Textures
4) It is recommended to fill out the rest of these fields for optimal performance:
* Animations
* Collision Box
* Eye Height
## License
Change Player Model code is licensed under the GNU LGPL, version 3 or later.
Change Player Model code is licensed under the GNU GPL, version 3 (it is not yet known whether only under this version or including later ones, so assume only version 3 for now).
### Credits
* Most of this mod's source code is based from [skinsdb](https://github.com/minetest-mods/skinsdb), commit [8054293](https://github.com/minetest-mods/skinsdb/commit/8054293c2c7617c503fc1c15005f74f063e0e71f). As this code is licensed under the GNU GPLv3, the code here is also under that license.
* Some icons used originate from GTK 3.13.2, latest tagged versions at https://gitlab.gnome.org/GNOME/gtk/tree/3.13.2/gtk/stock-icons. As with the rest of that software, the icons used are licensed under the GNU LGPLv2+. They are:
* 16/gtk-convert.*
* 24/gtk-convert.svg
* 24/image-missing.svg

62
api.lua Normal file
View File

@ -0,0 +1,62 @@
-- get current model
local storage = minetest.get_mod_storage()
function chchar.get_player_model(player)
if player:get_attribute("chchar:model_key") then
storage:set_string(player:get_player_name(), player:get_attribute("chchar:model_key"))
player:set_attribute("chchar:model_key", nil)
end
local model = storage:get_string(player:get_player_name())
return chchar.get(model) or chchar.get(chchar.default)
end
-- Assign model to player
function chchar.assign_player_model(player, model)
local model_obj
if type(model) == "string" then
model_obj = chchar.get(model)
else
model_obj = model
end
if not model_obj then
return false
end
if model_obj:is_applicable_for_player(player:get_player_name()) then
local model_key = model_obj:get_key()
if model_key == chchar.default then
model_key = ""
end
storage:set_string(player:get_player_name(), model_key)
else
return false
end
return true
end
-- update visuals
function chchar.update_player_model(player)
chchar.get_player_model(player):apply_model_to_player(player)
--[[
if skins.armor_loaded then
-- all needed is wrapped and implemented in 3d_armor mod
armor:set_player_armor(player)
else
-- do updates manually without 3d_armor
chchar.get_player_model(player):apply_model_to_player(player)
if minetest.global_exists("sfinv") and sfinv.enabled then
sfinv.set_player_inventory_formspec(player)
end
end--]]
end
-- Assign and update - should be used on selection externally
function chchar.set_player_model(player, model)
local success = chchar.assign_player_model(player, model)
if success then
chchar.get_player_model(player):set_model(player)
chchar.update_player_model(player)
end
return success
end

103
chatcommands.lua Normal file
View File

@ -0,0 +1,103 @@
local S = chchar.S
local function show_selection_formspec(player)
local context = chchar.get_formspec_context(player)
local name = player:get_player_name()
local model = chchar.get_player_model(player)
local formspec = "size[8,8]"..chchar.get_model_info_formspec(model)
formspec = formspec..chchar.get_model_selection_formspec(player, context, 3.5)
minetest.show_formspec(name, 'chchar_show_ui', formspec)
end
minetest.register_chatcommand("chchar", {
params = "[set] <model key> | show [<model key>] | list | list private | list public | reset | [ui]",
description = S("Show, list or set player's model"),
func = function(name, param)
local player = minetest.get_player_by_name(name)
if not player then
return false, S("The player").." "..name.." "..S("was not found.")
end
-- parse command line
local command, parameter
local words = param:split(" ")
local word = words[1]
if word == 'set' or word == 'list' or word == 'reset' or word == 'show' or word == 'ui' then
command = word
parameter = words[2]
elseif chchar.get(word) then
command = 'set'
parameter = word
elseif not word then
command = 'ui'
else
return false, S("Unknown command").." "..word..", "..S("see /help chchar for supported parameters.")
end
if command == "set" then
local success = chchar.set_player_model(player, parameter)
if success then
return true, S("Player model for").." "..name.." "..S("set to").." "..parameter
else
return false, S("Invalid model").." "..parameter
end
elseif command == "reset" then
chchar.set_player_model(player, "character")
return true, S("Player model reset to the default.")
elseif command == "list" then
local list
if parameter == "private" then
list = chchar.get_modellist_with_meta("playername", name)
elseif parameter == "public" then
list = chchar.get_modellist_for_player()
elseif not parameter then
list = chchar.get_modellist_for_player(name)
else
return false, S("Unknown parameter"), parameter
end
local current_model_key = chchar.get_player_model(player):get_key()
for _, model in ipairs(list) do
local info = model:get_key().." - "
..S("Name").."="..model:get_meta_string("name").." "
..S("Author").."="..model:get_meta_string("author").." "
..S("License").."="..model:get_meta_string("license")
if model:get_key() == current_model_key then
info = minetest.colorize("#00FFFF", info)
end
minetest.chat_send_player(name, info)
end
elseif command == "show" then
local model
if parameter then
model = models.get(parameter)
else
model = chchar.get_player_model(player)
end
if not model then
return false, S("Invalid model")
end
local formspec = "size[8,3]"..chchar.get_model_info_formspec(model)
minetest.show_formspec(name, 'chchar_show_model', formspec)
elseif command == "ui" then
show_selection_formspec(player)
end
end,
})
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "chchar_show_ui" then
return
end
local context = chchar.get_formspec_context(player)
local action = chchar.on_model_selection_receive_fields(player, context, fields)
if action == 'set' then
minetest.close_formspec(player:get_player_name(), formname)
elseif action == 'page' then
show_selection_formspec(player)
end
end)

View File

@ -1 +1,4 @@
player_api
intllib?
unified_inventory?
sfinv?

124
formspecs.lua Normal file
View File

@ -0,0 +1,124 @@
local S = chchar.S
function chchar.get_formspec_context(player)
if player then
local playername = player:get_player_name()
chchar.ui_context[playername] = chchar.ui_context[playername] or {}
return chchar.ui_context[playername]
else
return {}
end
end
-- Show model info in a graphical window
function chchar.get_model_info_formspec(model)
--local texture = model:get_texture()
local icon = model:get_icon()
local m_codename = model:get_meta_string("codename")
local m_filename = model:get_meta_string("filename")
local m_name = model:get_meta_string("name")
local m_author = model:get_meta_string("author")
local m_license = model:get_meta_string("license")
local m_format = model:get_meta_string("format")
-- overview page
local formspec = "image[0,.75;2,2;"..model:get_icon().."]"
--local formspec = "image[0,.75;1,2;"..model:get_preview().."]"
--if texture then
-- formspec = formspec.."label[6,.5;"..S("Raw texture")..":]"
-- .."image[6,1;2,1;"..model:get_texture().."]"
--end
if m_name ~= "" then
formspec = formspec.."label[2,.5;"..S("Name")..": "..minetest.formspec_escape(m_name).."]"
end
if m_codename ~= "" then
formspec = formspec.."label[2,1;"..S("Technical Name")..": "..minetest.formspec_escape(m_codename).."]"
end
if m_filename ~= "" then
formspec = formspec.."label[2,1.5;"..S("File Name")..": "..minetest.formspec_escape(m_filename).."]"
end
if m_author ~= "" then
formspec = formspec.."label[2,2;"..S("Author")..": "..minetest.formspec_escape(m_author).."]"
end
if m_license ~= "" then
formspec = formspec.."label[2,2.5;"..S("License")..": "..minetest.formspec_escape(m_license).."]"
end
if m_format ~= "" then
formspec = formspec.."label[2,3;"..S("Format")..": "..minetest.formspec_escape(m_format:upper()).."]"
end
return formspec
end
function chchar.get_model_selection_formspec(player, context, y_delta)
context.chchar_list = chchar.get_modellist_for_player(player:get_player_name())
context.total_pages = 1
for i, model in ipairs(context.chchar_list) do
local page = math.floor((i-1) / 16)+1
model:set_meta("inv_page", page)
model:set_meta("inv_page_index", (i-1)%16+1)
context.total_pages = page
end
context.chchar_page = context.chchar_page or chchar.get_player_model(player):get_meta("inv_page") or 1
context.dropdown_values = nil
local page = context.chchar_page
local formspec = ""
for i = (page-1)*16+1, page*16 do
local model = context.chchar_list[i]
if not model then
break
end
local index_p = model:get_meta("inv_page_index")
local x = (index_p-1) % 8
local y
if index_p > 8 then
y = y_delta + 1.9
else
y = y_delta
end
formspec = formspec.."image_button["..x..","..y..";1,1;"..
model:get_icon()..";chchar_set$"..i..";]"..
"tooltip[chchar_set$"..i..";"..minetest.formspec_escape(model:get_meta_string("name")).."]"
end
if context.total_pages > 1 then
local page_prev = page - 1
local page_next = page + 1
if page_prev < 1 then
page_prev = context.total_pages
end
if page_next > context.total_pages then
page_next = 1
end
local page_list = ""
context.dropdown_values = {}
for pg=1, context.total_pages do
local pagename = S("Page").." "..pg.."/"..context.total_pages
context.dropdown_values[pagename] = pg
if pg > 1 then page_list = page_list.."," end
page_list = page_list..pagename
end
formspec = formspec
.."button[0,"..(y_delta+4.0)..";1,.5;chchar_page$"..page_prev..";<<]"
.."dropdown[0.9,"..(y_delta+3.88)..";6.5,.5;chchar_selpg;"..page_list..";"..page.."]"
.."button[7,"..(y_delta+4.0)..";1,.5;chchar_page$"..page_next..";>>]"
end
return formspec
end
function chchar.on_model_selection_receive_fields(player, context, fields)
for field, _ in pairs(fields) do
local current = string.split(field, "$", 2)
if current[1] == "chchar_set" then
chchar.set_player_model(player, context.chchar_list[tonumber(current[2])])
return 'set'
elseif current[1] == "chchar_page" then
context.chchar_page = tonumber(current[2])
return 'page'
end
end
if fields.chchar_selpg then
context.chchar_page = tonumber(context.dropdown_values[fields.chchar_selpg])
return 'page'
end
end

128
init.lua
View File

@ -1,11 +1,117 @@
--print("This file will be run at load time!")
--minetest.register_chatcommand("chchar"
--{
-- params = "[<model_name>] [<costume_name>]",
-- description = "Set the model and optionally costume name for the current player",
-- func = function(name, param)
-- local name = player:get_player_name()
-- player_api.set_model(player, model_name)
-- end,
--}
--)
--[[
Change Player Model (chchar), based on Unified Skins for Minetest
Copyright (c) 2012 cornernote, Dean Montgomery
Rework 2017 by bell07
Copyright (c) 2019, 2020 Richard Qian
License: GPLv3
Boilerplate to support localized strings if intllib mod is installed.
--]]
chchar = {}
chchar.modpath = minetest.get_modpath(minetest.get_current_modname())
chchar.default = "character"
local S
if minetest.get_modpath("intllib") then
chchar.S = intllib.Getter()
else
chchar.S = function(s) return s end
end
dofile(chchar.modpath.."/model_meta_api.lua")
dofile(chchar.modpath.."/api.lua")
dofile(chchar.modpath.."/model_list.lua")
dofile(chchar.modpath.."/formspecs.lua")
dofile(chchar.modpath.."/chatcommands.lua")
-- Unified inventory page/integration
if minetest.get_modpath("unified_inventory") then
dofile(chchar.modpath.."/unified_inventory_page.lua")
end
if minetest.get_modpath("sfinv") then
dofile(chchar.modpath.."/sfinv_page.lua")
end
--[[
-- ie.loadfile does not exist?
chchar.ie = minetest.request_insecure_environment()
chchar.http = minetest.request_http_api()
dofile(chchar.modpath.."/model_downloader.lua")
chchar.ie = nil
chchar.http = nil
-- 3d_armor compatibility
if minetest.global_exists("armor") then
skins.armor_loaded = true
armor.get_player_skin = function(self, name)
local skin = skins.get_player_skin(minetest.get_player_by_name(name))
return skin:get_texture()
end
armor.get_preview = function(self, name)
local skin = skins.get_player_skin(minetest.get_player_by_name(name))
return skin:get_preview()
end
armor.update_player_visuals = function(self, player)
if not player then
return
end
local skin = skins.get_player_skin(player)
skin:apply_skin_to_player(player)
armor:run_callbacks("on_update", player)
end
end
if minetest.global_exists("clothing") and clothing.player_textures then
skins.clothing_loaded = true
clothing:register_on_update(skins.update_player_skin)
end--]]
-- Update model on join
chchar.ui_context = {}
minetest.register_on_joinplayer(function(player)
chchar.update_player_model(player)
end)
minetest.register_on_leaveplayer(function(player)
chchar.ui_context[player:get_player_name()] = nil
end)
--[[
if minetest.global_exists("player_api") then
-- Minetest-5 and above compatible
player_api.register_model("skinsdb_3d_armor_character_5.b3d", {
animation_speed = 30,
textures = {
"blank.png",
"blank.png",
"blank.png",
"blank.png"
},
animations = {
stand = {x=0, y=79},
lay = {x=162, y=166},
walk = {x=168, y=187},
mine = {x=189, y=198},
walk_mine = {x=200, y=219},
sit = {x=81, y=160},
},
})
else
-- Minetest-0.4 compatible
default.player_register_model("skinsdb_3d_armor_character.b3d", {
animation_speed = 30,
textures = {
"blank.png",
"blank.png",
"blank.png",
"blank.png",
},
animations = {
stand = {x=0, y=79},
lay = {x=162, y=166},
walk = {x=168, y=187},
mine = {x=189, y=198},
walk_mine = {x=200, y=219},
sit = {x=81, y=160},
},
})
end--]]

20
locale/template.txt Normal file
View File

@ -0,0 +1,20 @@
# Template
Raw texture =
Name =
Author =
Change =
Page =
License =
Description =
Show, list or set player's model =
The player =
was not found =
Unknown command =
see /help chchar for supported parameters =
Player model for =
set to =
Player model reset to the default. =
Invalid model =
Unknown parameter =
Unknown model =

30
meta/README.md Normal file
View File

@ -0,0 +1,30 @@
For each model in "/models" folder a metadata file can be applied with "txt" suffix in "/meta". See character.txt for model character.b3d for reference. A '*' means an optional item.
The file contains:
- Model name
- Author*
- License*
- Animation Speed* - How many frames per second the animations play. Default: 30
- Textures - A list of all textures that the model uses. It is possible to leave this field blank, but the model will then not have an optimal appearance.
- Animations* - A list of animations that the model will use on certain actions. Each X and Y point are the starting and ending frames as set in the Blender timeline, respectively. Format for each item is {animation_name:start_frame,end_frame}; (without braces)
Standard animations:
stand = {x = 0, y = 79}
lay = {x = 162, y = 166}
walk = {x = 168, y = 187}
mine = {x = 189, y = 198}
walk_mine = {x = 200, y = 219}
sit = {x = 81, y = 160}
Formatted version would be: stand:0,79;lay:162,166;walk:168,187;mine:189,198;walk_mine:200,219;sit:81,160
- Collision Box* - A list of 2 XYZ coordinates (6 points) in meters relative from the origin of the model. It creates a cube or prism depending on the location of the coordinates. Format is {X1,Y1,Z1,X2,Y2,Z2} Default: {-0.3,0.0,-0.3,0.3,1.7,0.3} (without braces)
- Stepheight* - How fast this model moves on the ground. Default: 0.6
- Eye height* - How far up the Y-axis in meters the camera will be, in 1st-person view. Default: 1.47

9
meta/character.txt Normal file
View File

@ -0,0 +1,9 @@
name = Sam 0
author = Jordach
license = CC BY-SA 3.0
animation_speed = 30
textures = character.png,character_eyes.png
animations => stand:0,79;lay:162,166;walk:168,187;mine:189,198;walk_mine:200,219;sit:81,160
collisionbox = -0.3,0.0,-0.3,0.3,1.7,0.3
stepheight = 0.6
eye_height = 1.47

View File

@ -2,4 +2,5 @@ name = chchar
author = Worldblender
description = Allows players to change the default character model for another one, optionally with different animations.
depends = player_api
optional_depends = intllib,unified_inventory,sfinv
title = Change Player Model

147
model_downloader.lua Normal file
View File

@ -0,0 +1,147 @@
-- 3D model downloader script - rewrite currently incomplete and disabled
local S = chchar.S
local _ID_ = "Lua 3D Model Downloader"
local internal = {}
internal.errors = {}
-- Binary downloads are required
if not core.features.httpfetch_binary_data then
internal.errors[#internal.errors + 1] =
"Feature 'httpfetch_binary_data' is missing. Update Minetest."
end
-- Insecure environment for saving textures and meta
local ie, http = chchar.ie, chchar.http
if not ie or not http then
internal.errors[#internal.errors + 1] = "Insecure environment is required. " ..
"Please add chchar to `secure.trusted_mods` in minetest.conf"
end
minetest.register_chatcommand("skinsdb_download_skins", {
params = "<skindb start page> <amount of pages>",
description = S("Downloads the specified range of skins and shuts down the server"),
privs = {server=true},
func = function(name, param)
if #internal.errors > 0 then
return false, "Cannot run " .. _ID_ .. ":\n\t" ..
table.concat(internal.errors, "\n\t")
end
local parts = string.split(param, " ")
local start = tonumber(parts[1])
local len = tonumber(parts[2])
if not (start and len and len > 0) then
return false, "Invalid page number or amount of pages"
end
internal.get_pages_count(internal.fetch_function, start, len)
return true, "Started downloading..."
end,
})
if #internal.errors > 0 then
return -- Nonsense to load something that's not working
end
-- http://minetest.fensta.bplaced.net/api/apidoku.md
local root_url = "http://minetest.fensta.bplaced.net"
local page_url = root_url .. "/api/v2/get.json.php?getlist&page=%i&outformat=base64" -- [1] = Page#
local preview_url = root_url .. "/skins/1/%i.png" -- [1] = ID
local mod_path = skins.modpath
local meta_path = mod_path .. "/meta/"
local skins_path = mod_path .. "/textures/"
-- Fancy debug wrapper to download an URL
local function fetch_url(url, callback)
http.fetch({
url = url,
user_agent = _ID_
}, function(result)
if result.succeeded then
if result.code ~= 200 then
core.log("warning", ("%s: STATUS=%i URL=%s"):format(
_ID_, result.code, url))
end
return callback(result.data)
end
core.log("warning", ("%s: Failed to download URL=%s"):format(
_ID_, url))
end)
end
-- Insecure workaround since meta/ and textures/ cannot be written to
local function unsafe_file_write(path, contents)
local f = ie.io.open(path, "w")
f:write(contents)
f:close()
end
-- Takes a valid skin table from the Skins Database and saves it
local function safe_single_skin(skin)
local meta = {
skin.name,
skin.author,
skin.license
}
local name = "character_" .. skin.id
-- core.safe_file_write does not work here
unsafe_file_write(
meta_path .. name .. ".txt",
table.concat(meta, "\n")
)
unsafe_file_write(
skins_path .. name .. ".png",
core.decode_base64(skin.img)
)
fetch_url(preview_url:format(skin.id), function(preview)
unsafe_file_write(skins_path .. name .. "_preview.png", preview)
end)
core.log("action", ("%s: Completed skin %s"):format(_ID_, name))
end
-- Get total pages since it'll just return the last page all over again
internal.get_pages_count = function(callback, ...)
local vars = {...}
fetch_url(page_url:format(1) .. "&per_page=1", function(data)
local list = core.parse_json(data)
-- "per_page" defaults to 20 if left away (docs say something else, though)
callback(math.ceil(list.pages / 20), unpack(vars))
end)
end
-- Function to fetch a range of pages
internal.fetch_function = function(pages_total, start_page, len)
start_page = math.max(start_page, 1)
local end_page = math.min(start_page + len - 1, pages_total)
for page_n = start_page, end_page do
local page_cpy = page_n
fetch_url(page_url:format(page_n), function(data)
core.log("action", ("%s: Page %i"):format(_ID_, page_cpy))
local list = core.parse_json(data)
for i, skin in pairs(list.skins) do
assert(skin.type == "image/png")
assert(skin.id ~= "")
if skin.id ~= 1 then -- Skin 1 is bundled with skinsdb
safe_single_skin(skin)
end
end
if page_cpy == end_page then
local log = _ID_ .. " finished downloading all skins. " ..
"Shutting down server to reload media cache"
core.log("action", log)
core.request_shutdown(log, true, 3 --[[give some time for pending requests]])
end
end)
end
end

182
model_list.lua Normal file
View File

@ -0,0 +1,182 @@
local models_dir_list = minetest.get_dir_list(chchar.modpath.."/models")
local icons_dir_list = minetest.get_dir_list(chchar.modpath.."/textures")
table.insert(models_dir_list, 1, "character.b3d") -- Add the default model from player_api as the first model in this list
-- Load only the formats that are allowed to be loaded
local file_ext_2d = {"png", "jpg", "bmp", "tga", "pcx", "ppm", "psd", "wal", "rgb"}
local file_ext_3d = {"x", "b3d", "md2", "obj"}
for _, fn in pairs(models_dir_list) do
local name, sort_id, assignment, is_preview, playername
local nameparts = string.gsub(fn, "[.]", "_"):split("_")
local filetype = nameparts[#nameparts]:lower()
-- check allowed file extension (3D model)
if table.indexof(file_ext_3d, filetype) > 0 then
print("Found model file "..fn)
-- cut filename extension
table.remove(nameparts, #nameparts)
-- check if optional icon exists
has_icon = false
iconname = ""
for iconfile = 1, #icons_dir_list, 1 do
runname = icons_dir_list[iconfile]
iconparts = string.gsub(runname, "[.]", "_"):split("_")
if iconparts[1] == nameparts[1] and iconparts[2] == 'icon' and table.indexof(file_ext_2d, iconparts[3]) > 0 then
print("Found icon file "..runname)
has_icon = true
iconname = runname
end
end
-- Build technical model name
name = table.concat(nameparts, '_')
-- Get player name
if nameparts[1] == "player" then
playername = nameparts[2]
table.remove(nameparts, 1)
sort_id = 0
else
sort_id = 5000
end
-- Get sort index
if tonumber(nameparts[#nameparts]) then
sort_id = sort_id + nameparts[#nameparts]
end
local model_obj = chchar.get(name) or chchar.new(name)
model_obj:set_meta("codename", name)
model_obj:set_meta("filename", fn)
model_obj:set_meta("format", filetype)
model_obj:set_icon(iconname)
model_obj:set_meta("_sort_id", sort_id)
if playername then
model_obj:set_meta("assignment", "player:"..playername)
model_obj:set_meta("playername", playername)
end
-- How to parse config files from https://stackoverflow.com/q/55523750
local file = io.open(chchar.modpath.."/meta/"..name..".txt", "r")
if file then
print("Found meta file for "..fn)
for line in file:lines() do
if line:find("name = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("name", data[2])
elseif line:find("author = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("author", data[2])
elseif line:find("license = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("license", data[2])
elseif line:find("animation_speed = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("animation_speed", data[2])
elseif line:find("stepheight = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("stepheight", data[2])
elseif line:find("eye_height = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("eye_height", data[2])
elseif line:find("collisionbox = ") ~= nil then
local data = line:split(" = ")
model_obj:set_meta("collisionbox", data[2]:split())
elseif line:find("textures = ") ~= nil then
local data = line:split(" = ")[2]:split()
if #data < 2 then
model_obj:set_meta("textures", {data})
else
model_obj:set_meta("textures", data)
end
elseif line:find("animations => ") ~= nil then
local data = line:split(" => ")[2]:split(";")
-- Inserting nested tables from https://stackoverflow.com/a/22598242
local animtable = {}
for item = 1, #data, 1 do
local subdata = data[item]:split(":")
local subnum = subdata[2]:split()
animname = subdata[1]
animtable[animname] = subnum
end
model_obj:set_meta("animations", animtable)
end
end
file:close()
else
-- remove player / character prefix if further naming given
if nameparts[2] and not tonumber(nameparts[2]) then
table.remove(nameparts, 1)
end
model_obj:set_meta("name", table.concat(nameparts, ' '))
end
-- Register model with player_api, excluding the default model
if fn ~= "character.b3d" then
player_api.register_model(fn, {
animation_speed = model_obj:get_meta("animation_speed") or 30,
textures = model_obj:get_meta("textures") or {"blank.png"},
animations = model_obj:get_meta("animations") or {},
collisionbox = model_obj:get_meta("collisionbox") or {-0.3, 0.0, -0.3, 0.3, 1.7, 0.3},
stepheight = model_obj:get_meta("stepheight") or 0.6,
eye_height = model_obj:get_meta("eye_height") or 1.47,
})
print("registered model "..fn)
end
end
end
local function models_sort(modelslist)
table.sort(modelslist, function(a,b)
local a_id = a:get_meta("_sort_id") or 10000
local b_id = b:get_meta("_sort_id") or 10000
if a_id ~= b_id then
return a:get_meta("_sort_id") < b:get_meta("_sort_id")
else
return a:get_meta("name") < b:get_meta("name")
end
end)
end
--[[
-- (obsolete) get skinlist. If assignment given ("mod:wardrobe" or "player:bell07") select skins matches the assignment. select_unassigned selects the skins without any assignment too
function skins.get_skinlist(assignment, select_unassigned)
minetest.log("deprecated", "skins.get_skinlist() is deprecated. Use skins.get_skinlist_for_player() instead")
local skinslist = {}
for _, skin in pairs(skins.meta) do
if not assignment or
assignment == skin:get_meta("assignment") or
(select_unassigned and skin:get_meta("assignment") == nil) then
table.insert(skinslist, skin)
end
end
skins_sort(skinslist)
return skinslist
end
--]]
-- Get modellist for player. If no player given, public models only selected
function chchar.get_modellist_for_player(playername)
local modelslist = {}
for _, model in pairs(chchar.meta) do
if model:is_applicable_for_player(playername) and model:get_meta("in_inventory_list") ~= false then
table.insert(modelslist, model)
end
end
models_sort(modelslist)
return modelslist
end
-- Get modellist selected by metadata
function chchar.get_modellist_with_meta(key, value)
assert(key, "key parameter for chchar.get_modellist_with_meta() missed")
local modelslist = {}
for _, model in pairs(chchar.meta) do
if model:get_meta(key) == value then
table.insert(modelslist, model)
end
end
models_sort(modelslist)
return modelslist
end

155
model_meta_api.lua Normal file
View File

@ -0,0 +1,155 @@
chchar.meta = {}
local model_class = {}
model_class.__index = model_class
chchar.model_class = model_class
-----------------------
-- Class methods
-----------------------
-- constructor
function chchar.new(key, object)
assert(key, 'Unique models key required, like "character_1"')
local self = object or {}
setmetatable(self, model_class)
self.__index = model_class
self._key = key
self._sort_id = 0
chchar.meta[key] = self
return self
end
-- getter
function chchar.get(key)
return chchar.meta[key]
end
-- Model methods
-- In this implementation it is just access to attrubutes wrapped
-- but this way allow to redefine the functionality for more complex models provider
function model_class:get_key()
return self._key
end
function model_class:set_meta(key, value)
self[key] = value
end
function model_class:get_meta(key)
return self[key]
end
function model_class:get_meta_string(key)
return tostring(self:get_meta(key) or "")
end
function model_class:set_texture(value)
self._texture = value
end
function model_class:get_texture()
return self._texture
end
function model_class:set_icon(value)
self._icon = value
end
function model_class:get_icon()
return self._icon or "image-missing.png"
end
function model_class:apply_model_to_player(player)
--[[
local function concat_texture(base, ext)
if base == "blank.png" then
return ext
elseif ext == "blank.png" then
return base
else
return base .. "^" .. ext
end
end
--]]
local playername = player:get_player_name()
player_api.set_model(player, self:get_meta("filename"))
--[[
local ver = self:get_meta("format") or "1.0"
if minetest.global_exists("player_api") then
-- Minetest-5 compatible
player_api.set_model(player, "skinsdb_3d_armor_character_5.b3d")
else
-- Minetest-0.4 compatible
default.player_set_model(player, "skinsdb_3d_armor_character.b3d")
end
local v10_texture = "blank.png"
local v18_texture = "blank.png"
local armor_texture = "blank.png"
local wielditem_texture = "blank.png"
if ver == "1.8" then
v18_texture = self:get_texture()
else
v10_texture = self:get_texture()
end
--[[
-- Support for clothing
if skins.clothing_loaded and clothing.player_textures[playername] then
local cape = clothing.player_textures[playername].cape
local layers = {}
for k, v in pairs(clothing.player_textures[playername]) do
if k ~= "skin" and k ~= "cape" then
table.insert(layers, v)
end
end
local overlay = table.concat(layers, "^")
v10_texture = concat_texture(v10_texture, cape)
v18_texture = concat_texture(v18_texture, overlay)
end
-- Support for armor
if skins.armor_loaded then
local armor_textures = armor.textures[playername]
if armor_textures then
armor_texture = concat_texture(armor_texture, armor_textures.armor)
wielditem_texture = concat_texture(wielditem_texture, armor_textures.wielditem)
end
end
if minetest.global_exists("player_api") then
-- Minetest-5 compatible
player_api.set_textures(player, {
v10_texture,
v18_texture,
armor_texture,
wielditem_texture,
})
else
-- Minetest-0.4 compatible
default.player_set_textures(player, {
v10_texture,
v18_texture,
armor_texture,
wielditem_texture,
})
end
--]]
player:set_properties({
visual_size = {
x = self:get_meta("visual_size_x") or 1,
y = self:get_meta("visual_size_y") or 1
}
})
end
function model_class:set_model(player)
-- The set_skin is used on skins selection
-- This means the method could be redefined to start an furmslec
-- See character_creator for example
end
function model_class:is_applicable_for_player(playername)
local assigned_player = self:get_meta("playername")
return assigned_player == nil or assigned_player == true or
(assigned_player:lower() == playername:lower())
end

5
models/README.md Normal file
View File

@ -0,0 +1,5 @@
Copy your 3D model files here. Only four formats will be accepted by Minetest:
* Blitz3D (.b3d)
* Quake2 models (.md2)
* Maya/Wavefront (.obj)
* Microsoft DirectX (.x)

21
sfinv_page.lua Normal file
View File

@ -0,0 +1,21 @@
local S = chchar.S
-- generate the current formspec
local function get_formspec(player, context)
local model = chchar.get_player_model(player)
local formspec = chchar.get_model_info_formspec(model)
formspec = formspec..chchar.get_model_selection_formspec(player, context, 4)
return formspec
end
sfinv.register_page("chchar:overview", {
title = "Models",
get = function(self, player, context)
-- collect models data
return sfinv.make_formspec(player, context, get_formspec(player, context))
end,
on_player_receive_fields = function(self, player, context, fields)
chchar.on_model_selection_receive_fields(player, context, fields)
sfinv.set_player_inventory_formspec(player)
end
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

BIN
textures/chchar_button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

133
textures/gtk-convert_16.svg Executable file
View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16px"
height="16px"
id="svg4908"
sodipodi:version="0.32"
inkscape:version="0.44+devel"
sodipodi:docbase="/home/andreas/project/gtk/16"
sodipodi:docname="gtk-convert.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:export-filename="/home/andreas/project/gtk-convert.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
sodipodi:modified="true">
<defs
id="defs4910">
<linearGradient
inkscape:collect="always"
id="linearGradient5899">
<stop
style="stop-color:#f66d6d;stop-opacity:1"
offset="0"
id="stop5901" />
<stop
style="stop-color:#d00e0e;stop-opacity:1"
offset="1"
id="stop5903" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5899"
id="radialGradient5905"
cx="10.628995"
cy="11.018212"
fx="10.628995"
fy="11.018212"
r="2.8514368"
gradientTransform="matrix(0.9387545,-1.1022182e-2,1.2105798e-2,0.917556,0.6110392,1.1229117)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.197802"
inkscape:cx="12.98387"
inkscape:cy="9.5174568"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:window-width="1674"
inkscape:window-height="969"
inkscape:window-x="0"
inkscape:window-y="26" />
<metadata
id="metadata4913">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
sodipodi:type="arc"
style="opacity:1;fill:url(#radialGradient5905);fill-opacity:1.0;stroke:#790000;stroke-width:0.92762601;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4926"
sodipodi:cx="11.352475"
sodipodi:cy="11.630198"
sodipodi:rx="2.3876238"
sodipodi:ry="2.2524753"
d="M 13.740099 11.630198 A 2.3876238 2.2524753 0 1 1 8.9648514,11.630198 A 2.3876238 2.2524753 0 1 1 13.740099 11.630198 z"
transform="matrix(1.0470662,0,0,1.1098901,0.1132071,-0.9082411)" />
<path
sodipodi:type="arc"
style="opacity:0.58695652;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4928"
sodipodi:cx="11.712872"
sodipodi:cy="12.125743"
sodipodi:rx="1.1262376"
sodipodi:ry="1.1262376"
d="M 12.839109 12.125743 A 1.1262376 1.1262376 0 1 1 10.586634,12.125743 A 1.1262376 1.1262376 0 1 1 12.839109 12.125743 z"
transform="matrix(0.8879121,0,0,0.8879121,0.5999996,0.2334061)" />
<path
style="fill:#3465a4;fill-rule:evenodd;stroke:#132c52;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:1"
d="M 0.5,5.8999999 L 3.5387914,0.47747524 L 8.5000002,5.5391088 L 4.2213588,9.4999998 L 0.5,5.8999999 z "
id="path5907"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:0.54891304;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
d="M 3,2.4641089 L 3.680198,1.3266089 L 4,1.6757426 L 4,8.6170792 L 3,7.6034653 L 3,2.4641089 z "
id="rect5909"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.43478261"
d="M 1.1351485,5.8486987 L 3,2.5076731 L 3,7.5495049 L 1.1351485,5.8486987 z "
id="path5911"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.32608696"
d="M 1.729703,5.7522277 L 3.7747525,2.090099 L 7.0675743,5.5034652 L 4.2121287,8.1351485 L 1.729703,5.7522277 z "
id="path5914"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 9.044769,2.4999999 L 11.480646,2.4999999 C 12.01156,2.4192207 12.478763,3.0711021 12.5,3.4999999 L 12.5,5.4999998"
id="path5916"
sodipodi:nodetypes="cccc" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 10,5 L 15,5 L 12.487086,8 L 10,5 z "
id="path5918" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

172
textures/gtk-convert_24.svg Executable file
View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
id="svg4908"
sodipodi:version="0.32"
inkscape:version="0.44+devel"
version="1.0"
sodipodi:docbase="/home/andreas/project/gtk/24"
sodipodi:docname="gtk-convert.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:export-filename="/home/andreas/project/gtk/24/gtk-convert.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
sodipodi:modified="true">
<defs
id="defs4910">
<linearGradient
inkscape:collect="always"
id="linearGradient5948">
<stop
style="stop-color:#eeeeec;stop-opacity:1;"
offset="0"
id="stop5950" />
<stop
style="stop-color:#eeeeec;stop-opacity:0;"
offset="1"
id="stop5952" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5904">
<stop
style="stop-color:#f20000;stop-opacity:1"
offset="0"
id="stop5906" />
<stop
style="stop-color:#b00000;stop-opacity:1"
offset="1"
id="stop5908" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5904"
id="radialGradient5910"
cx="16.60972"
cy="17.044081"
fx="16.60972"
fy="17.044081"
r="4.9853533"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8665774,0.3622763,-0.3153673,0.754369,7.4985738,-1.772133)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5948"
id="linearGradient5954"
x1="16.231808"
y1="16.568331"
x2="17.704367"
y2="18.141708"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="18.711881"
inkscape:cy="10.748197"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
width="24px"
height="24px"
inkscape:window-width="1674"
inkscape:window-height="969"
inkscape:window-x="0"
inkscape:window-y="26" />
<metadata
id="metadata4913">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
style="fill:none;fill-rule:evenodd;stroke:#15315b;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 1.4999999,11.58685 L 6.0833329,15.516192 L 12.499999,11.542198 L 5.6178032,4.4425904 L 1.4999999,11.58685 z "
id="path4923" />
<path
style="fill:#204a87;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 6,15 L 6,5.531158 L 12,11.330226 L 6,15 z "
id="path5894"
sodipodi:nodetypes="cccc" />
<path
style="fill:#729fcf;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 5.0306677,14 L 5.0306677,6.473504 L 2,11.424191 L 5.0306677,14 z "
id="path5896"
sodipodi:nodetypes="cccc" />
<path
style="fill:#b6cee7;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 6,5.6160448 L 5.6791892,5.2784591 L 5.0225843,6.4560847 L 5,13.995216 L 5.9812162,14.884752 L 6,5.6160448 z "
id="path5898"
sodipodi:nodetypes="cccccc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.23369565"
d="M 2.6344456,11.290236 L 6.1116291,14.334887 L 11.223258,11.245584 L 5.8437193,6.0669774 L 2.6344456,11.290236 z "
id="path5900"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:type="arc"
style="opacity:1;fill:url(#radialGradient5910);fill-opacity:1;stroke:#8d0000;stroke-width:0.90642792;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path5902"
sodipodi:cx="17.525761"
sodipodi:cy="17.905054"
sodipodi:rx="4.5321393"
sodipodi:ry="4.5321393"
d="M 22.0579 17.905054 A 4.5321393 4.5321393 0 1 1 12.993621,17.905054 A 4.5321393 4.5321393 0 1 1 22.0579 17.905054 z"
transform="matrix(1.1032317,0,0,1.1032318,-1.8349745,-1.2534248)" />
<path
sodipodi:type="arc"
style="opacity:0.20652173;fill:url(#linearGradient5954);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path5912"
sodipodi:cx="16.699705"
sodipodi:cy="16.855742"
sodipodi:rx="1.6074584"
sodipodi:ry="1.6074584"
d="M 18.307163 16.855742 A 1.6074584 1.6074584 0 1 1 15.092247,16.855742 A 1.6074584 1.6074584 0 1 1 18.307163 16.855742 z"
transform="matrix(1.5552502,0,0,1.5552506,-9.4722197,-9.7149015)" />
<path
sodipodi:type="arc"
style="opacity:0.30434783;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.96299076;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path5922"
sodipodi:cx="17.870581"
sodipodi:cy="18.285204"
sodipodi:rx="3.8519626"
sodipodi:ry="3.8519626"
d="M 21.722543 18.285204 A 3.8519626 3.8519626 0 1 1 14.018618,18.285204 A 3.8519626 3.8519626 0 1 1 21.722543 18.285204 z"
transform="matrix(1.0384317,0,0,1.0384316,-1.0573766,-0.4879334)" />
<path
style="fill:#edd400;fill-rule:evenodd;stroke:#a48600;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 15.508502,6.4991071 L 13,6.4991071 L 17.5625,11.499107 L 22,6.4991071 L 19.489631,6.4991071 C 19.489631,6.4991071 19.993728,0.52600781 15,0.4757553 C 9.9620777,0.42550279 8.5303301,3.3812816 8.5303301,3.3812816 C 8.5303301,3.3812816 8.3091456,4.5552427 8.7950679,5.0883883 C 9.7644658,4.2049764 10.167099,3.4891404 12.96875,3.4572916 C 15.739151,3.4254428 15.508502,6.4991071 15.508502,6.4991071 z "
id="path5966"
sodipodi:nodetypes="ccccczcczc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.65217391"
d="M 15.25,7.5 L 17.5,10 L 19.75,7.5 L 18.5,7.5 C 18.510417,3.8020833 17.895833,1.5729167 15.03125,1.53125 C 13.03125,1.5416666 11.5625,1.8645833 10.3125,2.8749999 C 14.177083,1.5520833 16.885417,3.0729166 16.5,7.5 L 15.25,7.5 z "
id="path5968"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
textures/image-missing.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

239
textures/image-missing.svg Executable file
View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
id="svg2"
inkscape:label="Pozadí"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docbase="/home/cornelius/GFX/ikony/GTK/my-stuff/24x24"
sodipodi:docname="gtk-missing-image.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs3">
<linearGradient
inkscape:collect="always"
id="linearGradient4916">
<stop
style="stop-color:#888a85;stop-opacity:1"
offset="0"
id="stop4918" />
<stop
style="stop-color:#5d5f5b;stop-opacity:0;"
offset="1"
id="stop4920" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient6108">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6110" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop6112" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient6076">
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="0"
id="stop6078" />
<stop
style="stop-color:#cbcbcb;stop-opacity:1"
offset="1"
id="stop6080" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient6056">
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="0"
id="stop6058" />
<stop
style="stop-color:#d3d7cf;stop-opacity:1"
offset="1"
id="stop6060" />
</linearGradient>
<filter
inkscape:collect="always"
x="-0.14148472"
width="1.2829694"
y="-0.70742352"
height="2.414847"
id="filter6052">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.1790392"
id="feGaussianBlur6054" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6056"
id="linearGradient6062"
x1="9.7099533"
y1="14.716651"
x2="24.285765"
y2="-5.9535513"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.117647,0,0,1.1,-1.4117645,-1.15)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6076"
id="linearGradient6082"
x1="17.68836"
y1="4.5279388"
x2="18.561535"
y2="3.7493312"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3472897,0,0,1.3356241,-6.7302791,-1.5076969)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6108"
id="linearGradient6114"
x1="16.552481"
y1="2.9626985"
x2="16.552481"
y2="7.8355665"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9803921,0,0,1.1,1.2941182,-0.8000001)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4916"
id="linearGradient4922"
x1="18.963779"
y1="6.504528"
x2="14.04664"
y2="3.6734154"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="20.182764"
inkscape:cx="19.575284"
inkscape:cy="14.566885"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="24px"
height="24px"
inkscape:showpageshadow="false"
showgrid="true"
inkscape:window-width="967"
inkscape:window-height="792"
inkscape:window-x="256"
inkscape:window-y="29" />
<metadata
id="metadata6">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>gtk Missing image</dc:title>
<dc:date>29.3.2007</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>Josef Vybíral</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>missing image</rdf:li>
</rdf:Bag>
</dc:subject>
<cc:license
rdf:resource="http://creativecommons.org/licenses/LGPL/2.1/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/LGPL/2.1/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
<cc:requires
rdf:resource="http://web.resource.org/cc/SourceCode" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Vrstva 1"
inkscape:groupmode="layer"
id="layer1">
<rect
ry="1.6350584"
rx="1.6350582"
y="19"
x="2.0000019"
height="4"
width="19.999998"
id="rect5870"
style="opacity:0.12663755;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;filter:url(#filter6052)" />
<path
style="color:#000000;fill:url(#linearGradient6062);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:0.9999997px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 4.3274179,0.49999959 L 15.796562,0.49999959 L 21.078809,5.8278139 C 21.360792,6.1872781 21.511555,6.5467422 21.5,6.9062064 L 21.5,20.701435 C 21.5,21.69784 20.684972,22.5 19.672582,22.5 L 4.3274179,22.5 C 3.3150284,22.5 2.4999999,21.69784 2.4999999,20.701435 L 2.4999999,2.2985637 C 2.4999999,1.3021592 3.3150284,0.49999959 4.3274179,0.49999959 z "
id="rect4899"
sodipodi:nodetypes="cccccccccc" />
<path
sodipodi:nodetypes="ccccccccc"
id="path6084"
d="M 5.3489939,1.4999999 L 15.221123,1.4999999 L 20.5,7.2473524 L 20.462646,19.683268 C 20.462646,20.689737 19.637994,21.499999 18.613652,21.499999 L 5.3489939,21.499999 C 4.3246511,21.499999 3.4999997,20.689737 3.4999997,19.683268 L 3.4999997,3.3167312 C 3.4999997,2.310262 4.3246511,1.4999999 5.3489939,1.4999999 z "
style="color:#000000;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.99999964px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
style="opacity:1;color:#000000;fill:#d3d7cf;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 6 5 L 6 6 L 6 17 L 6 18 L 18 18 L 18 17 L 18 6 L 17 6 L 17 17 L 7 17 L 7 6 L 15 6 L 15 5 L 7 5 L 6 5 z "
id="rect5899" />
<path
sodipodi:nodetypes="ccccc"
id="path6104"
d="M 16,2.0708482 L 20.981955,6.91941 L 21,8 L 16.036229,7.9999999 L 16,2.0708482 z "
style="fill:url(#linearGradient6114);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:url(#linearGradient6082);fill-opacity:1.0;fill-rule:evenodd;stroke:url(#linearGradient4922);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 21.004529,6.5000002 L 15.536602,6.5 L 15.500001,0.84683065"
id="path6074"
sodipodi:nodetypes="ccc" />
<g
id="g5906"
transform="translate(-0.5,-0.5)">
<path
id="path5902"
d="M 10,9 L 15,14"
style="fill:none;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:none;fill-rule:evenodd;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 15,9 L 10,14"
id="path5904" />
</g>
<path
id="path5910"
d="M 7,6 L 7,7 L 7,17 L 8,17 L 8,7 L 15,7 L 15,6 L 8,6 L 7,6 z "
style="opacity:1;color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
textures/ui_misc_form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,48 @@
local S = chchar.S
unified_inventory.register_page("chchar", {
get_formspec = function(player)
local model = chchar.get_player_model(player)
local formspec = "background[0.06,0.99;7.92,7.52;ui_misc_form.png]"..chchar.get_model_info_formspec(model)..
"button[.75,3.7;6.5,.5;chchar_page;"..S("Change").."]"
return {formspec=formspec}
end,
})
unified_inventory.register_button("chchar", {
type = "image",
image = "chchar_button.png",
})
local function get_formspec(player)
local context = chchar.get_formspec_context(player)
local formspec = "background[0.06,0.99;7.92,7.52;ui_misc_form.png]"..
chchar.get_model_selection_formspec(player, context, -0.2)
return formspec
end
unified_inventory.register_page("chchar_page", {
get_formspec = function(player)
return {formspec=get_formspec(player)}
end
})
-- click button handlers
minetest.register_on_player_receive_fields(function(player, formname, fields)
if fields.chchar then
unified_inventory.set_inventory_formspec(player, "craft")
return
end
if formname ~= "" then
return
end
local context = chchar.get_formspec_context(player)
local action = chchar.on_model_selection_receive_fields(player, context, fields)
if action == 'set' then
unified_inventory.set_inventory_formspec(player, "chchar")
elseif action == 'page' then
unified_inventory.set_inventory_formspec(player, "chchar_page")
end
end)