Compare commits
No commits in common. "examples" and "master" have entirely different histories.
88
.gitignore
vendored
Normal file
88
.gitignore
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
|
||||||
|
# Created by https://www.gitignore.io/api/ruby,code,linux,jekyll
|
||||||
|
# Edit at https://www.gitignore.io/?templates=ruby,code,linux,jekyll
|
||||||
|
|
||||||
|
### Code ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/spellright.dict
|
||||||
|
|
||||||
|
### Jekyll ###
|
||||||
|
_site/
|
||||||
|
.sass-cache/
|
||||||
|
.jekyll-cache/
|
||||||
|
.jekyll-metadata
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### Ruby ###
|
||||||
|
*.gem
|
||||||
|
*.rbc
|
||||||
|
/.config
|
||||||
|
/coverage/
|
||||||
|
/InstalledFiles
|
||||||
|
/pkg/
|
||||||
|
/spec/reports/
|
||||||
|
/spec/examples.txt
|
||||||
|
/test/tmp/
|
||||||
|
/test/version_tmp/
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# Used by dotenv library to load environment variables.
|
||||||
|
# .env
|
||||||
|
|
||||||
|
# Ignore Byebug command history file.
|
||||||
|
.byebug_history
|
||||||
|
|
||||||
|
## Specific to RubyMotion:
|
||||||
|
.dat*
|
||||||
|
.repl_history
|
||||||
|
build/
|
||||||
|
*.bridgesupport
|
||||||
|
build-iPhoneOS/
|
||||||
|
build-iPhoneSimulator/
|
||||||
|
|
||||||
|
## Specific to RubyMotion (use of CocoaPods):
|
||||||
|
#
|
||||||
|
# We recommend against adding the Pods directory to your .gitignore. However
|
||||||
|
# you should judge for yourself, the pros and cons are mentioned at:
|
||||||
|
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||||
|
# vendor/Pods/
|
||||||
|
|
||||||
|
## Documentation cache and generated files:
|
||||||
|
/.yardoc/
|
||||||
|
/_yardoc/
|
||||||
|
/doc/
|
||||||
|
/rdoc/
|
||||||
|
|
||||||
|
## Environment normalization:
|
||||||
|
/.bundle/
|
||||||
|
/vendor/bundle
|
||||||
|
/lib/bundler/man/
|
||||||
|
|
||||||
|
# for a library or gem, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# Gemfile.lock
|
||||||
|
# .ruby-version
|
||||||
|
# .ruby-gemset
|
||||||
|
|
||||||
|
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
||||||
|
.rvmrc
|
||||||
|
|
||||||
|
# End of https://www.gitignore.io/api/ruby,code,linux,jekyll
|
31
.gitlab-ci.yml
Normal file
31
.gitlab-ci.yml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
image: jekyll/jekyll:4
|
||||||
|
|
||||||
|
variables:
|
||||||
|
JEKYLL_ENV: production
|
||||||
|
LC_ALL: C.UTF-8
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- rm Gemfile.lock
|
||||||
|
- bundle install
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
interruptible: true
|
||||||
|
script:
|
||||||
|
- bundle exec jekyll build -d test
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- test
|
||||||
|
except:
|
||||||
|
- master
|
||||||
|
|
||||||
|
pages:
|
||||||
|
stage: deploy
|
||||||
|
interruptible: true
|
||||||
|
script:
|
||||||
|
- bundle exec jekyll build -d public
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
only:
|
||||||
|
- master
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"spellright.language": [
|
||||||
|
"en_GB"
|
||||||
|
],
|
||||||
|
"spellright.documentTypes": [
|
||||||
|
"latex",
|
||||||
|
"plaintext",
|
||||||
|
"markdown"
|
||||||
|
]
|
||||||
|
}
|
9
.vscode/spellright.dict
vendored
Normal file
9
.vscode/spellright.dict
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
metatables
|
||||||
|
Lua
|
||||||
|
metatable
|
||||||
|
singleplayer
|
||||||
|
|
||||||
|
Staticdata
|
||||||
|
biomes
|
||||||
|
|
||||||
|
Voronoi
|
@ -1 +0,0 @@
|
|||||||
default
|
|
@ -1,14 +0,0 @@
|
|||||||
print("This file will be run at load time!")
|
|
||||||
|
|
||||||
minetest.register_node("example:node", {
|
|
||||||
description = "This is a node",
|
|
||||||
tiles = {
|
|
||||||
"mymod_node.png",
|
|
||||||
"mymod_node.png",
|
|
||||||
"mymod_node.png",
|
|
||||||
"mymod_node.png",
|
|
||||||
"mymod_node.png",
|
|
||||||
"mymod_node.png"
|
|
||||||
},
|
|
||||||
groups = {cracky = 1}
|
|
||||||
})
|
|
@ -1,6 +0,0 @@
|
|||||||
minetest.register_node("mymod:diamond", {
|
|
||||||
description = "Alien Diamond",
|
|
||||||
tiles = {"mymod_diamond.png"},
|
|
||||||
is_ground_content = true,
|
|
||||||
groups = {cracky=3, stone=1}
|
|
||||||
})
|
|
@ -1,13 +0,0 @@
|
|||||||
minetest.register_craftitem("mymod:diamond_chair", {
|
|
||||||
description = "Diamond Chair",
|
|
||||||
inventory_image = "mymod_diamond_chair.png"
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_craft({
|
|
||||||
output = "mymod:diamond_chair 99",
|
|
||||||
recipe = {
|
|
||||||
{"mymod:diamond_fragments", "", ""},
|
|
||||||
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""},
|
|
||||||
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""}
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,4 +0,0 @@
|
|||||||
minetest.register_craftitem("mymod:diamond_fragments", {
|
|
||||||
description = "Alien Diamond Fragments",
|
|
||||||
inventory_image = "mymod_diamond_fragments.png"
|
|
||||||
})
|
|
@ -1,30 +0,0 @@
|
|||||||
minetest.register_craftitem("mymod:mudpie", {
|
|
||||||
description = "Alien Mud Pie",
|
|
||||||
inventory_image = "myfood_mudpie.png",
|
|
||||||
on_use = minetest.item_eat(20)
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_craftitem("mymod:mudpie_ex", {
|
|
||||||
description = "Alien Mud Pie Extended",
|
|
||||||
inventory_image = "myfood_mudpie.png",
|
|
||||||
on_use = function(itemstack, user, pointed_thing)
|
|
||||||
hp_change = 20
|
|
||||||
replace_with_item = nil
|
|
||||||
|
|
||||||
minetest.chat_send_player(user:get_player_name(), "You ate an alien mud pie!")
|
|
||||||
|
|
||||||
-- Support for hunger mods using minetest.register_on_item_eat
|
|
||||||
for _, callback in pairs(minetest.registered_on_item_eats) do
|
|
||||||
local result = callback(hp_change, replace_with_item, itemstack, user, pointed_thing)
|
|
||||||
if result then
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if itemstack:take_item() ~= nil then
|
|
||||||
user:set_hp(user:get_hp() + hp_change)
|
|
||||||
end
|
|
||||||
|
|
||||||
return itemstack
|
|
||||||
end
|
|
||||||
})
|
|
@ -1,184 +0,0 @@
|
|||||||
minetest.register_node("mymod:diamond", {
|
|
||||||
description = "Alien Diamond",
|
|
||||||
tiles = {
|
|
||||||
"mymod_diamond_up.png",
|
|
||||||
"mymod_diamond_down.png",
|
|
||||||
"mymod_diamond_right.png",
|
|
||||||
"mymod_diamond_left.png",
|
|
||||||
"mymod_diamond_back.png",
|
|
||||||
"mymod_diamond_front.png"
|
|
||||||
},
|
|
||||||
is_ground_content = true,
|
|
||||||
groups = {cracky = 3},
|
|
||||||
drop = "mymod:diamond_fragments"
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:air", {
|
|
||||||
description = "MyAir (you hacker you!)",
|
|
||||||
drawtype = "airlike",
|
|
||||||
|
|
||||||
paramtype = "light",
|
|
||||||
-- ^ Allows light to propagate through the node with the
|
|
||||||
-- light value falling by 1 per node.
|
|
||||||
|
|
||||||
sunlight_propagates = true, -- Sunlight shines through
|
|
||||||
walkable = false, -- Would make the player collide with the air node
|
|
||||||
pointable = false, -- You can't select the node
|
|
||||||
diggable = false, -- You can't dig the node
|
|
||||||
buildable_to = true, -- Nodes can be replace this node.
|
|
||||||
-- (you can place a node and remove the air node
|
|
||||||
-- that used to be there)
|
|
||||||
|
|
||||||
air_equivalent = true,
|
|
||||||
drop = "",
|
|
||||||
groups = {not_in_creative_inventory=1}
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Some properties have been removed as they are beyond the scope of this chapter.
|
|
||||||
minetest.register_node(":default:water_source", {
|
|
||||||
drawtype = "liquid",
|
|
||||||
paramtype = "light",
|
|
||||||
|
|
||||||
inventory_image = minetest.inventorycube("default_water.png"),
|
|
||||||
-- ^ this is required to stop the inventory image from being animated
|
|
||||||
|
|
||||||
tiles = {
|
|
||||||
{
|
|
||||||
name = "default_water_source_animated.png",
|
|
||||||
animation = {
|
|
||||||
type = "vertical_frames",
|
|
||||||
aspect_w = 16,
|
|
||||||
aspect_h = 16,
|
|
||||||
length = 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
special_tiles = {
|
|
||||||
-- New-style water source material (mostly unused)
|
|
||||||
{
|
|
||||||
name = "default_water_source_animated.png",
|
|
||||||
animation = {type = "vertical_frames", aspect_w = 16, aspect_h = 16, length = 2.0},
|
|
||||||
backface_culling = false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Behavior
|
|
||||||
--
|
|
||||||
walkable = false, -- The player falls through
|
|
||||||
pointable = false, -- The player can't highlight it
|
|
||||||
diggable = false, -- The player can't dig it
|
|
||||||
buildable_to = true, -- Nodes can be replace this node
|
|
||||||
|
|
||||||
alpha = 160,
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Liquid Properties
|
|
||||||
--
|
|
||||||
drowning = 1,
|
|
||||||
liquidtype = "source",
|
|
||||||
|
|
||||||
liquid_alternative_flowing = "default:water_flowing",
|
|
||||||
-- ^ when the liquid is flowing
|
|
||||||
|
|
||||||
liquid_alternative_source = "default:water_source",
|
|
||||||
-- ^ when the liquid is a source
|
|
||||||
|
|
||||||
liquid_viscosity = WATER_VISC,
|
|
||||||
-- ^ how far
|
|
||||||
|
|
||||||
post_effect_color = {a=64, r=100, g=100, b=200},
|
|
||||||
-- ^ color of screen when the player is submerged
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:obsidian_glass", {
|
|
||||||
description = "Obsidian Glass",
|
|
||||||
drawtype = "glasslike",
|
|
||||||
tiles = {"default_obsidian_glass.png"},
|
|
||||||
paramtype = "light",
|
|
||||||
is_ground_content = false,
|
|
||||||
sunlight_propagates = true,
|
|
||||||
sounds = default.node_sound_glass_defaults(),
|
|
||||||
groups = {cracky=3,oddly_breakable_by_hand=3},
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:glass", {
|
|
||||||
description = "Glass",
|
|
||||||
drawtype = "glasslike_framed",
|
|
||||||
|
|
||||||
tiles = {"default_glass.png", "default_glass_detail.png"},
|
|
||||||
inventory_image = minetest.inventorycube("default_glass.png"),
|
|
||||||
|
|
||||||
paramtype = "light",
|
|
||||||
sunlight_propagates = true, -- Sunlight can shine through block
|
|
||||||
is_ground_content = false, -- Stops caves from being generated over this node.
|
|
||||||
|
|
||||||
groups = {cracky = 3, oddly_breakable_by_hand = 3},
|
|
||||||
sounds = default.node_sound_glass_defaults()
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:leaves", {
|
|
||||||
description = "Leaves",
|
|
||||||
drawtype = "allfaces_optional",
|
|
||||||
tiles = {"default_leaves.png"}
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:torch", {
|
|
||||||
description = "Foobar Torch",
|
|
||||||
drawtype = "torchlike",
|
|
||||||
tiles = {
|
|
||||||
{"foobar_torch_floor.png"},
|
|
||||||
{"foobar_torch_ceiling.png"},
|
|
||||||
{"foobar_torch_wall.png"}
|
|
||||||
},
|
|
||||||
inventory_image = "foobar_torch_floor.png",
|
|
||||||
wield_image = "default_torch_floor.png",
|
|
||||||
light_source = LIGHT_MAX-1,
|
|
||||||
|
|
||||||
-- Determines how the torch is selected, ie: the wire box around it.
|
|
||||||
-- each value is { x1, y1, z1, x2, y2, z2 }
|
|
||||||
-- (x1, y1, y1) is the bottom front left corner
|
|
||||||
-- (x2, y2, y2) is the opposite - top back right.
|
|
||||||
-- Similar to the nodebox format.
|
|
||||||
selection_box = {
|
|
||||||
type = "wallmounted",
|
|
||||||
wall_top = {-0.1, 0.5-0.6, -0.1, 0.1, 0.5, 0.1},
|
|
||||||
wall_bottom = {-0.1, -0.5, -0.1, 0.1, -0.5+0.6, 0.1},
|
|
||||||
wall_side = {-0.5, -0.3, -0.1, -0.5+0.3, 0.3, 0.1},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:stair_stone", {
|
|
||||||
drawtype = "nodebox",
|
|
||||||
paramtype = "light",
|
|
||||||
node_box = {
|
|
||||||
type = "fixed",
|
|
||||||
fixed = {
|
|
||||||
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
|
||||||
{-0.5, 0, 0, 0.5, 0.5, 0.5},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_node("mymod:sign", {
|
|
||||||
drawtype = "nodebox",
|
|
||||||
node_box = {
|
|
||||||
type = "wallmounted",
|
|
||||||
|
|
||||||
-- Ceiling
|
|
||||||
wall_top = {
|
|
||||||
{-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}
|
|
||||||
},
|
|
||||||
|
|
||||||
-- Floor
|
|
||||||
wall_bottom = {
|
|
||||||
{-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}
|
|
||||||
},
|
|
||||||
|
|
||||||
-- Wall
|
|
||||||
wall_side = {
|
|
||||||
{-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
@ -1,17 +0,0 @@
|
|||||||
minetest.register_node("aliens:grass", {
|
|
||||||
description = "Alien Grass",
|
|
||||||
light_source = 3, -- The node radiates light. Values can be from 1 to 15
|
|
||||||
tiles = {"aliens_grass.png"},
|
|
||||||
groups = {choppy=1},
|
|
||||||
on_use = minetest.item_eat(20)
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_abm({
|
|
||||||
nodenames = {"default:dirt_with_grass"},
|
|
||||||
neighbors = {"default:water_source", "default:water_flowing"},
|
|
||||||
interval = 10.0, -- Run every 10 seconds
|
|
||||||
chance = 50, -- Select every 1 in 50 nodes
|
|
||||||
action = function(pos, node, active_object_count, active_object_count_wider)
|
|
||||||
minetest.set_node({x = pos.x, y = pos.y + 1, z = pos.z}, {name = "aliens:grass"})
|
|
||||||
end
|
|
||||||
})
|
|
@ -1,9 +0,0 @@
|
|||||||
minetest.register_chatcommand("antigravity",
|
|
||||||
func = function(name, param)
|
|
||||||
local player = minetest.get_player_by_name(name)
|
|
||||||
player:set_physics_override({
|
|
||||||
gravity = 0.1 -- set gravity to 10% of its original value
|
|
||||||
-- (0.1 * 9.81)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
})
|
|
@ -1,26 +0,0 @@
|
|||||||
-- Show form when the /formspec command is used.
|
|
||||||
minetest.register_chatcommand("formspec", {
|
|
||||||
func = function(name, param)
|
|
||||||
minetest.show_formspec(name, "first_formspec:form",
|
|
||||||
"size[4,3]" ..
|
|
||||||
"label[0,0;Hello, " .. name .. "]" ..
|
|
||||||
"field[1,1.5;3,1;name;Name;]" ..
|
|
||||||
"button_exit[1,2;2,1;exit;Save]")
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Register callback
|
|
||||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
||||||
if formname ~= "first_formspec:form" then
|
|
||||||
-- Formname is not mymod:form,
|
|
||||||
-- exit callback.
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Send message to player.
|
|
||||||
minetest.chat_send_player(player:get_player_name(), "You said: " .. fields.name .. "!")
|
|
||||||
|
|
||||||
-- Return true to stop other minetest.register_on_player_receive_fields
|
|
||||||
-- from receiving this submission.
|
|
||||||
return true
|
|
||||||
end)
|
|
@ -1,63 +0,0 @@
|
|||||||
guessing = {}
|
|
||||||
|
|
||||||
local _contexts = {}
|
|
||||||
|
|
||||||
minetest.register_on_leaveplayer(function(player)
|
|
||||||
_contexts[player:get_player_name()] = nil
|
|
||||||
end)
|
|
||||||
|
|
||||||
local function get_context(name)
|
|
||||||
local context = _contexts[name] or {}
|
|
||||||
_contexts[name] = context
|
|
||||||
return context
|
|
||||||
end
|
|
||||||
|
|
||||||
function guessing.get_formspec(name)
|
|
||||||
local context = get_context(name)
|
|
||||||
context.target = context.target or math.random(1, 10)
|
|
||||||
|
|
||||||
local text
|
|
||||||
if not context.guess then
|
|
||||||
text = "I'm thinking of a number... Make a guess!"
|
|
||||||
elseif context.guess == context.target then
|
|
||||||
text = "Hurray, you got it!"
|
|
||||||
elseif context.guess > context.target then
|
|
||||||
text = "To high!"
|
|
||||||
else
|
|
||||||
text = "To low!"
|
|
||||||
end
|
|
||||||
|
|
||||||
local formspec = {
|
|
||||||
"size[6,3.476]",
|
|
||||||
"real_coordinates[true]",
|
|
||||||
"label[0.375,0.5;", minetest.formspec_escape(text), "]",
|
|
||||||
"field[0.375,1.25;5.25,0.8;number;Number;]",
|
|
||||||
"button[1.5,2.3;3,0.8;guess;Guess]"
|
|
||||||
}
|
|
||||||
|
|
||||||
-- table.concat is faster than ..
|
|
||||||
return table.concat(formspec, "")
|
|
||||||
end
|
|
||||||
|
|
||||||
function guessing.show_to(name)
|
|
||||||
minetest.show_formspec(name, "guessing:game", guessing.get_formspec(name))
|
|
||||||
end
|
|
||||||
|
|
||||||
minetest.register_chatcommand("game", {
|
|
||||||
func = function(name)
|
|
||||||
guessing.show_to(name)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
||||||
if formname ~= "guessing:game" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if fields.guess then
|
|
||||||
local name = player:get_player_name()
|
|
||||||
local context = get_context(name)
|
|
||||||
context.guess = tonumber(fields.number)
|
|
||||||
guessing.show_to(name)
|
|
||||||
end
|
|
||||||
end)
|
|
@ -1,51 +0,0 @@
|
|||||||
--
|
|
||||||
-- Step 1) set context when player requests the formspec
|
|
||||||
--
|
|
||||||
|
|
||||||
-- land_formspec_context[playername] gives the player's context.
|
|
||||||
local land_formspec_context = {}
|
|
||||||
|
|
||||||
minetest.register_chatcommand("land", {
|
|
||||||
func = function(name, param)
|
|
||||||
if param == "" then
|
|
||||||
minetest.chat_send_player(name, "Incorrect parameters - supply a land ID")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Save information
|
|
||||||
land_formspec_context[name] = {id = param}
|
|
||||||
|
|
||||||
minetest.show_formspec(name, "mylandowner:edit",
|
|
||||||
"size[4,4]" ..
|
|
||||||
"field[1,1;3,1;plot;Plot Name;]" ..
|
|
||||||
"field[1,2;3,1;owner;Owner;]" ..
|
|
||||||
"button_exit[1,3;2,1;exit;Save]")
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Step 2) retrieve context when player submits the form
|
|
||||||
--
|
|
||||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
||||||
if formname ~= "mylandowner:edit" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Load information
|
|
||||||
local context = land_formspec_context[player:get_player_name()]
|
|
||||||
|
|
||||||
if context then
|
|
||||||
minetest.chat_send_player(player:get_player_name(), "Id " .. context.id .. " is now called " ..
|
|
||||||
fields.plot .. " and owned by " .. fields.owner)
|
|
||||||
|
|
||||||
-- Delete context if it is no longer going to be used
|
|
||||||
land_formspec_context[player:get_player_name()] = nil
|
|
||||||
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
-- Fail gracefully if the context does not exist.
|
|
||||||
minetest.chat_send_player(player:get_player_name(), "Something went wrong, try again.")
|
|
||||||
end
|
|
||||||
end)
|
|
@ -1,9 +0,0 @@
|
|||||||
minetest.register_on_joinplayer(function(player)
|
|
||||||
local idx = player:hud_add({
|
|
||||||
hud_elem_type = "text",
|
|
||||||
position = {x = 1, y = 0},
|
|
||||||
offset = {x=-100, y = 20},
|
|
||||||
scale = {x = 100, y = 100},
|
|
||||||
text = "My Text"
|
|
||||||
})
|
|
||||||
end)
|
|
@ -1,26 +0,0 @@
|
|||||||
My Super Special Mod
|
|
||||||
====================
|
|
||||||
|
|
||||||
Adds magic, rainbows and other special things.
|
|
||||||
|
|
||||||
Version: 1.1
|
|
||||||
Licence: LGPL 2.1 or later
|
|
||||||
|
|
||||||
Dependencies: default mod (found in minetest_game)
|
|
||||||
|
|
||||||
Report bugs or request help on the forum topic.
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
|
|
||||||
Unzip the archive, rename the folder to to modfoldername and
|
|
||||||
place it in minetest/mods/minetest/
|
|
||||||
|
|
||||||
( Linux: If you have a linux system-wide installation place
|
|
||||||
it in ~/.minetest/mods/minetest/. )
|
|
||||||
|
|
||||||
( If you only want this to be used in a single world, place
|
|
||||||
the folder in worldmods/ in your worlddirectory. )
|
|
||||||
|
|
||||||
For further information or help see:
|
|
||||||
http://wiki.minetest.com/wiki/Installing_Mods
|
|
@ -1 +0,0 @@
|
|||||||
Adds magic, rainbows and other special things.
|
|
@ -1,2 +0,0 @@
|
|||||||
-- Nothing here!
|
|
||||||
print("9_releasing_a_mod: this mod does nothing!")
|
|
10
Gemfile
Normal file
10
Gemfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "jekyll"
|
||||||
|
gem "webrick"
|
||||||
|
|
||||||
|
group :jekyll_plugins do
|
||||||
|
gem "jekyll-sitemap"
|
||||||
|
gem "jekyll-redirect-from"
|
||||||
|
gem "jekyll-sass-converter", "~> 2.0"
|
||||||
|
end
|
75
Gemfile.lock
Normal file
75
Gemfile.lock
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
addressable (2.7.0)
|
||||||
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
|
colorator (1.1.0)
|
||||||
|
concurrent-ruby (1.1.8)
|
||||||
|
em-websocket (0.5.2)
|
||||||
|
eventmachine (>= 0.12.9)
|
||||||
|
http_parser.rb (~> 0.6.0)
|
||||||
|
eventmachine (1.2.7)
|
||||||
|
ffi (1.15.0)
|
||||||
|
forwardable-extended (2.6.0)
|
||||||
|
http_parser.rb (0.6.0)
|
||||||
|
i18n (1.8.10)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
jekyll (4.2.0)
|
||||||
|
addressable (~> 2.4)
|
||||||
|
colorator (~> 1.0)
|
||||||
|
em-websocket (~> 0.5)
|
||||||
|
i18n (~> 1.0)
|
||||||
|
jekyll-sass-converter (~> 2.0)
|
||||||
|
jekyll-watch (~> 2.0)
|
||||||
|
kramdown (~> 2.3)
|
||||||
|
kramdown-parser-gfm (~> 1.0)
|
||||||
|
liquid (~> 4.0)
|
||||||
|
mercenary (~> 0.4.0)
|
||||||
|
pathutil (~> 0.9)
|
||||||
|
rouge (~> 3.0)
|
||||||
|
safe_yaml (~> 1.0)
|
||||||
|
terminal-table (~> 2.0)
|
||||||
|
jekyll-redirect-from (0.16.0)
|
||||||
|
jekyll (>= 3.3, < 5.0)
|
||||||
|
jekyll-sass-converter (2.1.0)
|
||||||
|
sassc (> 2.0.1, < 3.0)
|
||||||
|
jekyll-sitemap (1.4.0)
|
||||||
|
jekyll (>= 3.7, < 5.0)
|
||||||
|
jekyll-watch (2.2.1)
|
||||||
|
listen (~> 3.0)
|
||||||
|
kramdown (2.3.1)
|
||||||
|
rexml
|
||||||
|
kramdown-parser-gfm (1.1.0)
|
||||||
|
kramdown (~> 2.0)
|
||||||
|
liquid (4.0.3)
|
||||||
|
listen (3.5.1)
|
||||||
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
|
mercenary (0.4.0)
|
||||||
|
pathutil (0.16.2)
|
||||||
|
forwardable-extended (~> 2.6)
|
||||||
|
public_suffix (4.0.6)
|
||||||
|
rb-fsevent (0.11.0)
|
||||||
|
rb-inotify (0.10.1)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
rexml (3.2.5)
|
||||||
|
rouge (3.26.0)
|
||||||
|
safe_yaml (1.0.5)
|
||||||
|
sassc (2.4.0)
|
||||||
|
ffi (~> 1.9)
|
||||||
|
terminal-table (2.0.0)
|
||||||
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
unicode-display_width (1.7.0)
|
||||||
|
webrick (1.7.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
jekyll
|
||||||
|
jekyll-redirect-from
|
||||||
|
jekyll-sitemap
|
||||||
|
webrick
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.2.16
|
427
LICENSE
Normal file
427
LICENSE
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
Attribution-ShareAlike 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||||
|
License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||||
|
License"). To the extent this Public License may be interpreted as a
|
||||||
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
|
such rights in consideration of benefits the Licensor receives from
|
||||||
|
making the Licensed Material available under these terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. BY-SA Compatible License means a license listed at
|
||||||
|
creativecommons.org/compatiblelicenses, approved by Creative
|
||||||
|
Commons as essentially the equivalent of this Public License.
|
||||||
|
|
||||||
|
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
e. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
g. License Elements means the license attributes listed in the name
|
||||||
|
of a Creative Commons Public License. The License Elements of this
|
||||||
|
Public License are Attribution and ShareAlike.
|
||||||
|
|
||||||
|
h. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
i. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
k. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
l. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
m. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. Additional offer from the Licensor -- Adapted Material.
|
||||||
|
Every recipient of Adapted Material from You
|
||||||
|
automatically receives an offer from the Licensor to
|
||||||
|
exercise the Licensed Rights in the Adapted Material
|
||||||
|
under the conditions of the Adapter's License You apply.
|
||||||
|
|
||||||
|
c. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
b. ShareAlike.
|
||||||
|
|
||||||
|
In addition to the conditions in Section 3(a), if You Share
|
||||||
|
Adapted Material You produce, the following conditions also apply.
|
||||||
|
|
||||||
|
1. The Adapter's License You apply must be a Creative Commons
|
||||||
|
license with the same License Elements, this version or
|
||||||
|
later, or a BY-SA Compatible License.
|
||||||
|
|
||||||
|
2. You must include the text of, or the URI or hyperlink to, the
|
||||||
|
Adapter's License You apply. You may satisfy this condition
|
||||||
|
in any reasonable manner based on the medium, means, and
|
||||||
|
context in which You Share Adapted Material.
|
||||||
|
|
||||||
|
3. You may not offer or impose any additional or different terms
|
||||||
|
or conditions on, or apply any Effective Technological
|
||||||
|
Measures to, Adapted Material that restrict exercise of the
|
||||||
|
rights granted under the Adapter's License You apply.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material,
|
||||||
|
including for purposes of Section 3(b); and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public
|
||||||
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
|
its public licenses to material it publishes and in those instances
|
||||||
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
|
material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
|
public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
136
README.md
Normal file
136
README.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Luanti Modding Book
|
||||||
|
|
||||||
|
[![Build status](https://gitlab.com/rubenwardy/minetest_modding_book/badges/master/pipeline.svg)](https://gitlab.com/rubenwardy/minetest_modding_book/pipelines)<br>
|
||||||
|
[Read Online](https://rubenwardy.com/minetest_modding_book/)
|
||||||
|
|
||||||
|
Book written by rubenwardy.
|
||||||
|
License: CC-BY-SA 3.0
|
||||||
|
|
||||||
|
## Finding your way around
|
||||||
|
|
||||||
|
* `_data/` - Contains list of languages
|
||||||
|
* `_layouts/` - Layouts to wrap around each page.
|
||||||
|
* `static/` - CSS, images, scripts.
|
||||||
|
* `_<lang>/`
|
||||||
|
* `<section>/` - Markdown files for each chapter.
|
||||||
|
|
||||||
|
## Contributing chapters
|
||||||
|
|
||||||
|
* Create a pull request with a new chapter in markdown.
|
||||||
|
* Write a new chapter in the text editor of your choice and
|
||||||
|
[send them to me](https://rubenwardy.com/contact/).
|
||||||
|
|
||||||
|
I'm happy to fix the formatting of any chapters. It is
|
||||||
|
the writing which is the hard bit, not the formatting.
|
||||||
|
|
||||||
|
### Chapter and Writing Guide
|
||||||
|
|
||||||
|
Grammar and such:
|
||||||
|
|
||||||
|
* British English, except when referring common code words like `color` and
|
||||||
|
`initialize`.
|
||||||
|
* Prefer pronounless text, but `you` if you must. Never `we` nor `I`.
|
||||||
|
* Titles and subheadings should be in Title Case.
|
||||||
|
* References to code (such as function names) should be formatted as \`inline-code`.
|
||||||
|
* Italics used for emphasis, not necessarily for technical words.
|
||||||
|
* Full stops and correct punctionation, except for lists without full sentences.
|
||||||
|
|
||||||
|
Formatting:
|
||||||
|
|
||||||
|
* Do not rely on anything that isn't printable to a physical book.
|
||||||
|
* Any links must be invisible - ie: if they're removed, then the chapter must
|
||||||
|
still make sense.
|
||||||
|
* Table of contents for each chapter with anchor links.
|
||||||
|
* Add `your turn`s to the end of a chapter when relevant.
|
||||||
|
|
||||||
|
### Making a Chapter
|
||||||
|
|
||||||
|
To create a new chapter, make a new file in _en/section/.
|
||||||
|
Name it something that explains what the chapter is about.
|
||||||
|
Replace spaces with underscores ( _ )
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: Chapter Name
|
||||||
|
layout: default
|
||||||
|
root: ..
|
||||||
|
idx: 4.5
|
||||||
|
long_notice:
|
||||||
|
level: tip
|
||||||
|
title: This is a long tip!
|
||||||
|
message: This is a very long tip, so it would be unreadable if
|
||||||
|
placed in the main body of the chapter. Therefore,
|
||||||
|
it is a good idea to put it in the frontmatter instead.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chapter Name
|
||||||
|
|
||||||
|
Write a paragraph or so explaining what will be covered in this chapter.
|
||||||
|
Explain why/how these concepts are useful in modding
|
||||||
|
|
||||||
|
* [List the](#list-the)
|
||||||
|
* [Parts in](#parts-in)
|
||||||
|
* [This Chapter](#this-chapter)
|
||||||
|
|
||||||
|
## List the
|
||||||
|
|
||||||
|
{% include notice.html notice=page.long_notice %}
|
||||||
|
|
||||||
|
Paragraphs
|
||||||
|
|
||||||
|
\```lua
|
||||||
|
code
|
||||||
|
\```
|
||||||
|
|
||||||
|
## Parts in
|
||||||
|
|
||||||
|
## This Chapter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
If you are editing or creating a particular chapter, then use commit messages like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Getting Started - corrected typos
|
||||||
|
Entities - created chapter
|
||||||
|
```
|
||||||
|
|
||||||
|
Just use a normal style commit message otherwise.
|
||||||
|
|
||||||
|
## Adding a new language
|
||||||
|
|
||||||
|
1. Copy `_en/` to your language code
|
||||||
|
2. Add entry to `_data/languages.yml`
|
||||||
|
3. Add entry to `collections` in `_config.yml`
|
||||||
|
4. Add your language to the if else in `layouts/default.html`
|
||||||
|
5. Translate your language code folder (that you made in step 1)
|
||||||
|
You can translate the file paths, just make sure you keep any ids the same.
|
||||||
|
|
||||||
|
|
||||||
|
## Using Jeykll
|
||||||
|
|
||||||
|
I use [Jekyll](http://jekyllrb.com/) 3.8.0
|
||||||
|
|
||||||
|
# For Debian/Ubuntu based:
|
||||||
|
sudo apt install ruby-dev
|
||||||
|
gem install jekyll github-pages
|
||||||
|
|
||||||
|
### Building as a website
|
||||||
|
|
||||||
|
You can build it as a website using [Jekyll](http://jekyllrb.com/)
|
||||||
|
|
||||||
|
$ jekyll build
|
||||||
|
|
||||||
|
Goes to _site/
|
||||||
|
|
||||||
|
### Webserver for Development
|
||||||
|
|
||||||
|
You can start a webserver on localhost which will automatically
|
||||||
|
rebuild pages when you modify their markdown source.
|
||||||
|
|
||||||
|
$ jekyll serve
|
||||||
|
|
||||||
|
|
||||||
|
This serves at <http://localhost:4000> on my computer, but the port
|
||||||
|
may be different. Check the console for the "server address"
|
@ -1,7 +0,0 @@
|
|||||||
Examples for Minetest Modding Book
|
|
||||||
==================================
|
|
||||||
|
|
||||||
Some of the chapter folders are modpacks. As the mod configurator (eg: configure
|
|
||||||
on world select) doesn't support modpacks in modpacks, you need to take out all
|
|
||||||
of these folders and install them into the mod location.
|
|
||||||
For example, minetest/mods/1_folders and minetest/mods/3_nodes_items_crafting
|
|
1
_conf_wc.yml
Normal file
1
_conf_wc.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
calc_word_count: true
|
16
_config.yml
Normal file
16
_config.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
url: "https://rubenwardy.com"
|
||||||
|
baseurl: "/minetest_modding_book"
|
||||||
|
|
||||||
|
sass:
|
||||||
|
# nested (default), compact, compressed, expanded
|
||||||
|
style: compressed
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- jekyll-sitemap
|
||||||
|
- jekyll-redirect-from
|
||||||
|
|
||||||
|
collections:
|
||||||
|
en:
|
||||||
|
output: true
|
||||||
|
it:
|
||||||
|
output: true
|
9
_data/languages.yml
Normal file
9
_data/languages.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# cta = call to action (used when prompting user their language is available)
|
||||||
|
|
||||||
|
- code: en
|
||||||
|
name: English (UK)
|
||||||
|
cta: This book is available in English
|
||||||
|
|
||||||
|
- code: it
|
||||||
|
name: Italiano
|
||||||
|
cta: Questo libro è disponibile in italiano
|
243
_en/advmap/biomesdeco.md
Normal file
243
_en/advmap/biomesdeco.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
---
|
||||||
|
title: Biomes and Decorations
|
||||||
|
author: Shara
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 6.1
|
||||||
|
description: Create biomes and decorations to customise the map
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The ability to register biomes and decorations is vital when aiming to create an
|
||||||
|
interesting and varied in-game environment. This chapter teaches you how to
|
||||||
|
register biomes, how to control biome distribution, and how to place decorations in biomes.
|
||||||
|
|
||||||
|
- [What are Biomes?](#what-are-biomes)
|
||||||
|
- [Biome Placement](#biome-placement)
|
||||||
|
- [Heat and Humidity](#heat-and-humidity)
|
||||||
|
- [Visualising Boundaries using Voronoi Diagrams](#visualising-boundaries-using-voronoi-diagrams)
|
||||||
|
- [Creating a Voronoi Diagram using Geogebra](#creating-a-voronoi-diagram-using-geogebra)
|
||||||
|
- [Registering a Biome](#registering-a-biome)
|
||||||
|
- [What are Decorations?](#what-are-decorations)
|
||||||
|
- [Registering a Simple Decoration](#registering-a-simple-decoration)
|
||||||
|
- [Registering a Schematic Decoration](#registering-a-schematic-decoration)
|
||||||
|
- [Mapgen Aliases](#mapgen-aliases)
|
||||||
|
|
||||||
|
## What are Biomes?
|
||||||
|
|
||||||
|
A Minetest biome is a specific in-game environment. When registering biomes, you
|
||||||
|
can determine the types of nodes that appear in them during map generation.
|
||||||
|
Some of the most common types of node that may vary between biomes include:
|
||||||
|
|
||||||
|
* Top node: This is the node most commonly found on the surface. A well-known
|
||||||
|
example would be "Dirt with Grass" from Minetest Game.
|
||||||
|
* Filler node: This is the layer immediately beneath the top node.
|
||||||
|
In biomes with grass, it will often be dirt.
|
||||||
|
* Stone node: This is the node you most commonly see underground.
|
||||||
|
* Water node: This is usually a liquid and will be the node that appears
|
||||||
|
where you would expect bodies of water.
|
||||||
|
|
||||||
|
Other types of node can also vary between biomes, providing an opportunity
|
||||||
|
to create vastly different environments within the same game.
|
||||||
|
|
||||||
|
## Biome Placement
|
||||||
|
|
||||||
|
### Heat and Humidity
|
||||||
|
|
||||||
|
It is not enough to simply register a biome; you must also decide where it can
|
||||||
|
occur in game. This is done by assigning a heat and a humidity value to each biome.
|
||||||
|
|
||||||
|
You should think carefully about these values; they determine which biomes can
|
||||||
|
be neighbours to each other. Poor decisions could result in what is meant to
|
||||||
|
be a hot desert sharing a border with a glacier, and other improbable
|
||||||
|
combinations which you may prefer to avoid.
|
||||||
|
|
||||||
|
In game, heat and humidity values at any point of the map will usually be between
|
||||||
|
0 and 100. The values gradually change, increasing or decreasing as you move
|
||||||
|
around the map. The biome at any given point will be determined by which of the
|
||||||
|
registered biomes has heat and humidity values closest to those at that position on the map.
|
||||||
|
|
||||||
|
Because the changes in heat and humidity are gradual, it is good practice to assign
|
||||||
|
heat and humidity values to biomes based on reasonable expectations about that
|
||||||
|
biome’s environment. For example:
|
||||||
|
|
||||||
|
* A desert might have high heat and low humidity.
|
||||||
|
* A snowy forest might have low heat and a medium humidity value.
|
||||||
|
* A swamp biome would generally have high humidity.
|
||||||
|
*
|
||||||
|
In practice, this means that, as long as you have a diverse range of biomes, you
|
||||||
|
are likely to find that the biomes which border each other form a logical progression.
|
||||||
|
|
||||||
|
### Visualising Boundaries using Voronoi Diagrams
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/biomes_voronoi.png" alt="Vernoi">
|
||||||
|
<figcaption>
|
||||||
|
Voronoi diagram, showing the closest point.
|
||||||
|
<span class="credit">By <a href="https://en.wikipedia.org/wiki/Voronoi_diagram#/media/File:Euclidean_Voronoi_diagram.svg">Balu Ertl</a>, CC BY-SA 4.0.</span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Fine-tuning heat and humidity values for biomes is
|
||||||
|
easier if you can visualise the relationship between the biomes you are using.
|
||||||
|
This is most important if you are creating a full set of your own biomes, but
|
||||||
|
can also be helpful if you are adding a biome to an existing set.
|
||||||
|
|
||||||
|
The simplest way to visualise which biomes may share borders is to create a
|
||||||
|
Voronoi diagram, which can be used to show which point on a 2-dimensional
|
||||||
|
diagram any given position is closest to.
|
||||||
|
|
||||||
|
A Voronoi diagram can reveal where biomes that should border each other do not,
|
||||||
|
and where biomes that should not border each other do. It can also give a
|
||||||
|
general insight into how common biomes will be in-game, with larger and more
|
||||||
|
central biomes being more common than smaller biomes or biomes that are located
|
||||||
|
on the outer edge of the diagram.
|
||||||
|
|
||||||
|
This is done by marking a point for each biome based on heat and humidity values,
|
||||||
|
where the x-axis is heat and the y-axis is humidity. The diagram is then
|
||||||
|
divided into areas, such that every position in a given area is closer to the
|
||||||
|
point inside that area than it is to any other point on the diagram.
|
||||||
|
|
||||||
|
Each area represents a biome. If two areas share a border, the biomes they
|
||||||
|
represent in-game can be located next to each other. The length of the border
|
||||||
|
shared between two areas, compared to the length shared with other areas, will
|
||||||
|
tell you how frequently two biomes are likely to be found next to each other.
|
||||||
|
|
||||||
|
### Creating a Voronoi Diagram using Geogebra
|
||||||
|
|
||||||
|
As well as drawing them by hand, you can also create Voronoi diagrams using
|
||||||
|
programs such as [Geogebra](https://www.geogebra.org).
|
||||||
|
|
||||||
|
1. Create points by selecting the point tool in the toolbar (icon is a point with 'A'),
|
||||||
|
and then clicking the chart. You can drag points around or explicitly set their
|
||||||
|
position in the left sidebar. You should also give each point a label, to make things clearer.
|
||||||
|
|
||||||
|
1. Next, create the voronoi by entering the following function into the
|
||||||
|
input box in the left sidebar:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Voronoi({ A, B, C, D, E })
|
||||||
|
```
|
||||||
|
|
||||||
|
Where the each point is inside the curly brackets, separated by commas. You should now
|
||||||
|
|
||||||
|
3. Profit! You should now have a voronoi diagram with all draggable points.
|
||||||
|
|
||||||
|
|
||||||
|
## Registering a Biome
|
||||||
|
|
||||||
|
The following code registers a simple biome named grasslands biome:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_biome({
|
||||||
|
name = "grasslands",
|
||||||
|
node_top = "default:dirt_with_grass",
|
||||||
|
depth_top = 1,
|
||||||
|
node_filler = "default:dirt",
|
||||||
|
depth_filler = 3,
|
||||||
|
y_max = 1000,
|
||||||
|
y_min = -3,
|
||||||
|
heat_point = 50,
|
||||||
|
humidity_point = 50,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This biome has one layer of Dirt with Grass nodes on the surface, and three layers
|
||||||
|
of Dirt nodes beneath this. It does not specify a stone node, so the node defined
|
||||||
|
in the mapgen alias registration for `mapgen_stone` will be present underneath the dirt.
|
||||||
|
|
||||||
|
There are many options when registering a biome, and these are documented
|
||||||
|
in the [Minetest Lua API Reference](https://minetest.gitlab.io/minetest/definition-tables/#biome-definition),
|
||||||
|
as always.
|
||||||
|
|
||||||
|
You don’t need to define every option for every biome you create, but in some cases failure
|
||||||
|
to define either a specific option, or a suitable mapgen alias, can result in map generation errors.
|
||||||
|
|
||||||
|
## What are Decorations?
|
||||||
|
|
||||||
|
Decorations are either nodes or schematics that can be placed on the map at mapgen.
|
||||||
|
Some common examples include flowers, bushes, and trees. Other more creative uses
|
||||||
|
may include hanging icicles or stalagmites in caves, underground crystal formations,
|
||||||
|
or even the placement of small buildings.
|
||||||
|
|
||||||
|
Decorations can be restricted to specific biomes, by height, or by which nodes
|
||||||
|
they can be placed on. They are often used to develop the environment of a biome
|
||||||
|
by ensuring it has specific plants, trees or other features.
|
||||||
|
|
||||||
|
## Registering a Simple Decoration
|
||||||
|
|
||||||
|
Simple decorations are used to place single node decorations on the map during
|
||||||
|
map generation. You must specify the node that is to be placed as a decoration,
|
||||||
|
details for where it can be placed, and how frequently it occurs.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_decoration({
|
||||||
|
deco_type = "simple",
|
||||||
|
place_on = {"base:dirt_with_grass"},
|
||||||
|
sidelen = 16,
|
||||||
|
fill_ratio = 0.1,
|
||||||
|
biomes = {"grassy_plains"},
|
||||||
|
y_max = 200,
|
||||||
|
y_min = 1,
|
||||||
|
decoration = "plants:grass",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the node named `plants:grass` will be placed in the biome named
|
||||||
|
grassy_plains on top of `base:dirt_with_grass` nodes, between the heights of `y = 1` and `y = 200`.
|
||||||
|
|
||||||
|
The fill_ratio value determines how frequently the decoration appears, with higher
|
||||||
|
values up to 1 resulting in a great number of decorations being placed. It is possible
|
||||||
|
to instead use noise parameters to determine placement.
|
||||||
|
|
||||||
|
## Registering a Schematic Decoration
|
||||||
|
|
||||||
|
Schematic decorations are very similar to simple decoration, but involve the placement
|
||||||
|
of a schematic instead of the placement of a single node. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_decoration({
|
||||||
|
deco_type = "schematic",
|
||||||
|
place_on = {"base:desert_sand"},
|
||||||
|
sidelen = 16,
|
||||||
|
fill_ratio = 0.0001,
|
||||||
|
biomes = {"desert"},
|
||||||
|
y_max = 200,
|
||||||
|
y_min = 1,
|
||||||
|
schematic = core.get_modpath("plants") .. "/schematics/cactus.mts",
|
||||||
|
flags = "place_center_x, place_center_z",
|
||||||
|
rotation = "random",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example the cactus.mts schematic is placed in desert biomes. You need to provide
|
||||||
|
a path to a schematic, which in this case is stored in a dedicated schematic directory within the mod.
|
||||||
|
|
||||||
|
This example also sets flags to center the placement of the schematic, and the rotation
|
||||||
|
is set to random. The random rotation of schematics when they are placed as decorations
|
||||||
|
helps introduce more variation when asymmetrical schematics are used.
|
||||||
|
|
||||||
|
|
||||||
|
## Mapgen Aliases
|
||||||
|
|
||||||
|
Existing games should already include suitable mapgen aliases, so you only need
|
||||||
|
to consider registering mapgen aliases of your own if you are making your own game.
|
||||||
|
|
||||||
|
Mapgen aliases provide information to the core mapgen, and can be registered in the form:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_alias("mapgen_stone", "base:smoke_stone")
|
||||||
|
```
|
||||||
|
|
||||||
|
At a minimum you should register:
|
||||||
|
|
||||||
|
* mapgen_stone
|
||||||
|
* mapgen_water_source
|
||||||
|
* mapgen_river_water_source
|
||||||
|
|
||||||
|
If you are not defining cave liquid nodes for all biomes, you should also register:
|
||||||
|
|
||||||
|
* mapgen_lava_source
|
211
_en/advmap/lvm.md
Normal file
211
_en/advmap/lvm.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
title: Lua Voxel Manipulators
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 6.2
|
||||||
|
description: Learn how to use LVMs to speed up map operations.
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/lvm.html
|
||||||
|
- /en/map/lvm.html
|
||||||
|
mapgen_object:
|
||||||
|
level: warning
|
||||||
|
title: LVMs and Mapgen
|
||||||
|
message: Don't use `core.get_voxel_manip()` with mapgen, as it can cause glitches.
|
||||||
|
Use `core.get_mapgen_object("voxelmanip")` instead.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The functions outlined in the [Basic Map Operations](../map/environment.html) chapter
|
||||||
|
are convenient and easy to use, but for large areas they are inefficient.
|
||||||
|
Every time you call `set_node` or `get_node`, your mod needs to communicate with
|
||||||
|
the engine. This results in constant individual copying operations between the
|
||||||
|
engine and your mod, which is slow and will quickly decrease the performance of
|
||||||
|
your game. Using a Lua Voxel Manipulator (LVM) can be a better alternative.
|
||||||
|
|
||||||
|
- [Concepts](#concepts)
|
||||||
|
- [Reading into the LVM](#reading-into-the-lvm)
|
||||||
|
- [Reading Nodes](#reading-nodes)
|
||||||
|
- [Writing Nodes](#writing-nodes)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
An LVM allows you to load large areas of the map into your mod's memory.
|
||||||
|
You can then read and write this data without further interaction with the
|
||||||
|
engine and without running any callbacks, which means that these
|
||||||
|
operations are very fast. Once done, you can then write the area back into
|
||||||
|
the engine and run any lighting calculations.
|
||||||
|
|
||||||
|
## Reading into the LVM
|
||||||
|
|
||||||
|
You can only load a cubic area into an LVM, so you need to work out the minimum
|
||||||
|
and maximum positions that you need to modify. Then you can create and read into
|
||||||
|
an LVM. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local vm = core.get_voxel_manip()
|
||||||
|
local emin, emax = vm:read_from_map(pos1, pos2)
|
||||||
|
```
|
||||||
|
|
||||||
|
For performance reasons, an LVM will almost never read the exact area you tell it to.
|
||||||
|
Instead, it will likely read a larger area. The larger area is given by `emin` and `emax`,
|
||||||
|
which stand for *emerged min pos* and *emerged max pos*. An LVM will load the area
|
||||||
|
it contains for you - whether that involves loading from memory, from disk, or
|
||||||
|
calling the map generator.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.mapgen_object %}
|
||||||
|
|
||||||
|
## Reading Nodes
|
||||||
|
|
||||||
|
To read the types of nodes at particular positions, you'll need to use `get_data()`.
|
||||||
|
This returns a flat array where each entry represents the type of a
|
||||||
|
particular node.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = vm:get_data()
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get param2 and lighting data using the methods `get_light_data()` and `get_param2_data()`.
|
||||||
|
|
||||||
|
You'll need to use `emin` and `emax` to work out where a node is in the flat arrays
|
||||||
|
given by the above methods. There's a helper class called `VoxelArea` which handles
|
||||||
|
the calculation for you.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local a = VoxelArea:new{
|
||||||
|
MinEdge = emin,
|
||||||
|
MaxEdge = emax
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Get node's index
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
|
||||||
|
-- Read node
|
||||||
|
print(data[idx])
|
||||||
|
```
|
||||||
|
|
||||||
|
When you run this, you'll notice that `data[vi]` is an integer. This is because
|
||||||
|
the engine doesn't store nodes using strings, for performance reasons.
|
||||||
|
Instead, the engine uses an integer called a content ID.
|
||||||
|
You can find out the content ID for a particular type of node with
|
||||||
|
`get_content_id()`. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local c_stone = core.get_content_id("default:stone")
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then check whether the node is stone:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
if data[idx] == c_stone then
|
||||||
|
print("is stone!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Content IDs of a node type may change during load time, so it is recommended that
|
||||||
|
you don't try getting them during this time.
|
||||||
|
|
||||||
|
Nodes in an LVM data array are stored in reverse co-ordinate order, so you should
|
||||||
|
always iterate in the order `z, y, x`. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for z = min.z, max.z do
|
||||||
|
for y = min.y, max.y do
|
||||||
|
for x = min.x, max.x do
|
||||||
|
-- vi, voxel index, is a common variable name here
|
||||||
|
local vi = a:index(x, y, z)
|
||||||
|
if data[vi] == c_stone then
|
||||||
|
print("is stone!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason for this touches on the topic of computer architecture. Reading from RAM is rather
|
||||||
|
costly, so CPUs have multiple levels of caching. If the data that a process requests
|
||||||
|
is in the cache, it can very quickly retrieve it. If the data is not in the cache,
|
||||||
|
then a cache miss occurs and it will fetch the data it needs from RAM. Any data
|
||||||
|
surrounding the requested data is also fetched and then replaces the data in the cache. This is
|
||||||
|
because it's quite likely that the process will ask for data near that location again. This means
|
||||||
|
a good rule of optimisation is to iterate in a way that you read data one after
|
||||||
|
another, and avoid *cache thrashing*.
|
||||||
|
|
||||||
|
## Writing Nodes
|
||||||
|
|
||||||
|
First, you need to set the new content ID in the data array:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for z = min.z, max.z do
|
||||||
|
for y = min.y, max.y do
|
||||||
|
for x = min.x, max.x do
|
||||||
|
local vi = a:index(x, y, z)
|
||||||
|
if data[vi] == c_stone then
|
||||||
|
data[vi] = c_air
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
When you finish setting nodes in the LVM, you then need to upload the data
|
||||||
|
array to the engine:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
vm:set_data(data)
|
||||||
|
vm:write_to_map(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
For setting lighting and param2 data, use the appropriately named
|
||||||
|
`set_light_data()` and `set_param2_data()` methods.
|
||||||
|
|
||||||
|
`write_to_map()` takes a Boolean which is true if you want lighting to be
|
||||||
|
calculated. If you pass false, you need to recalculate lighting at a future
|
||||||
|
time using `core.fix_light`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function grass_to_dirt(pos1, pos2)
|
||||||
|
local c_dirt = core.get_content_id("default:dirt")
|
||||||
|
local c_grass = core.get_content_id("default:dirt_with_grass")
|
||||||
|
|
||||||
|
-- Read data into LVM
|
||||||
|
local vm = core.get_voxel_manip()
|
||||||
|
local emin, emax = vm:read_from_map(pos1, pos2)
|
||||||
|
local a = VoxelArea:new{
|
||||||
|
MinEdge = emin,
|
||||||
|
MaxEdge = emax
|
||||||
|
}
|
||||||
|
local data = vm:get_data()
|
||||||
|
|
||||||
|
-- Modify data
|
||||||
|
for z = pos1.z, pos2.z do
|
||||||
|
for y = pos1.y, pos2.y do
|
||||||
|
for x = pos1.x, pos2.x do
|
||||||
|
local vi = a:index(x, y, z)
|
||||||
|
if data[vi] == c_grass then
|
||||||
|
data[vi] = c_dirt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write data
|
||||||
|
vm:set_data(data)
|
||||||
|
vm:write_to_map(true)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Create `replace_in_area(from, to, pos1, pos2)`, which replaces all instances of
|
||||||
|
`from` with `to` in the area given, where `from` and `to` are node names.
|
||||||
|
* Make a function which rotates all chest nodes by 90°.
|
||||||
|
* Make a function which uses an LVM to cause mossy cobble to spread to nearby
|
||||||
|
stone and cobble nodes.
|
||||||
|
Does your implementation cause mossy cobble to spread more than a distance of one node each
|
||||||
|
time? If so, how could you stop this?
|
187
_en/basics/getting_started.md
Normal file
187
_en/basics/getting_started.md
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
---
|
||||||
|
title: Getting Started
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 1.1
|
||||||
|
description: Learn how to make a mod folder, including init.lua, mod.conf and more.
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/folders.html
|
||||||
|
- /en/basics/folders.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Understanding the basic structure of a mod's folder is an essential skill when
|
||||||
|
creating mods. In this chapter, you'll learn about how modding in Minetest works
|
||||||
|
and create your first mod.
|
||||||
|
|
||||||
|
- [What are Games and Mods?](#what-are-games-and-mods)
|
||||||
|
- [Where are mods stored?](#where-are-mods-stored)
|
||||||
|
- [Creating your first mod](#creating-your-first-mod)
|
||||||
|
- [Mod directory](#mod-directory)
|
||||||
|
- [mod.conf](#modconf)
|
||||||
|
- [init.lua](#initlua)
|
||||||
|
- [Summary](#summary)
|
||||||
|
- [Dependencies](#dependencies)
|
||||||
|
- [Mod Packs](#mod-packs)
|
||||||
|
|
||||||
|
|
||||||
|
## What are Games and Mods?
|
||||||
|
|
||||||
|
The power of Minetest is the ability to easily develop games without the need
|
||||||
|
to create your own voxel graphics, voxel algorithms, or fancy networking code.
|
||||||
|
|
||||||
|
In Minetest, a game is a collection of modules which work together to provide the
|
||||||
|
content and behaviour of a game.
|
||||||
|
A module, commonly known as a mod, is a collection of scripts and resources.
|
||||||
|
It's possible to make a game using only one mod, but this is rarely done because it
|
||||||
|
reduces the ease by which parts of the game can be adjusted and replaced
|
||||||
|
independently of others.
|
||||||
|
|
||||||
|
It's also possible to distribute mods outside of a game, in which case they
|
||||||
|
are also *mods* in the more traditional sense - modifications. These mods adjust
|
||||||
|
or extend the features of a game.
|
||||||
|
|
||||||
|
Both the mods contained in a game and third-party mods use the same API.
|
||||||
|
|
||||||
|
This book will cover the main parts of the Minetest API,
|
||||||
|
and is applicable for both game developers and modders.
|
||||||
|
|
||||||
|
|
||||||
|
## Where are mods stored?
|
||||||
|
|
||||||
|
<a name="mod-locations"></a>
|
||||||
|
|
||||||
|
Each mod has its own directory where its Lua code, textures, models, and
|
||||||
|
sounds are placed. Minetest checks in several different locations for
|
||||||
|
mods. These locations are commonly called *mod load paths*.
|
||||||
|
|
||||||
|
For a given world/save game, three mod locations are checked.
|
||||||
|
They are, in order:
|
||||||
|
|
||||||
|
1. Game mods. These are the mods that form the game that the world is running.
|
||||||
|
Eg: `minetest/games/minetest_game/mods/`, `/usr/share/minetest/games/minetest/`
|
||||||
|
2. Global mods, the location to which mods are nearly always installed to.
|
||||||
|
If in doubt, place them here.
|
||||||
|
Eg: `minetest/mods/`
|
||||||
|
3. World mods, the location to store mods which are specific to a
|
||||||
|
particular world.
|
||||||
|
Eg: `minetest/worlds/world/worldmods/`
|
||||||
|
|
||||||
|
`minetest` is the user-data directory. You can find the location of the
|
||||||
|
user-data directory by opening up Minetest and clicking
|
||||||
|
"Open User Data Directory" in the Credits tab.
|
||||||
|
|
||||||
|
When loading mods, Minetest will check each of the above locations in order.
|
||||||
|
If it encounters a mod with a name the same as one found previously, the later
|
||||||
|
mod will be loaded in place of the earlier mod. This means that you can override
|
||||||
|
game mods by placing a mod with the same name in the global mod location.
|
||||||
|
|
||||||
|
|
||||||
|
## Creating your first mod
|
||||||
|
|
||||||
|
### Mod directory
|
||||||
|
|
||||||
|
Go to the global mods directory (About > Open user data directory > mods) and
|
||||||
|
create a new folder called "mymod". `mymod` is the mod name.
|
||||||
|
|
||||||
|
Each mod should have a unique *mod name*, a technical identifier (id) used to
|
||||||
|
refer to the mod. Mod names can include letters, numbers, and underscores. A
|
||||||
|
good name should describe what the mod does, and the directory that contains
|
||||||
|
the components of a mod must have the same name as the mod name. To find out if
|
||||||
|
a mod name is available, try searching for it on
|
||||||
|
[content.minetest.net](https://content.minetest.net).
|
||||||
|
|
||||||
|
mymod
|
||||||
|
├── textures
|
||||||
|
│ └── mymod_node.png files
|
||||||
|
├── init.lua
|
||||||
|
└── mod.conf
|
||||||
|
|
||||||
|
Mods only require an init.lua file;
|
||||||
|
however, mod.conf is recommended and other components may be needed
|
||||||
|
depending on the mod's functionality.
|
||||||
|
|
||||||
|
### mod.conf
|
||||||
|
|
||||||
|
Create a mod.conf file with the following content:
|
||||||
|
|
||||||
|
```
|
||||||
|
name = mymod
|
||||||
|
description = Adds foo, bar, and bo.
|
||||||
|
depends = default
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is used for mod metadata including the mod's name, description, and other
|
||||||
|
information.
|
||||||
|
|
||||||
|
### init.lua
|
||||||
|
|
||||||
|
Create an init.lua file with the following content:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print("This file will be run at load time!")
|
||||||
|
|
||||||
|
core.register_node("mymod:node", {
|
||||||
|
description = "This is a node",
|
||||||
|
tiles = {"mymod_node.png"},
|
||||||
|
groups = {cracky = 1}
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "mymod:node 3",
|
||||||
|
recipe = { "default:dirt", "default:stone" },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The init.lua file is the entrypoint to a mod, and runs when the mod is loaded.
|
||||||
|
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
|
||||||
|
This mod has the name "mymod". It has two text files: init.lua and mod.conf. The
|
||||||
|
script prints a message and then registers a node and a craft recipe – these
|
||||||
|
will be explained later on. There's a single dependency, the
|
||||||
|
[default mod](https://content.minetest.net/metapackages/default/), which is
|
||||||
|
usually found in Minetest Game. There is also a texture in textures/ for the
|
||||||
|
node.
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
A dependency occurs when a mod requires another mod to be loaded before itself.
|
||||||
|
One mod may require another mod's code, items, or other resources to be
|
||||||
|
available for it to use.
|
||||||
|
|
||||||
|
There are two types of dependencies: hard and optional dependencies.
|
||||||
|
Both require the mod to be loaded first. If the mod being depended on isn't
|
||||||
|
available, a hard dependency will cause the mod to fail to load, while an optional
|
||||||
|
dependency might lead to fewer features being enabled.
|
||||||
|
|
||||||
|
An optional dependency is useful if you want to optionally support another mod;
|
||||||
|
it can enable extra content if the user wishes to use both the mods at the same
|
||||||
|
time.
|
||||||
|
|
||||||
|
Dependencies are specified in a comma-separated list in mod.conf.
|
||||||
|
|
||||||
|
depends = modone, modtwo
|
||||||
|
optional_depends = modthree
|
||||||
|
|
||||||
|
## Mod Packs
|
||||||
|
|
||||||
|
Mods can be grouped into mod packs, which allow multiple mods to be packaged
|
||||||
|
and moved together. They are useful if you want to supply multiple mods to
|
||||||
|
a player, but don't want to make them download each one individually.
|
||||||
|
|
||||||
|
modpack1
|
||||||
|
├── modpack.conf (required) - signals that this is a mod pack
|
||||||
|
├── mod1
|
||||||
|
│ └── ... mod files
|
||||||
|
└── mymod (optional)
|
||||||
|
└── ... mod files
|
||||||
|
|
||||||
|
Please note that a modpack is not a *game*.
|
||||||
|
Games have their own organisational structure which will be explained in the
|
||||||
|
Games chapter.
|
197
_en/basics/lua.md
Normal file
197
_en/basics/lua.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
title: Lua Scripting
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 1.2
|
||||||
|
description: A basic introduction to Lua, including a guide on global/local scope.
|
||||||
|
redirect_from: /en/chapters/lua.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you'll learn about scripting in Lua, the tools required
|
||||||
|
to help with this, and some techniques that you may find useful.
|
||||||
|
|
||||||
|
- [Programming](#programming)
|
||||||
|
- [Coding in Lua](#coding-in-lua)
|
||||||
|
- [Code Editors](#code-editors)
|
||||||
|
- [Local and Global Scope](#local-and-global-scope)
|
||||||
|
- [Locals should be used as much as possible](#locals-should-be-used-as-much-as-possible)
|
||||||
|
- [Including other Lua Scripts](#including-other-lua-scripts)
|
||||||
|
|
||||||
|
|
||||||
|
## Programming
|
||||||
|
|
||||||
|
Programming is the action of taking a problem, such as sorting a list
|
||||||
|
of items, and turning it into steps that a computer can understand.
|
||||||
|
Teaching you the logical process of programming is beyond the scope of this book;
|
||||||
|
however, the following websites are quite useful in developing this:
|
||||||
|
|
||||||
|
* [Codecademy](http://www.codecademy.com/) is one of the best resources for
|
||||||
|
learning to write code. It provides an interactive tutorial experience.
|
||||||
|
* [Scratch](https://scratch.mit.edu) is a good resource for starting from
|
||||||
|
absolute basics, and learning the problem-solving techniques required to program.
|
||||||
|
It's great for children and teenagers.
|
||||||
|
* [Programming with Mosh](https://www.youtube.com/user/programmingwithmosh) is
|
||||||
|
a good YouTube series to learn programming.
|
||||||
|
|
||||||
|
### Coding in Lua
|
||||||
|
|
||||||
|
It's also beyond the scope of this book to teach Lua coding.
|
||||||
|
The [Programming in Lua (PiL)](https://www.lua.org/pil/contents.html) book is an
|
||||||
|
excellent introduction to Lua programming.
|
||||||
|
|
||||||
|
|
||||||
|
## Code Editors
|
||||||
|
|
||||||
|
A code editor with code highlighting is sufficient for writing scripts in Lua.
|
||||||
|
Code highlighting uses different colours for words and characters
|
||||||
|
depending on what they represent. This allows you to easily notice
|
||||||
|
mistakes and inconsistencies.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function ctf.post(team,msg)
|
||||||
|
if not ctf.team(team) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if not ctf.team(team).log then
|
||||||
|
ctf.team(team).log = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(ctf.team(team).log,1,msg)
|
||||||
|
ctf.save()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Keywords in this example are highlighted, including `if`, `then`, `end`, and `return`.
|
||||||
|
Functions which come with Lua by default, such as `table.insert`, are also highlighted.
|
||||||
|
|
||||||
|
Commonly used editors which are well-suited for Lua include:
|
||||||
|
|
||||||
|
* [VSCode](https://code.visualstudio.com/):
|
||||||
|
open source (as Code-OSS or VSCodium), popular, and has
|
||||||
|
[plugins for Minetest](https://marketplace.visualstudio.com/items?itemName=GreenXenith.minetest-tools).
|
||||||
|
* [Notepad++](http://notepad-plus-plus.org/): simple, Windows-only
|
||||||
|
|
||||||
|
Other suitable editors are also available.
|
||||||
|
|
||||||
|
|
||||||
|
## Local and Global Scope
|
||||||
|
|
||||||
|
Whether a variable is local or global determines where it can be written to or
|
||||||
|
read from. Global variables can be accessed from anywhere in the script file,
|
||||||
|
and from any other mod:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function one()
|
||||||
|
foo = "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
function two()
|
||||||
|
print(dump(foo)) -- Output: "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
one()
|
||||||
|
two()
|
||||||
|
```
|
||||||
|
|
||||||
|
In constrast, a local variable is only accessible from where it is defined.
|
||||||
|
Lua defaults to variables being global, so you need to explicitly use the
|
||||||
|
`local` keyword:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Accessible from within this script file
|
||||||
|
local one = 1
|
||||||
|
|
||||||
|
function myfunc()
|
||||||
|
-- Accessible from within this function
|
||||||
|
local two = one + one
|
||||||
|
|
||||||
|
if two == one then
|
||||||
|
-- Accessible from within this if statement
|
||||||
|
local three = one + two
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Locals should be used as much as possible
|
||||||
|
|
||||||
|
Local variables should be used whenever possible. Mods should only create one
|
||||||
|
global at most, with the same name as the mod. Creating other globals is sloppy
|
||||||
|
coding, and Minetest will warn about this:
|
||||||
|
|
||||||
|
Assignment to undeclared global 'foo' inside function at init.lua:2
|
||||||
|
|
||||||
|
To correct this, use "local":
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function one()
|
||||||
|
local foo = "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
function two()
|
||||||
|
print(dump(foo)) -- Output: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
one()
|
||||||
|
two()
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember that nil means **not initialised**. The variable hasn't been assigned a
|
||||||
|
value yet, doesn't exist, or has been uninitialised (meaning set to nil).
|
||||||
|
|
||||||
|
Functions are variables of a special type, but should also be made local,
|
||||||
|
because other mods could have functions with the same names.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function foo(bar)
|
||||||
|
return bar * 2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
To allow mods to call your functions, you should create a table with the same
|
||||||
|
name as the mod and add your function to it. This table is often called an API
|
||||||
|
table or namespace.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymod = {}
|
||||||
|
|
||||||
|
function mymod.foo(bar)
|
||||||
|
return "foo" .. bar
|
||||||
|
end
|
||||||
|
|
||||||
|
-- In another mod, or script:
|
||||||
|
mymod.foo("foobar")
|
||||||
|
```
|
||||||
|
|
||||||
|
`function mymod.foo()` is equivalent to `mymod.foo = function()`, it's just a
|
||||||
|
nicer way to write it.
|
||||||
|
|
||||||
|
## Including other Lua Scripts
|
||||||
|
|
||||||
|
The recommended way to include other Lua scripts in a mod is to use *dofile*.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
dofile(core.get_modpath("modname") .. "/script.lua")
|
||||||
|
```
|
||||||
|
|
||||||
|
A script can return a value, which is useful for sharing private locals:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- script.lua
|
||||||
|
local module = {}
|
||||||
|
module.message = "Hello World!"
|
||||||
|
return module
|
||||||
|
|
||||||
|
-- init.lua
|
||||||
|
local ret = dofile(core.get_modpath("modname") .. "/script.lua")
|
||||||
|
print(ret.message) -- Hello world!
|
||||||
|
```
|
||||||
|
|
||||||
|
[Later chapters](../quality/clean_arch.html) will discuss how best to split up
|
||||||
|
code for a mod.
|
93
_en/games/games.md
Normal file
93
_en/games/games.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Creating Games
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 7.1
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The power of Minetest is the ability to easily develop games without the need
|
||||||
|
to create your own voxel graphics, voxel algorithms, or fancy networking code.
|
||||||
|
|
||||||
|
- [What is a Game?](#what-is-a-game)
|
||||||
|
- [Game Directory](#game-directory)
|
||||||
|
- [Inter-game Compatibility](#inter-game-compatibility)
|
||||||
|
- [API Compatibility](#api-compatibility)
|
||||||
|
- [Groups and Aliases](#groups-and-aliases)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## What is a Game?
|
||||||
|
|
||||||
|
Games are a collection of mods which work together to make a cohesive game.
|
||||||
|
A good game has a consistent underlying theme and a direction, for example,
|
||||||
|
it could be a classic crafter miner with hard survival elements, or
|
||||||
|
it could be a space simulation game with a steampunk automation aesthetic.
|
||||||
|
|
||||||
|
Game design is a complex topic and is actually a whole field of expertise.
|
||||||
|
It's beyond the scope of the book to more than briefly touch on it.
|
||||||
|
|
||||||
|
## Game Directory
|
||||||
|
|
||||||
|
The structure and location of a game will seem rather familiar after working
|
||||||
|
with mods.
|
||||||
|
Games are found in a game location, such as `minetest/games/foo_game`.
|
||||||
|
|
||||||
|
foo_game
|
||||||
|
├── game.conf
|
||||||
|
├── menu
|
||||||
|
│ ├── header.png
|
||||||
|
│ ├── background.png
|
||||||
|
│ └── icon.png
|
||||||
|
├── minetest.conf
|
||||||
|
├── mods
|
||||||
|
│ └── ... mods
|
||||||
|
├── README.txt
|
||||||
|
└── settingtypes.txt
|
||||||
|
|
||||||
|
The only thing that is required is a mods folder, but `game.conf` and `menu/icon.png`
|
||||||
|
are recommended.
|
||||||
|
|
||||||
|
## Inter-game Compatibility
|
||||||
|
|
||||||
|
### API Compatibility
|
||||||
|
|
||||||
|
It's a good idea to try to keep as much API compatibility with Minetest Game as
|
||||||
|
convenient, as it'll make porting mods to another game much easier.
|
||||||
|
|
||||||
|
The best way to keep compatibility with another game is to keep API compatibility
|
||||||
|
with any mods which have the same name.
|
||||||
|
That is, if a mod uses the same name as another mod, even if third-party,
|
||||||
|
it should have a compatible API.
|
||||||
|
For example, if a game includes a mod called `doors`, then it should have the
|
||||||
|
same API as `doors` in Minetest Game.
|
||||||
|
|
||||||
|
API compatibility for a mod is the sum of the following things:
|
||||||
|
|
||||||
|
* Lua API table - All documented/advertised functions in the global table which shares the same name.
|
||||||
|
For example, `mobs.register_mob`.
|
||||||
|
* Registered Nodes/Items - The presence of items.
|
||||||
|
|
||||||
|
Small breakages aren't that bad, such as not having a random utility
|
||||||
|
function that was only actually used internally, but bigger breakages
|
||||||
|
related to core features are very bad.
|
||||||
|
|
||||||
|
It's difficult to maintain API compatibility with a disgusting mega God-mod like
|
||||||
|
*default* in Minetest Game, in which case the game shouldn't include a mod named
|
||||||
|
default.
|
||||||
|
|
||||||
|
API compatibility also applies to other third-party mods and games,
|
||||||
|
so try to make sure that any new mods have a unique mod name.
|
||||||
|
To check whether a mod name has been taken, search for it on
|
||||||
|
[content.minetest.net](https://content.minetest.net/).
|
||||||
|
|
||||||
|
### Groups and Aliases
|
||||||
|
|
||||||
|
Groups and Aliases are both useful tools in keeping compatibility between games,
|
||||||
|
as it allows item names to be different between different games. Common nodes
|
||||||
|
like stone and wood should have groups to indicate the material. It's also a
|
||||||
|
good idea to provide aliases from default nodes to any direct replacements.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Create a simple game where the player gains points from digging special blocks.
|
36
_en/index.md
Normal file
36
_en/index.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: Front Cover
|
||||||
|
layout: default
|
||||||
|
description: An easy guide to learn how to create mods for Minetest
|
||||||
|
homepage: true
|
||||||
|
no_header: true
|
||||||
|
root: ..
|
||||||
|
idx: 0.1
|
||||||
|
---
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Luanti Modding Book (formerly Minetest)</h1>
|
||||||
|
|
||||||
|
<span>by <a href="https://rubenwardy.com" rel="author">rubenwardy</a></span>
|
||||||
|
<span>with editing by <a href="http://rc.minetest.tv/">Shara</a></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Minetest uses Lua scripts to provide modding support.
|
||||||
|
This book aims to teach you how to create your own mods, starting from the basics.
|
||||||
|
Each chapter focuses on a particular part of the API, and will soon get you making
|
||||||
|
your own mods.
|
||||||
|
|
||||||
|
As well as [reading this book online](https://rubenwardy.com/minetest_modding_book),
|
||||||
|
you can also [download it in HTML form](https://gitlab.com/rubenwardy/minetest_modding_book/-/releases).
|
||||||
|
|
||||||
|
### Feedback and Contributions
|
||||||
|
|
||||||
|
Noticed a mistake, or want to give feedback? Make sure to tell me about it.
|
||||||
|
|
||||||
|
* Create a [GitLab Issue](https://gitlab.com/rubenwardy/minetest_modding_book/-/issues).
|
||||||
|
* Post in the [Forum Topic](https://forum.minetest.net/viewtopic.php?f=14&t=10729).
|
||||||
|
* [Contact me](https://rubenwardy.com/contact/).
|
||||||
|
* Fancy contributing?
|
||||||
|
[Read the README](https://gitlab.com/rubenwardy/minetest_modding_book/-/blob/master/README.md).
|
206
_en/items/callbacks.md
Normal file
206
_en/items/callbacks.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
---
|
||||||
|
title: Node and Item Callbacks
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.15
|
||||||
|
description: Learn about callbacks, actions, and events, including on_use, on_punch, on_place, on_rightclick
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Minetest heavily uses a callback-based modding design. A callback is a function
|
||||||
|
that you give to an API and is called when an event happens. For example, you
|
||||||
|
can provide an `on_punch` function in a node definition to be called when a player
|
||||||
|
punches a node. There are also global callbacks like
|
||||||
|
`core.register_on_punchnode` to receive events for all nodes.
|
||||||
|
|
||||||
|
- [Item Callbacks](#item-callbacks)
|
||||||
|
- [on_use](#on_use)
|
||||||
|
- [on_place and on_secondary_use](#on_place-and-on_secondary_use)
|
||||||
|
- [on_drop](#on_drop)
|
||||||
|
- [after_use](#after_use)
|
||||||
|
- [item_place vs place_item](#item_place-vs-place_item)
|
||||||
|
- [Node Callbacks](#node-callbacks)
|
||||||
|
- [Right-clicking and placing a node](#right-clicking-and-placing-a-node)
|
||||||
|
- [Punching and digging](#punching-and-digging)
|
||||||
|
- [...and more!](#and-more)
|
||||||
|
|
||||||
|
|
||||||
|
## Item Callbacks
|
||||||
|
|
||||||
|
When a player has a node, craftitem, or tool in their inventory, they may trigger
|
||||||
|
certain events:
|
||||||
|
|
||||||
|
| Callback | Default binding | Default value |
|
||||||
|
|------------------|---------------------------|----------------------------------------------|
|
||||||
|
| on_use | left-click | nil |
|
||||||
|
| on_place | right-click on a node | `core.item_place` |
|
||||||
|
| on_secondary_use | right-click not on a node | `core.item_secondary_use` (does nothing) |
|
||||||
|
| on_drop | Q | `core.item_drop` |
|
||||||
|
| after_use | digging a node | nil |
|
||||||
|
|
||||||
|
|
||||||
|
### on_use
|
||||||
|
|
||||||
|
Having a use callback prevents the item from being used to dig nodes. One common
|
||||||
|
use of the use callback is for food:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("mymod:mudpie", {
|
||||||
|
description = "Alien Mud Pie",
|
||||||
|
inventory_image = "myfood_mudpie.png",
|
||||||
|
on_use = core.item_eat(20),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The number supplied to the core.item_eat function is the number of hit
|
||||||
|
points healed when this food is consumed. Each heart icon the player has is
|
||||||
|
worth two hitpoints. A player can usually have up to 10 hearts, which is equal
|
||||||
|
to 20 hitpoints.
|
||||||
|
|
||||||
|
core.item_eat() is a function that returns a function, setting it as the
|
||||||
|
on_use callback. This means the code above is equivalent to this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("mymod:mudpie", {
|
||||||
|
description = "Alien Mud Pie",
|
||||||
|
inventory_image = "myfood_mudpie.png",
|
||||||
|
on_use = function(...)
|
||||||
|
return core.do_item_eat(20, nil, ...)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
By understanding how item_eat works by simply returning a function, it's
|
||||||
|
possible to modify it to do more complex behaviour like playing a custom sound.
|
||||||
|
|
||||||
|
|
||||||
|
### on_place and on_secondary_use
|
||||||
|
|
||||||
|
The difference between `on_place` and `on_secondary_use` is that `on_place` is
|
||||||
|
called when the player is pointing at a node and `on_secondary_use` when the
|
||||||
|
player isn't.
|
||||||
|
|
||||||
|
Both callbacks are called for all types of items. `on_place` defaults to the
|
||||||
|
`core.item_place` function, which handles calling the `on_rightclick`
|
||||||
|
callback of the pointed node or placing the wielded item if it is a node.
|
||||||
|
|
||||||
|
|
||||||
|
### on_drop
|
||||||
|
|
||||||
|
on_drop is called when the player requests to drop an item, for example using
|
||||||
|
the drop key (Q) or dragging it outside of the inventory. It defaults to the
|
||||||
|
`core.item_drop` function, which will handle dropping the item.
|
||||||
|
|
||||||
|
|
||||||
|
### after_use
|
||||||
|
|
||||||
|
`after_use` is called when digging a node and allows you to customise how wear
|
||||||
|
is applied to a tool. If after_use doesn't exist, then it is the same as:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
after_use = function(itemstack, user, node, digparams)
|
||||||
|
itemstack:add_wear(digparams.wear)
|
||||||
|
return itemstack
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## item_place vs place_item
|
||||||
|
|
||||||
|
Minetest's API includes many different built-in callback implementations for you
|
||||||
|
to use. These callbacks are named with the item type first, for example,
|
||||||
|
`core.item_place` and `core.node_dig`. Some callback implementations are
|
||||||
|
used directly whereas some are functions that return the callback:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_item("mymod:example", {
|
||||||
|
on_place = core.item_place,
|
||||||
|
on_use = core.item_eat(10),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest's API also includes built-in functions that _do_ something. These are
|
||||||
|
often named in a confusingly similar way to built-in callback implementations
|
||||||
|
but have the verb first. Examples include `core.place_item` and
|
||||||
|
`core.dig_node` - these functions allow you to dig and place nodes with a
|
||||||
|
similar effect to players.
|
||||||
|
|
||||||
|
|
||||||
|
## Node Callbacks
|
||||||
|
|
||||||
|
When a node is in an inventory, it uses Item Callbacks, as discussed above. When
|
||||||
|
a node is placed in the world, it uses Node Callbacks. There are quite a lot of
|
||||||
|
node callbacks, too many to discuss in this book. However, quite a few of them
|
||||||
|
will be talked about later in the book.
|
||||||
|
|
||||||
|
Several of the callbacks are related to node operations such as placing and
|
||||||
|
removing from the world. It's important to note that node operation callbacks
|
||||||
|
like these aren't called from bulk changes - those that set a large number of
|
||||||
|
nodes at once - for performance reasons. Therefore, you can't rely on these
|
||||||
|
callbacks to always be called.
|
||||||
|
|
||||||
|
|
||||||
|
### Right-clicking and placing a node
|
||||||
|
|
||||||
|
When the user right-clicks with an item whilst pointing at a node, the item's
|
||||||
|
`on_place` callback is called. By default, this is set to `core.item_place`.
|
||||||
|
If the pointed node has an `on_rightclick` callback and sneak (shift) is held,
|
||||||
|
then the `on_rightclick` callback is called. Otherwise, `core.item_place`
|
||||||
|
will place the node.
|
||||||
|
|
||||||
|
Placing a node will call both `on_construct` and `after_place_node`.
|
||||||
|
`on_construct` is called by any node set event that wasn't in bulk and is just
|
||||||
|
given the node's position and value .`after_place_node` is only called by node
|
||||||
|
place, and so has more information - such as the placer and itemstack.
|
||||||
|
|
||||||
|
It's important to note that players aren't the only objects that can place
|
||||||
|
nodes; it's common for mobs and mods to place nodes. To account for this,
|
||||||
|
`placer` could be a player, entity, or nil.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:mynode", {
|
||||||
|
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
|
||||||
|
if clicker:is_player() then
|
||||||
|
core.chat_send_player(clicker:get_player_name(), "Hello world!")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
on_construct = function(pos, node)
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("infotext", "My node!")
|
||||||
|
end,
|
||||||
|
after_place_node = function(pos, placer, itemstack, pointed_thing)
|
||||||
|
-- Make sure to check placer
|
||||||
|
if placer and placer:is_player() then
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("owner", placer:get_player_name())
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Punching and digging
|
||||||
|
|
||||||
|
Punching is when the player left-clicks for a short period. If the wielded item
|
||||||
|
has an `on_use` callback, this will be called. Otherwise, the `on_punch`
|
||||||
|
callback on the pointed node will be called.
|
||||||
|
|
||||||
|
When the player attempts to dig a node, the `on_dig` callback on the node will be called.
|
||||||
|
This defaults to `core.node_dig`, which will check for area protection, wear
|
||||||
|
out the tool, remove the node, and run the `after_dig_node` callback.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:mynode", {
|
||||||
|
on_punch = function(pos, node, puncher, pointed_thing)
|
||||||
|
if puncher:is_player() then
|
||||||
|
core.chat_send_player(puncher:get_player_name(), "Ow!")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### ...and more!
|
||||||
|
|
||||||
|
Check out Minetest's Lua API reference for a list of all node callbacks, and
|
||||||
|
more information on the callbacks above.
|
98
_en/items/creating_textures.md
Normal file
98
_en/items/creating_textures.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
title: Creating Textures
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.2
|
||||||
|
description: An introduction to making textures in your editor of choice, and a guide on GIMP.
|
||||||
|
redirect_from: /en/chapters/creating_textures.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Being able to create and optimise textures is a very useful skill when
|
||||||
|
developing for Minetest.
|
||||||
|
There are many techniques relevant to working on pixel art textures,
|
||||||
|
and understanding these techniques will greatly improve
|
||||||
|
the quality of the textures you create.
|
||||||
|
|
||||||
|
Detailed approaches to creating good pixel art are outside the scope
|
||||||
|
of this book, and instead only the most relevant basic techniques
|
||||||
|
will be covered.
|
||||||
|
There are many [good online tutorials](http://www.photonstorm.com/art/tutorials-art/16x16-pixel-art-tutorial)
|
||||||
|
available, which cover pixel art in much more detail.
|
||||||
|
|
||||||
|
- [Techniques](#techniques)
|
||||||
|
- [Using the Pencil](#using-the-pencil)
|
||||||
|
- [Tiling](#tiling)
|
||||||
|
- [Transparency](#transparency)
|
||||||
|
- [Color Palettes](#color-palettes)
|
||||||
|
- [Editors](#editors)
|
||||||
|
- [MS Paint](#ms-paint)
|
||||||
|
- [Aseprite / LibreSprite](#aseprite--libresprite)
|
||||||
|
- [GIMP](#gimp)
|
||||||
|
|
||||||
|
## Techniques
|
||||||
|
|
||||||
|
### Using the Pencil
|
||||||
|
|
||||||
|
The pencil tool is available in most editors. When set to its lowest size,
|
||||||
|
it allows you to edit one pixel at a time without changing any other parts
|
||||||
|
of the image. By manipulating the pixels one at a time, you create clear
|
||||||
|
and sharp textures without unintended blurring. It also gives you a high
|
||||||
|
level of precision and control.
|
||||||
|
|
||||||
|
### Tiling
|
||||||
|
|
||||||
|
Textures used for nodes should generally be designed to tile. This means
|
||||||
|
when you place multiple nodes with the same texture together, the edges line
|
||||||
|
up correctly.
|
||||||
|
|
||||||
|
<!-- IMAGE NEEDED - cobblestone that tiles correctly -->
|
||||||
|
|
||||||
|
If you fail to match the edges correctly, the result is far less pleasing
|
||||||
|
to look at.
|
||||||
|
|
||||||
|
<!-- IMAGE NEEDED - node that doesn't tile correctly -->
|
||||||
|
|
||||||
|
### Transparency
|
||||||
|
|
||||||
|
Transparency is important when creating textures for nearly all craftitems
|
||||||
|
and some nodes, such as glass.
|
||||||
|
Not all editors support transparency, so make sure you choose an
|
||||||
|
editor which is suitable for the textures you wish to create.
|
||||||
|
|
||||||
|
### Color Palettes
|
||||||
|
|
||||||
|
Using a consistent color palette is an easy way to make your art look a lot
|
||||||
|
better. It's a good idea to use one with a limited number of colors, perhaps 32
|
||||||
|
at most. Premade palettes can be found at
|
||||||
|
[lospec.com](https://lospec.com/palette-list).
|
||||||
|
|
||||||
|
## Editors
|
||||||
|
|
||||||
|
### MS Paint
|
||||||
|
|
||||||
|
MS Paint is a simple editor which can be useful for basic texture
|
||||||
|
design; however, it does not support transparency.
|
||||||
|
This usually won't matter when making textures for the sides of nodes,
|
||||||
|
but if you need transparency in your textures you should choose a
|
||||||
|
different editor.
|
||||||
|
|
||||||
|
### Aseprite / LibreSprite
|
||||||
|
|
||||||
|
[Aseprite](https://www.aseprite.org/) is a proprietary pixel art editor.
|
||||||
|
It contains a lot of useful features by default such as color palettes and
|
||||||
|
animation tools.
|
||||||
|
|
||||||
|
[LibreSprite](https://libresprite.github.io/) is an open-source fork of Aseprite
|
||||||
|
from before it went proprietary.
|
||||||
|
|
||||||
|
### GIMP
|
||||||
|
|
||||||
|
GIMP is commonly used in the Minetest community. It has quite a high
|
||||||
|
learning curve because many of its features are not immediately
|
||||||
|
obvious.
|
||||||
|
|
||||||
|
When using GIMP, make sure to use the Pencil tool with the Pixel brush and a
|
||||||
|
size of 1. It's also advisable to select the "Hard edge" checkbox for the Eraser
|
||||||
|
tool.
|
356
_en/items/inventories.md
Normal file
356
_en/items/inventories.md
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
---
|
||||||
|
title: ItemStacks and Inventories
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.4
|
||||||
|
description: Manipulate InvRefs and ItemStacks
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/inventories.html
|
||||||
|
- /en/chapters/itemstacks.html
|
||||||
|
- /en/inventories/inventories.html
|
||||||
|
- /en/inventories/itemstacks.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to use and manipulate inventories, whether
|
||||||
|
that be a player inventory, a node inventory, or a detached inventory.
|
||||||
|
|
||||||
|
- [What are ItemStacks and Inventories?](#what-are-itemstacks-and-inventories)
|
||||||
|
- [ItemStacks](#itemstacks)
|
||||||
|
- [Inventory Locations](#inventory-locations)
|
||||||
|
- [Node Inventories](#node-inventories)
|
||||||
|
- [Player Inventories](#player-inventories)
|
||||||
|
- [Detached Inventories](#detached-inventories)
|
||||||
|
- [Lists](#lists)
|
||||||
|
- [Size and Width](#size-and-width)
|
||||||
|
- [Checking Contents](#checking-contents)
|
||||||
|
- [Modifying Inventories and ItemStacks](#modifying-inventories-and-itemstacks)
|
||||||
|
- [Adding to a List](#adding-to-a-list)
|
||||||
|
- [Taking Items](#taking-items)
|
||||||
|
- [Manipulating Stacks](#manipulating-stacks)
|
||||||
|
- [Wear](#wear)
|
||||||
|
- [Lua Tables](#lua-tables)
|
||||||
|
|
||||||
|
## What are ItemStacks and Inventories?
|
||||||
|
|
||||||
|
An ItemStack is the data behind a single cell in an inventory.
|
||||||
|
|
||||||
|
An *inventory* is a collection of *inventory lists*, each of which is a 2D grid
|
||||||
|
of ItemStacks. Inventory lists are referred to as *lists* in the context of
|
||||||
|
inventories.
|
||||||
|
|
||||||
|
Players and nodes only have a single inventory; lists enable you to have
|
||||||
|
multiple grids within that inventory. By default, the player has the "main" list
|
||||||
|
for the bulk of its inventory and a few lists for the crafting system.
|
||||||
|
|
||||||
|
## ItemStacks
|
||||||
|
|
||||||
|
ItemStacks have four components to them: `name`, `count`, `wear`, and metadata.
|
||||||
|
|
||||||
|
The item name may be the item name of a registered item, an alias, or an unknown
|
||||||
|
item name. Unknown items are common when users uninstall mods, or when mods
|
||||||
|
remove items without precautions, such as registering aliases.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_name())
|
||||||
|
stack:set_name("default:dirt")
|
||||||
|
|
||||||
|
if not stack:is_known() then
|
||||||
|
print("Is an unknown item!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The count will always be 0 or greater. Through normal gameplay, the count should
|
||||||
|
be no more than the maximum stack size of the item - `stack_max`. However, admin
|
||||||
|
commands and buggy mods may result in stacks exceeding the maximum size.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_stack_max())
|
||||||
|
```
|
||||||
|
|
||||||
|
An ItemStack can be empty, in which case the count will be 0.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_count())
|
||||||
|
stack:set_count(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
ItemStacks can be constructed in multiple ways using the ItemStack function:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
ItemStack() -- name="", count=0
|
||||||
|
ItemStack("default:pick_stone") -- count=1
|
||||||
|
ItemStack("default:stone 30")
|
||||||
|
ItemStack({ name = "default:wood", count = 10 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Item metadata is an unlimited key-value store for data about the item. Key-value
|
||||||
|
means that you use a name (called the key) to access the data (called the
|
||||||
|
value). Some keys have special meaning, such as `description` which is used to
|
||||||
|
have a per-stack item description. This will be covered in more detail in the
|
||||||
|
[Storage and Metadata](../map/storage.html) chapter.
|
||||||
|
|
||||||
|
## Inventory Locations
|
||||||
|
|
||||||
|
An Inventory Location is where and how the inventory is stored. There are three
|
||||||
|
types of inventory location: player, node, and detached. An inventory is
|
||||||
|
directly tied to one and only one location - updating the inventory will cause
|
||||||
|
it to update immediately.
|
||||||
|
|
||||||
|
### Node Inventories
|
||||||
|
|
||||||
|
Node inventories are related to the position of a specific node, such as a
|
||||||
|
chest. The node must be loaded because it is stored in
|
||||||
|
[node metadata](../map/storage.html#metadata).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
on_punch = function(pos, node)
|
||||||
|
local inv = core.get_inventory({ type="node", pos=pos })
|
||||||
|
-- now use the inventory
|
||||||
|
end,
|
||||||
|
```
|
||||||
|
|
||||||
|
The above obtains an *inventory reference*, commonly referred to as *InvRef*.
|
||||||
|
Inventory references are used to manipulate an inventory.
|
||||||
|
*Reference* means that the data isn't actually stored inside that object,
|
||||||
|
but the object instead directly updates the data in-place.
|
||||||
|
|
||||||
|
The location of an inventory reference can be found like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local location = inv:get_location()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Player Inventories
|
||||||
|
|
||||||
|
Player inventories can be obtained similarly or using a player reference.
|
||||||
|
The player must be online to access their inventory.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({ type="player", name="player1" })
|
||||||
|
-- or
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detached Inventories
|
||||||
|
|
||||||
|
A detached inventory is one that is independent of players or nodes. Detached
|
||||||
|
inventories also don't save over a restart.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({
|
||||||
|
type="detached", name="inventory_name" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike the other types of inventory, you must first create a detached inventory
|
||||||
|
before accessing it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.create_detached_inventory("inventory_name")
|
||||||
|
```
|
||||||
|
|
||||||
|
The `create_detached_inventory` function accepts 3 arguments, where only the
|
||||||
|
first - the inventory name - is required. The second argument takes a table of
|
||||||
|
callbacks, which can be used to control how players interact with the inventory:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Input only detached inventory
|
||||||
|
core.create_detached_inventory("inventory_name", {
|
||||||
|
allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
|
||||||
|
return count -- allow moving
|
||||||
|
end,
|
||||||
|
|
||||||
|
allow_put = function(inv, listname, index, stack, player)
|
||||||
|
return stack:get_count() -- allow putting
|
||||||
|
end,
|
||||||
|
|
||||||
|
allow_take = function(inv, listname, index, stack, player)
|
||||||
|
return 0 -- don't allow taking
|
||||||
|
end,
|
||||||
|
|
||||||
|
on_put = function(inv, listname, index, stack, player)
|
||||||
|
core.chat_send_all(player:get_player_name() ..
|
||||||
|
" gave " .. stack:to_string() ..
|
||||||
|
" to the donation chest from " .. core.pos_to_string(player:get_pos()))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Permission callbacks - ie: those starting with `allow_` - return the number
|
||||||
|
of items to transfer, with 0 being used to prevent transfer completely.
|
||||||
|
|
||||||
|
On the contrary, action callbacks - starting with `on_` - don't have a return value.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Inventory Lists are a concept used to allow multiple grids to be stored inside a
|
||||||
|
single location. This is especially useful for the player as there are several
|
||||||
|
common lists that all games have, such as the *main* inventory and *craft*
|
||||||
|
slots.
|
||||||
|
|
||||||
|
### Size and Width
|
||||||
|
|
||||||
|
Lists have a size, which is the total number of cells in the grid, and a width,
|
||||||
|
which is only used within the engine.
|
||||||
|
The width of the list is not used when drawing the inventory in a window,
|
||||||
|
because the code behind the window determines the width to use.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:set_size("main", 32) then
|
||||||
|
inv:set_width("main", 8)
|
||||||
|
print("size: " .. inv:get_size("main"))
|
||||||
|
print("width: " .. inv:get_width("main"))
|
||||||
|
else
|
||||||
|
print("Error! Invalid itemname or size to set_size()")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_size` will fail and return false if the listname or size is invalid.
|
||||||
|
For example, the new size may be too small to fit all the current items
|
||||||
|
in the inventory.
|
||||||
|
|
||||||
|
### Checking Contents
|
||||||
|
|
||||||
|
`is_empty` can be used to see if a list contains any items:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:is_empty("main") then
|
||||||
|
print("The list is empty!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`contains_item` can be used to see if a list contains a specific item:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:contains_item("main", "default:stone") then
|
||||||
|
print("I've found some stone!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modifying Inventories and ItemStacks
|
||||||
|
|
||||||
|
### Adding to a List
|
||||||
|
|
||||||
|
`add_item` adds items to a list (in this case `"main"`). In the example below,
|
||||||
|
the maximum stack size is also respected:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = ItemStack("default:stone 99")
|
||||||
|
local leftover = inv:add_item("main", stack)
|
||||||
|
if leftover:get_count() > 0 then
|
||||||
|
print("Inventory is full! " ..
|
||||||
|
leftover:get_count() .. " items weren't added")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Taking Items
|
||||||
|
|
||||||
|
To remove items from a list:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local taken = inv:remove_item("main", stack)
|
||||||
|
print("Took " .. taken:get_count())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manipulating Stacks
|
||||||
|
|
||||||
|
You can modify individual stacks by first getting them:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = inv:get_stack(listname, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then modifying them by setting properties or by using the methods which
|
||||||
|
respect `stack_size`:
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = ItemStack("default:stone 50")
|
||||||
|
local to_add = ItemStack("default:stone 100")
|
||||||
|
local leftover = stack:add_item(to_add)
|
||||||
|
local taken = stack:take_item(19)
|
||||||
|
|
||||||
|
print("Could not add" .. leftover:get_count() .. " of the items.")
|
||||||
|
-- ^ will be 51
|
||||||
|
|
||||||
|
print("Have " .. stack:get_count() .. " items")
|
||||||
|
-- ^ will be 80
|
||||||
|
-- min(50+100, stack_max) - 19 = 80
|
||||||
|
-- where stack_max = 99
|
||||||
|
```
|
||||||
|
|
||||||
|
`add_item` will add items to an ItemStack and return any that could not be added.
|
||||||
|
`take_item` will take up to the number of items but may take less, and returns the stack taken.
|
||||||
|
|
||||||
|
Finally, set the item stack:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
inv:set_stack(listname, 0, stack)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wear
|
||||||
|
|
||||||
|
Tools can have wear; wear shows a progress bar and makes the tool break when completely worn.
|
||||||
|
Wear is a number out of 65535; the higher it is, the more worn the tool is.
|
||||||
|
|
||||||
|
Wear can be manipulated using `add_wear()`, `get_wear()`, and `set_wear(wear)`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = ItemStack("default:pick_mese")
|
||||||
|
local max_uses = 10
|
||||||
|
|
||||||
|
-- This is done automatically when you use a tool that digs things
|
||||||
|
-- It increases the wear of an item by one use.
|
||||||
|
stack:add_wear(65535 / (max_uses - 1))
|
||||||
|
```
|
||||||
|
|
||||||
|
When digging a node, the amount of wear a tool gets may depend on the node
|
||||||
|
being dug. So max_uses varies depending on what is being dug.
|
||||||
|
|
||||||
|
## Lua Tables
|
||||||
|
|
||||||
|
ItemStacks and Inventories can be converted to and from tables.
|
||||||
|
This is useful for copying and bulk operations.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Entire inventory
|
||||||
|
local data = inv1:get_lists()
|
||||||
|
inv2:set_lists(data)
|
||||||
|
|
||||||
|
-- One list
|
||||||
|
local listdata = inv1:get_list("main")
|
||||||
|
inv2:set_list("main", listdata)
|
||||||
|
```
|
||||||
|
|
||||||
|
The table of lists returned by `get_lists()` will be in this form:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
list_one = {
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
-- inv:get_size("list_one") elements
|
||||||
|
},
|
||||||
|
list_two = {
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
-- inv:get_size("list_two") elements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`get_list()` will return a single list as just a list of ItemStacks.
|
||||||
|
|
||||||
|
One important thing to note is that the set methods above don't change the size
|
||||||
|
of the lists.
|
||||||
|
This means that you can clear a list by setting it to an empty table and it won't
|
||||||
|
decrease in size:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
inv:set_list("main", {})
|
||||||
|
```
|
446
_en/items/node_drawtypes.md
Normal file
446
_en/items/node_drawtypes.md
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
---
|
||||||
|
title: Node Drawtypes
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.3
|
||||||
|
description: Guide to all drawtypes, including node boxes/nodeboxes and mesh nodes.
|
||||||
|
redirect_from: /en/chapters/node_drawtypes.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The method by which a node is drawn is called a *drawtype*. There are many
|
||||||
|
available drawtypes. The behaviour of a drawtype can be controlled
|
||||||
|
by providing properties in the node type definition. These properties
|
||||||
|
are fixed for all instances of this node. It is possible to control some properties
|
||||||
|
per-node using something called `param2`.
|
||||||
|
|
||||||
|
In the previous chapter, the concept of nodes and items was introduced, but a
|
||||||
|
full definition of a node wasn't given. The Minetest world is a 3D grid of
|
||||||
|
positions. Each position is called a node, and consists of the node type
|
||||||
|
(name) and two parameters (param1 and param2). The function
|
||||||
|
`core.register_node` is a bit misleading in that it doesn't actually
|
||||||
|
register a node - it registers a new *type* of node.
|
||||||
|
|
||||||
|
The node params are used to control how a node is individually rendered.
|
||||||
|
`param1` is used to store the lighting of a node, and the meaning of
|
||||||
|
`param2` depends on the `paramtype2` property of the node type definition.
|
||||||
|
|
||||||
|
- [Cubic Nodes: Normal and Allfaces](#cubic-nodes-normal-and-allfaces)
|
||||||
|
- [Glasslike Nodes](#glasslike-nodes)
|
||||||
|
- [Glasslike_Framed](#glasslike_framed)
|
||||||
|
- [Airlike Nodes](#airlike-nodes)
|
||||||
|
- [Lighting and Sunlight Propagation](#lighting-and-sunlight-propagation)
|
||||||
|
- [Liquid Nodes](#liquid-nodes)
|
||||||
|
- [Node Boxes](#node-boxes)
|
||||||
|
- [Wallmounted Node Boxes](#wallmounted-node-boxes)
|
||||||
|
- [Mesh Nodes](#mesh-nodes)
|
||||||
|
- [Signlike Nodes](#signlike-nodes)
|
||||||
|
- [Plantlike Nodes](#plantlike-nodes)
|
||||||
|
- [Firelike Nodes](#firelike-nodes)
|
||||||
|
- [More Drawtypes](#more-drawtypes)
|
||||||
|
|
||||||
|
|
||||||
|
## Cubic Nodes: Normal and Allfaces
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_normal.png" alt="Normal Drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Normal Drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
The normal drawtype is typically used to render a cubic node.
|
||||||
|
If the side of a normal node is against a solid side, then that side won't be rendered,
|
||||||
|
resulting in a large performance gain.
|
||||||
|
|
||||||
|
In contrast, the allfaces drawtype will still render the inner side when up against
|
||||||
|
a solid node. This is good for nodes with partially transparent sides, such as
|
||||||
|
leaf nodes. You can use the allfaces_optional drawtype to allow users to opt-out
|
||||||
|
of the slower drawing, in which case it'll act like a normal node.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:diamond", {
|
||||||
|
description = "Alien Diamond",
|
||||||
|
tiles = {"mymod_diamond.png"},
|
||||||
|
groups = {cracky = 3},
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_node("default:leaves", {
|
||||||
|
description = "Leaves",
|
||||||
|
drawtype = "allfaces_optional",
|
||||||
|
tiles = {"default_leaves.png"}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the normal drawtype is the default drawtype, so you don't need to explicitly
|
||||||
|
specify it.
|
||||||
|
|
||||||
|
|
||||||
|
## Glasslike Nodes
|
||||||
|
|
||||||
|
The difference between glasslike and normal nodes is that placing a glasslike node
|
||||||
|
next to a normal node won't cause the side of the normal node to be hidden.
|
||||||
|
This is useful because glasslike nodes tend to be transparent, and so using a normal
|
||||||
|
drawtype would result in the ability to see through the world.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_glasslike_edges.png" alt="Glasslike's Edges">
|
||||||
|
<figcaption>
|
||||||
|
Glasslike's Edges
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:obsidian_glass", {
|
||||||
|
description = "Obsidian Glass",
|
||||||
|
drawtype = "glasslike",
|
||||||
|
tiles = {"default_obsidian_glass.png"},
|
||||||
|
paramtype = "light",
|
||||||
|
is_ground_content = false,
|
||||||
|
sunlight_propagates = true,
|
||||||
|
sounds = default.node_sound_glass_defaults(),
|
||||||
|
groups = {cracky=3,oddly_breakable_by_hand=3},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Glasslike_Framed
|
||||||
|
|
||||||
|
This makes the node's edge go around the whole thing with a 3D effect, rather
|
||||||
|
than individual nodes, like the following:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_glasslike_framed.png" alt="Glasslike_framed's Edges">
|
||||||
|
<figcaption>
|
||||||
|
Glasslike_Framed's Edges
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
You can use the glasslike_framed_optional drawtype to allow the user to *opt-in*
|
||||||
|
to the framed appearance.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:glass", {
|
||||||
|
description = "Glass",
|
||||||
|
drawtype = "glasslike_framed",
|
||||||
|
tiles = {"default_glass.png", "default_glass_detail.png"},
|
||||||
|
inventory_image = core.inventorycube("default_glass.png"),
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true, -- Sunlight can shine through block
|
||||||
|
groups = {cracky = 3, oddly_breakable_by_hand = 3},
|
||||||
|
sounds = default.node_sound_glass_defaults()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Airlike Nodes
|
||||||
|
|
||||||
|
These nodes are not rendered and thus have no textures.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("myair:air", {
|
||||||
|
description = "MyAir (you hacker you!)",
|
||||||
|
drawtype = "airlike",
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true,
|
||||||
|
|
||||||
|
walkable = false, -- Would make the player collide with the air node
|
||||||
|
pointable = false, -- You can't select the node
|
||||||
|
diggable = false, -- You can't dig the node
|
||||||
|
buildable_to = true, -- Nodes can replace this node.
|
||||||
|
-- (you can place a node and remove the air node
|
||||||
|
-- that used to be there)
|
||||||
|
|
||||||
|
air_equivalent = true,
|
||||||
|
drop = "",
|
||||||
|
groups = {not_in_creative_inventory=1}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Lighting and Sunlight Propagation
|
||||||
|
|
||||||
|
The lighting of a node is stored in param1. In order to work out how to shade
|
||||||
|
a node's side, the light value of the neighbouring node is used.
|
||||||
|
Because of this, solid nodes don't have light values because they block light.
|
||||||
|
|
||||||
|
By default, a node type won't allow light to be stored in any node instances.
|
||||||
|
It's usually desirable for some nodes such as glass and air to be able to
|
||||||
|
let light through. To do this, there are two properties which need to be defined:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true,
|
||||||
|
```
|
||||||
|
|
||||||
|
The first line means that param1 does, in fact, store the light level.
|
||||||
|
The second line means that sunlight should go through this node without decreasing in value.
|
||||||
|
|
||||||
|
|
||||||
|
## Liquid Nodes
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_liquid.png" alt="Liquid Drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Liquid Drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Each type of liquid requires two node definitions - one for the liquid source, and
|
||||||
|
another for flowing liquid.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Some properties have been removed as they are beyond
|
||||||
|
-- the scope of this chapter.
|
||||||
|
core.register_node("default:water_source", {
|
||||||
|
drawtype = "liquid",
|
||||||
|
paramtype = "light",
|
||||||
|
|
||||||
|
inventory_image = core.inventorycube("default_water.png"),
|
||||||
|
-- ^ this is required to stop the inventory image from being animated
|
||||||
|
|
||||||
|
tiles = {
|
||||||
|
{
|
||||||
|
name = "default_water_source_animated.png",
|
||||||
|
animation = {
|
||||||
|
type = "vertical_frames",
|
||||||
|
aspect_w = 16,
|
||||||
|
aspect_h = 16,
|
||||||
|
length = 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
special_tiles = {
|
||||||
|
-- New-style water source material (mostly unused)
|
||||||
|
{
|
||||||
|
name = "default_water_source_animated.png",
|
||||||
|
animation = {type = "vertical_frames", aspect_w = 16,
|
||||||
|
aspect_h = 16, length = 2.0},
|
||||||
|
backface_culling = false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Behavior
|
||||||
|
--
|
||||||
|
walkable = false, -- The player falls through
|
||||||
|
pointable = false, -- The player can't highlight it
|
||||||
|
diggable = false, -- The player can't dig it
|
||||||
|
buildable_to = true, -- Nodes can be replace this node
|
||||||
|
|
||||||
|
alpha = 160,
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Liquid Properties
|
||||||
|
--
|
||||||
|
drowning = 1,
|
||||||
|
liquidtype = "source",
|
||||||
|
|
||||||
|
liquid_alternative_flowing = "default:water_flowing",
|
||||||
|
-- ^ when the liquid is flowing
|
||||||
|
|
||||||
|
liquid_alternative_source = "default:water_source",
|
||||||
|
-- ^ when the liquid is a source
|
||||||
|
|
||||||
|
liquid_viscosity = WATER_VISC,
|
||||||
|
-- ^ how fast
|
||||||
|
|
||||||
|
liquid_range = 8,
|
||||||
|
-- ^ how far
|
||||||
|
|
||||||
|
post_effect_color = {a=64, r=100, g=100, b=200},
|
||||||
|
-- ^ colour of screen when the player is submerged
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Flowing nodes have a similar definition, but with a different name and animation.
|
||||||
|
See default:water_flowing in the default mod in minetest_game for a full example.
|
||||||
|
|
||||||
|
|
||||||
|
## Node Boxes
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_nodebox.gif" alt="Nodebox drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Nodebox drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Node boxes allow you to create a node which is not cubic, but is instead made out
|
||||||
|
of as many cuboids as you like.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("stairs:stair_stone", {
|
||||||
|
drawtype = "nodebox",
|
||||||
|
paramtype = "light",
|
||||||
|
node_box = {
|
||||||
|
type = "fixed",
|
||||||
|
fixed = {
|
||||||
|
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
||||||
|
{-0.5, 0, 0, 0.5, 0.5, 0.5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The most important part is the node box table:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
||||||
|
{-0.5, 0, 0, 0.5, 0.5, 0.5}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each row is a cuboid which are joined to make a single node.
|
||||||
|
The first three numbers are the co-ordinates, from -0.5 to 0.5 inclusive, of
|
||||||
|
the bottom front left most corner, the last three numbers are the opposite corner.
|
||||||
|
They are in the form X, Y, Z, where Y is up.
|
||||||
|
|
||||||
|
You can use the [NodeBoxEditor](https://forum.minetest.net/viewtopic.php?f=14&t=2840) to
|
||||||
|
create node boxes by dragging the edges, it is more visual than doing it by hand.
|
||||||
|
|
||||||
|
|
||||||
|
### Wallmounted Node Boxes
|
||||||
|
|
||||||
|
Sometimes you want different nodeboxes for when it is placed on the floor, wall, or ceiling like with torches.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:sign_wall", {
|
||||||
|
drawtype = "nodebox",
|
||||||
|
node_box = {
|
||||||
|
type = "wallmounted",
|
||||||
|
|
||||||
|
-- Ceiling
|
||||||
|
wall_top = {
|
||||||
|
{-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Floor
|
||||||
|
wall_bottom = {
|
||||||
|
{-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Wall
|
||||||
|
wall_side = {
|
||||||
|
{-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mesh Nodes
|
||||||
|
|
||||||
|
Whilst node boxes are generally easier to make, they are limited in that
|
||||||
|
they can only consist of cuboids. Node boxes are also unoptimised;
|
||||||
|
Inner faces will still be rendered even when they're completely hidden.
|
||||||
|
|
||||||
|
A face is a flat surface on a mesh. An inner face occurs when the faces of two
|
||||||
|
different node boxes overlap, causing parts of the node box model to be
|
||||||
|
invisible but still rendered.
|
||||||
|
|
||||||
|
You can register a mesh node as so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:meshy", {
|
||||||
|
drawtype = "mesh",
|
||||||
|
|
||||||
|
-- Holds the texture for each "material"
|
||||||
|
tiles = {
|
||||||
|
"mymod_meshy.png"
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Path to the mesh
|
||||||
|
mesh = "mymod_meshy.b3d",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure that the mesh is available in a `models` directory.
|
||||||
|
Most of the time the mesh should be in your mod's folder, however, it's okay to
|
||||||
|
share a mesh provided by another mod you depend on. For example, a mod that
|
||||||
|
adds more types of furniture may want to share the model provided by a basic
|
||||||
|
furniture mod.
|
||||||
|
|
||||||
|
|
||||||
|
## Signlike Nodes
|
||||||
|
|
||||||
|
Signlike nodes are flat nodes with can be mounted on the sides of other nodes.
|
||||||
|
|
||||||
|
Despite the name of this drawtype, signs don't actually tend to use signlike but
|
||||||
|
instead use the `nodebox` drawtype to provide a 3D effect. The `signlike` drawtype
|
||||||
|
is, however, commonly used by ladders.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:ladder_wood", {
|
||||||
|
drawtype = "signlike",
|
||||||
|
|
||||||
|
tiles = {"default_ladder_wood.png"},
|
||||||
|
|
||||||
|
-- Required: store the rotation in param2
|
||||||
|
paramtype2 = "wallmounted",
|
||||||
|
|
||||||
|
selection_box = {
|
||||||
|
type = "wallmounted",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Plantlike Nodes
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_plantlike.png" alt="Plantlike Drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Plantlike Drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Plantlike nodes draw their tiles in an X like pattern.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:papyrus", {
|
||||||
|
drawtype = "plantlike",
|
||||||
|
|
||||||
|
-- Only one texture used
|
||||||
|
tiles = {"default_papyrus.png"},
|
||||||
|
|
||||||
|
selection_box = {
|
||||||
|
type = "fixed",
|
||||||
|
fixed = {-6 / 16, -0.5, -6 / 16, 6 / 16, 0.5, 6 / 16},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firelike Nodes
|
||||||
|
|
||||||
|
Firelike is similar to plantlike, except that it is designed to "cling" to walls
|
||||||
|
and ceilings.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_firelike.png" alt="Firelike nodes">
|
||||||
|
<figcaption>
|
||||||
|
Firelike nodes
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:clingere", {
|
||||||
|
drawtype = "firelike",
|
||||||
|
|
||||||
|
-- Only one texture used
|
||||||
|
tiles = { "mymod:clinger" },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## More Drawtypes
|
||||||
|
|
||||||
|
This is not a comprehensive list, there are more types including:
|
||||||
|
|
||||||
|
* Fencelike
|
||||||
|
* Plantlike rooted - for underwater plants
|
||||||
|
* Raillike - for cart tracks
|
||||||
|
* Torchlike - for 2D wall/floor/ceiling nodes.
|
||||||
|
The torches in Minetest Game actually use two different node definitions of
|
||||||
|
mesh nodes (default:torch and default:torch_wall).
|
||||||
|
|
||||||
|
As always, read the [Lua API documentation](https://minetest.gitlab.io/minetest/nodes/#node-drawtypes)
|
||||||
|
for the complete list.
|
345
_en/items/nodes_items_crafting.md
Normal file
345
_en/items/nodes_items_crafting.md
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
---
|
||||||
|
title: Nodes, Items, and Crafting
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.1
|
||||||
|
description: Learn how to register node, items, and craft recipes using register_node, register_item, and register_craft.
|
||||||
|
redirect_from: /en/chapters/nodes_items_crafting.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Registering new nodes and craftitems, and creating craft recipes, are
|
||||||
|
basic requirements for many mods.
|
||||||
|
|
||||||
|
- [What are Nodes and Items?](#what-are-nodes-and-items)
|
||||||
|
- [Registering Items](#registering-items)
|
||||||
|
- [Item Names](#item-names)
|
||||||
|
- [Item Aliases](#item-aliases)
|
||||||
|
- [Textures](#textures)
|
||||||
|
- [Registering a basic node](#registering-a-basic-node)
|
||||||
|
- [Crafting](#crafting)
|
||||||
|
- [Shaped](#shaped)
|
||||||
|
- [Shapeless](#shapeless)
|
||||||
|
- [Cooking and Fuel](#cooking-and-fuel)
|
||||||
|
- [Groups](#groups)
|
||||||
|
- [Tools, Capabilities, and Dig Types](#tools-capabilities-and-dig-types)
|
||||||
|
|
||||||
|
## What are Nodes and Items?
|
||||||
|
|
||||||
|
Nodes, craftitems, and tools are all Items. An item is something that could be
|
||||||
|
found in an inventory - even if it isn't possible through normal gameplay.
|
||||||
|
|
||||||
|
A node is an item that can be placed or be found in the world. Every position
|
||||||
|
in the world must be occupied with one and only one node - seemingly blank
|
||||||
|
positions are usually air nodes.
|
||||||
|
|
||||||
|
A craftitem can't be placed and is only found in inventories or as a dropped item
|
||||||
|
in the world.
|
||||||
|
|
||||||
|
A tool is like a craftitem but has the ability to wear. As you use the tool, the
|
||||||
|
wear bar goes down until the tool breaks. Tools can also never be stacked. In
|
||||||
|
the future, it's likely that craftitems and tools will merge into one type of
|
||||||
|
item, as the distinction between them is rather artificial.
|
||||||
|
|
||||||
|
## Registering Items
|
||||||
|
|
||||||
|
Item definitions consist of an *item name* and a *definition table*.
|
||||||
|
The definition table contains attributes that affect the behaviour of the item.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("modname:itemname", {
|
||||||
|
description = "My Special Item",
|
||||||
|
inventory_image = "modname_itemname.png"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Names
|
||||||
|
|
||||||
|
Every item has an item name used to refer to it, which should be in the
|
||||||
|
following format:
|
||||||
|
|
||||||
|
modname:itemname
|
||||||
|
|
||||||
|
The modname is the name of the mod in which the item is registered, and the item
|
||||||
|
name is the name of the item itself. The item name should be relevant to what
|
||||||
|
the item is and can't already be registered.
|
||||||
|
|
||||||
|
Both `modname` and `itemname` should only contain lowercase letters, numbers,
|
||||||
|
and underscores.
|
||||||
|
|
||||||
|
### Item Aliases
|
||||||
|
|
||||||
|
Items can also have *aliases* pointing to their name. An *alias* is a
|
||||||
|
pseudo-item name that results in the engine treating any occurrences of the
|
||||||
|
alias as if it were the item name. There are two main common uses of this:
|
||||||
|
|
||||||
|
* Renaming removed items to something else.
|
||||||
|
There may be unknown nodes in the world and in inventories if an item is
|
||||||
|
removed from a mod without any corrective code.
|
||||||
|
* Adding a shortcut. `/giveme dirt` is easier than `/giveme default:dirt`.
|
||||||
|
|
||||||
|
Registering an alias is pretty simple. A good way to remember the order of the
|
||||||
|
arguments is `from → to` where *from* is the alias and *to* is the target.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_alias("dirt", "default:dirt")
|
||||||
|
```
|
||||||
|
|
||||||
|
Mods need to make sure to resolve aliases before dealing directly with item names,
|
||||||
|
as the engine won't do this.
|
||||||
|
This is pretty simple though:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
itemname = core.registered_aliases[itemname] or itemname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Textures
|
||||||
|
|
||||||
|
Textures should be placed in the textures/ folder with names in the format
|
||||||
|
`modname_itemname.png`.\\
|
||||||
|
JPEG textures are supported, but they do not support transparency and are generally
|
||||||
|
bad quality at low resolutions.
|
||||||
|
It is often better to use the PNG format.
|
||||||
|
|
||||||
|
Textures in Minetest are usually 16 by 16 pixels. They can be any resolution,
|
||||||
|
but it is recommended that they are in the order of 2, for example, 16, 32, 64,
|
||||||
|
or 128. This is because other resolutions may not be supported correctly on
|
||||||
|
older devices, especially phones, resulting in degraded performance.
|
||||||
|
|
||||||
|
## Registering a basic node
|
||||||
|
|
||||||
|
Registering nodes is similar to registering items, just with a different
|
||||||
|
function:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:diamond", {
|
||||||
|
description = "Alien Diamond",
|
||||||
|
tiles = {"mymod_diamond.png"},
|
||||||
|
is_ground_content = true,
|
||||||
|
groups = {cracky=3, stone=1}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Node definitions can contain any property in an item definition, and also
|
||||||
|
contain additional properties specific to nodes.
|
||||||
|
|
||||||
|
The `tiles` property is a table of texture names the node will use.
|
||||||
|
When there is only one texture, this texture is used on every side.
|
||||||
|
To give a different texture per-side, supply the names of 6 textures in this order:
|
||||||
|
|
||||||
|
up (+Y), down (-Y), right (+X), left (-X), back (+Z), front (-Z).
|
||||||
|
(+Y, -Y, +X, -X, +Z, -Z)
|
||||||
|
|
||||||
|
Remember that +Y is upwards in Minetest, as is the convention with
|
||||||
|
most 3D computer games.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:diamond", {
|
||||||
|
description = "Alien Diamond",
|
||||||
|
tiles = {
|
||||||
|
"mymod_diamond_up.png", -- y+
|
||||||
|
"mymod_diamond_down.png", -- y-
|
||||||
|
"mymod_diamond_right.png", -- x+
|
||||||
|
"mymod_diamond_left.png", -- x-
|
||||||
|
"mymod_diamond_back.png", -- z+
|
||||||
|
"mymod_diamond_front.png", -- z-
|
||||||
|
},
|
||||||
|
is_ground_content = true,
|
||||||
|
groups = {cracky = 3},
|
||||||
|
drop = "mymod:diamond_fragments"
|
||||||
|
-- ^ Rather than dropping diamond, drop mymod:diamond_fragments
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `is_ground_content` attribute allows caves to be generated over the stone.
|
||||||
|
This is essential for any node which may be placed during map generation underground.
|
||||||
|
Caves are cut out of the world after all the other nodes in an area have generated.
|
||||||
|
|
||||||
|
## Crafting
|
||||||
|
|
||||||
|
There are several types of crafting recipe available, indicated by the `type`
|
||||||
|
property.
|
||||||
|
|
||||||
|
* shaped - Ingredients must be in the correct position.
|
||||||
|
* shapeless - It doesn't matter where the ingredients are,
|
||||||
|
just that there is the right amount.
|
||||||
|
* cooking - Recipes for the furnace to use.
|
||||||
|
* fuel - Defines items which can be burned in furnaces.
|
||||||
|
* tool_repair - Defines items which can be tool repaired.
|
||||||
|
|
||||||
|
Craft recipes are not items, so they do not use Item Names to uniquely
|
||||||
|
identify themselves.
|
||||||
|
|
||||||
|
### Shaped
|
||||||
|
|
||||||
|
Shaped recipes are when the ingredients need to be in the right shape or
|
||||||
|
pattern to work. In the example below, the fragments need to be in a
|
||||||
|
chair-like pattern for the craft to work.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shaped",
|
||||||
|
output = "mymod:diamond_chair 99",
|
||||||
|
recipe = {
|
||||||
|
{"mymod:diamond_fragments", "", ""},
|
||||||
|
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""},
|
||||||
|
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
One thing to note is the blank column on the right-hand side.
|
||||||
|
This means that there *must* be an empty column to the right of the shape, otherwise
|
||||||
|
this won't work.
|
||||||
|
If this empty column shouldn't be required, then the empty strings can be left
|
||||||
|
out like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
output = "mymod:diamond_chair 99",
|
||||||
|
recipe = {
|
||||||
|
{"mymod:diamond_fragments", "" },
|
||||||
|
{"mymod:diamond_fragments", "mymod:diamond_fragments"},
|
||||||
|
{"mymod:diamond_fragments", "mymod:diamond_fragments"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The type field isn't actually needed for shaped crafts, as shaped is the
|
||||||
|
default craft type.
|
||||||
|
|
||||||
|
### Shapeless
|
||||||
|
|
||||||
|
Shapeless recipes are a type of recipe which is used when it doesn't matter
|
||||||
|
where the ingredients are placed, just that they're there.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "mymod:diamond 3",
|
||||||
|
recipe = {
|
||||||
|
"mymod:diamond_fragments",
|
||||||
|
"mymod:diamond_fragments",
|
||||||
|
"mymod:diamond_fragments",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cooking and Fuel
|
||||||
|
|
||||||
|
Recipes with the type "cooking" are not made in the crafting grid,
|
||||||
|
but are cooked in furnaces, or other cooking tools that might be found in mods.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "cooking",
|
||||||
|
output = "mymod:diamond_fragments",
|
||||||
|
recipe = "default:coalblock",
|
||||||
|
cooktime = 10,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The only real difference in the code is that the recipe is just a single item,
|
||||||
|
compared to being in a table (between braces).
|
||||||
|
They also have an optional "cooktime" parameter which
|
||||||
|
defines how long the item takes to cook.
|
||||||
|
If this is not set, it defaults to 3.
|
||||||
|
|
||||||
|
The recipe above works when the coal block is in the input slot,
|
||||||
|
with some form of fuel below it.
|
||||||
|
It creates diamond fragments after 10 seconds!
|
||||||
|
|
||||||
|
This type is an accompaniment to the cooking type, as it defines
|
||||||
|
what can be burned in furnaces and other cooking tools from mods.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "fuel",
|
||||||
|
recipe = "mymod:diamond",
|
||||||
|
burntime = 300,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
They don't have an output like other recipes, but they have a burn time
|
||||||
|
which defines how long they will last as fuel in seconds.
|
||||||
|
So, the diamond is good as fuel for 300 seconds!
|
||||||
|
|
||||||
|
## Groups
|
||||||
|
|
||||||
|
Items can be members of many groups and groups can have many members.
|
||||||
|
Groups are defined using the `groups` property in the definition table
|
||||||
|
and have an associated value.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
groups = {cracky = 3, wood = 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are several reasons you use groups.
|
||||||
|
Firstly, groups are used to describe properties such as dig types and flammability.
|
||||||
|
Secondly, groups can be used in a craft recipe instead of an item name to allow
|
||||||
|
any item in the group to be used.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "mymod:diamond_thing 3",
|
||||||
|
recipe = {"group:wood", "mymod:diamond"}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Tools, Capabilities, and Dig Types
|
||||||
|
|
||||||
|
Dig types are groups which are used to define how strong a node is when dug
|
||||||
|
with different tools.
|
||||||
|
A dig type group with a higher associated value means the node is easier
|
||||||
|
and quicker to cut.
|
||||||
|
It's possible to combine multiple dig types to allow the more efficient use
|
||||||
|
of multiple types of tools.
|
||||||
|
A node with no dig types cannot be dug by any tools.
|
||||||
|
|
||||||
|
|
||||||
|
| Group | Best Tool | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| crumbly | spade | Dirt, sand |
|
||||||
|
| cracky | pickaxe | Tough (but brittle) stuff like stone |
|
||||||
|
| snappy | *any* | Can be cut using fine tools;<br>e.g. leaves, smallplants, wire, sheets of metal |
|
||||||
|
| choppy | axe | Can be cut using a sharp force; e.g. trees, wooden planks |
|
||||||
|
| fleshy | sword | Living things like animals and the player.<br>This could imply some blood effects when hitting. |
|
||||||
|
| explody | ? | Especially prone to explosions |
|
||||||
|
| oddly_breakable_by_hand | *any* | Torches and such - very quick to dig |
|
||||||
|
|
||||||
|
|
||||||
|
Every tool has a tool capability.
|
||||||
|
A capability includes a list of supported dig types, and associated properties
|
||||||
|
for each type such as dig times and the amount of wear.
|
||||||
|
Tools can also have a maximum supported hardness for each type, which makes
|
||||||
|
it possible to prevent weaker tools from digging harder nodes.
|
||||||
|
It's very common for tools to include all dig types in their capabilities,
|
||||||
|
with the less suitable ones having very inefficient properties.
|
||||||
|
If the item a player is currently wielding doesn't have an explicit tool
|
||||||
|
capability, then the capability of the current hand is used instead.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_tool("mymod:tool", {
|
||||||
|
description = "My Tool",
|
||||||
|
inventory_image = "mymod_tool.png",
|
||||||
|
tool_capabilities = {
|
||||||
|
full_punch_interval = 1.5,
|
||||||
|
max_drop_level = 1,
|
||||||
|
groupcaps = {
|
||||||
|
crumbly = {
|
||||||
|
maxlevel = 2,
|
||||||
|
uses = 20,
|
||||||
|
times = { [1]=1.60, [2]=1.20, [3]=0.80 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
damage_groups = {fleshy=2},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Groupcaps is the list of supported dig types for digging nodes.
|
||||||
|
Damage groups are for controlling how tools damage objects, which will be
|
||||||
|
discussed later in the Objects, Players, and Entities chapter.
|
234
_en/map/environment.md
Normal file
234
_en/map/environment.md
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
---
|
||||||
|
title: Basic Map Operations
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.1
|
||||||
|
description: Basic operations like set_node and get_node
|
||||||
|
redirect_from: /en/chapters/environment.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to perform basic actions on the map, such as
|
||||||
|
adding, removing, and finding nodes.
|
||||||
|
|
||||||
|
- [Map Structure](#map-structure)
|
||||||
|
- [Reading](#reading)
|
||||||
|
- [Reading Nodes](#reading-nodes)
|
||||||
|
- [Finding Nodes](#finding-nodes)
|
||||||
|
- [Writing](#writing)
|
||||||
|
- [Writing Nodes](#writing-nodes)
|
||||||
|
- [Removing Nodes](#removing-nodes)
|
||||||
|
- [Loading Blocks](#loading-blocks)
|
||||||
|
- [Deleting Blocks](#deleting-blocks)
|
||||||
|
|
||||||
|
## Map Structure
|
||||||
|
|
||||||
|
The Minetest map is split into MapBlocks, each MapBlocks being a cube of
|
||||||
|
size 16. As players travel around the map, MapBlocks are created, loaded,
|
||||||
|
activated, and unloaded. Areas of the map which are not yet loaded are full of
|
||||||
|
*ignore* nodes, an impassable unselectable placeholder node. Empty space is
|
||||||
|
full of *air* nodes, an invisible node you can walk through.
|
||||||
|
|
||||||
|
An active MapBlock is one which is loaded and has updates running on it.
|
||||||
|
|
||||||
|
Loaded map blocks are often referred to as *active blocks*. Active Blocks can be
|
||||||
|
read from or written to by mods or players, and have active entities. The Engine
|
||||||
|
also performs operations on the map, such as performing liquid physics.
|
||||||
|
|
||||||
|
MapBlocks can either be loaded from the world database or generated. MapBlocks
|
||||||
|
will be generated up to the map generation limit (`mapgen_limit`) which is set
|
||||||
|
to its maximum value, 31000, by default. Existing MapBlocks can, however, be
|
||||||
|
loaded from the world database outside of the generation limit.
|
||||||
|
|
||||||
|
## Reading
|
||||||
|
|
||||||
|
### Reading Nodes
|
||||||
|
|
||||||
|
You can read from the map once you have a position:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local node = core.get_node({ x = 1, y = 3, z = 4 })
|
||||||
|
print(dump(node)) --> { name=.., param1=.., param2=.. }
|
||||||
|
```
|
||||||
|
|
||||||
|
If the position is a decimal, it will be rounded to the containing node.
|
||||||
|
The function will always return a table containing the node information:
|
||||||
|
|
||||||
|
* `name` - The node name, which will be *ignore* when the area is unloaded.
|
||||||
|
* `param1` - See the node definition. This will commonly be light.
|
||||||
|
* `param2` - See the node definition.
|
||||||
|
|
||||||
|
It's worth noting that the function won't load the containing block if the block
|
||||||
|
is inactive, but will instead return a table with `name` being `ignore`.
|
||||||
|
|
||||||
|
You can use `core.get_node_or_nil` instead, which will return `nil` rather
|
||||||
|
than a table with a name of `ignore`. It still won't load the block, however.
|
||||||
|
This may still return `ignore` if a block actually contains ignore.
|
||||||
|
This will happen near the edge of the map as defined by the map generation
|
||||||
|
limit (`mapgen_limit`).
|
||||||
|
|
||||||
|
### Finding Nodes
|
||||||
|
|
||||||
|
Minetest offers a number of helper functions to speed up common map actions.
|
||||||
|
The most commonly used of these are for finding nodes.
|
||||||
|
|
||||||
|
For example, say we wanted to make a certain type of plant that grows
|
||||||
|
better near mese; you would need to search for any nearby mese nodes,
|
||||||
|
and adapt the growth rate accordingly.
|
||||||
|
|
||||||
|
`core.find_node_near` will return the first found node in a certain radius
|
||||||
|
which matches the node names or groups given. In the following example,
|
||||||
|
we look for a mese node within 5 nodes of the position:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local grow_speed = 1
|
||||||
|
local node_pos = core.find_node_near(pos, 5, { "default:mese" })
|
||||||
|
if node_pos then
|
||||||
|
core.chat_send_all("Node found at: " .. dump(node_pos))
|
||||||
|
grow_speed = 2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's say, for example, that the growth rate increases the more mese there is
|
||||||
|
nearby. You should then use a function that can find multiple nodes in the area:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos1 = vector.subtract(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local pos2 = vector.add(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local pos_list =
|
||||||
|
core.find_nodes_in_area(pos1, pos2, { "default:mese" })
|
||||||
|
local grow_speed = 1 + #pos_list
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code finds the number of nodes in a *cuboid volume*. This is different
|
||||||
|
to `find_node_near`, which uses the distance to the position (ie: a *sphere*). In
|
||||||
|
order to fix this, we will need to manually check the range ourselves:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos1 = vector.subtract(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local pos2 = vector.add(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local pos_list =
|
||||||
|
core.find_nodes_in_area(pos1, pos2, { "default:mese" })
|
||||||
|
local grow_speed = 1
|
||||||
|
for i=1, #pos_list do
|
||||||
|
local delta = vector.subtract(pos_list[i], pos)
|
||||||
|
if delta.x*delta.x + delta.y*delta.y + delta.z*delta.z <= 5*5 then
|
||||||
|
grow_speed = grow_speed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the code will correctly increase `grow_speed` based on mese nodes in range.
|
||||||
|
|
||||||
|
Note how we compared the squared distance from the position, rather than square
|
||||||
|
rooting it to obtain the actual distance. This is because computers find square
|
||||||
|
roots computationally expensive, so they should avoided as much as possible.
|
||||||
|
|
||||||
|
There are more variations of the above two functions, such as
|
||||||
|
`find_nodes_with_meta` and `find_nodes_in_area_under_air`, which work similarly
|
||||||
|
and are useful in other circumstances.
|
||||||
|
|
||||||
|
## Writing
|
||||||
|
|
||||||
|
### Writing Nodes
|
||||||
|
|
||||||
|
You can use `set_node` to write to the map. Each call to set_node will cause
|
||||||
|
lighting to be recalculated and node callbacks to run, which means that set_node
|
||||||
|
is fairly slow for large numbers of nodes.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.set_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
|
||||||
|
|
||||||
|
local node = core.get_node({ x = 1, y = 3, z = 4 })
|
||||||
|
print(node.name) --> default:mese
|
||||||
|
```
|
||||||
|
|
||||||
|
set_node will remove any associated metadata or inventory from that position.
|
||||||
|
This isn't desirable in all circumstances, especially if you're using multiple
|
||||||
|
node definitions to represent one conceptual node. An example of this is the
|
||||||
|
furnace node - whilst you conceptually think of it as one node, it's actually
|
||||||
|
two.
|
||||||
|
|
||||||
|
You can set a node without deleting metadata or the inventory like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.swap_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Nodes
|
||||||
|
|
||||||
|
A node must always be present. To remove a node, you set the position to `air`.
|
||||||
|
|
||||||
|
The following two lines will both remove a node, and are both identical:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.remove_node(pos)
|
||||||
|
core.set_node(pos, { name = "air" })
|
||||||
|
```
|
||||||
|
|
||||||
|
In fact, remove_node is just a helper function that calls set_node with `"air"`.
|
||||||
|
|
||||||
|
## Loading Blocks
|
||||||
|
|
||||||
|
You can use `core.emerge_area` to load map blocks. Emerge area is asynchronous,
|
||||||
|
meaning the blocks won't be loaded instantly. Instead, they will be loaded
|
||||||
|
soon in the future, and the callback will be called each time.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Load a 20x20x20 area
|
||||||
|
local halfsize = { x = 10, y = 10, z = 10 }
|
||||||
|
local pos1 = vector.subtract(pos, halfsize)
|
||||||
|
local pos2 = vector.add (pos, halfsize)
|
||||||
|
|
||||||
|
local context = {} -- persist data between callback calls
|
||||||
|
core.emerge_area(pos1, pos2, emerge_callback, context)
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest will call `emerge_callback` whenever it loads a block, with some
|
||||||
|
progress information.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function emerge_callback(pos, action,
|
||||||
|
num_calls_remaining, context)
|
||||||
|
-- On first call, record number of blocks
|
||||||
|
if not context.total_blocks then
|
||||||
|
context.total_blocks = num_calls_remaining + 1
|
||||||
|
context.loaded_blocks = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Increment number of blocks loaded
|
||||||
|
context.loaded_blocks = context.loaded_blocks + 1
|
||||||
|
|
||||||
|
-- Send progress message
|
||||||
|
if context.total_blocks == context.loaded_blocks then
|
||||||
|
core.chat_send_all("Finished loading blocks!")
|
||||||
|
else
|
||||||
|
local perc = 100 * context.loaded_blocks / context.total_blocks
|
||||||
|
local msg = string.format("Loading blocks %d/%d (%.2f%%)",
|
||||||
|
context.loaded_blocks, context.total_blocks, perc)
|
||||||
|
core.chat_send_all(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This is not the only way of loading blocks; using an
|
||||||
|
[Lua Voxel Manipulator (LVM)](../advmap/lvm.html) will also cause the
|
||||||
|
encompassed blocks to be loaded synchronously.
|
||||||
|
|
||||||
|
## Deleting Blocks
|
||||||
|
|
||||||
|
You can use delete_blocks to delete a range of map blocks:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Delete a 20x20x20 area
|
||||||
|
local halfsize = { x = 10, y = 10, z = 10 }
|
||||||
|
local pos1 = vector.subtract(pos, halfsize)
|
||||||
|
local pos2 = vector.add (pos, halfsize)
|
||||||
|
|
||||||
|
core.delete_area(pos1, pos2)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will delete all map blocks in that area, *inclusive*. This means that some
|
||||||
|
nodes will be deleted outside the area as they will be on a mapblock which overlaps
|
||||||
|
the area bounds.
|
361
_en/map/objects.md
Normal file
361
_en/map/objects.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
---
|
||||||
|
title: Objects, Players, and Entities
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.4
|
||||||
|
description: Using an ObjectRef
|
||||||
|
degrad:
|
||||||
|
level: warning
|
||||||
|
title: Degrees and Radians
|
||||||
|
message: Attachment rotation is set in degrees, whereas object rotation is in radians.
|
||||||
|
Make sure to convert to the correct angle measurement.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to manipulate objects and how to define your
|
||||||
|
own.
|
||||||
|
|
||||||
|
- [What are Objects, Players, and Entities?](#what-are-objects-players-and-entities)
|
||||||
|
- [Position and Velocity](#position-and-velocity)
|
||||||
|
- [Object Properties](#object-properties)
|
||||||
|
- [Entities](#entities)
|
||||||
|
- [Health and Damage](#health-and-damage)
|
||||||
|
- [Health Points (HP)](#health-points-hp)
|
||||||
|
- [Punch, Damage Groups, and Armor Groups](#punch-damage-groups-and-armor-groups)
|
||||||
|
- [Example Damage Calculation](#example-damage-calculation)
|
||||||
|
- [Attachments](#attachments)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## What are Objects, Players, and Entities?
|
||||||
|
|
||||||
|
Players and Entities are both types of Objects. An object is something that can move
|
||||||
|
independently of the node grid and has properties such as velocity and scale.
|
||||||
|
Objects aren't items, and they have their own separate registration system.
|
||||||
|
|
||||||
|
There are a few differences between Players and Entities.
|
||||||
|
The biggest one is that Players are player-controlled, whereas Entities are mod-controlled.
|
||||||
|
This means that the velocity of a player cannot be set by mods - players are client-side,
|
||||||
|
and entities are server-side.
|
||||||
|
Another difference is that Players will cause map blocks to be loaded, whereas Entities
|
||||||
|
will just be saved and become inactive.
|
||||||
|
|
||||||
|
This distinction is muddied by the fact that Entities are controlled using a table
|
||||||
|
which is referred to as a Lua entity, as discussed later.
|
||||||
|
|
||||||
|
## Position and Velocity
|
||||||
|
|
||||||
|
`get_pos` and `set_pos` exist to allow you to get and set an entity's position.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local object = core.get_player_by_name("bob")
|
||||||
|
local pos = object:get_pos()
|
||||||
|
object:set_pos({ x = pos.x, y = pos.y + 1, z = pos.z })
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_pos` immediately sets the position, with no animation. If you'd like to
|
||||||
|
smoothly animate an object to the new position, you should use `move_to`.
|
||||||
|
This, unfortunately, only works for entities.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
object:move_to({ x = pos.x, y = pos.y + 1, z = pos.z })
|
||||||
|
```
|
||||||
|
|
||||||
|
An important thing to think about when dealing with entities is network latency.
|
||||||
|
In an ideal world, messages about entity movements would arrive immediately,
|
||||||
|
in the correct order, and with a similar interval as to how you sent them.
|
||||||
|
However, unless you're in singleplayer, this isn't an ideal world.
|
||||||
|
Messages will take a while to arrive. Position messages may arrive out of order,
|
||||||
|
resulting in some `set_pos` calls being skipped as there's no point going to
|
||||||
|
a position older than the current known position.
|
||||||
|
Moves may not be similarly spaced, which makes it difficult to use them for animation.
|
||||||
|
All this results in the client seeing different things to the server, which is something
|
||||||
|
you need to be aware of.
|
||||||
|
|
||||||
|
## Object Properties
|
||||||
|
|
||||||
|
Object properties are used to tell the client how to render and deal with an
|
||||||
|
object. It's not possible to define custom properties, because the properties are
|
||||||
|
for the engine to use, by definition.
|
||||||
|
|
||||||
|
Unlike nodes, objects have a dynamic rather than set appearance.
|
||||||
|
You can change how an object looks, among other things, at any time by updating
|
||||||
|
its properties.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
object:set_properties({
|
||||||
|
visual = "mesh",
|
||||||
|
mesh = "character.b3d",
|
||||||
|
textures = {"character_texture.png"},
|
||||||
|
visual_size = {x=1, y=1},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The updated properties will be sent to all players in range.
|
||||||
|
This is very useful to get a large amount of variety very cheaply, such as having
|
||||||
|
different skins per-player.
|
||||||
|
|
||||||
|
As shown in the next section, entities can have initial properties
|
||||||
|
provided in their definition.
|
||||||
|
The default Player properties are defined in the engine, however, so you'll
|
||||||
|
need to use `set_properties()` in `on_joinplayer` to set the properties for newly
|
||||||
|
joined players.
|
||||||
|
|
||||||
|
## Entities
|
||||||
|
|
||||||
|
An Entity has a definition table that resembles an item definition table.
|
||||||
|
This table can contain callback methods, initial object properties, and custom
|
||||||
|
members.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local MyEntity = {
|
||||||
|
initial_properties = {
|
||||||
|
hp_max = 1,
|
||||||
|
physical = true,
|
||||||
|
collide_with_objects = false,
|
||||||
|
collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},
|
||||||
|
visual = "wielditem",
|
||||||
|
visual_size = {x = 0.4, y = 0.4},
|
||||||
|
textures = {""},
|
||||||
|
spritediv = {x = 1, y = 1},
|
||||||
|
initial_sprite_basepos = {x = 0, y = 0},
|
||||||
|
},
|
||||||
|
|
||||||
|
message = "Default message",
|
||||||
|
}
|
||||||
|
|
||||||
|
function MyEntity:set_message(msg)
|
||||||
|
self.message = msg
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Entity definitions differ in one very important way from Item definitions.
|
||||||
|
When an entity is emerged (ie: loaded or created), a new table is created for
|
||||||
|
that entity that *inherits* from the definition table.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This inheritance is done using a metatables.
|
||||||
|
Metatables are an important Lua feature that you will need to be aware of, as it
|
||||||
|
is an essential part of the Lua language. In layman's terms, a metatable allows
|
||||||
|
you to control how the table behaves when using certain Lua syntax. The most
|
||||||
|
common use of metatables is the ability to use another table as a prototype,
|
||||||
|
defaulting to the other table's properties and methods when they do not exist in
|
||||||
|
the current table.
|
||||||
|
Say you want to access `a.x`. If the table `a` has that member, then it will be
|
||||||
|
returned as normal. However, if the table doesn't have that member and the
|
||||||
|
metatable lists a table `b` as a prototype, then table `b` will be checked to
|
||||||
|
see if it has that member.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Both an ObjectRef and an entity table provide ways to get the counterpart:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local entity = object:get_luaentity()
|
||||||
|
local object = entity.object
|
||||||
|
print("entity is at " .. core.pos_to_string(object:get_pos()))
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a number of available callbacks for use with entities.
|
||||||
|
A complete list can be found in [lua_api.md](https://minetest.gitlab.io/minetest/minetest-namespace-reference/#registered-definition-tables).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function MyEntity:on_step(dtime)
|
||||||
|
local pos = self.object:get_pos()
|
||||||
|
local pos_down = vector.subtract(pos, vector.new(0, 1, 0))
|
||||||
|
|
||||||
|
local delta
|
||||||
|
if core.get_node(pos_down).name == "air" then
|
||||||
|
delta = vector.new(0, -1, 0)
|
||||||
|
elseif core.get_node(pos).name == "air" then
|
||||||
|
delta = vector.new(0, 0, 1)
|
||||||
|
else
|
||||||
|
delta = vector.new(0, 1, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
delta = vector.multiply(delta, dtime)
|
||||||
|
|
||||||
|
self.object:move_to(vector.add(pos, delta))
|
||||||
|
end
|
||||||
|
|
||||||
|
function MyEntity:on_punch(hitter)
|
||||||
|
core.chat_send_player(hitter:get_player_name(), self.message)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, if you were to spawn and use this entity, you'd notice that the message
|
||||||
|
would be forgotten when the entity becomes inactive then active again.
|
||||||
|
This is because the message isn't saved.
|
||||||
|
Rather than saving everything in the entity table, Minetest gives you control over
|
||||||
|
how to save things.
|
||||||
|
Staticdata is a string which contains all the custom information that
|
||||||
|
needs to stored.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function MyEntity:get_staticdata()
|
||||||
|
return core.write_json({
|
||||||
|
message = self.message,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function MyEntity:on_activate(staticdata, dtime_s)
|
||||||
|
if staticdata ~= "" and staticdata ~= nil then
|
||||||
|
local data = core.parse_json(staticdata) or {}
|
||||||
|
self:set_message(data.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest may call `get_staticdata()` as many times as it wants and at any time.
|
||||||
|
This is because Minetest doesn't wait for a MapBlock to become inactive to save
|
||||||
|
it, as this would result in data loss. MapBlocks are saved roughly every 18
|
||||||
|
seconds, so you should notice a similar interval for `get_staticdata()` being called.
|
||||||
|
|
||||||
|
`on_activate()`, on the other hand, will only be called when an entity becomes
|
||||||
|
active either from the MapBlock becoming active or from the entity spawning.
|
||||||
|
This means that staticdata could be empty.
|
||||||
|
|
||||||
|
Finally, you need to register the type table using the aptly named `register_entity`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_entity("mymod:entity", MyEntity)
|
||||||
|
```
|
||||||
|
|
||||||
|
The entity can be spawned by a mod like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos = { x = 1, y = 2, z = 3 }
|
||||||
|
local obj = core.add_entity(pos, "mymod:entity", nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
The third parameter is the initial staticdata.
|
||||||
|
To set the message, you can use the entity table method:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
obj:get_luaentity():set_message("hello!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Players with the *give* [privilege](../players/privileges.html) can
|
||||||
|
use a [chat command](../players/chat.html) to spawn entities:
|
||||||
|
|
||||||
|
/spawnentity mymod:entity
|
||||||
|
|
||||||
|
|
||||||
|
## Health and Damage
|
||||||
|
|
||||||
|
### Health Points (HP)
|
||||||
|
|
||||||
|
Each object has a Health Points (HP) number, which represents the current health.
|
||||||
|
Players have a maximum hp set using the `hp_max` object property.
|
||||||
|
An object will die if its hp reaches 0.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local hp = object:get_hp()
|
||||||
|
object:set_hp(hp + 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Punch, Damage Groups, and Armor Groups
|
||||||
|
|
||||||
|
Damage is the reduction of an object's HP. An object can *punch* another object to
|
||||||
|
inflict damage. A punch isn't necessarily an actual punch - it can be an
|
||||||
|
explosion, a sword slash, or something else.
|
||||||
|
|
||||||
|
The total damage is calculated by multiplying the punch's damage groups with the
|
||||||
|
target's vulnerabilities. This is then limited depending on how recent the last
|
||||||
|
punch was. We will go over an example of this calculation in a bit.
|
||||||
|
|
||||||
|
Just like [node dig groups](../items/nodes_items_crafting.html#tools-capabilities-and-dig-types),
|
||||||
|
these groups can take any name and do not need to be registered. However, it's
|
||||||
|
common to use the same group names as with node digging.
|
||||||
|
|
||||||
|
How vulnerable an object is to particular types of damage depends on its
|
||||||
|
`armor_groups`. Despite its misleading name, `armor_groups` specify the
|
||||||
|
percentage damage taken from particular damage groups, not the resistance. If a
|
||||||
|
damage group is not listed in an object's armor groups, that object is
|
||||||
|
completely invulnerable to it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
target:set_armor_groups({
|
||||||
|
fleshy = 90,
|
||||||
|
crumbly = 50,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, the object will take 90% of `fleshy` damage and 50% of
|
||||||
|
`crumbly` damage.
|
||||||
|
|
||||||
|
When a player punches an object, the damage groups come from the item they are
|
||||||
|
currently wielding. In other cases, mods decide which damage groups are used.
|
||||||
|
|
||||||
|
### Example Damage Calculation
|
||||||
|
|
||||||
|
Let's punch the `target` object:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local tool_capabilities = {
|
||||||
|
full_punch_interval = 0.8,
|
||||||
|
damage_groups = { fleshy = 5, choppy = 10 },
|
||||||
|
|
||||||
|
-- This is only used for digging nodes, but is still required
|
||||||
|
max_drop_level=1,
|
||||||
|
groupcaps={
|
||||||
|
fleshy={times={[1]=2.5, [2]=1.20, [3]=0.35}, uses=30, maxlevel=2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local time_since_last_punch = tool_capabilities.full_punch_interval
|
||||||
|
target:punch(object, time_since_last_punch, tool_capabilities)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's work out what the damage will be. The punch's damage groups are
|
||||||
|
`fleshy=5` and `choppy=10`, and `target` will take 90% damage from fleshy and 0%
|
||||||
|
from choppy.
|
||||||
|
|
||||||
|
First, we multiply the damage groups by the vulnerability and sum the result.
|
||||||
|
We then multiply by a number between 0 or 1 depending on the `time_since_last_punch`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
= (5*90/100 + 10*0/100) * limit(time_since_last_punch / full_punch_interval, 0, 1)
|
||||||
|
= (5*90/100 + 10*0/100) * 1
|
||||||
|
= 4.5
|
||||||
|
```
|
||||||
|
|
||||||
|
As HP is an integer, the damage is rounded to 5 points.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Attachments
|
||||||
|
|
||||||
|
Attached objects will move when the parent - the object they are attached to -
|
||||||
|
is moved. An attached object is said to be a child of the parent.
|
||||||
|
An object can have an unlimited number of children, but at most one parent.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
child:set_attach(parent, bone, position, rotation)
|
||||||
|
```
|
||||||
|
|
||||||
|
An object's `get_pos()` will always return the global position of the object, no
|
||||||
|
matter whether it is attached or not.
|
||||||
|
`set_attach` takes a relative position, but not as you'd expect.
|
||||||
|
The attachment position is relative to the parent's origin as scaled up by 10 times.
|
||||||
|
So, `0,5,0` would be half a node above the parent's origin.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.degrad %}
|
||||||
|
|
||||||
|
For 3D models with animations, the bone argument is used to attach the entity
|
||||||
|
to a bone.
|
||||||
|
3D animations are based on skeletons - a network of bones in the model where
|
||||||
|
each bone can be given a position and rotation to change the model, for example,
|
||||||
|
to move the arm.
|
||||||
|
Attaching to a bone is useful if you want to make a character hold something:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
obj:set_attach(player,
|
||||||
|
"Arm_Right", -- default bone
|
||||||
|
{x=0.2, y=6.5, z=3}, -- default position
|
||||||
|
{x=-100, y=225, z=90}) -- default rotation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Make a windmill by combining nodes and an entity.
|
||||||
|
* Make a mob of your choice (using just the entity API, and without using any other mods).
|
247
_en/map/storage.md
Normal file
247
_en/map/storage.md
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
title: Storage and Metadata
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.3
|
||||||
|
description: Mod Storage, NodeMetaRef (get_meta).
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/node_metadata.html
|
||||||
|
- /en/map/node_metadata.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how you can store data.
|
||||||
|
|
||||||
|
- [Metadata](#metadata)
|
||||||
|
- [What is Metadata?](#what-is-metadata)
|
||||||
|
- [Obtaining a Metadata Object](#obtaining-a-metadata-object)
|
||||||
|
- [Reading and Writing](#reading-and-writing)
|
||||||
|
- [Special Keys](#special-keys)
|
||||||
|
- [Storing Tables](#storing-tables)
|
||||||
|
- [Private Metadata](#private-metadata)
|
||||||
|
- [Lua Tables](#lua-tables)
|
||||||
|
- [Mod Storage](#mod-storage)
|
||||||
|
- [Databases](#databases)
|
||||||
|
- [Deciding Which to Use](#deciding-which-to-use)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
### What is Metadata?
|
||||||
|
|
||||||
|
In Minetest, Metadata is a key-value store used to attach custom data to something.
|
||||||
|
You can use metadata to store information against a Node, Player, or ItemStack.
|
||||||
|
|
||||||
|
Each type of metadata uses the exact same API.
|
||||||
|
Metadata stores values as strings, but there are a number of methods to
|
||||||
|
convert and store other primitive types.
|
||||||
|
|
||||||
|
Some keys in metadata may have special meaning.
|
||||||
|
For example, `infotext` in node metadata is used to store the tooltip which shows
|
||||||
|
when hovering over the node using the crosshair.
|
||||||
|
To avoid conflicts with other mods, you should use the standard namespace
|
||||||
|
convention for keys: `modname:keyname`.
|
||||||
|
The exception is for conventional data such as the owner name which is stored as
|
||||||
|
`owner`.
|
||||||
|
|
||||||
|
Metadata is data about data.
|
||||||
|
The data itself, such as a node's type or an stack's count, is not metadata.
|
||||||
|
|
||||||
|
### Obtaining a Metadata Object
|
||||||
|
|
||||||
|
If you know the position of a node, you can retrieve its metadata:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local meta = core.get_meta({ x = 1, y = 2, z = 3 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Player and ItemStack metadata are obtained using `get_meta()`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pmeta = player:get_meta()
|
||||||
|
local imeta = stack:get_meta()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading and Writing
|
||||||
|
|
||||||
|
In most cases, `get_<type>()` and `set_<type>()` methods will be used to read
|
||||||
|
and write to meta.
|
||||||
|
Metadata stores strings, so the string methods will directly set and get the value.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(meta:get_string("foo")) --> ""
|
||||||
|
meta:set_string("foo", "bar")
|
||||||
|
print(meta:get_string("foo")) --> "bar"
|
||||||
|
```
|
||||||
|
|
||||||
|
All of the typed getters will return a neutral default value if the key doesn't
|
||||||
|
exist, such as `""` or `0`.
|
||||||
|
You can use `get()` to return a string or nil.
|
||||||
|
|
||||||
|
As Metadata is a reference, any changes will be updated to the source automatically.
|
||||||
|
ItemStacks aren't references however, so you'll need to update the itemstack in the
|
||||||
|
inventory.
|
||||||
|
|
||||||
|
The non-typed getters and setters will convert to and from strings:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(meta:get_int("count")) --> 0
|
||||||
|
meta:set_int("count", 3)
|
||||||
|
print(meta:get_int("count")) --> 3
|
||||||
|
print(meta:get_string("count")) --> "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Keys
|
||||||
|
|
||||||
|
`infotext` is used in Node Metadata to show a tooltip when hovering the crosshair over a node.
|
||||||
|
This is useful when showing the ownership or status of a node.
|
||||||
|
|
||||||
|
`description` is used in ItemStack Metadata to override the description when
|
||||||
|
hovering over the stack in an inventory.
|
||||||
|
You can use colours by encoding them with `core.colorize()`.
|
||||||
|
|
||||||
|
`owner` is a common key used to store the username of the player that owns the
|
||||||
|
item or node.
|
||||||
|
|
||||||
|
### Storing Tables
|
||||||
|
|
||||||
|
Tables must be converted to strings before they can be stored.
|
||||||
|
Minetest offers two formats for doing this: Lua and JSON.
|
||||||
|
|
||||||
|
The Lua method tends to be a lot faster and matches the format Lua
|
||||||
|
uses for tables, while JSON is a more standard format, is better
|
||||||
|
structured, and is well suited for when you need to exchange information
|
||||||
|
with another program.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = { username = "player1", score = 1234 }
|
||||||
|
meta:set_string("foo", core.serialize(data))
|
||||||
|
|
||||||
|
data = core.deserialize(meta:get_string("foo"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Private Metadata
|
||||||
|
|
||||||
|
By default, all node metadata is sent to the client.
|
||||||
|
You can mark keys as private to prevent this.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
meta:set_string("secret", "asd34dn")
|
||||||
|
meta:mark_as_private("secret")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lua Tables
|
||||||
|
|
||||||
|
You can convert to and from Lua tables using `to_table` and `from_table`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local tmp = meta:to_table()
|
||||||
|
tmp.foo = "bar"
|
||||||
|
meta:from_table(tmp)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mod Storage
|
||||||
|
|
||||||
|
Mod storage uses the exact same API as Metadata, although it's not technically
|
||||||
|
Metadata.
|
||||||
|
Mod storage is per-mod, and can only be obtained during load time in order to
|
||||||
|
know which mod is requesting it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local storage = core.get_mod_storage()
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now manipulate the storage just like metadata:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
storage:set_string("foo", "bar")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Databases
|
||||||
|
|
||||||
|
If the mod is likely to be used on a server and will store lots of data,
|
||||||
|
it's a good idea to offer a database storage method.
|
||||||
|
You should make this optional by separating how the data is stored and where
|
||||||
|
it is used.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local backend
|
||||||
|
if use_database then
|
||||||
|
backend =
|
||||||
|
dofile(core.get_modpath("mymod") .. "/backend_sqlite.lua")
|
||||||
|
else
|
||||||
|
backend =
|
||||||
|
dofile(core.get_modpath("mymod") .. "/backend_storage.lua")
|
||||||
|
end
|
||||||
|
|
||||||
|
backend.get_foo("a")
|
||||||
|
backend.set_foo("a", { score = 3 })
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend_storage.lua file should include a mod storage implementation:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local storage = core.get_mod_storage()
|
||||||
|
local backend = {}
|
||||||
|
|
||||||
|
function backend.set_foo(key, value)
|
||||||
|
storage:set_string(key, core.serialize(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
function backend.get_foo(key)
|
||||||
|
return core.deserialize(storage:get_string(key))
|
||||||
|
end
|
||||||
|
|
||||||
|
return backend
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend_sqlite would do a similar thing, but use the Lua *lsqlite3* library
|
||||||
|
instead of mod storage.
|
||||||
|
|
||||||
|
Using a database such as SQLite requires using an insecure environment.
|
||||||
|
An insecure environment is a table that is only available to mods
|
||||||
|
explicitly whitelisted by the user, and it contains a less restricted
|
||||||
|
copy of the Lua API which could be abused if available to malicious mods.
|
||||||
|
Insecure environments will be covered in more detail in the
|
||||||
|
[Security](../quality/security.html) chapter.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ie = core.request_insecure_environment()
|
||||||
|
assert(ie, "Please add mymod to secure.trusted_mods in the settings")
|
||||||
|
|
||||||
|
local _sql = ie.require("lsqlite3")
|
||||||
|
-- Prevent other mods from using the global sqlite3 library
|
||||||
|
if sqlite3 then
|
||||||
|
sqlite3 = nil
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Teaching about SQL or how to use the lsqlite3 library is out of scope for this book.
|
||||||
|
|
||||||
|
## Deciding Which to Use
|
||||||
|
|
||||||
|
The type of method you use depends on what the data is about,
|
||||||
|
how it is formatted, and how large it is.
|
||||||
|
As a guideline, small data is up to 10K, medium data is up to 10MB, and large
|
||||||
|
data is any size above that.
|
||||||
|
|
||||||
|
Node metadata is a good choice when you need to store node-related data.
|
||||||
|
Storing medium data is fairly efficient if you make it private.
|
||||||
|
|
||||||
|
Item metadata should not be used to store anything but small amounts of data as it is not
|
||||||
|
possible to avoid sending it to the client.
|
||||||
|
The data will also be copied every time the stack is moved, or accessed from Lua.
|
||||||
|
|
||||||
|
Mod storage is good for medium data but writing large data may be inefficient.
|
||||||
|
It's better to use a database for large data to avoid having to write all the
|
||||||
|
data out on every save.
|
||||||
|
|
||||||
|
Databases are only viable for servers due to the
|
||||||
|
need to whitelist the mod to access an insecure environment.
|
||||||
|
They're well suited for large data sets.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Make a node which disappears after it has been punched five times.
|
||||||
|
(Use `on_punch` in the node definition and `core.set_node`.)
|
110
_en/map/timers.md
Normal file
110
_en/map/timers.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
title: Node Timers and ABMs
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.2
|
||||||
|
description: Learn how to make ABMs to change blocks.
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/abms.html
|
||||||
|
- /en/map/abms.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Periodically running a function on certain nodes is a common task.
|
||||||
|
Minetest provides two methods of doing this: Active Block Modifiers (ABMs) and node timers.
|
||||||
|
|
||||||
|
ABMs scan all loaded MapBlocks looking for nodes that match a criteria.
|
||||||
|
They are best suited for nodes which are frequently found in the world,
|
||||||
|
such as grass.
|
||||||
|
They have a high CPU overhead, but a low memory and storage overhead.
|
||||||
|
|
||||||
|
For nodes that are uncommon or already use metadata, such as furnaces
|
||||||
|
and machines, node timers should be used instead.
|
||||||
|
Node timers work by keeping track of pending timers in each MapBlock, and then
|
||||||
|
running them when they expire.
|
||||||
|
This means that timers don't need to search all loaded nodes to find matches,
|
||||||
|
but instead require slightly more memory and storage for the tracking
|
||||||
|
of pending timers.
|
||||||
|
|
||||||
|
- [Node Timers](#node-timers)
|
||||||
|
- [Active Block Modifiers](#active-block-modifiers)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Node Timers
|
||||||
|
|
||||||
|
Node timers are directly tied to a single node.
|
||||||
|
You can manage node timers by obtaining a NodeTimerRef object.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local timer = core.get_node_timer(pos)
|
||||||
|
timer:start(10.5) -- in seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
When a node timer is up, the `on_timer` method in the node's definition table will
|
||||||
|
be called. The method only takes a single parameter, the position of the node:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("autodoors:door_open", {
|
||||||
|
on_timer = function(pos)
|
||||||
|
core.set_node(pos, { name = "autodoors:door" })
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Returning true in `on_timer` will cause the timer to run again for the same interval.
|
||||||
|
It's also possible to use `get_node_timer(pos)` inside of `on_timer`, just make
|
||||||
|
sure you return false to avoid conflict.
|
||||||
|
|
||||||
|
You may have noticed a limitation with timers: for optimisation reasons, it's
|
||||||
|
only possible to have one type of timer per node type, and only one timer running per node.
|
||||||
|
|
||||||
|
|
||||||
|
## Active Block Modifiers
|
||||||
|
|
||||||
|
Alien grass, for the purposes of this chapter, is a type of grass which
|
||||||
|
has a chance to appear near water.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("aliens:grass", {
|
||||||
|
description = "Alien Grass",
|
||||||
|
light_source = 3, -- The node radiates light. Min 0, max 14
|
||||||
|
tiles = {"aliens_grass.png"},
|
||||||
|
groups = {choppy=1},
|
||||||
|
on_use = core.item_eat(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_abm({
|
||||||
|
nodenames = {"default:dirt_with_grass"},
|
||||||
|
neighbors = {"default:water_source", "default:water_flowing"},
|
||||||
|
interval = 10.0, -- Run every 10 seconds
|
||||||
|
chance = 50, -- One node has a chance of 1 in 50 to get selected
|
||||||
|
action = function(pos, node, active_object_count,
|
||||||
|
active_object_count_wider)
|
||||||
|
local pos = {x = pos.x, y = pos.y + 1, z = pos.z}
|
||||||
|
core.set_node(pos, {name = "aliens:grass"})
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This ABM runs every ten seconds, and for each matching node, there is
|
||||||
|
a 1 in 50 chance of it running.
|
||||||
|
If the ABM runs on a node, an alien grass node is placed above it.
|
||||||
|
Please be warned, this will delete any node previously located in that position.
|
||||||
|
To prevent this you should include a check using core.get_node to make sure there is space for the grass.
|
||||||
|
|
||||||
|
Specifying a neighbour is optional.
|
||||||
|
If you specify multiple neighbours, only one of them needs to be
|
||||||
|
present to meet the requirements.
|
||||||
|
|
||||||
|
Specifying chance is also optional.
|
||||||
|
If you don't specify the chance, the ABM will always run when the other conditions are met.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Midas touch: Make water turn to gold blocks with a 1 in 100 chance, every 5 seconds.
|
||||||
|
* Decay: Make wood turn into dirt when water is a neighbour.
|
||||||
|
* Burnin': Make every air node catch on fire. (Tip: "air" and "fire:basic_flame").
|
||||||
|
Warning: expect the game to crash.
|
197
_en/players/chat.md
Normal file
197
_en/players/chat.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
title: Chat and Commands
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.2
|
||||||
|
description: Registering a chatcommand and handling chat messages with register_on_chat_message
|
||||||
|
redirect_from: /en/chapters/chat.html
|
||||||
|
cmd_online:
|
||||||
|
level: warning
|
||||||
|
title: Offline players can run commands
|
||||||
|
message: |
|
||||||
|
A player name is passed instead of a player object because mods
|
||||||
|
can run commands on behalf of offline players. For example, the IRC
|
||||||
|
bridge allows players to run commands without joining the game.
|
||||||
|
|
||||||
|
So make sure that you don't assume that the player is online.
|
||||||
|
You can check by seeing if `core.get_player_by_name` returns a player.
|
||||||
|
|
||||||
|
cb_cmdsprivs:
|
||||||
|
level: warning
|
||||||
|
title: Privileges and Chat Commands
|
||||||
|
message: |
|
||||||
|
The shout privilege isn't needed for a player to trigger this callback.
|
||||||
|
This is because chat commands are implemented in Lua, and are just
|
||||||
|
chat messages that begin with a /.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Mods can interact with player chat, including
|
||||||
|
sending messages, intercepting messages, and registering chat commands.
|
||||||
|
|
||||||
|
- [Sending Messages](#sending-messages)
|
||||||
|
- [To All Players](#to-all-players)
|
||||||
|
- [To Specific Players](#to-specific-players)
|
||||||
|
- [Chat Commands](#chat-commands)
|
||||||
|
- [Accepting Multiple Arguments](#accepting-multiple-arguments)
|
||||||
|
- [Using string.split](#using-stringsplit)
|
||||||
|
- [Using Lua patterns](#using-lua-patterns)
|
||||||
|
- [Intercepting Messages](#intercepting-messages)
|
||||||
|
|
||||||
|
## Sending Messages
|
||||||
|
|
||||||
|
### To All Players
|
||||||
|
|
||||||
|
To send a message to every player in the game, call the `chat_send_all` function.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.chat_send_all("This is a chat message to all players")
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example of how this appears in-game:
|
||||||
|
|
||||||
|
<player1> Look at this entrance
|
||||||
|
This is a chat message to all players
|
||||||
|
<player2> What about it?
|
||||||
|
|
||||||
|
The message appears on a separate line to distinguish it from in-game player chat.
|
||||||
|
|
||||||
|
### To Specific Players
|
||||||
|
|
||||||
|
To send a message to a specific player, call the `chat_send_player` function:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.chat_send_player("player1", "This is a chat message for player1")
|
||||||
|
```
|
||||||
|
|
||||||
|
This message displays in the same manner as messages to all players, but is
|
||||||
|
only visible to the named player, in this case, player1.
|
||||||
|
|
||||||
|
## Chat Commands
|
||||||
|
|
||||||
|
To register a chat command, for example `/foo`, use `register_chatcommand`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_chatcommand("foo", {
|
||||||
|
privs = {
|
||||||
|
interact = true,
|
||||||
|
},
|
||||||
|
func = function(name, param)
|
||||||
|
return true, "You said " .. param .. "!"
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above snippet, `interact` is listed as a required
|
||||||
|
[privilege](privileges.html) meaning that only players with the `interact` privilege can run the command.
|
||||||
|
|
||||||
|
`param` is a string containing everything a player writes after the chatcommand
|
||||||
|
name. For example, if a user types `/grantme one,two,three` then `param` will be
|
||||||
|
`one,two,three`.
|
||||||
|
|
||||||
|
Chat commands can return up to two values,
|
||||||
|
the first being a Boolean indicating success, and the second being a
|
||||||
|
message to send to the user.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.cmd_online %}
|
||||||
|
|
||||||
|
### Accepting Multiple Arguments
|
||||||
|
|
||||||
|
<a name="complex-subcommands"></a>
|
||||||
|
|
||||||
|
`param` gives you all the arguments to a chat command in a single string. It's
|
||||||
|
common for chat commands to need to extract multiple arguments. There are two
|
||||||
|
ways of doing this, either using Minetest's string split or Lua patterns.
|
||||||
|
|
||||||
|
#### Using string.split
|
||||||
|
|
||||||
|
A string can be split up into words using `string.split(" ")`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local parts = param:split(" ")
|
||||||
|
local cmd = parts[1]
|
||||||
|
|
||||||
|
if cmd == "join" then
|
||||||
|
local team_name = parts[2]
|
||||||
|
team.join(name, team_name)
|
||||||
|
return true, "Joined team!"
|
||||||
|
elseif cmd == "max_users" then
|
||||||
|
local team_name = parts[2]
|
||||||
|
local max_users = tonumber(parts[3])
|
||||||
|
if team_name and max_users then
|
||||||
|
return true, "Set max users of team " .. team_name .. " to " .. max_users
|
||||||
|
else
|
||||||
|
return false, "Usage: /team max_users <team_name> <number>"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return false, "Command needed"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Lua patterns
|
||||||
|
|
||||||
|
[Lua patterns](https://www.lua.org/pil/20.2.html) are a way of extracting stuff
|
||||||
|
from text using rules. They're best suited for when there are arguments that can
|
||||||
|
contain spaces or more control is needed on how parameters are captured.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local to, msg = param:match("^([%a%d_-]+) (.+)$")
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code implements `/msg <to> <message>`. Let's go through left to right:
|
||||||
|
|
||||||
|
* `^` means match the start of the string.
|
||||||
|
* `()` is a matching group - anything that matches stuff in here will be
|
||||||
|
returned from string.match.
|
||||||
|
* `[]` means accept characters in this list.
|
||||||
|
* `%a` means accept any letter and `%d` means accept any digit.
|
||||||
|
* `[%a%d_-]` means accept any letter or digit or `_` or `-`.
|
||||||
|
* `+` means match the thing before one or more times.
|
||||||
|
* `.` means match any character in this context.
|
||||||
|
* `$` means match the end of the string.
|
||||||
|
|
||||||
|
Put simply, the pattern matches the name (a word with only letters/numbers/-/_),
|
||||||
|
then a space, then the message (one or more of any character). The name and
|
||||||
|
message are returned, because they're surrounded by parentheses.
|
||||||
|
|
||||||
|
That's how most mods implement complex chat commands. A better guide to Lua
|
||||||
|
Patterns would probably be the
|
||||||
|
[lua-users.org tutorial](http://lua-users.org/wiki/PatternsTutorial)
|
||||||
|
or the [PIL documentation](https://www.lua.org/pil/20.2.html).
|
||||||
|
|
||||||
|
## Intercepting Messages
|
||||||
|
|
||||||
|
To intercept a message, use register_on_chat_message:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_chat_message(function(name, message)
|
||||||
|
print(name .. " said " .. message)
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
By returning false, you allow the chat message to be sent by the default
|
||||||
|
handler. You can actually remove the line `return false` and it would still
|
||||||
|
work the same, because `nil` is returned implicitly and is treated like false.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.cb_cmdsprivs %}
|
||||||
|
|
||||||
|
You should make sure you take into account that it may be a chat command,
|
||||||
|
or the user may not have `shout`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_chat_message(function(name, message)
|
||||||
|
if message:sub(1, 1) == "/" then
|
||||||
|
print(name .. " ran chat command")
|
||||||
|
elseif core.check_player_privs(name, { shout = true }) then
|
||||||
|
print(name .. " said " .. message)
|
||||||
|
else
|
||||||
|
print(name .. " tried to say " .. message ..
|
||||||
|
" but doesn't have shout")
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
```
|
379
_en/players/formspecs.md
Normal file
379
_en/players/formspecs.md
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
---
|
||||||
|
title: GUIs (Formspecs)
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.5
|
||||||
|
description: Learn how to display GUIs using formspecs
|
||||||
|
redirect_from: /en/chapters/formspecs.html
|
||||||
|
submit_vuln:
|
||||||
|
level: warning
|
||||||
|
title: Malicious clients can submit anything at anytime
|
||||||
|
message: You should never trust a formspec submission. A malicious client
|
||||||
|
can submit anything they like at any time - even if you never showed
|
||||||
|
them the formspec. This means that you should check privileges
|
||||||
|
and make sure that they should be allowed to perform the action.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/formspec_example.png" alt="Furnace Inventory">
|
||||||
|
<figcaption>
|
||||||
|
Screenshot of furnace formspec, labelled.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
In this chapter we will learn how to create a formspec and display it to the user.
|
||||||
|
A formspec is the specification code for a form.
|
||||||
|
In Minetest, forms are windows such as the player inventory and can contain a
|
||||||
|
variety of elements, such as labels, buttons and fields.
|
||||||
|
|
||||||
|
Note that if you do not need to get user input, for example when you only need
|
||||||
|
to provide information to the player, you should consider using
|
||||||
|
[Heads Up Display (HUD)](hud.html) elements instead of forms, because
|
||||||
|
unexpected windows tend to disrupt gameplay.
|
||||||
|
|
||||||
|
- [Real or Legacy Coordinates](#real-or-legacy-coordinates)
|
||||||
|
- [Anatomy of a Formspec](#anatomy-of-a-formspec)
|
||||||
|
- [Elements](#elements)
|
||||||
|
- [Header](#header)
|
||||||
|
- [Guessing Game](#guessing-game)
|
||||||
|
- [Padding and Spacing](#padding-and-spacing)
|
||||||
|
- [Receiving Formspec Submissions](#receiving-formspec-submissions)
|
||||||
|
- [Contexts](#contexts)
|
||||||
|
- [Formspec Sources](#formspec-sources)
|
||||||
|
- [Node Meta Formspecs](#node-meta-formspecs)
|
||||||
|
- [Player Inventory Formspecs](#player-inventory-formspecs)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
|
||||||
|
## Real or Legacy Coordinates
|
||||||
|
|
||||||
|
In older versions of Minetest, formspecs were inconsistent. The way that different
|
||||||
|
elements were positioned varied in unexpected ways; it was hard to predict the
|
||||||
|
placement of elements and align them. Minetest 5.1.0 contains a feature
|
||||||
|
called real coordinates which aims to rectify this by introducing a consistent
|
||||||
|
coordinate system. The use of real coordinates is highly recommended, and so
|
||||||
|
this chapter will use them exclusively.
|
||||||
|
|
||||||
|
Using a formspec_version of 2 or above will enable real coordinates.
|
||||||
|
|
||||||
|
## Anatomy of a Formspec
|
||||||
|
|
||||||
|
### Elements
|
||||||
|
|
||||||
|
Formspec is a domain-specific language with an unusual format.
|
||||||
|
It consists of a number of elements with the following form:
|
||||||
|
|
||||||
|
type[param1;param2]
|
||||||
|
|
||||||
|
The element type is declared and then any parameters are given
|
||||||
|
in square brackets. Multiple elements can be joined together, or placed
|
||||||
|
on multiple lines, like so:
|
||||||
|
|
||||||
|
foo[param1]bar[param1]
|
||||||
|
bo[param1]
|
||||||
|
|
||||||
|
|
||||||
|
Elements are items such as text boxes or buttons, or can be metadata such
|
||||||
|
as size or background. You should refer to
|
||||||
|
[lua_api.md](https://minetest.gitlab.io/minetest/formspec/)
|
||||||
|
for a list of all possible elements.
|
||||||
|
|
||||||
|
|
||||||
|
### Header
|
||||||
|
|
||||||
|
The header of a formspec contains information which must appear first. This
|
||||||
|
includes the size of the formspec, the position, the anchor, and whether the
|
||||||
|
game-wide theme should be applied.
|
||||||
|
|
||||||
|
The elements in the header must be defined in a specific order, otherwise you
|
||||||
|
will see an error. This order is given in the above paragraph, and, as always,
|
||||||
|
documented in the Lua API reference.
|
||||||
|
|
||||||
|
The size is in formspec slots - a unit of measurement which is roughly
|
||||||
|
around 64 pixels, but varies based on the screen density and scaling
|
||||||
|
settings of the client. Here's a formspec which is `2,2` in size:
|
||||||
|
|
||||||
|
formspec_version[4]
|
||||||
|
size[2,2]
|
||||||
|
|
||||||
|
Notice how we explicitly defined the formspec language version.
|
||||||
|
Without this, the legacy system will instead be used instead - which will
|
||||||
|
prevent the use of consistent element positioning and other new features.
|
||||||
|
|
||||||
|
The position and anchor elements are used to place the formspec on the screen.
|
||||||
|
The position sets where on the screen the formspec will be, and defaults to
|
||||||
|
the center (`0.5,0.5`). The anchor sets where on the formspec the position is,
|
||||||
|
allowing you to line the formspec up with the edge of the screen. The formspec
|
||||||
|
can be placed to the left of the screen like so:
|
||||||
|
|
||||||
|
formspec_version[4]
|
||||||
|
size[2,2]
|
||||||
|
position[0,0.5]
|
||||||
|
anchor[0,0.5]
|
||||||
|
|
||||||
|
This sets the anchor to the left middle edge of the formspec box, and then the
|
||||||
|
position of that anchor to the left of the screen.
|
||||||
|
|
||||||
|
|
||||||
|
## Guessing Game
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/formspec_guessing.png" alt="Guessing Formspec">
|
||||||
|
<figcaption>
|
||||||
|
The guessing game formspec.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
The best way to learn is to make something, so let's make a guessing game.
|
||||||
|
The principle is simple: the mod decides on a number, then the player makes
|
||||||
|
guesses on the number. The mod then says if the guess is higher or lower then
|
||||||
|
the actual number.
|
||||||
|
|
||||||
|
First, let's make a function to create the formspec code. It's good practice to
|
||||||
|
do this, as it makes it easier to reuse elsewhere.
|
||||||
|
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
guessing = {}
|
||||||
|
|
||||||
|
function guessing.get_formspec(name)
|
||||||
|
-- TODO: display whether the last guess was higher or lower
|
||||||
|
local text = "I'm thinking of a number... Make a guess!"
|
||||||
|
|
||||||
|
local formspec = {
|
||||||
|
"formspec_version[4]",
|
||||||
|
"size[6,3.476]",
|
||||||
|
"label[0.375,0.5;", core.formspec_escape(text), "]",
|
||||||
|
"field[0.375,1.25;5.25,0.8;number;Number;]",
|
||||||
|
"button[1.5,2.3;3,0.8;guess;Guess]"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- table.concat is faster than string concatenation - `..`
|
||||||
|
return table.concat(formspec, "")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above code, we place a field, a label, and a button. A field allows text
|
||||||
|
entry, and a button is used to submit the form. You'll notice that the elements
|
||||||
|
are positioned carefully in order to add padding and spacing, this will be explained
|
||||||
|
later.
|
||||||
|
|
||||||
|
Next, we want to allow the player to show the formspec. The main way to do this
|
||||||
|
is using `show_formspec`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function guessing.show_to(name)
|
||||||
|
core.show_formspec(name, "guessing:game", guessing.get_formspec(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_chatcommand("game", {
|
||||||
|
func = function(name)
|
||||||
|
guessing.show_to(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `show_formspec` function accepts a player name, the formspec name, and the
|
||||||
|
formspec itself. The formspec name should be a valid itemname, ie: in the format
|
||||||
|
`modname:itemname`.
|
||||||
|
|
||||||
|
|
||||||
|
### Padding and Spacing
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/formspec_padding_spacing.png" alt="Padding and spacing">
|
||||||
|
<figcaption>
|
||||||
|
The guessing game formspec.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Padding is the gap between the edge of the formspec and its contents, or between unrelated
|
||||||
|
elements, shown in red. Spacing is the gap between related elements, shown in blue.
|
||||||
|
|
||||||
|
It is fairly standard to have a padding of `0.375` and a spacing of `0.25`.
|
||||||
|
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
|
||||||
|
### Receiving Formspec Submissions
|
||||||
|
|
||||||
|
When `show_formspec` is called, the formspec is sent to the client to be displayed.
|
||||||
|
For formspecs to be useful, information needs to be returned from the client to server.
|
||||||
|
The method for this is called formspec field submission, and for `show_formspec`, that
|
||||||
|
submission is received using a global callback:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player, formname, fields)
|
||||||
|
if formname ~= "guessing:game" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if fields.guess then
|
||||||
|
local pname = player:get_player_name()
|
||||||
|
core.chat_send_all(pname .. " guessed " .. fields.number)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The function given in `core.register_on_player_receive_fields` is called
|
||||||
|
every time a user submits a form. Most callbacks will need to check the formname given
|
||||||
|
to the function, and exit if it is not the right form; however, some callbacks
|
||||||
|
may need to work on multiple forms, or on all forms.
|
||||||
|
|
||||||
|
The `fields` parameter to the function is a table of the values submitted by the
|
||||||
|
user, indexed by strings. Named elements will appear in the field under their own
|
||||||
|
name, depending on the event. Some elements will only be submitted if they caused
|
||||||
|
the event, such as buttons, and some elements will always appear in submissions,
|
||||||
|
such as fields.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.submit_vuln %}
|
||||||
|
|
||||||
|
So, now the formspec is sent to the client and the client sends information back.
|
||||||
|
The next step is to somehow generate and remember the target value, and to update
|
||||||
|
the formspec based on guesses. The way to do this is using a concept called
|
||||||
|
"contexts".
|
||||||
|
|
||||||
|
|
||||||
|
### Contexts
|
||||||
|
|
||||||
|
In many cases you want core.show_formspec to give information
|
||||||
|
to the callback which you don't want to send to the client. This might include
|
||||||
|
what a chat command was called with, or what the dialog is about. In this case,
|
||||||
|
the target value that needs to be remembered.
|
||||||
|
|
||||||
|
A context is a per-player table to store information, and the contexts for all
|
||||||
|
online players are stored in a file-local variable:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local _contexts = {}
|
||||||
|
local function get_context(name)
|
||||||
|
local context = _contexts[name] or {}
|
||||||
|
_contexts[name] = context
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
_contexts[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we need to modify the show code to update the context
|
||||||
|
before showing the formspec:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function guessing.show_to(name)
|
||||||
|
local context = get_context(name)
|
||||||
|
context.target = context.target or math.random(1, 10)
|
||||||
|
|
||||||
|
local fs = guessing.get_formspec(name, context)
|
||||||
|
core.show_formspec(name, "guessing:game", fs)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
We also need to modify the formspec generation code to use the context:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function guessing.get_formspec(name, context)
|
||||||
|
local text
|
||||||
|
if not context.guess then
|
||||||
|
text = "I'm thinking of a number... Make a guess!"
|
||||||
|
elseif context.guess == context.target then
|
||||||
|
text = "Hurray, you got it!"
|
||||||
|
elseif context.guess > context.target then
|
||||||
|
text = "Too high!"
|
||||||
|
else
|
||||||
|
text = "Too low!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it's good practice for `get_formspec` to only read the context, and not
|
||||||
|
update it at all. This can make the function simpler, and also easier to test.
|
||||||
|
|
||||||
|
And finally, we need to update the handler to update the context with the guess:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if fields.guess then
|
||||||
|
local name = player:get_player_name()
|
||||||
|
local context = get_context(name)
|
||||||
|
context.guess = tonumber(fields.number)
|
||||||
|
guessing.show_to(name)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Formspec Sources
|
||||||
|
|
||||||
|
There are three different ways that a formspec can be delivered to the client:
|
||||||
|
|
||||||
|
* [show_formspec](#guessing-game): the method used above, fields are received by `register_on_player_receive_fields`.
|
||||||
|
* [Node Meta Formspecs](#node-meta-formspecs): the node contains a formspec in its meta data, and the client
|
||||||
|
shows it *immediately* when the player rightclicks. Fields are received by a
|
||||||
|
method in the node definition called `on_receive_fields`.
|
||||||
|
* [Player Inventory Formspecs](#player-inventory-formspecs): the formspec is sent to the client at some point, and then
|
||||||
|
shown immediately when the player presses `i`. Fields are received by
|
||||||
|
`register_on_player_receive_fields`.
|
||||||
|
|
||||||
|
### Node Meta Formspecs
|
||||||
|
|
||||||
|
`core.show_formspec` is not the only way to show a formspec; you can also
|
||||||
|
add formspecs to a [node's metadata](../map/storage.html). For example,
|
||||||
|
this is used with chests to allow for faster opening times -
|
||||||
|
you don't need to wait for the server to send the player the chest formspec.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:rightclick", {
|
||||||
|
description = "Rightclick me!",
|
||||||
|
tiles = {"mymod_rightclick.png"},
|
||||||
|
groups = {cracky = 1},
|
||||||
|
after_place_node = function(pos, placer)
|
||||||
|
-- This function is run when the chest node is placed.
|
||||||
|
-- The following code sets the formspec for chest.
|
||||||
|
-- Meta is a way of storing data onto a node.
|
||||||
|
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("formspec",
|
||||||
|
"formspec_version[4]" ..
|
||||||
|
"size[5,5]" ..
|
||||||
|
"label[1,1;This is shown on right click]" ..
|
||||||
|
"field[1,2;2,1;x;x;]")
|
||||||
|
end,
|
||||||
|
on_receive_fields = function(pos, formname, fields, player)
|
||||||
|
if fields.quit then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
print(fields.x)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Formspecs set this way do not trigger the same callback. In order to
|
||||||
|
receive form input for meta formspecs, you must include an
|
||||||
|
`on_receive_fields` entry when registering the node.
|
||||||
|
|
||||||
|
This style of callback triggers when you press enter
|
||||||
|
in a field, which is impossible with `core.show_formspec`;
|
||||||
|
however, this kind of form can only be shown by right-clicking on a
|
||||||
|
node. It cannot be triggered programmatically.
|
||||||
|
|
||||||
|
### Player Inventory Formspecs
|
||||||
|
|
||||||
|
The player inventory formspec is the one shown when the player presses i.
|
||||||
|
The global callback is used to receive events from this formspec, and the
|
||||||
|
formname is `""`.
|
||||||
|
|
||||||
|
There are a number of different mods which allow multiple mods to customise the
|
||||||
|
player inventory. Minetest Game uses
|
||||||
|
[SFINV](https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md).
|
||||||
|
|
||||||
|
|
||||||
|
### Your Turn
|
||||||
|
|
||||||
|
* Extend the Guessing Game to keep track of each player's top score, where the
|
||||||
|
top score is how many guesses it took.
|
||||||
|
* Make a node called "Inbox" where users can open up a formspec and leave messages.
|
||||||
|
This node should store the placers' name as `owner` in the meta, and should use
|
||||||
|
`show_formspec` to show different formspecs to different players.
|
294
_en/players/hud.md
Normal file
294
_en/players/hud.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
---
|
||||||
|
title: HUD
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.6
|
||||||
|
description: Learn how to display HUD elements
|
||||||
|
redirect_from: /en/chapters/hud.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Heads Up Display (HUD) elements allow you to show text, images, and other graphical elements.
|
||||||
|
|
||||||
|
The HUD doesn't accept user input; for that, you should use a [formspec](formspecs.html).
|
||||||
|
|
||||||
|
- [Positioning](#positioning)
|
||||||
|
- [Position and Offset](#position-and-offset)
|
||||||
|
- [Alignment](#alignment)
|
||||||
|
- [Scoreboard](#scoreboard)
|
||||||
|
- [Text Elements](#text-elements)
|
||||||
|
- [Parameters](#parameters)
|
||||||
|
- [Our Example](#our-example)
|
||||||
|
- [Image Elements](#image-elements)
|
||||||
|
- [Parameters](#parameters-1)
|
||||||
|
- [Scale](#scale)
|
||||||
|
- [Changing an Element](#changing-an-element)
|
||||||
|
- [Storing IDs](#storing-ids)
|
||||||
|
- [Other Elements](#other-elements)
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
|
||||||
|
### Position and Offset
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img
|
||||||
|
width="300"
|
||||||
|
src="{{ page.root }}//static/hud_diagram_center.svg"
|
||||||
|
alt="Diagram showing a centered text element">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Screens come in a variety of different physical sizes and resolutions, and
|
||||||
|
the HUD needs to work well on all screen types.
|
||||||
|
|
||||||
|
Minetest's solution to this is to specify the location of an element using both
|
||||||
|
a percentage position and an offset.
|
||||||
|
The percentage position is relative to the screen size, so to place an element
|
||||||
|
in the centre of the screen, you would need to provide a percentage position of half
|
||||||
|
the screen, e.g. (50%, 50%), and an offset of (0, 0).
|
||||||
|
|
||||||
|
The offset is then used to move an element relative to the percentage position.
|
||||||
|
|
||||||
|
<div style="clear:both;"></div>
|
||||||
|
|
||||||
|
### Alignment
|
||||||
|
|
||||||
|
Alignment is where the result of position and offset is on the element -
|
||||||
|
for example, `{x = -1.0, y = 0.0}` will make the result of position and offset point
|
||||||
|
to the left of the element's bounds. This is particularly useful when you want to
|
||||||
|
make a text element aligned to the left, centre, or right.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
width="500"
|
||||||
|
src="{{ page.root }}//static/hud_diagram_alignment.svg"
|
||||||
|
alt="Diagram showing alignment">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
The above diagram shows 3 windows (blue), each with a single HUD element (yellow)
|
||||||
|
and a different alignment each time. The arrow is the result of the position
|
||||||
|
and offset calculation.
|
||||||
|
|
||||||
|
### Scoreboard
|
||||||
|
|
||||||
|
For the purposes of this chapter, you will learn how to position and update a
|
||||||
|
score panel like so:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_final.png"
|
||||||
|
alt="screenshot of the HUD we're aiming for">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
In the above screenshot, all the elements have the same percentage position
|
||||||
|
(100%, 50%) - but different offsets. This allows the whole thing to be anchored
|
||||||
|
to the right of the window, but to resize without breaking.
|
||||||
|
|
||||||
|
## Text Elements
|
||||||
|
|
||||||
|
You can create a HUD element once you have a copy of the player object:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local player = core.get_player_by_name("username")
|
||||||
|
local idx = player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 0.5, y = 0.5},
|
||||||
|
offset = {x = 0, y = 0},
|
||||||
|
text = "Hello world!",
|
||||||
|
alignment = {x = 0, y = 0}, -- center aligned
|
||||||
|
scale = {x = 100, y = 100}, -- covered later
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `hud_add` function returns an element ID - this can be used later to modify
|
||||||
|
or remove a HUD element.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
The element's type is given using the `hud_elem_type` property in the definition
|
||||||
|
table. The meaning of other properties varies based on this type.
|
||||||
|
|
||||||
|
`scale` is the maximum bounds of text; text outside these bounds is cropped, e.g.: `{x=100, y=100}`.
|
||||||
|
|
||||||
|
`number` is the text's colour, and is in [hexadecimal form](http://www.colorpicker.com/), e.g.: `0xFF0000`.
|
||||||
|
|
||||||
|
|
||||||
|
### Our Example
|
||||||
|
|
||||||
|
Let's go ahead and place all the text in our score panel:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Get the dig and place count from storage, or default to 0
|
||||||
|
local meta = player:get_meta()
|
||||||
|
local digs_text = "Digs: " .. meta:get_int("score:digs")
|
||||||
|
local places_text = "Places: " .. meta:get_int("score:places")
|
||||||
|
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -120, y = -25},
|
||||||
|
text = "Stats",
|
||||||
|
alignment = 0,
|
||||||
|
scale = { x = 100, y = 30},
|
||||||
|
number = 0xFFFFFF,
|
||||||
|
})
|
||||||
|
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -180, y = 0},
|
||||||
|
text = digs_text,
|
||||||
|
alignment = -1,
|
||||||
|
scale = { x = 50, y = 10},
|
||||||
|
number = 0xFFFFFF,
|
||||||
|
})
|
||||||
|
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -70, y = 0},
|
||||||
|
text = places_text,
|
||||||
|
alignment = -1,
|
||||||
|
scale = { x = 50, y = 10},
|
||||||
|
number = 0xFFFFFF,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This results in the following:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_text.png"
|
||||||
|
alt="screenshot of the HUD we're aiming for">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
|
||||||
|
## Image Elements
|
||||||
|
|
||||||
|
Image elements are created in a very similar way to text elements:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "image",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -220, y = 0},
|
||||||
|
text = "score_background.png",
|
||||||
|
scale = { x = 1, y = 1},
|
||||||
|
alignment = { x = 1, y = 0 },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You will now have this:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_background_img.png"
|
||||||
|
alt="screenshot of the HUD so far">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
The `text` field is used to provide the image name.
|
||||||
|
|
||||||
|
If a co-ordinate is positive, then it is a scale factor with 1 being the
|
||||||
|
original image size, 2 being double the size, and so on.
|
||||||
|
However, if a co-ordinate is negative, it is a percentage of the screen size.
|
||||||
|
For example, `x=-100` is 100% of the width.
|
||||||
|
|
||||||
|
### Scale
|
||||||
|
|
||||||
|
Let's make the progress bar for our score panel as an example of scale:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local percent = tonumber(meta:get("score:score") or 0.2)
|
||||||
|
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "image",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -215, y = 23},
|
||||||
|
text = "score_bar_empty.png",
|
||||||
|
scale = { x = 1, y = 1},
|
||||||
|
alignment = { x = 1, y = 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "image",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -215, y = 23},
|
||||||
|
text = "score_bar_full.png",
|
||||||
|
scale = { x = percent, y = 1},
|
||||||
|
alignment = { x = 1, y = 0 },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
We now have a HUD that looks like the one in the first post!
|
||||||
|
There is one problem however, it won't update when the stats change.
|
||||||
|
|
||||||
|
## Changing an Element
|
||||||
|
|
||||||
|
You can use the ID returned by the `hud_add` method to update it or remove it later.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
text = "Hello world!",
|
||||||
|
-- parameters removed for brevity
|
||||||
|
})
|
||||||
|
|
||||||
|
player:hud_change(idx, "text", "New Text")
|
||||||
|
player:hud_remove(idx)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `hud_change` method takes the element ID, the property to change, and the new
|
||||||
|
value. The above call changes the `text` property from "Hello World" to "New text".
|
||||||
|
|
||||||
|
This means that doing the `hud_change` immediately after the `hud_add` is
|
||||||
|
functionally equivalent to the following, in a rather inefficient way:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
text = "New Text",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storing IDs
|
||||||
|
|
||||||
|
```lua
|
||||||
|
score = {}
|
||||||
|
local saved_huds = {}
|
||||||
|
|
||||||
|
function score.update_hud(player)
|
||||||
|
local player_name = player:get_player_name()
|
||||||
|
|
||||||
|
-- Get the dig and place count from storage, or default to 0
|
||||||
|
local meta = player:get_meta()
|
||||||
|
local digs_text = "Digs: " .. meta:get_int("score:digs")
|
||||||
|
local places_text = "Places: " .. meta:get_int("score:places")
|
||||||
|
local percent = tonumber(meta:get("score:score") or 0.2)
|
||||||
|
|
||||||
|
local ids = saved_huds[player_name]
|
||||||
|
if ids then
|
||||||
|
player:hud_change(ids["places"], "text", places_text)
|
||||||
|
player:hud_change(ids["digs"], "text", digs_text)
|
||||||
|
player:hud_change(ids["bar_foreground"],
|
||||||
|
"scale", { x = percent, y = 1 })
|
||||||
|
else
|
||||||
|
ids = {}
|
||||||
|
saved_huds[player_name] = ids
|
||||||
|
|
||||||
|
-- create HUD elements and set ids into `ids`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_on_joinplayer(score.update_hud)
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
saved_huds[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Other Elements
|
||||||
|
|
||||||
|
Read [lua_api.md](https://minetest.gitlab.io/minetest/hud/) for a complete list of HUD elements.
|
77
_en/players/player_physics.md
Normal file
77
_en/players/player_physics.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: Player Physics
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.4
|
||||||
|
description: Learn how to make a player run faster, jump higher or simply float
|
||||||
|
redirect_from: /en/chapters/player_physics.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Player physics can be modified using physics overrides.
|
||||||
|
Physics overrides can set the walking speed, jump speed,
|
||||||
|
and gravity constants.
|
||||||
|
Physics overrides are set on a player-by-player basis
|
||||||
|
and are multipliers.
|
||||||
|
For example, a value of 2 for gravity would make gravity twice as strong.
|
||||||
|
|
||||||
|
- [Basic Example](#basic-example)
|
||||||
|
- [Available Overrides](#available-overrides)
|
||||||
|
- [Old Movement Behaviour](#old-movement-behaviour)
|
||||||
|
- [Mod Incompatibility](#mod-incompatibility)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
Here is an example of how to add an antigravity command, which
|
||||||
|
puts the caller in low G:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_chatcommand("antigravity", {
|
||||||
|
func = function(name, param)
|
||||||
|
local player = core.get_player_by_name(name)
|
||||||
|
player:set_physics_override({
|
||||||
|
gravity = 0.1, -- set gravity to 10% of its original value
|
||||||
|
-- (0.1 * 9.81)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Overrides
|
||||||
|
|
||||||
|
`player:set_physics_override()` is given a table of overrides.\\
|
||||||
|
According to [lua_api.md](https://minetest.gitlab.io/minetest/class-reference/#player-only-no-op-for-other-objects),
|
||||||
|
these can be:
|
||||||
|
|
||||||
|
* speed: multiplier to default walking speed value (default: 1)
|
||||||
|
* jump: multiplier to default jump value (default: 1)
|
||||||
|
* gravity: multiplier to default gravity value (default: 1)
|
||||||
|
* sneak: whether the player can sneak (default: true)
|
||||||
|
|
||||||
|
### Old Movement Behaviour
|
||||||
|
|
||||||
|
Player movement prior to the 0.4.16 release included the sneak glitch, which
|
||||||
|
allows various movement glitches, including the ability
|
||||||
|
to climb an 'elevator' made from a certain placement of nodes by sneaking
|
||||||
|
(pressing shift) and pressing space to ascend. Though the behaviour was
|
||||||
|
unintended, it has been preserved in overrides due to its use on many servers.
|
||||||
|
|
||||||
|
Two overrides are needed to fully restore old movement behaviour:
|
||||||
|
|
||||||
|
* new_move: whether the player uses new movement (default: true)
|
||||||
|
* sneak_glitch: whether the player can use 'sneak elevators' (default: false)
|
||||||
|
|
||||||
|
## Mod Incompatibility
|
||||||
|
|
||||||
|
Please be warned that mods which override the same physics value of a player tend
|
||||||
|
to be incompatible with each other. When setting an override, it overwrites
|
||||||
|
any overrides that have been set before. This means that if multiple overrides set a
|
||||||
|
player's speed, only the last one to run will be in effect.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* **Sonic**: Set the speed multiplier to a high value (at least 6) when a player joins the game.
|
||||||
|
* **Super bounce**: Increase the jump value so that the player can jump 20 metres (1 metre is 1 node).
|
||||||
|
* **Space**: Make gravity decrease as the player gets higher.
|
138
_en/players/privileges.md
Normal file
138
_en/players/privileges.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
title: Privileges
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.1
|
||||||
|
description: Registering privs.
|
||||||
|
redirect_from: /en/chapters/privileges.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Privileges, often called privs for short, give players the ability to perform
|
||||||
|
certain actions. Server owners can grant and revoke privileges to control
|
||||||
|
which abilities each player has.
|
||||||
|
|
||||||
|
- [When to use Privileges](#when-to-use-privileges)
|
||||||
|
- [Declaring Privileges](#declaring-privileges)
|
||||||
|
- [Checking for Privileges](#checking-for-privileges)
|
||||||
|
- [Getting and Setting Privileges](#getting-and-setting-privileges)
|
||||||
|
- [Adding Privileges to basic_privs](#adding-privileges-to-basicprivs)
|
||||||
|
|
||||||
|
## When to use Privileges
|
||||||
|
|
||||||
|
A privilege should give a player the ability to do something.
|
||||||
|
Privileges are **not** for indicating class or status.
|
||||||
|
|
||||||
|
**Good Privileges:**
|
||||||
|
|
||||||
|
* interact
|
||||||
|
* shout
|
||||||
|
* noclip
|
||||||
|
* fly
|
||||||
|
* kick
|
||||||
|
* ban
|
||||||
|
* vote
|
||||||
|
* worldedit
|
||||||
|
* area_admin - admin functions of one mod is ok
|
||||||
|
|
||||||
|
**Bad Privileges:**
|
||||||
|
|
||||||
|
* moderator
|
||||||
|
* admin
|
||||||
|
* elf
|
||||||
|
* dwarf
|
||||||
|
|
||||||
|
## Declaring Privileges
|
||||||
|
|
||||||
|
Use `register_privilege` to declare a new privilege:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_privilege("vote", {
|
||||||
|
description = "Can vote on issues",
|
||||||
|
give_to_singleplayer = true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`give_to_singleplayer` defaults to true when not specified, so it isn't
|
||||||
|
actually needed in the above definition.
|
||||||
|
|
||||||
|
## Checking for Privileges
|
||||||
|
|
||||||
|
To quickly check whether a player has all the required privileges:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local has, missing = core.check_player_privs(player_or_name, {
|
||||||
|
interact = true,
|
||||||
|
vote = true })
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, `has` is true if the player has all the privileges needed.
|
||||||
|
If `has` is false, then `missing` will contain a key-value table
|
||||||
|
of the missing privileges.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local has, missing = core.check_player_privs(name, {
|
||||||
|
interact = true,
|
||||||
|
vote = true })
|
||||||
|
|
||||||
|
if has then
|
||||||
|
print("Player has all privs!")
|
||||||
|
else
|
||||||
|
print("Player is missing privs: " .. dump(missing))
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't need to check the missing privileges, you can put
|
||||||
|
`check_player_privs` directly into the if statement.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if not core.check_player_privs(name, { interact=true }) then
|
||||||
|
return false, "You need interact for this!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting and Setting Privileges
|
||||||
|
|
||||||
|
Player privileges can be accessed or modified regardless of the player
|
||||||
|
being online.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local privs = core.get_player_privs(name)
|
||||||
|
print(dump(privs))
|
||||||
|
|
||||||
|
privs.vote = true
|
||||||
|
core.set_player_privs(name, privs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Privileges are always specified as a key-value table with the key being
|
||||||
|
the privilege name and the value being a boolean.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
fly = true,
|
||||||
|
interact = true,
|
||||||
|
shout = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Privileges to basic_privs
|
||||||
|
|
||||||
|
Players with the `basic_privs` privilege are able to grant and revoke a limited
|
||||||
|
set of privileges. It's common to give this privilege to moderators so that
|
||||||
|
they can grant and revoke `interact` and `shout`, but can't grant themselves or other
|
||||||
|
players privileges with greater potential for abuse such as `give` and `server`.
|
||||||
|
|
||||||
|
To add a privilege to `basic_privs`, and adjust which privileges your moderators can
|
||||||
|
grant and revoke from other players, you must change the `basic_privs` setting.
|
||||||
|
|
||||||
|
By default, `basic_privs` has the following value:
|
||||||
|
|
||||||
|
basic_privs = interact, shout
|
||||||
|
|
||||||
|
To add `vote`, update this to:
|
||||||
|
|
||||||
|
basic_privs = interact, shout, vote
|
||||||
|
|
||||||
|
This will allow players with `basic_privs` to grant and revoke the `vote` privilege.
|
5
_en/players/sfinv.md
Normal file
5
_en/players/sfinv.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
sitemap: false
|
||||||
|
redirect_from: /en/chapters/sfinv.html
|
||||||
|
redirect_to: "https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md"
|
||||||
|
---
|
253
_en/quality/clean_arch.md
Normal file
253
_en/quality/clean_arch.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
title: Intro to Clean Architectures
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.4
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Once your mod reaches a respectable size, you'll find it harder and harder to
|
||||||
|
keep the code clean and free of bugs. This is an especially big problem when using
|
||||||
|
a dynamically typed language like Lua, given that the compiler gives you very little
|
||||||
|
compiler-time help when it comes to things like making sure that types are used correctly.
|
||||||
|
|
||||||
|
This chapter covers important concepts needed to keep your code clean,
|
||||||
|
and common design patterns to achieve that. Please note that this chapter isn't
|
||||||
|
meant to be prescriptive, but to instead give you an idea of the possibilities.
|
||||||
|
There is no one good way of designing a mod, and good mod design is very subjective.
|
||||||
|
|
||||||
|
- [Cohesion, Coupling, and Separation of Concerns](#cohesion-coupling-and-separation-of-concerns)
|
||||||
|
- [Observer](#observer)
|
||||||
|
- [Model-View-Controller](#model-view-controller)
|
||||||
|
- [API-View](#api-view)
|
||||||
|
- [Conclusion](#conclusion)
|
||||||
|
|
||||||
|
|
||||||
|
## Cohesion, Coupling, and Separation of Concerns
|
||||||
|
|
||||||
|
Without any planning, a programming project will tend to gradually descend into
|
||||||
|
spaghetti code. Spaghetti code is characterised by a lack of structure - all the
|
||||||
|
code is thrown in together with no clear boundaries. This ultimately makes a
|
||||||
|
project completely unmaintainable, ending in its abandonment.
|
||||||
|
|
||||||
|
The opposite of this is to design your project as a collection of interacting
|
||||||
|
smaller programs or areas of code. <!-- Weird wording? -->
|
||||||
|
|
||||||
|
> Inside every large program, there is a small program trying to get out.
|
||||||
|
>
|
||||||
|
> --C.A.R. Hoare
|
||||||
|
|
||||||
|
This should be done in such a way that you achieve Separation of Concerns -
|
||||||
|
each area should be distinct and address a separate need or concern.
|
||||||
|
|
||||||
|
These programs/areas should have the following two properties:
|
||||||
|
|
||||||
|
* **High Cohesion** - the area should be closely/tightly related.
|
||||||
|
* **Low Coupling** - keep dependencies between areas as low as possible, and avoid
|
||||||
|
relying on internal implementations. It's a very good idea to make sure you have
|
||||||
|
a low amount of coupling, as this means that changing the APIs of certain areas
|
||||||
|
will be more feasible.
|
||||||
|
|
||||||
|
Note that these apply both when thinking about the relationship between mods,
|
||||||
|
and the relationship between areas inside a mod.
|
||||||
|
|
||||||
|
|
||||||
|
## Observer
|
||||||
|
|
||||||
|
A simple way to separate different areas of code is to use the Observer pattern.
|
||||||
|
|
||||||
|
Let's take the example of unlocking an achievement when a player first kills a
|
||||||
|
rare animal. The naïve approach would be to have achievement code in the mob
|
||||||
|
kill function, checking the mob name and unlocking the award if it matches.
|
||||||
|
This is a bad idea, however, as it makes the mobs mod coupled to the achievements
|
||||||
|
code. If you kept on doing this - for example, adding XP to the mob death code -
|
||||||
|
you could end up with a lot of messy dependencies.
|
||||||
|
|
||||||
|
Enter the Observer pattern. Instead of the mymobs mod caring about awards,
|
||||||
|
the mymobs mod exposes a way for other areas of code to register their
|
||||||
|
interest in an event and receive data about the event.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymobs.registered_on_death = {}
|
||||||
|
function mymobs.register_on_death(func)
|
||||||
|
table.insert(mymobs.registered_on_death, func)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- in mob death code
|
||||||
|
for i=1, #mymobs.registered_on_death do
|
||||||
|
mymobs.registered_on_death[i](entity, reason)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the other code registers its interest:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymobs.register_on_death(function(mob, reason)
|
||||||
|
if reason.type == "punch" and reason.object and
|
||||||
|
reason.object:is_player() then
|
||||||
|
awards.notify_mob_kill(reason.object, mob.name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
You may be thinking - wait a second, this looks awfully familiar. And you're right!
|
||||||
|
The Minetest API is heavily Observer-based to stop the engine having to care about
|
||||||
|
what is listening to something.
|
||||||
|
|
||||||
|
|
||||||
|
## Model-View-Controller
|
||||||
|
|
||||||
|
In the next chapter, we will discuss how to automatically test your
|
||||||
|
code and one of the problems we will have is how to separate your logic
|
||||||
|
(calculations, what should be done) from API calls (`core.*`, other mods)
|
||||||
|
as much as possible.
|
||||||
|
|
||||||
|
One way to do this is to think about:
|
||||||
|
|
||||||
|
* What **data** you have.
|
||||||
|
* What **actions** you can take with this data.
|
||||||
|
* How **events** (ie: formspec, punches, etc) trigger these actions, and how
|
||||||
|
these actions cause things to happen in the engine.
|
||||||
|
|
||||||
|
Let's take an example of a land protection mod. The data you have is the areas
|
||||||
|
and any associated metadata. Actions you can take are `create`, `edit`, or
|
||||||
|
`delete`. The events that trigger these actions are chat commands and formspec
|
||||||
|
receive fields. These are 3 areas that can usually be separated pretty well.
|
||||||
|
|
||||||
|
In your tests, you will be able to make sure that an action when triggered does
|
||||||
|
the right thing to the data. You won't need to test that an event calls an
|
||||||
|
action (as this would require using the Minetest API, and this area of code
|
||||||
|
should be made as small as possible anyway.)
|
||||||
|
|
||||||
|
You should write your data representation using Pure Lua. "Pure" in this context
|
||||||
|
means that the functions could run outside of Minetest - none of the engine's
|
||||||
|
functions are called.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Data
|
||||||
|
function land.create(name, area_name)
|
||||||
|
land.lands[area_name] = {
|
||||||
|
name = area_name,
|
||||||
|
owner = name,
|
||||||
|
-- more stuff
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function land.get_by_name(area_name)
|
||||||
|
return land.lands[area_name]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Your actions should also be pure, but calling other functions is more
|
||||||
|
acceptable than in the above.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Controller
|
||||||
|
function land.handle_create_submit(name, area_name)
|
||||||
|
-- process stuff
|
||||||
|
-- (ie: check for overlaps, check quotas, check permissions)
|
||||||
|
|
||||||
|
land.create(name, area_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function land.handle_creation_request(name)
|
||||||
|
-- This is a bad example, as explained later
|
||||||
|
land.show_create_formspec(name)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Your event handlers will have to interact with the Minetest API. You should keep
|
||||||
|
the number of calculations to a minimum, as you won't be able to test this area
|
||||||
|
very easily.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- View
|
||||||
|
function land.show_create_formspec(name)
|
||||||
|
-- Note how there's no complex calculations here!
|
||||||
|
return [[
|
||||||
|
size[4,3]
|
||||||
|
label[1,0;This is an example]
|
||||||
|
field[0,1;3,1;area_name;]
|
||||||
|
button_exit[0,2;1,1;exit;Exit]
|
||||||
|
]]
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_chatcommand("/land", {
|
||||||
|
privs = { land = true },
|
||||||
|
func = function(name)
|
||||||
|
land.handle_creation_request(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
land.handle_create_submit(player:get_player_name(),
|
||||||
|
fields.area_name)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above is the Model-View-Controller pattern. The model is a collection of data
|
||||||
|
with minimal functions. The view is a collection of functions which listen to
|
||||||
|
events and pass it to the controller, and also receives calls from the controller to
|
||||||
|
do something with the Minetest API. The controller is where the decisions and
|
||||||
|
most of the calculations are made.
|
||||||
|
|
||||||
|
The controller should have no knowledge about the Minetest API - notice how
|
||||||
|
there are no Minetest calls or any view functions that resemble them.
|
||||||
|
You should *NOT* have a function like `view.hud_add(player, def)`.
|
||||||
|
Instead, the view defines some actions that the controller can tell the view to do,
|
||||||
|
like `view.add_hud(info)` where info is a value or table which doesn't relate
|
||||||
|
to the Minetest API at all.
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img
|
||||||
|
width="100%"
|
||||||
|
src="{{ page.root }}/static/mvc_diagram.svg"
|
||||||
|
alt="Diagram showing a centered text element">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
It is important that each area only communicates with its direct neighbours,
|
||||||
|
as shown above, in order to reduce how much you need to change if you modify
|
||||||
|
an area's internals or externals. For example, to change the formspec you
|
||||||
|
would only need to edit the view. To change the view API, you would only need to
|
||||||
|
change the view and the controller, but not the model at all.
|
||||||
|
|
||||||
|
In practice, this design is rarely used because of the increased complexity
|
||||||
|
and because it doesn't give many benefits for most types of mods. Instead,
|
||||||
|
you will commonly see a less formal and strict kind of design -
|
||||||
|
variants of the API-View.
|
||||||
|
|
||||||
|
|
||||||
|
### API-View
|
||||||
|
|
||||||
|
In an ideal world, you'd have the above 3 areas perfectly separated with all
|
||||||
|
events going into the controller before going back to the normal view. But
|
||||||
|
this isn't the real world. A good compromise is to reduce the mod into two
|
||||||
|
parts:
|
||||||
|
|
||||||
|
* **API** - This was the model and controller above. There should be no uses of
|
||||||
|
`core.` here.
|
||||||
|
* **View** - This was also the view above. It's a good idea to structure this into separate
|
||||||
|
files for each type of event.
|
||||||
|
|
||||||
|
rubenwardy's [crafting mod](https://github.com/rubenwardy/crafting) roughly
|
||||||
|
follows this design. `api.lua` is almost all pure Lua functions handling the data
|
||||||
|
storage and controller-style calculations. `gui.lua` is the view for formspecs
|
||||||
|
and formspec submission, and `async_crafter.lua` is the view and controller for
|
||||||
|
a node formspec and node timers.
|
||||||
|
|
||||||
|
Separating the mod like this means that you can very easily test the API part,
|
||||||
|
as it doesn't use any Minetest APIs - as shown in the
|
||||||
|
[next chapter](unit_testing.html) and seen in the crafting mod.
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Good code design is subjective, and highly depends on the project you're making. As a
|
||||||
|
general rule, try to keep cohesion high and coupling low. Phrased differently,
|
||||||
|
keep related code together and unrelated code apart, and keep dependencies simple.
|
||||||
|
|
||||||
|
I highly recommend reading the [Game Programming Patterns](http://gameprogrammingpatterns.com/)
|
||||||
|
book. It's freely available to [read online](http://gameprogrammingpatterns.com/contents.html)
|
||||||
|
and goes into much more detail on common programming patterns relevant to games.
|
140
_en/quality/common_mistakes.md
Normal file
140
_en/quality/common_mistakes.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
title: Common Mistakes
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.1
|
||||||
|
redirect_from: /en/chapters/common_mistakes.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
This chapter details common mistakes, and how to avoid them.
|
||||||
|
|
||||||
|
- [Be Careful When Storing ObjectRefs (ie: players or entities)](#be-careful-when-storing-objectrefs-ie-players-or-entities)
|
||||||
|
- [Don't Trust Formspec Submissions](#dont-trust-formspec-submissions)
|
||||||
|
- [Set ItemStacks After Changing Them](#set-itemstacks-after-changing-them)
|
||||||
|
|
||||||
|
## Be Careful When Storing ObjectRefs (ie: players or entities)
|
||||||
|
|
||||||
|
An ObjectRef is invalidated when the player or entity it represents leaves
|
||||||
|
the game. This may happen when the player goes offline, or the entity is unloaded
|
||||||
|
or removed.
|
||||||
|
|
||||||
|
The methods of ObjectRefs will always return nil when invalid, since Minetest 5.2.
|
||||||
|
Any call will essentially be ignored.
|
||||||
|
|
||||||
|
You should avoid storing ObjectRefs where possible. If you do to store an
|
||||||
|
ObjectRef, you should make sure you check it before use, like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- This only works in Minetest 5.2+
|
||||||
|
if obj:get_pos() then
|
||||||
|
-- is valid!
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Don't Trust Formspec Submissions
|
||||||
|
|
||||||
|
Malicious clients can submit formspecs whenever they like, with
|
||||||
|
whatever content they like.
|
||||||
|
|
||||||
|
For example, the following code has a vulnerability which allows players to
|
||||||
|
give themselves moderator privileges:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function show_formspec(name)
|
||||||
|
if not core.check_player_privs(name, { privs = true }) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
core.show_formspec(name, "modman:modman", [[
|
||||||
|
size[3,2]
|
||||||
|
field[0,0;3,1;target;Name;]
|
||||||
|
button_exit[0,1;3,1;sub;Promote]
|
||||||
|
]])
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
-- BAD! Missing privilege check here!
|
||||||
|
|
||||||
|
local privs = core.get_player_privs(fields.target)
|
||||||
|
privs.kick = true
|
||||||
|
privs.ban = true
|
||||||
|
core.set_player_privs(fields.target, privs)
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a privilege check to solve this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
if not core.check_player_privs(name, { privs = true }) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- code
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Set ItemStacks After Changing Them
|
||||||
|
|
||||||
|
Have you noticed that it's simply called an `ItemStack` in the API, not an `ItemStackRef`,
|
||||||
|
similar to `InvRef`? This is because an `ItemStack` isn't a reference - it's a
|
||||||
|
copy. Stacks work on a copy of the data rather than the stack in the inventory.
|
||||||
|
This means that modifying a stack won't actually modify that stack in the inventory.
|
||||||
|
|
||||||
|
For example, don't do this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
local stack = inv:get_stack("main", 1)
|
||||||
|
stack:get_meta():set_string("description", "Partially eaten")
|
||||||
|
-- BAD! Modification will be lost
|
||||||
|
```
|
||||||
|
|
||||||
|
Do this instead:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
local stack = inv:get_stack("main", 1)
|
||||||
|
stack:get_meta():set_string("description", "Partially eaten")
|
||||||
|
inv:set_stack("main", 1, stack)
|
||||||
|
-- Correct! Item stack is set
|
||||||
|
```
|
||||||
|
|
||||||
|
The behaviour of callbacks is slightly more complicated. Modifying an `ItemStack` you
|
||||||
|
are given will change it for the caller too, and any subsequent callbacks. However,
|
||||||
|
it will only be saved in the engine if the callback caller sets it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_item_eat(function(hp_change, replace_with_item,
|
||||||
|
itemstack, user, pointed_thing)
|
||||||
|
itemstack:get_meta():set_string("description", "Partially eaten")
|
||||||
|
-- Almost correct! Data will be lost if another
|
||||||
|
-- callback cancels the behaviour
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
If no callbacks cancel this, the stack will be set and the description will be updated,
|
||||||
|
but if a callback does cancel this, then the update may be lost.
|
||||||
|
|
||||||
|
It's better to do this instead:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_item_eat(function(hp_change, replace_with_item,
|
||||||
|
itemstack, user, pointed_thing)
|
||||||
|
itemstack:get_meta():set_string("description", "Partially eaten")
|
||||||
|
user:get_inventory():set_stack("main", user:get_wield_index(),
|
||||||
|
itemstack)
|
||||||
|
-- Correct, description will always be set!
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the callbacks cancel or the callback runner doesn't set the stack,
|
||||||
|
then the update will still be set.
|
||||||
|
If the callbacks or the callback runner set the stack, then the use of
|
||||||
|
set_stack doesn't matter.
|
107
_en/quality/luacheck.md
Normal file
107
_en/quality/luacheck.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Automatic Error Checking
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.2
|
||||||
|
description: Use LuaCheck to find errors
|
||||||
|
redirect_from: /en/chapters/luacheck.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to use a tool called LuaCheck to automatically
|
||||||
|
scan your mod for any mistakes. This tool can be used in combination with your
|
||||||
|
editor to provide alerts to any mistakes.
|
||||||
|
|
||||||
|
- [Installing LuaCheck](#installing-luacheck)
|
||||||
|
- [Windows](#windows)
|
||||||
|
- [Linux](#linux)
|
||||||
|
- [Running LuaCheck](#running-luacheck)
|
||||||
|
- [Configuring LuaCheck](#configuring-luacheck)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Using with editor](#using-with-editor)
|
||||||
|
|
||||||
|
## Installing LuaCheck
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Simply download luacheck.exe from
|
||||||
|
[the Github Releases page](https://github.com/mpeterv/luacheck/releases).
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
First, you'll need to install LuaRocks:
|
||||||
|
|
||||||
|
sudo apt install luarocks
|
||||||
|
|
||||||
|
You can then install LuaCheck globally:
|
||||||
|
|
||||||
|
sudo luarocks install luacheck
|
||||||
|
|
||||||
|
Check that it's installed with the following command:
|
||||||
|
|
||||||
|
luacheck -v
|
||||||
|
|
||||||
|
## Running LuaCheck
|
||||||
|
|
||||||
|
The first time you run LuaCheck, it will probably pick up a lot of false
|
||||||
|
errors. This is because it still needs to be configured.
|
||||||
|
|
||||||
|
On Windows, open powershell or bash in the root folder of your project
|
||||||
|
and run `path\to\luacheck.exe .`
|
||||||
|
|
||||||
|
On Linux, run `luacheck .` whilst in the root folder of your project.
|
||||||
|
|
||||||
|
## Configuring LuaCheck
|
||||||
|
|
||||||
|
Create a file called .luacheckrc in the root of your project. This could be the
|
||||||
|
root of your game, modpack, or mod.
|
||||||
|
|
||||||
|
Put the following contents in it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
unused_args = false
|
||||||
|
allow_defined_top = true
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
"minetest",
|
||||||
|
}
|
||||||
|
|
||||||
|
read_globals = {
|
||||||
|
string = {fields = {"split"}},
|
||||||
|
table = {fields = {"copy", "getn"}},
|
||||||
|
|
||||||
|
-- Builtin
|
||||||
|
"vector", "ItemStack",
|
||||||
|
"dump", "DIR_DELIM", "VoxelArea", "Settings",
|
||||||
|
|
||||||
|
-- MTG
|
||||||
|
"default", "sfinv", "creative",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you'll need to test that it works by running LuaCheck. You should get a lot
|
||||||
|
fewer errors this time. Starting at the first error you get, modify the code to
|
||||||
|
remove the issue, or modify the configuration if the code is correct. See the list
|
||||||
|
below.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
* **accessing undefined variable foobar** - If `foobar` is meant to be a global,
|
||||||
|
add it to `read_globals`. Otherwise, add any missing `local`s to the mod.
|
||||||
|
* **setting non-standard global variable foobar** - If `foobar` is meant to be a global,
|
||||||
|
add it to `globals`. Remove from `read_globals` if present.
|
||||||
|
Otherwise, add any missing `local`s to the mod.
|
||||||
|
* **mutating read-only global variable 'foobar'** - Move `foobar` from `read_globals` to
|
||||||
|
`globals`, or stop writing to foobar.
|
||||||
|
|
||||||
|
## Using with editor
|
||||||
|
|
||||||
|
It is highly recommended that you find and install a plugin for your editor of choice
|
||||||
|
to show you errors without running a command. Most editors will likely have a plugin
|
||||||
|
available.
|
||||||
|
|
||||||
|
* **VSCode** - Ctrl+P, then paste: `ext install dwenegar.vscode-luacheck`
|
||||||
|
* **Sublime** - Install using package-control:
|
||||||
|
[SublimeLinter](https://github.com/SublimeLinter/SublimeLinter),
|
||||||
|
[SublimeLinter-luacheck](https://github.com/SublimeLinter/SublimeLinter-luacheck).
|
26
_en/quality/readmore.md
Normal file
26
_en/quality/readmore.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Read More
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.7
|
||||||
|
redirect_from: /en/chapters/readmore.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## List of Resources
|
||||||
|
|
||||||
|
After you've read this book, take a look at the following.
|
||||||
|
|
||||||
|
### Minetest Modding
|
||||||
|
|
||||||
|
* Minetest's Lua API Reference - [multiple page version](https://minetest.gitlab.io/minetest/class-reference/#player-only-no-op-for-other-objects) |
|
||||||
|
[single page version](https://github.com/minetest/minetest/blob/master/doc/lua_api.md).
|
||||||
|
* Look at [existing mods](https://forum.minetest.net/viewforum.php?f=11).
|
||||||
|
|
||||||
|
### Lua Programming
|
||||||
|
|
||||||
|
* [Programming in Lua (PIL)](http://www.lua.org/pil/).
|
||||||
|
|
||||||
|
### 3D Modelling
|
||||||
|
|
||||||
|
* [Blender 3D: Noob to pro](https://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro).
|
||||||
|
* [Using Blender with Minetest](http://wiki.minetest.net/Using_Blender).
|
166
_en/quality/releasing.md
Normal file
166
_en/quality/releasing.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
title: Releasing a Mod
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.6
|
||||||
|
redirect_from: /en/chapters/releasing.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Releasing, or publishing, a mod allows other people to make use of it. Once a mod has been
|
||||||
|
released it might be used in singleplayer games or on servers, including public servers.
|
||||||
|
|
||||||
|
- [Choosing a License](#choosing-a-license)
|
||||||
|
- [LGPL and CC-BY-SA](#lgpl-and-cc-by-sa)
|
||||||
|
- [CC0](#cc0)
|
||||||
|
- [MIT](#mit)
|
||||||
|
- [Packaging](#packaging)
|
||||||
|
- [README.txt](#readmetxt)
|
||||||
|
- [mod.conf / game.conf](#modconf--gameconf)
|
||||||
|
- [screenshot.png](#screenshotpng)
|
||||||
|
- [Uploading](#uploading)
|
||||||
|
- [Version Control Systems](#version-control-systems)
|
||||||
|
- [Releasing on ContentDB](#releasing-on-contentdb)
|
||||||
|
- [Forum Topic](#forum-topic)
|
||||||
|
|
||||||
|
## Choosing a License
|
||||||
|
|
||||||
|
You need to specify a license for your mod. This is important because it tells other
|
||||||
|
people the ways in which they are allowed to use your work. If your mod doesn't have
|
||||||
|
a license, people won't know whether they are allowed to modify, distribute or use your
|
||||||
|
mod on a public server.
|
||||||
|
|
||||||
|
Your code and your art need different things from the licenses they use. For example,
|
||||||
|
Creative Commons licenses shouldn't be used with source code,
|
||||||
|
but can be suitable choices for artistic works such as images, text and meshes.
|
||||||
|
|
||||||
|
You are allowed any license; however, mods which disallow derivatives are banned from the
|
||||||
|
official Minetest forum. (For a mod to be allowed on the forum, other developers must be
|
||||||
|
able to modify it and release the modified version.)
|
||||||
|
|
||||||
|
Please note that **public domain is not a valid licence**, because the definition varies
|
||||||
|
in different countries.
|
||||||
|
|
||||||
|
It is important to note that WTFPL is
|
||||||
|
[strongly discouraged](https://content.minetest.net/help/wtfpl/) and people may
|
||||||
|
choose not to use your mod if it has this license.
|
||||||
|
|
||||||
|
### LGPL and CC-BY-SA
|
||||||
|
|
||||||
|
This is a common license combination in the Minetest community, and is what
|
||||||
|
Minetest and Minetest Game use.
|
||||||
|
|
||||||
|
You license your code under LGPL 2.1 and your art under CC-BY-SA.
|
||||||
|
|
||||||
|
This means that:
|
||||||
|
|
||||||
|
* Anyone can modify, redistribute and sell modified or unmodified versions.
|
||||||
|
* If someone modifies your mod, they must give their version the same license.
|
||||||
|
* Your copyright notice must be kept.
|
||||||
|
|
||||||
|
### CC0
|
||||||
|
|
||||||
|
This license can be used for both code and art, and allows anyone to do what
|
||||||
|
they want with your work. This means they can modify, redistribute, sell, or
|
||||||
|
leave-out attribution.
|
||||||
|
|
||||||
|
### MIT
|
||||||
|
|
||||||
|
This is a common license for code. The only restriction it places on users
|
||||||
|
of your code is that they must include the same copyright notice and license
|
||||||
|
in any copies of the code or of substantial parts of the code.
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
There are some files that are recommended to include in your mod or game
|
||||||
|
before you release it.
|
||||||
|
|
||||||
|
### README.txt
|
||||||
|
|
||||||
|
The README file should state:
|
||||||
|
|
||||||
|
* What the mod/game does, how to use it.
|
||||||
|
* What the license is.
|
||||||
|
* Optionally:
|
||||||
|
* where to report problems or get help.
|
||||||
|
* credits
|
||||||
|
|
||||||
|
### mod.conf / game.conf
|
||||||
|
|
||||||
|
Make sure you add a description key to explain what your mod or game does. Be
|
||||||
|
concise without being vague. It should be short because it will be displayed in
|
||||||
|
the content installer which has limited space.
|
||||||
|
|
||||||
|
Good example:
|
||||||
|
|
||||||
|
description = Adds soup, cakes, bakes and juices.
|
||||||
|
|
||||||
|
Avoid this:
|
||||||
|
|
||||||
|
description = The food mod for Minetest. (<-- BAD! It's vague)
|
||||||
|
|
||||||
|
### screenshot.png
|
||||||
|
|
||||||
|
Screenshots should be 3:2 (3 pixels of width for every 2 pixels of height)
|
||||||
|
and have a minimum size of 300 x 200px.
|
||||||
|
|
||||||
|
The screenshot is displayed inside of Minetest as a thumbnail for the content.
|
||||||
|
|
||||||
|
## Uploading
|
||||||
|
|
||||||
|
So that a potential user can download your mod, you need to upload it somewhere
|
||||||
|
publicly accessible. There are several ways to do this, but you should use the
|
||||||
|
approach that works best for you, as long as it meets these requirements, and any
|
||||||
|
others which may be added by forum moderators:
|
||||||
|
|
||||||
|
* **Stable** - The hosting website should be unlikely to shut down without warning.
|
||||||
|
* **Direct link** - You should be able to click a link and download the file
|
||||||
|
without having to view another page.
|
||||||
|
* **Virus Free** - Scammy upload hosts may contain insecure adverts.
|
||||||
|
|
||||||
|
ContentDB allows you to upload zip files, and meets these criteria.
|
||||||
|
|
||||||
|
### Version Control Systems
|
||||||
|
|
||||||
|
A Version Control System (VCS) is software that manages changes to software,
|
||||||
|
often making it easier to distribute and receive contributed changes.
|
||||||
|
|
||||||
|
The majority of Minetest modders use Git and a website like GitHub to distribute
|
||||||
|
their code.
|
||||||
|
|
||||||
|
Using git can be difficult at first. If you need help with this please see:
|
||||||
|
|
||||||
|
* [Pro Git book](http://git-scm.com/book/en/v1/Getting-Started) - Free to read online.
|
||||||
|
|
||||||
|
## Releasing on ContentDB
|
||||||
|
|
||||||
|
ContentDB is the official place to find and distribute content such as mods,
|
||||||
|
games, and texture packs. Users can find content using the website, or download
|
||||||
|
and install using the integration built into the Minetest main menu.
|
||||||
|
|
||||||
|
Sign up to [ContentDB](https://content.minetest.net) and add your content.
|
||||||
|
Make sure to read the guidance given in the Help section.
|
||||||
|
|
||||||
|
## Forum Topic
|
||||||
|
|
||||||
|
You can also create a forum topic to let users discuss your creation.
|
||||||
|
|
||||||
|
Mod topics should be created in ["WIP Mods"](https://forum.minetest.net/viewforum.php?f=9) (Work In Progress)
|
||||||
|
forum, and Game topics in the ["WIP Games"](https://forum.minetest.net/viewforum.php?f=50) forum.
|
||||||
|
When you no longer consider your mod a work in progress, you can
|
||||||
|
[request that it be moved](https://forum.minetest.net/viewtopic.php?f=11&t=10418)
|
||||||
|
to "Mod Releases."
|
||||||
|
|
||||||
|
The forum topic should contain similar content to the README, but should
|
||||||
|
be more promotional and also include a link to download the mod.
|
||||||
|
It's a good idea to include screenshots of your mod in action, if possible.
|
||||||
|
|
||||||
|
The subject of topic must be in one of these formats:
|
||||||
|
|
||||||
|
* [Mod] Mod Title [modname]
|
||||||
|
* [Mod] Mod Title [version number] [modname]
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
* [Mod] More Blox [0.1] [moreblox]
|
110
_en/quality/security.md
Normal file
110
_en/quality/security.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
title: Security
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.3
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Security is very important in making sure that your mod doesn't cause the server
|
||||||
|
owner to lose data or control.
|
||||||
|
|
||||||
|
- [Core Concepts](#core-concepts)
|
||||||
|
- [Formspecs](#formspecs)
|
||||||
|
- [Never Trust Submissions](#never-trust-submissions)
|
||||||
|
- [Time of Check isn't Time of Use](#time-of-check-isnt-time-of-use)
|
||||||
|
- [(Insecure) Environments](#insecure-environments)
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
The most important concept in security is to **never trust the user**.
|
||||||
|
Anything the user submits should be treated as malicious.
|
||||||
|
This means that you should always check that the information they
|
||||||
|
enter is valid, that the user has the correct permissions,
|
||||||
|
and that they are otherwise allowed to do that action
|
||||||
|
(ie: in range or an owner).
|
||||||
|
|
||||||
|
A malicious action isn't necessarily the modification or destruction of data,
|
||||||
|
but can be accessing sensitive data, such as password hashes or
|
||||||
|
private messages.
|
||||||
|
This is especially bad if the server stores information such as emails or ages,
|
||||||
|
which some may do for verification purposes.
|
||||||
|
|
||||||
|
## Formspecs
|
||||||
|
|
||||||
|
### Never Trust Submissions
|
||||||
|
|
||||||
|
Any users can submit almost any formspec with any values at any time.
|
||||||
|
|
||||||
|
Here's some real code found in a mod:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
for key, field in pairs(fields) do
|
||||||
|
local x,y,z = string.match(key,
|
||||||
|
"goto_([%d-]+)_([%d-]+)_([%d-]+)")
|
||||||
|
if x and y and z then
|
||||||
|
player:set_pos({ x=tonumber(x), y=tonumber(y),
|
||||||
|
z=tonumber(z) })
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Can you spot the problem? A malicious user could submit a formspec containing
|
||||||
|
their own position values, allowing them to teleport to anywhere they wish to.
|
||||||
|
This could even be automated using client modifications to essentially replicate
|
||||||
|
the `/teleport` command with no need for a privilege.
|
||||||
|
|
||||||
|
The solution for this kind of issue is to use a
|
||||||
|
[Context](../players/formspecs.html#contexts), as shown previously in
|
||||||
|
the Formspecs chapter.
|
||||||
|
|
||||||
|
### Time of Check isn't Time of Use
|
||||||
|
|
||||||
|
Any users can submit any formspec with any values at any time, except where the
|
||||||
|
engine forbids it:
|
||||||
|
|
||||||
|
* A node formspec submission will be blocked if the user is too far away.
|
||||||
|
* From 5.0 onward, named formspecs will be blocked if they haven't been shown yet.
|
||||||
|
|
||||||
|
This means that you should check in the handler that the user meets the
|
||||||
|
conditions for showing the formspec in the first place, as well as any
|
||||||
|
corresponding actions.
|
||||||
|
|
||||||
|
The vulnerability caused by checking for permissions in the show formspec but not
|
||||||
|
in the handle formspec is called Time Of Check is not Time Of Use (TOCTOU).
|
||||||
|
|
||||||
|
|
||||||
|
## (Insecure) Environments
|
||||||
|
|
||||||
|
Minetest allows mods to request an unsandboxed environment, giving them access
|
||||||
|
to the full Lua API.
|
||||||
|
|
||||||
|
Can you spot the vulnerability in the following?
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ie = core.request_insecure_environment()
|
||||||
|
ie.os.execute(("path/to/prog %d"):format(3))
|
||||||
|
```
|
||||||
|
|
||||||
|
`string.format` is a function in the global shared table `string`.
|
||||||
|
A malicious mod could override this function and pass stuff to os.execute:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
string.format = function()
|
||||||
|
return "xdg-open 'http://example.com'"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The mod could pass something much more malicious than opening a website, such
|
||||||
|
as giving a remote user control over the machine.
|
||||||
|
|
||||||
|
Some rules for using an insecure environment:
|
||||||
|
|
||||||
|
* Always store it in a local and never pass it into a function.
|
||||||
|
* Make sure you can trust any input given to an insecure function, to avoid the
|
||||||
|
issue above. This means avoiding globally redefinable functions.
|
198
_en/quality/translations.md
Normal file
198
_en/quality/translations.md
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
title: Translation (i18n / l10n)
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.05
|
||||||
|
marked_text_encoding:
|
||||||
|
level: info
|
||||||
|
title: Marked Text Encoding
|
||||||
|
message: |
|
||||||
|
You don't need to know the exact format of marked text, but it might help
|
||||||
|
you understand.
|
||||||
|
|
||||||
|
```
|
||||||
|
"\27(T@mymod)Hello everyone!\27E"
|
||||||
|
```
|
||||||
|
|
||||||
|
* `\27` is the escape character - it's used to tell Minetest to pay attention as
|
||||||
|
something special is coming up. This is used for both translations and text
|
||||||
|
colorisation.
|
||||||
|
* `(T@mymod)` says that the following text is translatable using the `mymod`
|
||||||
|
textdomain.
|
||||||
|
* `Hello everyone!` is the translatable text in English, as passed to the
|
||||||
|
translator function.
|
||||||
|
* `\27E` is the escape character again and `E`, used to signal that the end has
|
||||||
|
been reached.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Adding support for translation to your mods and games allows more people to
|
||||||
|
enjoy them. According to Google Play, 64% of Minetest Android users don't have
|
||||||
|
English as their primary language. Minetest doesn't track stats for user
|
||||||
|
languages across all platforms, but there's likely to be a high proportion of
|
||||||
|
non-English speaking users.
|
||||||
|
|
||||||
|
Minetest allows you to translate your mods and games into different languages by
|
||||||
|
writing your text in English, and using translation files to map into other
|
||||||
|
languages. Translation is done on each player's client, allowing each player to
|
||||||
|
see a different language.
|
||||||
|
|
||||||
|
|
||||||
|
- [How does client-side translation work?](#how-does-client-side-translation-work)
|
||||||
|
- [Marked up text](#marked-up-text)
|
||||||
|
- [Translation files](#translation-files)
|
||||||
|
- [Format strings](#format-strings)
|
||||||
|
- [Best practices and Common Falsehoods about Translation](#best-practices-and-common-falsehoods-about-translation)
|
||||||
|
- [Server-side translations](#server-side-translations)
|
||||||
|
- [Conclusion](#conclusion)
|
||||||
|
|
||||||
|
|
||||||
|
## How does client-side translation work?
|
||||||
|
|
||||||
|
### Marked up text
|
||||||
|
|
||||||
|
The server needs to tell clients how to translate text. This is done by placing
|
||||||
|
control characters in text, telling Minetest where and how to translate
|
||||||
|
text. This is referred to as marked up text, and will be discussed more later.
|
||||||
|
|
||||||
|
To mark text as translatable, use a translator function (`S()`), obtained using
|
||||||
|
`core.get_translator(textdomain)`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local S = core.get_translator("mymod")
|
||||||
|
|
||||||
|
core.register_craftitem("mymod:item", {
|
||||||
|
description = S("My Item"),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument of `get_translator` is the `textdomain`, which acts as a
|
||||||
|
namespace. Rather than having all translations for a language stored in the same
|
||||||
|
file, translations are separated into textdomains, with a file per textdomain
|
||||||
|
per language. The textdomain should be the same as the mod name, as it helps
|
||||||
|
avoid mod conflicts.
|
||||||
|
|
||||||
|
Marked up text can be used in most places where human-readable text is accepted,
|
||||||
|
including formspecs, item def fields, infotext, and more. When including marked
|
||||||
|
text in formspecs, you need to escape the text using `core.formspec_escape`.
|
||||||
|
|
||||||
|
When the client encounters translatable text, such as that passed to
|
||||||
|
`description`, it looks it up in the player's language's translation file. If a
|
||||||
|
translation cannot be found, it falls back to the English translation.
|
||||||
|
|
||||||
|
Translatable marked up text contains the English source text, the textdomain,
|
||||||
|
and any additional arguments passed to `S()`. It's essentially a text encoding
|
||||||
|
of the `S` call, containing all the required information.
|
||||||
|
|
||||||
|
Another type of marked up text is that returned by `core.colorize`.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.marked_text_encoding %}
|
||||||
|
|
||||||
|
|
||||||
|
### Translation files
|
||||||
|
|
||||||
|
Translation files are media files that can be found in the `locale` folder for
|
||||||
|
each mod. Currently, the only supported format is `.tr`, but support for more
|
||||||
|
common formats is likely in the future. Translation files must be named
|
||||||
|
in the following way: `[textdomain].[lang].tr`.
|
||||||
|
|
||||||
|
Files in the `.tr` start with a comment specifying the textdomain, and then
|
||||||
|
further lines mapping from the English source text to the translation.
|
||||||
|
|
||||||
|
For example, `mymod.fr.tr`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# textdomain: mymod
|
||||||
|
Hello everyone!=Bonjour à tous !
|
||||||
|
I like grapefruit=J'aime le pamplemousse
|
||||||
|
```
|
||||||
|
|
||||||
|
You should create translation files based on your mod/game's source code,
|
||||||
|
using a tool like
|
||||||
|
[update_translations](https://github.com/minetest-tools/update_translations).
|
||||||
|
This tool will look for `S(` in your Lua code, and automatically create a
|
||||||
|
template that translators can use to translate into their language.
|
||||||
|
It also handles updating the translation files when your source changes.
|
||||||
|
|
||||||
|
You should always put literal text (`"`) inside S rather than using a variable,
|
||||||
|
as it helps tools find translations.
|
||||||
|
|
||||||
|
|
||||||
|
## Format strings
|
||||||
|
|
||||||
|
It's common to need to include variable information within a translation
|
||||||
|
string. It's important that text isn't just concatenated, as that prevents
|
||||||
|
translators from changing the order of variables within a sentence. Instead,
|
||||||
|
you should use the translation system's format/arguments system:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_joinplayer(function(player)
|
||||||
|
core.chat_send_all(S("Everyone, say hi to @1!", player:get_player_name()))
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to include a literal `@` in your translation, you'll need to escape
|
||||||
|
by writing `@@`.
|
||||||
|
|
||||||
|
You should avoid concatenation *within* a sentence, but it's recommended that
|
||||||
|
you join multiple sentences using concatenation. This helps translators by
|
||||||
|
keeping strings smaller.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
S("Hello @1!", player_name) .. " " .. S("You have @1 new messages.", #msgs)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Best practices and Common Falsehoods about Translation
|
||||||
|
|
||||||
|
* Avoid concatenating text and use format arguments instead. This gives
|
||||||
|
translators full control over changing the order of things.
|
||||||
|
* Create translation files automatically, using
|
||||||
|
[update_translations](https://github.com/minetest-tools/update_translations).
|
||||||
|
* It's common for variables to change the surrounding text, for example, with
|
||||||
|
gender and pluralisation. This is often hard to deal with, so is
|
||||||
|
frequently glossed over or worked around with gender neutral phrasings.
|
||||||
|
* Translations may be much longer or much smaller than the English text. Make
|
||||||
|
sure to leave plenty of space.
|
||||||
|
* Other languages may write numbers in a different way, for example, with commas
|
||||||
|
as decimal points. `1.000,23`, `1'000'000,32`
|
||||||
|
* Don't assume that other languages use capitalisation in the same way.
|
||||||
|
|
||||||
|
|
||||||
|
## Server-side translations
|
||||||
|
|
||||||
|
Sometimes you need to know the translation of text on the server, for example,
|
||||||
|
to sort or search text. You can use `get_player_information` to get a player's
|
||||||
|
language and `get_translated_string` to translate marked text.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local list = {
|
||||||
|
S("Hello world!"),
|
||||||
|
S("Potato")
|
||||||
|
}
|
||||||
|
|
||||||
|
core.register_chatcommand("find", {
|
||||||
|
func = function(name, param)
|
||||||
|
local info = core.get_player_information(name)
|
||||||
|
local language = info and info.language or "en"
|
||||||
|
|
||||||
|
for _, line in ipairs(list) do
|
||||||
|
local trans = core.get_translated_string(language, line)
|
||||||
|
if trans:contains(query) then
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The translation API allows making mods and games more accessible, but care is
|
||||||
|
needed in order to use it correctly.
|
||||||
|
|
||||||
|
Minetest is continuously improving, and the translation API is likely to be
|
||||||
|
extended in the future. For example, support for gettext translation files will
|
||||||
|
allow common translator tools and platforms (like weblate) to be used, and
|
||||||
|
there's likely to be support for pluralisation and gender added.
|
172
_en/quality/unit_testing.md
Normal file
172
_en/quality/unit_testing.md
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
title: Automatic Unit Testing
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.5
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Unit tests are an essential tool in proving and reassuring yourself that your code
|
||||||
|
is correct. This chapter will show you how to write tests for Minetest mods and
|
||||||
|
games using Busted. Writing unit tests for functions where you call Minetest
|
||||||
|
functions is quite difficult, but luckily [in the previous chapter](clean_arch.html),
|
||||||
|
we discussed how to structure your code avoid this.
|
||||||
|
|
||||||
|
- [Installing Busted](#installing-busted)
|
||||||
|
- [Your First Test](#your-first-test)
|
||||||
|
- [init.lua](#initlua)
|
||||||
|
- [api.lua](#apilua)
|
||||||
|
- [tests/api_spec.lua](#testsapi_speclua)
|
||||||
|
- [Mocking: Using External Functions](#mocking-using-external-functions)
|
||||||
|
- [Conclusion](#conclusion)
|
||||||
|
|
||||||
|
## Installing Busted
|
||||||
|
|
||||||
|
First, you'll need to install LuaRocks.
|
||||||
|
|
||||||
|
* Windows: Follow the [installation instructions on LuaRock's wiki](https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows).
|
||||||
|
* Debian/Ubuntu Linux: `sudo apt install luarocks`
|
||||||
|
|
||||||
|
Next, you should install Busted globally:
|
||||||
|
|
||||||
|
sudo luarocks install busted
|
||||||
|
|
||||||
|
Finally, check that it is installed:
|
||||||
|
|
||||||
|
busted --version
|
||||||
|
|
||||||
|
|
||||||
|
## Your First Test
|
||||||
|
|
||||||
|
Busted is Lua's leading unit test framework. Busted looks for Lua files with
|
||||||
|
names ending in `_spec`, and then executes them in a standalone Lua environment.
|
||||||
|
|
||||||
|
mymod/
|
||||||
|
├── init.lua
|
||||||
|
├── api.lua
|
||||||
|
└── tests
|
||||||
|
└── api_spec.lua
|
||||||
|
|
||||||
|
|
||||||
|
### init.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymod = {}
|
||||||
|
|
||||||
|
dofile(core.get_modpath("mymod") .. "/api.lua")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### api.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function mymod.add(x, y)
|
||||||
|
return x + y
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### tests/api_spec.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Look for required things in
|
||||||
|
package.path = "../?.lua;" .. package.path
|
||||||
|
|
||||||
|
-- Set mymod global for API to write into
|
||||||
|
_G.mymod = {} --_
|
||||||
|
-- Run api.lua file
|
||||||
|
require("api")
|
||||||
|
|
||||||
|
-- Tests
|
||||||
|
describe("add", function()
|
||||||
|
it("adds", function()
|
||||||
|
assert.equals(2, mymod.add(1, 1))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("supports negatives", function()
|
||||||
|
assert.equals(0, mymod.add(-1, 1))
|
||||||
|
assert.equals(-2, mymod.add(-1, -1))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now run the tests by opening a terminal in the mod's directory and
|
||||||
|
running `busted .`
|
||||||
|
|
||||||
|
It's important that the API file doesn't create the table itself, as globals in
|
||||||
|
Busted work differently. Any variable which would be global in Minetest is instead
|
||||||
|
a file local in busted. This would have been a better way for Minetest to do things,
|
||||||
|
but it's too late for that now.
|
||||||
|
|
||||||
|
Another thing to note is that any files you're testing should avoid calls to any
|
||||||
|
functions not inside of it. You tend to only write tests for a single file at once.
|
||||||
|
|
||||||
|
|
||||||
|
## Mocking: Using External Functions
|
||||||
|
|
||||||
|
Mocking is the practice of replacing functions that the thing you're testing depends
|
||||||
|
on. This can have two purposes; one, the function may not be available in the
|
||||||
|
test environment, and two, you may want to capture calls to the function and any
|
||||||
|
passed arguments.
|
||||||
|
|
||||||
|
If you follow the advice in the [Clean Architectures](clean_arch.html) chapter,
|
||||||
|
you'll already have a pretty clean file to test. You will still have to mock
|
||||||
|
things not in your area, however - for example, you'll have to mock the view when
|
||||||
|
testing the controller/API. If you didn't follow the advice, then things are a
|
||||||
|
little harder as you may have to mock the Minetest API.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- As above, make a table
|
||||||
|
_G.minetest = {}
|
||||||
|
|
||||||
|
-- Define the mock function
|
||||||
|
local chat_send_all_calls = {}
|
||||||
|
function core.chat_send_all(name, message)
|
||||||
|
table.insert(chat_send_all_calls, { name = name, message = message })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tests
|
||||||
|
describe("list_areas", function()
|
||||||
|
it("returns a line for each area", function()
|
||||||
|
chat_send_all_calls = {} -- reset table
|
||||||
|
|
||||||
|
mymod.list_areas_to_chat("singleplayer", "singleplayer")
|
||||||
|
|
||||||
|
assert.equals(2, #chat_send_all_calls)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("sends to right player", function()
|
||||||
|
chat_send_all_calls = {} -- reset table
|
||||||
|
|
||||||
|
mymod.list_areas_to_chat("singleplayer", "singleplayer")
|
||||||
|
|
||||||
|
for _, call in pairs(chat_send_all_calls) do --_
|
||||||
|
assert.equals("singleplayer", call.name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- The above two tests are actually pointless,
|
||||||
|
-- as this one tests both things
|
||||||
|
it("returns correct thing", function()
|
||||||
|
chat_send_all_calls = {} -- reset table
|
||||||
|
|
||||||
|
mymod.list_areas_to_chat("singleplayer", "singleplayer")
|
||||||
|
|
||||||
|
local expected = {
|
||||||
|
{ name = "singleplayer", message = "Town Hall (2,43,63)" },
|
||||||
|
{ name = "singleplayer", message = "Airport (43,45,63)" },
|
||||||
|
}
|
||||||
|
assert.same(expected, chat_send_all_calls)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Unit tests will greatly increase the quality and reliability of your project if used
|
||||||
|
well, but they require you to structure your code in a different way than usual.
|
||||||
|
|
||||||
|
For an example of a mod with lots of unit tests, see
|
||||||
|
[crafting by rubenwardy](https://github.com/rubenwardy/crafting).
|
21
_includes/notice.html
Normal file
21
_includes/notice.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% if include.notice %}
|
||||||
|
{% assign notice=include.notice %}
|
||||||
|
{% else %}
|
||||||
|
{% assign notice=include %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="notice notice-{{ notice.level }} {{ notice.classes }}">
|
||||||
|
{% if notice.level == "warning" %}
|
||||||
|
<span>⚠</span>
|
||||||
|
{% else if notice.level == "tip" %}
|
||||||
|
<span>i</span>
|
||||||
|
{% else %}
|
||||||
|
<span>?</span>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
{% if notice.title %}
|
||||||
|
<h2>{{ notice.title }}</h2>
|
||||||
|
{% endif %}
|
||||||
|
{{ notice.message | markdownify }}
|
||||||
|
</div>
|
||||||
|
</div>
|
209
_it/advmap/biomesdeco.md
Executable file
209
_it/advmap/biomesdeco.md
Executable file
@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
title: Biomi e decorazioni
|
||||||
|
author: Shara
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 6.1
|
||||||
|
description: Crea biomi e decorazioni per personalizzare la mappa
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
L'abilità di registrare biomi e decorazioni è vitale quando si vuole creare un ambiente di gioco variegato e interessante.
|
||||||
|
Questo capitolo mostra come registrare nuovi biomi, come controllarne la distribuzione, e come aggiungerci decorazioni.
|
||||||
|
|
||||||
|
- [Cosa sono i biomi?](#cosa-sono-i-biomi)
|
||||||
|
- [Collocare un bioma](#collocare-un-bioma)
|
||||||
|
- [Calore e umidità](#calore-e-umidità)
|
||||||
|
- [Visualizzare i confini usando i diagrammi di Voronoi](#visualizzare-i-confini-usando-i-diagrammi-di-voronoi)
|
||||||
|
- [Creare un diagramma di Voronoi usando Geogebra](#creare-un-diagramma-di-voronoi-usando-geogebra)
|
||||||
|
- [Registrare un bioma](#registrare-un-bioma)
|
||||||
|
- [Cosa sono le decorazioni?](#cosa-sono-le-decorazioni)
|
||||||
|
- [Registrare una decorazione semplice](#registrare-una-decorazione-semplice)
|
||||||
|
- [Registrare una decorazione composta (schematic)](#registrare-una-decorazione-composta-schematic)
|
||||||
|
- [Alias del generatore mappa](#alias-del-generatore-mappa)
|
||||||
|
|
||||||
|
## Cosa sono i biomi?
|
||||||
|
|
||||||
|
In Minetest, un bioma è un ambiente di gioco specifico. Quando viene registrato, se ne possono determinare i vari tipi di nodi che vi appariranno durante la generazione della mappa.
|
||||||
|
Alcuni dei tipi più comuni - che possono variare da bioma a bioma - sono:
|
||||||
|
|
||||||
|
* Nodo superficie: il nodo che si ha più probabilità di trovare sulla superficie.
|
||||||
|
Un esempio noto ai più è "Dirt with Grass" in Minetest Game.
|
||||||
|
* Nodo riempitivo: il livello immediatamente sotto al precedente.
|
||||||
|
Nei biomi con l'erba, corrisponde solitamente alla terra.
|
||||||
|
* Nodo di pietra: il nodo che si ha più probabilità di trovare sottoterra.
|
||||||
|
* Nodo d'acqua: è solitamente un liquido, ed è il nodo che appare dove ci si aspetterebbe di trovare masse d'acqua.
|
||||||
|
|
||||||
|
Si possono incontrare anche altri tipi di nodi tra i biomi, dando la possibilità di creare ambienti altamente variegati all'interno dello stesso gioco.
|
||||||
|
|
||||||
|
## Collocare un bioma
|
||||||
|
|
||||||
|
### Calore e umidità
|
||||||
|
|
||||||
|
Non è sufficiente registrare un bioma; bisogna anche decidere dove deve apparire.
|
||||||
|
Per farlo, si assegna un valore di calore e umidità a ognuno di essi.
|
||||||
|
|
||||||
|
Dovresti pensarci bene prima di inserire questi valori: essi determinano quali biomi
|
||||||
|
possono confinare tra di loro.
|
||||||
|
Decisioni frettolose potrebbero risultare in un torrido deserto che condivide i suoi confini con un ghiacciaio, e altre improbabili combinazioni che potresti voler evitare.
|
||||||
|
|
||||||
|
In gioco, calore e umidità vanno da un minimo di 0 a un massimo di 100.
|
||||||
|
Questi valori cambiano gradualmente, aumentando o diminuendo man mano che ci si sposta per la mappa.
|
||||||
|
Quale bioma apparirà viene determinato prendendo il bioma registrato che ha i valori di calore e umidità più simili a quel punto della mappa.
|
||||||
|
|
||||||
|
Dato che i cambiamenti di calore e umidità sono graduali, è buona norma assegnare questi valori ai biomi basandosi su cosa ci si aspetta realisticamente di trovare in un determinato bioma.
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
* Un deserto potrebbe avere alte temperature e poca umidità;
|
||||||
|
* Una foresta innevata potrebbe avere basse temperature e un'umidità moderata;
|
||||||
|
* Una palude ha senso se ha un'umidità elevata
|
||||||
|
|
||||||
|
Così facendo, questo significa che, finché si hanno più biomi, sarà più probabile trovare biomi confinanti che seguono una progressione logica.
|
||||||
|
|
||||||
|
### Visualizzare i confini usando i diagrammi di Voronoi
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/biomes_voronoi.png" alt="Voronoi">
|
||||||
|
<figcaption>
|
||||||
|
Diagramma di Voronoi che mostra il punto più vicino.
|
||||||
|
<span class="credit">Di <a href="https://en.wikipedia.org/wiki/Voronoi_diagram#/media/File:Euclidean_Voronoi_diagram.svg">Balu Ertl</a>, CC BY-SA 4.0.</span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Regolare i valori di calore e umidità risulta più facile se si riesce a visualizzare come i biomi entrano in relazione l'un con l'altro.
|
||||||
|
Questo è importante soprattutto se si sta creando un set completo di nuovi biomi personalizzati, ma può essere utile anche quando se ne vuole aggiungere soltanto uno a un set già predefinito.
|
||||||
|
|
||||||
|
Il modo più semplice per vedere quali biomi potrebbero condividere un confine è creare un diagramma di Voronoi, che può essere usato per mostrare in un diagramma 2D quali sono, date più posizioni (in nero nell'immagine), i punti nello spazio a loro più vicini (i bordi delle aree colorate).
|
||||||
|
|
||||||
|
Un diagramma di Voronoi è utile sia per rivelare eventuali accoppiamenti non desiderati che per dar un'idea generale della distribuzione dei biomi.
|
||||||
|
|
||||||
|
Per far ciò, viene segnato un punto per ogni bioma basandosi sui valori di calore e umidità, dove l'asse X è il calore e l'asse Y l'umidità.
|
||||||
|
Il diagramma è poi suddiviso in aree, in modo che ogni posizione in un'area specifica sia più vicina a un punto che a tutti gli altri.
|
||||||
|
|
||||||
|
Ogni area rappresenta un bioma. Se due aree condividono un confine, i biomi a loro associati possono essere trovati a confinare in gioco.
|
||||||
|
La lunghezza del confine condiviso tra due aree, comparata alla lunghezza condivisa con le altre, ti dirà quanto frequentemente due biomi sono propensi a essere trovati vicini.
|
||||||
|
|
||||||
|
### Creare un diagramma di Voronoi usando Geogebra
|
||||||
|
|
||||||
|
Oltre che farli a mano, per creare dei diagrammi di Voronoi si possono usare programmi come [Geogebra](https://www.geogebra.org/calculator).
|
||||||
|
|
||||||
|
1. Crea dei punti selezionando lo strumento per i punti dall'apposita interfaccia (l'icona del punto con la A) e cliccando per il piano.
|
||||||
|
Puoi trascinare i punti dove vuoi o impostare la loro posizione dal menù laterale a sinistra.
|
||||||
|
Dovresti anche rinominare ogni punto per rendere il tutto più chiaro.
|
||||||
|
|
||||||
|
2. Poi, crea il voronoi inserendo la seguente funzione nel menù laterale a sinistra, sotto i punti:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Voronoi({ A, B, C, D, E })
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove ogni punto è contenuto nelle graffe, separato da virgole.
|
||||||
|
|
||||||
|
3. Tadaan! Dovresti ora avere un diagramma di Voronoi con tutti i punti trascinabili.
|
||||||
|
|
||||||
|
## Registrare un bioma
|
||||||
|
|
||||||
|
Il seguente codice registra un semplice bioma chiamato "distesa_erbosa":
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_biome({
|
||||||
|
name = "distesa_erbosa",
|
||||||
|
node_top = "default:dirt_with_grass",
|
||||||
|
depth_top = 1,
|
||||||
|
node_filler = "default:dirt",
|
||||||
|
depth_filler = 3,
|
||||||
|
y_max = 1000,
|
||||||
|
y_min = -3,
|
||||||
|
heat_point = 50,
|
||||||
|
humidity_point = 50,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo bioma ha uno strato di "Dirt with Grass" sulla superficie, e tre strati di terra al di sotto.
|
||||||
|
Non specifica tuttavia un nodo di pietra, quindi il nodo definito nella registrazione dell'alias del generatore della mappa (*mapgen*) in `mapgen_stone` sarà presente sotto la terra.
|
||||||
|
|
||||||
|
Ci sono molte opzioni da personalizzare quando si registra un bioma, e le si possono trovare documentate nella [API](https://minetest.gitlab.io/minetest/definition-tables/#biome-definition) come al solito.
|
||||||
|
|
||||||
|
Non c'è bisogno di definire tutte le opzioni ogni volta che si crea un bioma, seppur in certi casi il dimenticarsi un'opzione specifica o un'alias di generazione della mappa appropriato porti a deli errori nella generazione.
|
||||||
|
|
||||||
|
## Cosa sono le decorazioni?
|
||||||
|
|
||||||
|
Le decorazioni sono o dei nodi o degli insiemi di nodi (*schematic*) che possono essere piazzati nella mappa durante la generazione.
|
||||||
|
Alcuni esempi comuni sono i fiori, i cespugli e gli alberi.
|
||||||
|
Altri usi più creativi possono includere stalattiti e stalagmiti nelle grotte, formazione di cristalli sottoterra o addirittura la collocazione di piccoli edifici.
|
||||||
|
|
||||||
|
Le decorazioni possono essere limitate a biomi o ad altezze specifiche, o ancora a determinati nodi.
|
||||||
|
Sono spesso usate per sviluppare l'atmosfera di un bioma, inserendo piante, alberi o altre caratteristiche che lo rendono particolare.
|
||||||
|
|
||||||
|
## Registrare una decorazione semplice
|
||||||
|
|
||||||
|
Le decorazioni semplici sono usate per piazzare un singolo nodo nella mappa durante la generazione.
|
||||||
|
Ricordati che devi specificare il nodo che vuoi usare in quanto decorazione, i dettagli di dove può essere piazzato, e quanto di frequente deve apparire.
|
||||||
|
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_decoration({
|
||||||
|
deco_type = "simple",
|
||||||
|
place_on = {"base:dirt_with_grass"},
|
||||||
|
sidelen = 16,
|
||||||
|
fill_ratio = 0.1,
|
||||||
|
biomes = {"distesa_erbosa"},
|
||||||
|
y_max = 200,
|
||||||
|
y_min = 1,
|
||||||
|
decoration = "piante:erba",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In questo caso, il nodo chiamato `piante:erba` verrà piazzato nel bioma "distesa_erbosa" in cima ai nodi a mo' di prato (`base:dirt_with_grass`) tra altitudine `y = 1` e `y = 20`.
|
||||||
|
|
||||||
|
Il valore `fill_ratio` determina quanto di frequente dovrà apparire, con valori più alti di 1 equivalenti a un grande numero di decorazioni piazzate.
|
||||||
|
È possibile, sennò, usare i parametri di disturbo (*noise parameters*) per determinare la collocazione.
|
||||||
|
|
||||||
|
## Registrare una decorazione composta (schematic)
|
||||||
|
|
||||||
|
Le schematic sono molto simili alle decorazioni semplici, solo che piazzano più nodi invece che uno solo.
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_decoration({
|
||||||
|
deco_type = "schematic",
|
||||||
|
place_on = {"base:desert_sand"},
|
||||||
|
sidelen = 16,
|
||||||
|
fill_ratio = 0.0001,
|
||||||
|
biomes = {"desert"},
|
||||||
|
y_max = 200,
|
||||||
|
y_min = 1,
|
||||||
|
schematic = core.get_modpath("plants") .. "/schematics/cactus.mts",
|
||||||
|
flags = "place_center_x, place_center_z",
|
||||||
|
rotation = "random",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In quest'esempio, viene piazzata la schematic cactus.mts nel bioma del deserto.
|
||||||
|
C'è bisogno di fornire il percorso nel quale andare a pescare il file, che in questo caso si trova in una cartella chiamata "schematics" all'interno della mod.
|
||||||
|
|
||||||
|
Sempre nell'esempio, inoltre, vengono impostati i contrassegni (le *flag*) per centrare il posizionamento della schematic, e la rotazione è impostata randomicamente.
|
||||||
|
Quest'ultima opzione agevola l'introduzione di una maggior variazione quando vengono usate schematic asimmetriche.
|
||||||
|
|
||||||
|
|
||||||
|
## Alias del generatore mappa
|
||||||
|
|
||||||
|
I giochi disponibili dovrebbero già includere un alias del generatore mappa (*mapgen*) adeguato, quindi devi solo prendere in considerazione se registrarne di personali alla creazione di un nuovo gioco.
|
||||||
|
|
||||||
|
Gli alias del generatore mappa forniscono informazioni al generatore principale, e possono essere registrati secondo lo schema:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_alias("mapgen_stone", "base:smoke_stone")
|
||||||
|
```
|
||||||
|
|
||||||
|
Almeno almeno dovresti registrare:
|
||||||
|
|
||||||
|
* mapgen_stone
|
||||||
|
* mapgen_water_source
|
||||||
|
* mapgen_river_water_source
|
||||||
|
|
||||||
|
Se non stai definendo nodi liquidi per le caverne di tutti i biomi, dovresti aggiungere anche:
|
||||||
|
|
||||||
|
* mapgen_lava_source
|
186
_it/advmap/lvm.md
Executable file
186
_it/advmap/lvm.md
Executable file
@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
title: Manipolatori di voxel Lua
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 6.2
|
||||||
|
description: Impara come usare gli LVM per accelerare le operazioni nella mappa.
|
||||||
|
redirect_from:
|
||||||
|
- /it/chapters/lvm.html
|
||||||
|
- /it/map/lvm.html
|
||||||
|
mapgen_object:
|
||||||
|
level: warning
|
||||||
|
title: LVM e generatore mappa
|
||||||
|
message: Non usare `core.get_voxel_manip()` con il generatore mappa, in quanto può causare glitch.
|
||||||
|
Usa invece `core.get_mapgen_object("voxelmanip")`.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Le funzioni introdotte nel capitolo [Mappa: operazioni base](../map/environment.html) sono comode e facili da usare, ma per le grandi aree non sono efficienti.
|
||||||
|
Ogni volta che `set_node` e `get_node` vengono chiamati da una mod, la mod deve comunicare con il motore di gioco.
|
||||||
|
Ciò risulta in una costante copia individuale dei singoli nodi, che è lenta e abbasserà notevolmente le performance del gioco.
|
||||||
|
Usare un Manipolatore di Voxel Lua (*Lua Voxel Manipulator*, da qui LVM) può essere un'alternativa migliore.
|
||||||
|
- [Concetti](#concetti)
|
||||||
|
- [Lettura negli LVM](#lettura-negli-lvm)
|
||||||
|
- [Lettura dei nodi](#lettura-dei-nodi)
|
||||||
|
- [Scrittura dei nodi](#scrittura-dei-nodi)
|
||||||
|
- [Esempio](#esempio)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
## Concetti
|
||||||
|
|
||||||
|
Un LVM permette di caricare grandi pezzi di mappa nella memoria della mod che ne ha bisogno.
|
||||||
|
Da lì si possono leggere e modificare i dati immagazzinati senza dover interagire ulteriormente col motore di gioco, e senza eseguire callback; in altre parole, l'operazione risulta molto più veloce.
|
||||||
|
Una volta fatto ciò, si può passare l'area modificata al motore di gioco ed eseguire eventuali calcoli riguardo la luce.
|
||||||
|
|
||||||
|
## Lettura negli LVM
|
||||||
|
|
||||||
|
Si possono caricare solamente aree cubiche negli LVM, quindi devi capire da te quali sono le posizioni minime e massime che ti servono per l'area da modificare.
|
||||||
|
Fatto ciò, puoi creare l'LVM:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local vm = core.get_voxel_manip()
|
||||||
|
local emin, emax = vm:read_from_map(pos1, pos2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Per questioni di performance, un LVM non leggerà quasi mai l'area esatta che gli è stata passata.
|
||||||
|
Al contrario, è molto probabile che ne leggerà una maggiore. Quest'ultima è data da `emin` ed `emax`, che stanno per posizione minima/massima emersa (*emerged min/max pos*).
|
||||||
|
Inoltre, un LVM caricherà in automatico l'area passatagli - che sia da memoria, da disco o dal generatore di mappa.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.mapgen_object %}
|
||||||
|
|
||||||
|
## Lettura dei nodi
|
||||||
|
|
||||||
|
Per leggere il tipo dei nodi in posizioni specifiche, avrai bisogno di usare `get_data()`.
|
||||||
|
Questo metodo ritorna un array monodimensionale dove ogni voce rappresenta il tipo.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = vm:get_data()
|
||||||
|
```
|
||||||
|
|
||||||
|
Si possono ottenere param2 e i dati della luce usando i metodi `get_light_data()` e `get_param2_data()`.
|
||||||
|
|
||||||
|
Avrai bisogno di usare `emin` e `emax` per capire dove si trova un nodo nei metodi sopraelencati.
|
||||||
|
C'è una classe di supporto per queste cose chiamate `VoxelArea` che gestisce i calcoli al posto tuo.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local a = VoxelArea:new{
|
||||||
|
MinEdge = emin,
|
||||||
|
MaxEdge = emax
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Ottiene l'indice del nodo
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
|
||||||
|
-- Legge il nodo
|
||||||
|
print(data[idx])
|
||||||
|
```
|
||||||
|
|
||||||
|
All'eseguire ciò, si noterà che `data[idx]` è un intero.
|
||||||
|
Questo perché il motore di gioco non salva i nodi come stringhe per motivi di performance; al contrario, usa un intero chiamato "ID di contenuto" (*content ID*).
|
||||||
|
Per scoprire qual è l'ID assegnato a un tipo di nodo, si usa `get_content_id()`.
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local c_pietra = core.get_content_id("default:stone")
|
||||||
|
```
|
||||||
|
|
||||||
|
Si può ora controllare se un nodo è effettivamente di pietra:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
if data[idx] == c_pietra then
|
||||||
|
print("è pietra!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Gli ID di contenuto di un nodo potrebbero cambiare durante la fase di caricamento, quindi è consigliato non tentare di ottenerli durante tale fase.
|
||||||
|
|
||||||
|
Le coordinate dei nodi nell'array di un LVM sono salvate in ordine inverso (`z, y, x`), quindi se le si vuole iterare, si tenga presente che si inizierà dalla Z:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for z = min.z, max.z do
|
||||||
|
for y = min.y, max.y do
|
||||||
|
for x = min.x, max.x do
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
if data[idx] == c_pietra then
|
||||||
|
print("è pietra!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Per capire la ragione di tale iterazione, bisogna parlare un attimo di architettura dei computer: leggere dalla RAM - la memoria principale - è alquanto dispendioso, quindi i processori hanno molteplici livelli di memoria a breve termine (la *cache*).
|
||||||
|
Se i dati richiesti da un processo sono in quest'ultima memoria, si possono ottenere velocemente.
|
||||||
|
Al contrario, se i dati lì non ci sono, verranno pescati dalla RAM *e* inseriti in quella a breve termine, nel caso dovessero servire di nuovo.
|
||||||
|
Questo significa che una buona regola per l'ottimizzazione è quella di iterare in modo che i dati vengano letti in sequenza, evitando di arrivare fino alla RAM ogni volta (*cache thrashing*).
|
||||||
|
|
||||||
|
## Scrittura dei nodi
|
||||||
|
|
||||||
|
Prima di tutto, bisogna impostare il nuovo ID nell'array:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
for z = min.z, max.z do
|
||||||
|
for y = min.y, max.y do
|
||||||
|
for x = min.x, max.x do
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
if data[idx] == c_pietra then
|
||||||
|
data[idx] = c_aria
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Una volta finito con le operazioni nell'LVM, bisogna passare l'array al motore di gioco:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
vm:set_data(data)
|
||||||
|
vm:write_to_map(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Per la luce e param2, invece si usano `set_light_data()` e `set_param2_data()`.
|
||||||
|
|
||||||
|
`write_to_map()` richiede un booleano che è `true` se si vuole che venga calcolata anche la luce.
|
||||||
|
Se si passa `false` invece, ci sarà bisogno di ricalcolarla in un secondo tempo usando `core.fix_light`.
|
||||||
|
|
||||||
|
## Esempio
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function da_erba_a_terra(pos1, pos2)
|
||||||
|
local c_terra = core.get_content_id("default:dirt")
|
||||||
|
local c_erba = core.get_content_id("default:dirt_with_grass")
|
||||||
|
-- legge i dati nella LVM
|
||||||
|
local vm = core.get_voxel_manip()
|
||||||
|
local emin, emax = vm:read_from_map(pos1, pos2)
|
||||||
|
local a = VoxelArea:new{
|
||||||
|
MinEdge = emin,
|
||||||
|
MaxEdge = emax
|
||||||
|
}
|
||||||
|
local data = vm:get_data()
|
||||||
|
|
||||||
|
-- modifica i dati
|
||||||
|
for z = pos1.z, pos2.z do
|
||||||
|
for y = pos1.y, pos2.y do
|
||||||
|
for x = pos1.x, pos2.x do
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
if data[idx] == c_erba then
|
||||||
|
data[idx] = c_terra
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- scrive i dati
|
||||||
|
vm:set_data(data)
|
||||||
|
vm:write_to_map(true)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Il tuo turno
|
||||||
|
|
||||||
|
* Crea una funzione `rimpiazza_in_area(da, a, pos1, pos2)`, che sostituisce tutte le istanze di `da` con `a` nell'area data, dove `da` e `a` sono i nomi dei nodi;
|
||||||
|
* Crea una funzione che ruota tutte le casse di 90°;
|
||||||
|
* Crea una funzione che usa un LVM per far espandere i nodi di muschio sui nodi di pietra e pietrisco confinanti.
|
||||||
|
La tua implementazione fa espandere il muschio di più di un blocco alla volta? Se sì, come puoi prevenire ciò?
|
169
_it/basics/getting_started.md
Normal file
169
_it/basics/getting_started.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
title: Per iniziare
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 1.1
|
||||||
|
description: Impara come si crea la cartella di una mod, un file init.lua, mod.conf e altro.
|
||||||
|
redirect_from:
|
||||||
|
- /it/chapters/folders.html
|
||||||
|
- /it/basics/folders.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Capire la struttura base della cartella di una mod è un requisito essenziale per creare qualsivoglia contenuto.
|
||||||
|
|
||||||
|
- [Cosa sono i giochi e le mod?](#cosa-sono-i-giochi-e-le-mod)
|
||||||
|
- [Dove vengono salvate le mod?](#dove-vengono-salvate-le-mod)
|
||||||
|
- [Cartella mod](#cartella-mod)
|
||||||
|
- [mod.conf](#modconf)
|
||||||
|
- [Dipendenze](#dipendenze)
|
||||||
|
- [Pacchetti mod](#pacchetti-mod-mod-pack)
|
||||||
|
- [Esempio](#esempio)
|
||||||
|
- [Cartella mod](#cartella-mod-1)
|
||||||
|
- [init.lua](#initlua)
|
||||||
|
- [mod.conf](#modconf-1)
|
||||||
|
|
||||||
|
|
||||||
|
## Cosa sono i giochi e le mod?
|
||||||
|
|
||||||
|
Il punto forte di Minetest è l'abilità di sviluppare facilmente giochi senza il bisogno di crearsi da zero il motore grafico, gli algoritmi voxel o tutta la parte di rete.
|
||||||
|
|
||||||
|
In Minetest, un gioco è un insieme di moduli che lavorano fianco a fianco per fornire il contenuto e il comportamento di un gioco.
|
||||||
|
Un modulo, solitamente conosciuto come "mod", è una collezione di script e risorse, e in teoria ne potrebbe bastare uno per creare un intero gioco.
|
||||||
|
Tuttavia, questo non accade spesso, perché ridurrebbe la comodità di poter sostituire o calibrare alcune parti in maniera indipendente dalle altre.
|
||||||
|
|
||||||
|
È poi anche possibile distribuire singolarmente le varie mod, che diventano mod nel senso più tradizionale del termine: modifiche, per calibrano o espandere le proprietà di un gioco.
|
||||||
|
|
||||||
|
Indipendentemente da come le si voglia usare (specifiche per un gioco o come estensioni generiche) usano la stessa API.
|
||||||
|
|
||||||
|
Questo libro coprirà le parti principali dell'API di Minetest, ed è pensato sia per chi sviluppa il motore di gioco (Minetest, in C++) che per chi crea mod.
|
||||||
|
|
||||||
|
|
||||||
|
## Dove vengono salvate le mod?
|
||||||
|
|
||||||
|
<a name="mod-locations"></a>
|
||||||
|
|
||||||
|
Ogni mod ha la sua cartella personale dove viene messo il suo codice in Lua, le sue texture, i suoi modelli e i suoi file audio.
|
||||||
|
Minetest esegue controlli in più posti e questi posti sono generalmente chiamati *percorsi di caricamento mod* (*mod load paths*).
|
||||||
|
|
||||||
|
Per un dato mondo/salvataggio, vengono controllati tre percorsi.
|
||||||
|
Essi sono, in ordine:
|
||||||
|
|
||||||
|
1. Mod di gioco. Queste sono le mod che compongono il gioco che il mondo sta eseguendo.
|
||||||
|
Es: `minetest/games/minetest_game/mods/`, `/usr/share/minetest/games/minetest/`
|
||||||
|
2. Mod globali. Il luogo dove le mod vengono quasi sempre installate. Se si è in dubbio, le si metta qui.
|
||||||
|
Es: `minetest/mods/`
|
||||||
|
3. Mod del mondo. Il luogo dove mettere le mod che sono specifiche di un dato mondo.
|
||||||
|
Es: `minetest/worlds/world/worldmods/`
|
||||||
|
|
||||||
|
Minetest controllerà questi percorsi nell'ordine sopraelencato.
|
||||||
|
In caso dovesse incontrare una mod con lo stesso nome di una incontrata in precedenza, l'ultima verrebbe caricata al posto della prima.
|
||||||
|
Ciò significa, per esempio, che è possibile sovrascriverne una di gioco se ve n'è una omonima nelle globali.
|
||||||
|
|
||||||
|
La posizione di ogni percorso dipende da quale sistema operativo si sta usando, e da come è stato installato Minetest.
|
||||||
|
|
||||||
|
* **Windows:**
|
||||||
|
* Per le versioni portatili, per esempio da un file .zip, vai dove hai estratto lo zip e cerca le cartelle `games`, `mods` e `worlds`.
|
||||||
|
* Per le versioni installate, per esempio da un setup.exe, guarda in C:\\\\Minetest o C:\\\\Games\\Minetest.
|
||||||
|
* **GNU/Linux:**
|
||||||
|
* Per le installazioni di sistema, guarda in `~/.minetest`.
|
||||||
|
Attenzione che `~` equivale alla cartella home dell'utente, e che i file e le cartelle che iniziano con un punto (`.`) sono nascosti di default.
|
||||||
|
* Per le installazioni portatili, guarda nella cartella di build.
|
||||||
|
* Per installazioni Flatpak, guarda in `~/.var/app/net.minetest.Minetest/.minetest/mods/`.
|
||||||
|
* **MacOS**
|
||||||
|
* Guarda in `~/Library/Application Support/minetest/`.
|
||||||
|
Attenzione che `~` equivale alla cartella home dell'utente, per esempio `/Users/USERNAME/`.
|
||||||
|
|
||||||
|
## Cartella mod
|
||||||
|
|
||||||
|
![Find the mod's directory]({{ page.root }}/static/folder_modfolder.jpg)
|
||||||
|
|
||||||
|
Il *nome mod* è usato per riferirsi a una mod e ognuna di esse dovrebbe averne uno unico.
|
||||||
|
Questi possono includere lettere, numeri e trattini bassi, e un buon nome dovrebbe descrivere brevemente cosa fa la mod (è anche consigliato rinominare la cartella della mod con il nome di quest'ultima).
|
||||||
|
Per scoprire se un nome è disponibile, prova a cercarlo su
|
||||||
|
[content.minetest.net](https://content.minetest.net).
|
||||||
|
|
||||||
|
|
||||||
|
lamiamod
|
||||||
|
├── init.lua (necessario) - Viene eseguito al lancio del gioco.
|
||||||
|
├── mod.conf (consigliato) - Contiene la descrizione e le dipendneze.
|
||||||
|
├── textures (opzionale)
|
||||||
|
│ └── ... qualsiasi texture o immagine
|
||||||
|
├── sounds (opzionale)
|
||||||
|
│ └── ... qualsiasi file audio
|
||||||
|
└── ... qualsiasi altro tipo di file o cartelle
|
||||||
|
|
||||||
|
Solo il file init.lua è necessario in una mod per eseguirla quando si avvia un gioco;
|
||||||
|
tuttavia è consigliato anche mod.conf, e altri componenti potrebbero essere richiesti a
|
||||||
|
seconda di quello che si vuole fare.
|
||||||
|
|
||||||
|
## mod.conf
|
||||||
|
|
||||||
|
Questo file è utilizzato per i metadati della mod, che includono il suo nome, la descrizione e altre informazioni.
|
||||||
|
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
name = lamiamod
|
||||||
|
description = Aggiunge X, Y, e Z
|
||||||
|
depends = mod1, mod2
|
||||||
|
|
||||||
|
### Dipendenze
|
||||||
|
|
||||||
|
Una dipendenza è quando (all'avvio) una o più mod vengono richieste da un'altra mod.
|
||||||
|
I motivi sono vari: potrebbe per esempio aver bisogno di parti del loro codice, degli oggetti, o in generale di risorse che queste forniscono.
|
||||||
|
|
||||||
|
Ci sono due tipi di dipendenze: forti e opzionali.
|
||||||
|
Entrambe richiedono che la mod richiesta venga caricata prima, con la differenza che se la dipendenza è forte e la mod non viene trovata, l'altra non verrà caricata, mentre se è opzionale, verranno semplicemente caricate meno funzionalità.
|
||||||
|
|
||||||
|
Le dipendenze sono specificate in un elenco separato da virgole in mod.conf.
|
||||||
|
|
||||||
|
depends = mod1, mod2
|
||||||
|
optional_depends = mod3
|
||||||
|
|
||||||
|
## Pacchetti mod (mod pack)
|
||||||
|
|
||||||
|
Le mod possono essere raggruppate in pacchetti che permettono di confezionarne e spostarne più alla volta.
|
||||||
|
Sono comodi se si vogliono fornire più mod a chi gioca, ma non si vuole al tempo stesso fargliele scaricare una per una.
|
||||||
|
|
||||||
|
pacchettomod1
|
||||||
|
├── modpack.lua (necessario) - segnala che è un pacchetto mod
|
||||||
|
├── mod1
|
||||||
|
│ └── ... file mod
|
||||||
|
└── mymod (opzionale)
|
||||||
|
└── ... file mod
|
||||||
|
|
||||||
|
Attenzione che un pacchetto mod non equivale a un *gioco*. I giochi hanno una propria struttura organizzativa che verrà spiegata nel loro apposito capitolo.
|
||||||
|
|
||||||
|
## Esempio
|
||||||
|
|
||||||
|
Segue un esempio che mette insieme tutto ciò discusso finora:
|
||||||
|
|
||||||
|
### Cartella mod
|
||||||
|
lamiamod
|
||||||
|
├── textures
|
||||||
|
│ └── lamiamod_nodo.png
|
||||||
|
├── init.lua
|
||||||
|
└── mod.conf
|
||||||
|
|
||||||
|
### init.lua
|
||||||
|
```lua
|
||||||
|
print("Questo file parte all'avvio!")
|
||||||
|
|
||||||
|
core.register_node("lamiamod:nodo", {
|
||||||
|
description = "Questo è un nodo",
|
||||||
|
tiles = {"lamiamod_nodo.png"},
|
||||||
|
groups = {cracky = 1}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### mod.conf
|
||||||
|
name = lamiamod
|
||||||
|
descriptions = Aggiunge un nodo
|
||||||
|
depends = default
|
||||||
|
|
||||||
|
Questa mod ha come nome "lamiamod". Ha due file di testo: init.lua e mod.conf.\\
|
||||||
|
Lo script stampa un messaggio e poi registra un nodo – che sarà spiegato nel prossimo capitolo.\\
|
||||||
|
C'è una sola dipendenza, la [mod default](https://content.minetest.net/metapackages/default/), che
|
||||||
|
si trova solitamente in Minetest Game.\\
|
||||||
|
C'è anche una texture in textures/ per il nodo.
|
176
_it/basics/lua.md
Normal file
176
_it/basics/lua.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
title: Programmare in Lua
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 1.2
|
||||||
|
description: Un'introduzione a Lua, con inclusa una guida alla portata globale/locale.
|
||||||
|
redirect_from: /it/chapters/lua.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
In questo capitolo parleremo della programmazione in Lua, degli strumenti necessari, e tratteremo alcune tecniche che troverai probabilmente utili.
|
||||||
|
|
||||||
|
- [Programmare](#programmare)
|
||||||
|
- [Programmare in Lua](#programmare-in-lua)
|
||||||
|
- [Editor di codice](#editor-di-codice)
|
||||||
|
- [Portata locale e globale](#portata-locale-e-globale)
|
||||||
|
- [Precedenza alla portata locale](#precedenza-alla-portata-locale)
|
||||||
|
- [Inclusione di altri script Lua](#inclusione-di-altri-script-lua)
|
||||||
|
|
||||||
|
|
||||||
|
## Programmare
|
||||||
|
|
||||||
|
Programmare è l'azione di prendere un problema, come ordinare una lista di oggetti, e tramutarlo in dei passaggi che il computer può comprendere.
|
||||||
|
|
||||||
|
Insegnarti i processi logici della programmazione non rientra nell'ambito di questo libro; tuttavia, i seguenti siti sono alquanto utili per approfondire l'argomento:
|
||||||
|
|
||||||
|
* [Codecademy](http://www.codecademy.com/) è una delle migliori risorse per imparare come scrivere codice; offre un'esperienza guidata interattiva.
|
||||||
|
* [Scratch](https://scratch.mit.edu) è una buona risorsa quando si comincia dalle basi assolute, imparando le tecniche di problem solving necessarie per la programmazione.\\
|
||||||
|
Scratch è *ideato per insegnare ai bambini* e non è un linguaggio serio di programmazione.
|
||||||
|
* [Programming with Mosh](https://www.youtube.com/user/programmingwithmosh) is
|
||||||
|
a good YouTube series to learn programming.
|
||||||
|
|
||||||
|
### Programmare in Lua
|
||||||
|
|
||||||
|
Neanche insegnarti come programmare in lua rientra nell'ambito di questo libro.
|
||||||
|
Tuttavia, se mastichi l'inglese puoi rifarti a quest'altro libro, ["Programming in Lua"](https://www.lua.org/pil/contents.html), per un'eccellente infarinatura sull'argomento. Se invece l'inglese non è il tuo forte, troverai comunque svariate guide in italiano in giro per la rete.
|
||||||
|
|
||||||
|
|
||||||
|
## Editor di codice
|
||||||
|
|
||||||
|
Un editor di codice con evidenziamento delle parole chiave è sufficiente per scrivere script in Lua.
|
||||||
|
L'evidenziamento assegna colori diversi a parole e caratteri diversi, a seconda del loro significato, permettendo quindi di individuare più facilmente eventuali errori e inconsistenze.
|
||||||
|
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function ctf.post(team,msg)
|
||||||
|
if not ctf.team(team) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if not ctf.team(team).log then
|
||||||
|
ctf.team(team).log = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(ctf.team(team).log,1,msg)
|
||||||
|
ctf.save()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Nel passaggio qui sopra, le parole chiave `if`, `then`, `end` e `return` sono evidenziate.
|
||||||
|
E Lo stesso vale per le funzioni interne di Lua come `table.insert`.
|
||||||
|
|
||||||
|
Tra gli editor più famosi che ben si prestano a lavorare in Lua, troviamo:
|
||||||
|
|
||||||
|
* [VSCode](https://code.visualstudio.com/) - software libero (come Code-OSS e VSCodium), rinomato, e che dispone di [estensioni per il modding su Minetest](https://marketplace.visualstudio.com/items?itemName=GreenXenith.minetest-tools).
|
||||||
|
* [Notepad++](http://notepad-plus-plus.org/) - Solo per Windows
|
||||||
|
|
||||||
|
(ne esistono ovviamente anche altri)
|
||||||
|
|
||||||
|
|
||||||
|
## Portata locale e globale
|
||||||
|
|
||||||
|
L'essere locale o globale di una variabile determina da dove è possibile accederci.
|
||||||
|
Una variabile locale è accessibile soltanto da dove viene definita. Ecco alcuni esempi:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Accessibile dall'interno dello script
|
||||||
|
local one = 1
|
||||||
|
|
||||||
|
function myfunc()
|
||||||
|
-- Accessibile dall'interno della funzione
|
||||||
|
local two = one + one
|
||||||
|
|
||||||
|
if two == one then
|
||||||
|
-- Accessible dall'interno del costrutto if
|
||||||
|
local three = one + two
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Mentre le variabili globali sono accessibili da qualsiasi script di qualsiasi mod.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function one()
|
||||||
|
foo = "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
function two()
|
||||||
|
print(dump(foo)) -- Output: "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
one()
|
||||||
|
two()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Precedenza alla portata locale
|
||||||
|
|
||||||
|
Le variabili locali dovrebbero venire usate il più possibile, con le mod che creano al massimo una globale corrispondente al nome della mod.
|
||||||
|
Crearne di ulteriori è considerato cattiva programmazione, e Minetest ci avviserà di ciò:
|
||||||
|
|
||||||
|
Assignment to undeclared global 'foo' inside function at init.lua:2
|
||||||
|
|
||||||
|
Per ovviare, usa `local`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function one()
|
||||||
|
local foo = "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
function two()
|
||||||
|
print(dump(foo)) -- Output: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
one()
|
||||||
|
two()
|
||||||
|
```
|
||||||
|
|
||||||
|
Ricorda che `nil` significa **non inizializzato**.
|
||||||
|
Ovvero la variabile non è stata ancora assegnata a un valore, non esiste o è stata deinizializzata (cioè impostata a `nil`)
|
||||||
|
|
||||||
|
La stessa cosa vale per le funzioni: esse sono variabili di tipo speciale, e dovrebbero essere dichiarate locali, in quanto altre mod potrebbero sennò avere funzioni con lo stesso nome.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function foo(bar)
|
||||||
|
return bar * 2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Per permettere alle mod di richiamare le tue funzioni, dovresti creare una tabella con lo stesso nome della mod e aggiungercele all'interno.
|
||||||
|
Questa tabella è spesso chiamata una API.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymod = {}
|
||||||
|
|
||||||
|
function mymod.foo(bar)
|
||||||
|
return "foo" .. bar
|
||||||
|
end
|
||||||
|
|
||||||
|
-- In un'altra mod o script:
|
||||||
|
mymod.foo("foobar")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inclusione di altri script Lua
|
||||||
|
|
||||||
|
Il metodo consigliato per includere in una mod altri script Lua è usare *dofile*.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
dofile(core.get_modpath("modname") .. "/script.lua")
|
||||||
|
```
|
||||||
|
|
||||||
|
Uno script può ritornare un valore, che è utile per condividere variabili locali private:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- script.lua
|
||||||
|
return "Hello world!"
|
||||||
|
|
||||||
|
-- init.lua
|
||||||
|
local ret = dofile(core.get_modpath("modname") .. "/script.lua")
|
||||||
|
print(ret) -- Hello world!
|
||||||
|
```
|
||||||
|
|
||||||
|
Nei [capitoli seguenti](../quality/clean_arch.html) si parlerà nel dettaglio di come suddividere il codice di una mod.
|
75
_it/games/games.md
Normal file
75
_it/games/games.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Creare giochi
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 7.1
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Il punto forte di Minetest è quello di poter sviluppare giochi con facilità senza il bisogno di costruirsi il proprio motore grafico voxel, i propri algoritmi voxel, o la propria parte network.
|
||||||
|
|
||||||
|
- [Cos'è un gioco?](#cosè-un-gioco)
|
||||||
|
- [Cartella di un gioco](#cartella-di-un-gioco)
|
||||||
|
- [Compatibilità tra giochi](#compatibilità-tra-giochi)
|
||||||
|
- [Compatibilità delle API](#compatibilità-delle-api)
|
||||||
|
- [Gruppi e alias](#gruppi-e-alias)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
## Cos'è un gioco?
|
||||||
|
|
||||||
|
I giochi sono una collezione di mod che lavorano insieme per creare un gioco coerente.
|
||||||
|
Un buon gioco ha una base consistente e una direzione: per esempio, potrebbe essere il classico gioco survival dove picconare e fabbricare oggetti, come potrebbe essere un simulatore spaziale con estetiche steampunk.
|
||||||
|
|
||||||
|
Il design di un gioco, tuttavia, è un argomento complesso, tanto che è una branca di specializzazione a parte.
|
||||||
|
L'intento del libro è giusto farne un accenno.
|
||||||
|
|
||||||
|
## Cartella di un gioco
|
||||||
|
|
||||||
|
La struttura e la collocazione di un gioco dovrebbero sembrare alquanto familiari dopo aver pasticciato con le mod.
|
||||||
|
Le cartelle dei giochi si trovano in `minetest/games/` e sono strutturate come segue:
|
||||||
|
|
||||||
|
mio_gioco
|
||||||
|
├── game.conf
|
||||||
|
├── menu
|
||||||
|
│ ├── header.png
|
||||||
|
│ ├── background.png
|
||||||
|
│ └── icon.png
|
||||||
|
├── minetest.conf
|
||||||
|
├── mods
|
||||||
|
│ └── ... mods
|
||||||
|
├── README.txt
|
||||||
|
└── settingtypes.txt
|
||||||
|
|
||||||
|
L'unica cosa necessaria è la cartella `mods`, ma è raccomandato anche l'inserimento di `game.conf` e `menu/icon.png`.
|
||||||
|
|
||||||
|
## Compatibilità tra giochi
|
||||||
|
|
||||||
|
### Compatibilità delle API
|
||||||
|
|
||||||
|
È buona norma provare a mantenere le API compatibili con quelle di Minetest Game quanto possibile, in quanto renderebbe il porting delle mod (in un altro gioco) molto più semplice.
|
||||||
|
|
||||||
|
Il modo migliore per mantenere la compatibilità tra un gioco e l'altro è di mantenere la stessa compatibilità nelle API delle mod che hanno lo stesso nome.
|
||||||
|
Cosicché, se una mod usa lo stesso nome di un'altra (come fare una mod chiamata `doors`, che già esiste in Minetest Game), non ci saranno problemi.
|
||||||
|
|
||||||
|
Questa compatibilità per le mod si traduce in due punti:
|
||||||
|
|
||||||
|
* Tabella API Lua - tutte le funzioni nella tabella globale (`mia_mod.funzionivarie`) che condividono lo stesso nome;
|
||||||
|
* Nodi e oggetti registrati.
|
||||||
|
|
||||||
|
Eventuali piccole rotture non sono la fine del mondo (come non avere una funzione che tanto veniva usata solo internamente), ma quando saltano le funzioni principali è un altro paio di maniche.
|
||||||
|
|
||||||
|
È difficile mantenere queste compatibilità con modpack disgustatamente grosse come la *default* in Minetest Game, dacché si dovrebbe evitare di chiamare una mod "default".
|
||||||
|
|
||||||
|
Infine, le compatibilità delle API si applicano anche a mod e giochi esterni, quindi assicurati che una mod nuova abbia un nome unico.
|
||||||
|
Per controllare se un nome è già stato preso, prova a cercarlo su [content.minetest.net](https://content.minetest.net/).
|
||||||
|
|
||||||
|
### Gruppi e alias
|
||||||
|
|
||||||
|
I gruppi e gli alias sono entrambi strumenti utili per mantenere la compatibilità tra giochi, in quanto permettono ai nomi degli oggetti di variare a seconda del gioco.
|
||||||
|
Nodi comuni come pietra e legno dovrebbero avere dei gruppi per indicarne il materiale.
|
||||||
|
È anche buona norma fornire degli alias che vanno dai nodi di base a qualsiasi eventuale rimpiazzo.
|
||||||
|
|
||||||
|
## Il tuo turno
|
||||||
|
|
||||||
|
* Crea un semplice gioco dove il giocatore guadagna punti allo scavare alcuni nodi speciali.
|
35
_it/index.md
Normal file
35
_it/index.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: Copertina
|
||||||
|
description: An easy guide to learn how to create mods for Minetest
|
||||||
|
layout: default
|
||||||
|
homepage: true
|
||||||
|
no_header: true
|
||||||
|
root: ..
|
||||||
|
idx: 0.1
|
||||||
|
---
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Minetest: Libro del Moddaggio</h1>
|
||||||
|
|
||||||
|
<span>di <a href="https://rubenwardy.com" rel="author">rubenwardy</a></span>
|
||||||
|
<span>con modifiche di <a href="http://rc.minetest.tv/">Shara</a></span>
|
||||||
|
<span>traduzione italiana di <a href="https://liberapay.com/Zughy/">Zughy</a></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
## Introduzione
|
||||||
|
|
||||||
|
Il moddaggio su Minetest è supportato grazie a script in Lua.
|
||||||
|
Questo libro mira a insegnarti come si crea una mod, iniziando dalle basi: ogni capitolo si concentra su un aspetto specifico dell'API, così da arrivare in breve tempo a farti creare i tuoi contenuti.
|
||||||
|
|
||||||
|
Oltre che [leggere questo libro su internet](https://rubenwardy.com/minetest_modding_book),
|
||||||
|
puoi anche [scaricarlo in HTML](https://github.com/rubenwardy/minetest_modding_book/releases).
|
||||||
|
|
||||||
|
### Riscontri e Contributi
|
||||||
|
|
||||||
|
Hai notato un errore o vuoi dirmi la tua? Assicurati di farmelo presente.
|
||||||
|
|
||||||
|
* Apri una [Segnalazione su GitLab](https://gitlab.com/rubenwardy/minetest_modding_book/-/issues).
|
||||||
|
* Rispondi alla [Discussione sul Forum](https://forum.minetest.net/viewtopic.php?f=14&t=10729).
|
||||||
|
* [Contattami (in inglese)](https://rubenwardy.com/contact/).
|
||||||
|
* Voglia di contribuire?
|
||||||
|
[Leggi il README](https://gitlab.com/rubenwardy/minetest_modding_book/-/blob/master/README.md).
|
184
_it/items/callbacks.md
Normal file
184
_it/items/callbacks.md
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
---
|
||||||
|
title: Richiami dei nodi e degli oggetti
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.15
|
||||||
|
description: Scopri i richiami, le azioni e gli eventi, come on_use, on_punch, on_place e on_rightclick
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Minetest usa una struttura di moddaggio estensivamente incentrata sui richiami. Un richiamo è una funzione che si dà a un'API e che viene chiamata quando l'evento registrato si verifica.
|
||||||
|
Per esempio, puoi aggiungere una funzione `on_punch` nella definizione di un nodo, che verrà chiamata quando questo viene colpito.
|
||||||
|
Ci sono poi anche dei richiami globali, come `core.register_on_punchnode`, che in questo caso verrà invocato al colpire qualsiasi nodo.
|
||||||
|
|
||||||
|
- [Richiami degli oggetti](#richiami-degli-oggetti)
|
||||||
|
- [on_use](#on_use)
|
||||||
|
- [on_place e on_secondary_use](#on_place-e-on_secondary_use)
|
||||||
|
- [on_drop](#on_drop)
|
||||||
|
- [after_use](#after_use)
|
||||||
|
- [item_place contro place_item](#item_place-contro-place_item)
|
||||||
|
- [Richiami dei nodi](#richiami-dei-nodi)
|
||||||
|
- [Tasto destro e nodi piazzati](#tasto-destro-e-nodi-piazzati)
|
||||||
|
- [Colpire e scavare](#colpire-e-scavare)
|
||||||
|
- [...e altro!](#e-altro)
|
||||||
|
|
||||||
|
|
||||||
|
## Richiami degli oggetti
|
||||||
|
|
||||||
|
Quando un giocatore ha un nodo, un oggetto fabbricabile o uno strumento nel proprio inventario, questi potrebbero innescare degli eventi:
|
||||||
|
|
||||||
|
| Richiamo | Assegnazione base | Valore base |
|
||||||
|
|------------------|---------------------------|----------------------------------------------|
|
||||||
|
| on_use | clic sinistro | nil |
|
||||||
|
| on_place | clic destro su un nodo | `core.item_place` |
|
||||||
|
| on_secondary_use | clic destro a vuoto | `core.item_secondary_use` (non fa nulla) |
|
||||||
|
| on_drop | Q | `core.item_drop` |
|
||||||
|
| after_use | allo scavare un nodo | nil |
|
||||||
|
|
||||||
|
|
||||||
|
### on_use
|
||||||
|
|
||||||
|
Sovrascrivere l'uso dell'oggetto impedisce che quest'ultimo possa essere usato per scavare nodi.
|
||||||
|
Un impiego comune di questo richiamo lo si trova nel cibo:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("miamod:fangotorta", {
|
||||||
|
description = "Torta aliena di fango",
|
||||||
|
inventory_image = "miamod_fangotorta.png",
|
||||||
|
on_use = core.item_eat(20),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Il numero fornito alla funzione core.item_eat è il numero di punti salute ripristinati al consumare il cibo.
|
||||||
|
In gioco ogni cuore equivale a due punti.
|
||||||
|
Un giocatore ha solitamente un massimo di 10 cuori, ovvero 20 punti salute, e quest'ultimi non devono per forza essere interi - bensì anche decimali.
|
||||||
|
|
||||||
|
`core.item_eat()` è una funzione che ritorna un'altra funzione, in questo caso quindi impostandola come richiamo di on_use.
|
||||||
|
Ciò significa che il codice in alto è alquanto simile al seguente:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("miamod:fangotorta", {
|
||||||
|
description = "Torta aliena di fango",
|
||||||
|
inventory_image = "miamod_fangotorta.png",
|
||||||
|
on_use = function(...)
|
||||||
|
return core.do_item_eat(20, nil, ...)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Capendo come funziona item_eat, è possibile modificarlo per operazioni più complesse
|
||||||
|
come per esempio riprodurre un suono personalizzato.
|
||||||
|
|
||||||
|
|
||||||
|
### on_place e on_secondary_use
|
||||||
|
|
||||||
|
La differenza tra `on_place` e `on_secondary_use` consiste nel fatto che `on_place` viene chiamato quando il giocatore sta puntando un nodo, mentre `on_secondary_use` quando non ne punta uno.
|
||||||
|
|
||||||
|
Entrambi i richiami sono invocati per tutti i tipi di oggetti.
|
||||||
|
`on_place` risponde alla funzione `core.item_place`, la quale o gestisce la chiamata a `on_rightclick` del nodo puntato, o piazza l'oggetto in mano se questo è un nodo.
|
||||||
|
|
||||||
|
|
||||||
|
### on_drop
|
||||||
|
|
||||||
|
`on_drop` viene chiamato quando il giocatore fa richiesta per buttare un oggetto, per esempio usando il tasto apposito (Q) o trascinando l'oggetto fuori dall'inventario.
|
||||||
|
Risponde alla funzione `core.item_drop`, la quale gestisce il buttare l'oggetto.
|
||||||
|
|
||||||
|
### after_use
|
||||||
|
|
||||||
|
`after_use` viene chiamato quando si scava un nodo, e permette di personalizzare come viene applicata l'usura a uno strumento.
|
||||||
|
Se `after_use` non esiste, è come se ci fosse scritto:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
after_use = function(itemstack, user, node, digparams)
|
||||||
|
itemstack:add_wear(digparams.wear)
|
||||||
|
return itemstack
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## item_place contro place_item
|
||||||
|
|
||||||
|
L'API di Minetest include varie implementazioni già pronte di richiami.
|
||||||
|
Queste seguono la nomenclatura "tipodioggetto_azione", per esempio `core.item_place` e `core.node_dig`.
|
||||||
|
Alcune sono usate direttamente, mentre altre sono funzioni che ritornano il richiamo vero e proprio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_item("miamod:esempio", {
|
||||||
|
on_place = core.item_place,
|
||||||
|
on_use = core.item_eat(10),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Inoltre, l'API di Minetest include funzioni già pronte che _fanno_ qualcosa.
|
||||||
|
Queste sono spesso chiamate con nomi che rischiano di farle confondere con le implementazioni dei richiami, tuttavia hanno un verbo all'inizio (per esempio `core.place_item` e `core.dig_node`, che permettono rispettivamente di scavare e piazzare nodi come se lo stesse facendo un giocatore).
|
||||||
|
|
||||||
|
|
||||||
|
## Richiami dei nodi
|
||||||
|
|
||||||
|
Quando un nodo si trova in un inventario, vengono invocati i richiami degli oggetti discussi poc'anzi.
|
||||||
|
Al contrario, quando un nodo è situato nel mondo, vengono invocati i richiami dei nodi.
|
||||||
|
Ce ne sono di svariati tipi, troppi per essere discussi in questo libro, tuttavia alcuni di questi verranno trattati nei capitoli successivi.
|
||||||
|
|
||||||
|
Molti richiami dei nodi sono collegati alle operazioni effettuate - appunto - sui nodi, come piazzarli e rimuoverli dal mondo.
|
||||||
|
È importante però sottolineare che, per motivi di prestazioni, operazioni come queste non vengono chiamate da modifiche in blocco (quelle che cambiano un grande numero di nodi in un colpo solo).
|
||||||
|
È meglio quindi non fare affidamento su un'esecuzione sicura al 100%.
|
||||||
|
|
||||||
|
|
||||||
|
### Tasto destro e nodi piazzati
|
||||||
|
|
||||||
|
Quando un utente preme col tasto destro un nodo mentre ha un oggetto in mano, viene invocato il richiamo `on_place` dell'oggetto.
|
||||||
|
Di base, questo è impostato a `core.item_place`.
|
||||||
|
Se il nodo puntato ha un richiamo `on_rightclick` e il tasto accovacciati (shift) è tenuto premuto, allora verrà chiamato `on_rightclick`.
|
||||||
|
Diversamente, `core.item_place` piazzerà il nodo.
|
||||||
|
|
||||||
|
Piazzare un nodo invocherà simultaneamente `on_construct` e `after_place_node`: il primo è chiamato da ogni evento che cambia i singoli nodi (quindi non in blocco) e ritorna la posizione e il valore del nodo.
|
||||||
|
`after_place_node` viene invece chiamato solamente al piazzare un nodo, contenendo di conseguenza più informazioni - come chi l'ha piazzato e l'ItemStack.
|
||||||
|
|
||||||
|
È importante notare che i giocatori non sono le uniche realtà che possono piazzare nodi; anche le entità e le mod possono farlo.
|
||||||
|
Per via di ciò, `place` potrebbe essere un giocatore, ma anche un'entità o `nil`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:mionodo", {
|
||||||
|
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
|
||||||
|
if clicker:is_player() then
|
||||||
|
core.chat_send_player(clicker:get_player_name(), "Ciao mondo!")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
on_construct = function(pos, node)
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("infotext", "Il mio nodo!")
|
||||||
|
end,
|
||||||
|
after_place_node = function(pos, placer, itemstack, pointed_thing)
|
||||||
|
-- controlla chi sta piazzando
|
||||||
|
if placer and placer:is_player() then
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("proprietario", placer:get_player_name())
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Colpire e scavare
|
||||||
|
|
||||||
|
Si ha un colpo quando un giocatore preme col tasto sinistro per un breve periodo.
|
||||||
|
Se l'oggetto in mano possiede un richiamo `on_use`, questo verrà chiamato.
|
||||||
|
Diversamente, verrà chiamato il richiamo `on_punch` sul nodo selezionato.
|
||||||
|
|
||||||
|
Quando il giocatore tenta di scavare un nodo, viene eseguito il richiamo `on_dig` del nodo.
|
||||||
|
Di base, ciò equivale a `core.node_dig`, che controlla eventuali protezioni dell'area, usura l'oggetto, rimuove il nodo, e ne esegue il richiamo `after_dig_node`.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:mionodo", {
|
||||||
|
on_punch = function(pos, node, puncher, pointed_thing)
|
||||||
|
if puncher:is_player() then
|
||||||
|
core.chat_send_player(puncher:get_player_name(), "Ahia!")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### ...e altro!
|
||||||
|
|
||||||
|
Dài un occhio alla API Lua di Minetest per una lista di tutti i richiami, e per avere più informazioni riguardo quelli vista qui sopra.
|
72
_it/items/creating_textures.md
Normal file
72
_it/items/creating_textures.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: Creare le texture
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.2
|
||||||
|
description: Un'introduzione sul come creare texture nel tuo editor di fiducia, e una guida a GIMP.
|
||||||
|
redirect_from: /it/chapters/creating_textures.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Essere in grado di creare e ottimizare le texture è un'abilità alquanto utile quando si sviluppa per Minetest.
|
||||||
|
Ci sono molti approcci sul come creare texture in pixel art, e capire questi approcci migliorerà nettamente la qualità dei tuoi lavori.
|
||||||
|
|
||||||
|
Fornire spiegazioni dettagliate non rientra tuttavia nell'ambito di questo libro: verranno quindi trattate solo le tecniche più semplici.
|
||||||
|
Se si vuole approfondire, ci sono comunque molti [buoni tutorial online](http://www.photonstorm.com/art/tutorials-art/16x16-pixel-art-tutorial) disponibili, che si occupano di pixel art in modo molto più dettagliato.
|
||||||
|
|
||||||
|
- [Tecniche](#tecniche)
|
||||||
|
- [Usare la matita](#usare-la-matita)
|
||||||
|
- [Piastrellatura (tiling)](#piastrellatura-tiling)
|
||||||
|
- [Trasparenza](#trasparenza)
|
||||||
|
- [Programmi](#programmi)
|
||||||
|
- [MS Paint](#ms-paint)
|
||||||
|
- [GIMP](#gimp)
|
||||||
|
|
||||||
|
## Tecniche
|
||||||
|
|
||||||
|
### Usare la matita
|
||||||
|
|
||||||
|
Lo strumento matita è disponibile nella maggior parte dei programmi di disegno.
|
||||||
|
Quando viene impostato alla dimensione minima, permette di disegnare un pixel alla volta senza alterare le atre parti dell'immagine.
|
||||||
|
Manipolando i singoli pixel si possono creare texture chiare e nette senza alcuna sfocatura non voluta, dando inoltre un alto livello di precisione e controllo.
|
||||||
|
|
||||||
|
### Piastrellatura (tiling)
|
||||||
|
|
||||||
|
Le texture usate per i nodi dovrebbero generalmente essere progettate per ripetersi come
|
||||||
|
delle piastrelle.
|
||||||
|
Questo significa che quando piazzi più nodi con la stessa texture vicini, i bordi dovranno allinearsi correttamente creando un effetto di continuità.
|
||||||
|
|
||||||
|
<!-- IMAGE NEEDED - cobblestone that tiles correctly -->
|
||||||
|
|
||||||
|
Se non riesci nell'allineamento, il risultato sarà molto meno
|
||||||
|
gradevole da vedere.
|
||||||
|
|
||||||
|
<!-- IMAGE NEEDED - node that doesn't tile correctly -->
|
||||||
|
|
||||||
|
### Trasparenza
|
||||||
|
|
||||||
|
La trasparenza è importante quando si creano texture per pressoché tutti gli oggetti fabbricabili e per alcuni nodi, come il vetro.
|
||||||
|
Non tutti i programmi supportano la trasparenza, perciò assicurati di sceglierne uno adatto ai tipi di texture che vuoi creare.
|
||||||
|
|
||||||
|
## Programmi
|
||||||
|
|
||||||
|
### MS Paint
|
||||||
|
|
||||||
|
MS Paint è un programma di disegno davvero semplice che può rivelarsi utile
|
||||||
|
per la creazione di texture base; tuttavia, non supporta la trasparenza.
|
||||||
|
Ciò di solitò non farà differenza finché ci si limiterà alle facce di un nodo (a parte nodi come il vetro),
|
||||||
|
tuttavia se la trasparenza è un requisito nelle tue texture dovresti guardare oltre.
|
||||||
|
|
||||||
|
### GIMP
|
||||||
|
|
||||||
|
GIMP viene impiegato spesso nella comunità di Minetest.
|
||||||
|
Ha una curva di apprendimento alquanto alta, dato che molte delle sue funzioni non risultano ovvie nell'immediato.
|
||||||
|
|
||||||
|
Quando usi GIMP, puoi selezionare la matita dalla Barra degli Strumenti:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/pixel_art_gimp_pencil.png" alt="La matita su GIMP">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
È anche consigliato spuntare l'opzione "Margine netto" per la gomma.
|
319
_it/items/inventories.md
Executable file
319
_it/items/inventories.md
Executable file
@ -0,0 +1,319 @@
|
|||||||
|
---
|
||||||
|
title: ItemStack e inventari
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.4
|
||||||
|
description: Manipola gli InvRef e gli ItemStack
|
||||||
|
redirect_from:
|
||||||
|
- /it/chapters/inventories.html
|
||||||
|
- /it/chapters/itemstacks.html
|
||||||
|
- /it/inventories/inventories.html
|
||||||
|
- /it/inventories/itemstacks.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
In questo capitolo, imparerai come usare e manipolare gli inventari, siano essi quelli di un giocatore, di un nodo o a sé stanti.
|
||||||
|
|
||||||
|
- [Cosa sono gli ItemStack e gli inventari?](#cosa-sono-gli-itemstack-e-gli-inventari)
|
||||||
|
- [ItemStack](#itemstack)
|
||||||
|
- [Collocazione inventari](#collocazione-inventari)
|
||||||
|
- [Liste](#liste)
|
||||||
|
- [Dimensione e ampiezza](#dimensione-e-ampiezza)
|
||||||
|
- [Controllare il contenuto](#controllare-il-contenuto)
|
||||||
|
- [Modificare inventari e ItemStack](#modificare-inventari-e-itemstack)
|
||||||
|
- [Aggiungere a una lista](#aggiungere-a-una-lista)
|
||||||
|
- [Rimuovere oggetti](#rimuovere-oggetti)
|
||||||
|
- [Manipolazione pile](#manipolazione-pile)
|
||||||
|
- [Usura](#usura)
|
||||||
|
- [Tabelle Lua](#tabelle-lua)
|
||||||
|
|
||||||
|
## Cosa sono gli ItemStack e gli inventari?
|
||||||
|
|
||||||
|
Un ItemStack ( lett. "pila di oggetti") è il dato dietro una singola cella di un inventario.
|
||||||
|
|
||||||
|
Un *inventario* è una collezione di *liste* apposite, ognuna delle quali è una griglia 2D di ItemStack.
|
||||||
|
Lo scopo di un inventario è quello di raggruppare più liste in un singolo oggetto (l'inventario appunto), in quanto a ogni giocatore e a ogni nodo ne può essere associato massimo uno.
|
||||||
|
|
||||||
|
## ItemStack
|
||||||
|
|
||||||
|
Gli ItemStack sono composti da quattro parametri: nome, quantità, durabilità e metadati.
|
||||||
|
|
||||||
|
Il nome dell'oggetto può essere il nome di un oggetto registrato, di uno sconosciuto (non registrato) o un alias.
|
||||||
|
Gli oggetti sconosciuti sono tipici di quando si disinstallano le mod, o quando le mod rimuovono degli oggetti senza nessun accorgimento, tipo senza registrarne un alias.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_name())
|
||||||
|
stack:set_name("default:dirt")
|
||||||
|
|
||||||
|
if not stack:is_known() then
|
||||||
|
print("È un oggetto sconosciuto!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
La quantità sarà sempre 0 o maggiore.
|
||||||
|
Durante una normale sessione di gioco, la quantità non dovrebbe mai essere maggiore della dimensione massima della pila dell'oggetto - `stack_max`.
|
||||||
|
Tuttavia, comandi da amministratore e mod fallate potrebbero portare a oggetti impilati che superano la grandezza massima.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_stack_max())
|
||||||
|
```
|
||||||
|
|
||||||
|
Un ItemStack può essere vuoto, nel qual caso avrà come quantità 0.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_count())
|
||||||
|
stack:set_count(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Gli ItemStack possono poi essere creati in diversi modi usando l'omonima funzione.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
ItemStack() -- name="", count=0
|
||||||
|
ItemStack("default:pick_stone") -- count=1
|
||||||
|
ItemStack("default:stone 30")
|
||||||
|
ItemStack({ name = "default:wood", count = 10 })
|
||||||
|
```
|
||||||
|
|
||||||
|
I metadati di un oggetto sono una o più coppie chiave-valore custodite in esso.
|
||||||
|
Chiave-valore significa che si usa un nome (la chiave) per accedere al dato corrispettivo (il valore).
|
||||||
|
Alcune chiavi hanno significati predefiniti, come `description` che è usato per specificare la descrizione di una pila di oggetti.
|
||||||
|
Questo sarà trattato più in dettaglio nel capitolo Storaggio e Metadati.
|
||||||
|
|
||||||
|
## Collocazione inventari
|
||||||
|
|
||||||
|
La collocazione di un inventario è dove e come un inventario viene conservato.
|
||||||
|
Ci sono tre tipi di collocazione: giocatore, nodo e separata.
|
||||||
|
Un inventario è direttamente legato a una e a una sola collocazione.
|
||||||
|
|
||||||
|
Gli inventari collocati nei nodi sono associati alle coordinate di un nodo specifico, come le casse.
|
||||||
|
Il nodo deve essere stato caricato perché viene salvato [nei suoi metadati](../map/storage.html#metadata).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({ type="node", pos={x=1, y=2, z=3} })
|
||||||
|
```
|
||||||
|
|
||||||
|
L'esempio in alto ottiene il *riferimento a un inventario*, comunemente definito *InvRef*.
|
||||||
|
Questi riferimenti sono usati per manipolare l'inventario, e son chiamati così perché i dati non sono davvero salvati dentro all'oggetto (in questo caso "inv"), bensì *puntano* a quei dati.
|
||||||
|
In questo modo, modificando "inv", stiamo in verità modificando l'inventario.
|
||||||
|
|
||||||
|
La collocazione di tali riferimenti può essere ottenuta nel seguente modo:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local location = inv:get_location()
|
||||||
|
```
|
||||||
|
|
||||||
|
Gli inventari dei giocatori si ottengono in maniera simile, oppure usando il riferimento a un giocatore (*PlayerRef*).
|
||||||
|
In entrambi casi, il giocatore deve essere connesso.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({ type="player", name="player1" })
|
||||||
|
-- oppure
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
```
|
||||||
|
|
||||||
|
Gli inventari separati, infine, sono quelli non collegati né a nodi né a giocatori, e al contrario degli altri, vengono persi dopo un riavvio.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({
|
||||||
|
type="detached", name="nome_inventario" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Un'ulteriore differenza, è che gli inventari separati devono essere creati prima di poterci accedere:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.create_detached_inventory("inventory_name")
|
||||||
|
```
|
||||||
|
|
||||||
|
La funzione `create_detached_inventory` accetta 3 parametri, di cui solo il primo - il nome - è necessario.
|
||||||
|
Il secondo parametro prende una tabella di callback, che possono essere utilizzati per controllare come i giocatori interagiscono con l'inventario:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Input only detached inventory
|
||||||
|
core.create_detached_inventory("inventory_name", {
|
||||||
|
allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
|
||||||
|
return count -- permette di spostare gli oggetti
|
||||||
|
end,
|
||||||
|
|
||||||
|
allow_put = function(inv, listname, index, stack, player)
|
||||||
|
return stack:get_count() -- permette di inserirli
|
||||||
|
end,
|
||||||
|
|
||||||
|
allow_take = function(inv, listname, index, stack, player)
|
||||||
|
return 0 -- non permette di rimuoverli
|
||||||
|
end,
|
||||||
|
|
||||||
|
on_put = function(inv, listname, index, stack, player)
|
||||||
|
core.chat_send_all(player:get_player_name() ..
|
||||||
|
" ha messo " .. stack:to_string() ..
|
||||||
|
" nella cassa delle donazioni da " .. core.pos_to_string(player:get_pos()))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
I callback dei permessi - quelle che iniziano con `allow_` - ritornano il numero degli oggetti da trasferire, e si usa 0 per impedirne del tutto l'azione.
|
||||||
|
|
||||||
|
I callback delle azioni - quelle che iniziano con `on_` - non ritornano invece alcun valore.
|
||||||
|
|
||||||
|
## Liste
|
||||||
|
|
||||||
|
Le liste negli inventari permettono di disporre più griglie nello stesso luogo (l'inventario).
|
||||||
|
Esse sono particolarmente utili per il giocatore, e infatti di base ogni gioco possiede già delle liste come *main* per il corpo principale dell'inventario e *craft* per l'area di fabbricazione.
|
||||||
|
|
||||||
|
### Dimensione e ampiezza
|
||||||
|
|
||||||
|
Le liste hanno una dimensione, equivalente al numero totale di celle nella griglia, e un'ampiezza, che è usata esclusivamente dentro il motore di gioco: quando viene disegnato un inventario in una finestra, infatti, il codice dietro di essa già determina che ampiezza usare.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:set_size("main", 32) then
|
||||||
|
inv:set_width("main", 8)
|
||||||
|
print("dimensione: " .. inv.get_size("main"))
|
||||||
|
print("ampiezza: " .. inv:get_width("main"))
|
||||||
|
else
|
||||||
|
print("Errore! Nome dell'oggetto o dimensione non validi")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_size` non andrà in porto e ritornerà "false" se il nome della lista o la dimensione dichiarata non risultano valide.
|
||||||
|
Per esempio, la nuova dimensione potrebbe essere troppo piccola per contenere gli oggetti attualmente presenti nell'inventario.
|
||||||
|
|
||||||
|
### Controllare il contenuto
|
||||||
|
|
||||||
|
`is_empty` può essere usato per vedere se una lista contiene o meno degli oggetti:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:is_empty("main") then
|
||||||
|
print("La lista è vuota!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`contains_item` può invece essere usato per vedere se la lista contiene un oggetto specifico:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:contains_item("main", "default:stone") then
|
||||||
|
print("Ho trovato della pietra!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modificare inventari e ItemStack
|
||||||
|
|
||||||
|
### Aggiungere a una lista
|
||||||
|
|
||||||
|
Per aggiungere degli oggetti a una lista (in questo caso "main") usiamo `add_item`.
|
||||||
|
Nell'esempio sottostante ci accertiamo anche di rispettare la dimensione:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = ItemStack("default:stone 99")
|
||||||
|
local leftover = inv:add_item("main", stack)
|
||||||
|
if leftover:get_count() > 0 then
|
||||||
|
print("L'inventario è pieno! " ..
|
||||||
|
leftover:get_count() .. " oggetti non sono stati aggiunti")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rimuovere oggetti
|
||||||
|
|
||||||
|
Per rimuovere oggetti da una lista, `remove_item`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local taken = inv:remove_item("main", stack)
|
||||||
|
print("Rimossi " .. taken:get_count())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manipolare pile
|
||||||
|
|
||||||
|
Puoi modificare le singole pile prima ottenendole:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = inv:get_stack(listname, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
E poi modificandole impostando le nuove proprietà o usando i metodi che rispettano `stack_size`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pila = ItemStack("default:stone 50")
|
||||||
|
local da_aggiungere = ItemStack("default:stone 100")
|
||||||
|
local resto = pila:add_item(da_aggiungere)
|
||||||
|
local rimossi = pila:take_item(19)
|
||||||
|
|
||||||
|
print("Impossibile aggiungere " .. resto:get_count() .. " degli oggetti.")
|
||||||
|
-- ^ sarà 51
|
||||||
|
|
||||||
|
print("Hai " .. pila:get_count() .. " oggetti")
|
||||||
|
-- ^ sarà 80
|
||||||
|
-- min(50+100, stack_max) - 19 = 80
|
||||||
|
-- dove stack_max = 99
|
||||||
|
```
|
||||||
|
|
||||||
|
`add_item` aggiungerà gli oggetti all'ItemStack e ritornerà quelli in eccesso.
|
||||||
|
`take_item` rimuoverà gli oggetti indicati (o meno se ce ne sono meno), e ritornerà l'ammontare rimosso.
|
||||||
|
|
||||||
|
Infine, si imposta la pila modificata:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
inv:set_stack(listname, 0, pila)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usura
|
||||||
|
|
||||||
|
Gli strumenti possono avere un livello di usura; essa è rappresentata da un barra progressiva e fa rompere lo strumento quando completamente logorato.
|
||||||
|
Nello specifico, l'usura è un numero da 0 a 65535: più è alto, più è consumato l'oggetto.
|
||||||
|
|
||||||
|
Il livello di usura può essere manipolato usando `add_wear()`, `get_wear()`, e `set_wear(wear)`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pila = ItemStack("default:pick_mese")
|
||||||
|
local usi_massimi = 10
|
||||||
|
|
||||||
|
-- Questo viene fatto in automatico quando usi uno strumento che scava cose.
|
||||||
|
-- Aumenta l'usura dell'oggetto dopo un uso
|
||||||
|
pila:add_wear(65535 / (usi_massimi - 1))
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando si scava un nodo, l'incremento di usura di uno strumento dipende da che tipo di nodo è.
|
||||||
|
Di conseguenza, `usi_massimi` varia a seconda di cos'è stato scavato.
|
||||||
|
|
||||||
|
## Tabelle Lua
|
||||||
|
|
||||||
|
Gli ItemStack e gli inventari possono essere convertiti in/dalle tabelle.
|
||||||
|
Questo è utile per operazioni di copiatura e immagazzinaggio.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Inventario intero
|
||||||
|
local data = inv1:get_lists()
|
||||||
|
inv2:set_lists(data)
|
||||||
|
|
||||||
|
-- Una lista
|
||||||
|
local listdata = inv1:get_list("main")
|
||||||
|
inv2:set_list("main", listdata)
|
||||||
|
```
|
||||||
|
|
||||||
|
La tabella di liste ritornata da `get_lists()` sarà nel seguente formato:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
lista_uno = {
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
-- inv:get_size("lista_uno") elementi
|
||||||
|
},
|
||||||
|
lista_due = {
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
ItemStack,
|
||||||
|
-- inv:get_size("lista_due") elementi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`get_list()` ritornerà una lista singola fatta di ItemStack.
|
||||||
|
|
||||||
|
Una cosa importante da sottolineare è che i metodi `set` qui in alto non cambiano la dimensione delle liste.
|
||||||
|
Questo significa che si può svuotare una lista dichiarandola uguale a una tabella vuota, e la sua dimensione tuttavia non cambierà:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
inv:set_list("main", {})
|
||||||
|
```
|
413
_it/items/node_drawtypes.md
Normal file
413
_it/items/node_drawtypes.md
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
---
|
||||||
|
title: Tipi di nodo
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.3
|
||||||
|
description: Guida su tutti i tipi di nodo, inclusi cuboidi e mesh.
|
||||||
|
redirect_from: /it/chapters/node_drawtypes.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Il metodo col quale un nodo viene disegnato in gioco è chiamato *drawtype*.
|
||||||
|
Ci sono diversi tipi di drawtype: il loro comportamento è determinato dalle proprietà impostate durante la definizione del tipo di nodo.
|
||||||
|
Queste proprietà sono fisse, uguali per tutte le istanze, tuttavia è possibile manipolarne alcune per singolo nodo usando una cosa chiamata `param2`.
|
||||||
|
|
||||||
|
Il concetto di nodo è stato introdotto nello scorso capitolo, ma non è mai stata data una definizione completa.
|
||||||
|
Il mondo di Minetest è una griglia 3D: un nodo è un punto di quella griglia ed è composto da un tipo (`name`) e due parametri (`param1` e `param2`).
|
||||||
|
Non farti inoltre ingannare dalla funzione `core.register_node`, in quanto è un po' fuorviante: essa non registra infatti un nuovo nodo (c'è solo una definizione di nodo), bensì un nuovo *tipo* di nodo.
|
||||||
|
|
||||||
|
I parametri sono infine usati per controllare come un nodo viene renderizzato individualmente: `param1` immagazzina le proprietà di luce, mentre il ruolo di `param2` dipende dalla proprietà `paramtype2`, la quale è situata nella definizione dei singoli tipi.
|
||||||
|
|
||||||
|
- [Nodi cubici: normali e a facciate piene](#nodi-cubici-normali-e-a-facciate-piene)
|
||||||
|
- [Nodi vitrei](#nodi-vitrei)
|
||||||
|
- [Vitreo incorniciato](#vitreo-incorniciato)
|
||||||
|
- [Nodi d'aria](#nodi-d-aria)
|
||||||
|
- [Luce e propagazione solare](#luce-e-propagazione-solare)
|
||||||
|
- [Nodi liquidi](#nodi-liquidi)
|
||||||
|
- [Nodi complessi](#nodi-complessi)
|
||||||
|
- [Nodi complessi a muro](#nodi-complessi-a-muro)
|
||||||
|
- [Nodi mesh](#nodi-mesh)
|
||||||
|
- [Nodi insegna](#nodi-insegna)
|
||||||
|
- [Nodi pianta](#nodi-pianta)
|
||||||
|
- [Nodi fiamma](#firelike-nodes)
|
||||||
|
- [Altri drawtype](#altri-drawtype)
|
||||||
|
|
||||||
|
|
||||||
|
## Nodi cubici: normali e a facciate piene
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_normal.png" alt="Drawtype normale">
|
||||||
|
<figcaption>
|
||||||
|
Drawtype normale
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Il *drawtype* normale è tipicamente usato per renderizzare un nodo cubico.
|
||||||
|
Se il lato di uno di questi nodi tocca un nodo solido, allora quel lato non sarà renderizzato, risultando in un grande guadagno sulle prestazioni.
|
||||||
|
|
||||||
|
Al contrario, i *drawtype* a facciate piene (*allfaces*) renderizzeranno comunque il lato interno quando è contro un nodo solido.
|
||||||
|
Ciò è buono per quei nodi con facce in parte trasparenti come le foglie.
|
||||||
|
Puoi inoltre usare il drawtype `allfaces_optional` per permettere agli utenti di fare opt-out dal rendering più pesante, facendo comportare il nodo come se fosse di tipo normale.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:diamante", {
|
||||||
|
description = "Diamante alieno",
|
||||||
|
tiles = {"miamod_diamante.png"},
|
||||||
|
groups = {cracky = 3},
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_node("default:foglie", {
|
||||||
|
description = "Foglie",
|
||||||
|
drawtype = "allfaces_optional",
|
||||||
|
tiles = {"default_foglie.png"}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Attenzione: il drawtype normale è quello predefinito, quindi non c'è bisogno di specificarlo ogni volta.
|
||||||
|
|
||||||
|
## Nodi vitrei
|
||||||
|
|
||||||
|
La differenza tra i nodi vitrei (*glasslike*) e quelli normali è che piazzando i primi vicino a un nodo normale, non nasconderanno il lato di quest'ultimo.
|
||||||
|
Questo è utile in quanto i nodi vitrei tendono a essere trasparenti, perciò permettono di vedere attraverso.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_glasslike_edges.png" alt="Bordi vitrei">
|
||||||
|
<figcaption>
|
||||||
|
Bordi vitrei
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:obsidian_glass", {
|
||||||
|
description = "Vetro d'ossidiana",
|
||||||
|
drawtype = "glasslike",
|
||||||
|
tiles = {"default_obsidian_glass.png"},
|
||||||
|
paramtype = "light",
|
||||||
|
is_ground_content = false,
|
||||||
|
sunlight_propagates = true,
|
||||||
|
sounds = default.node_sound_glass_defaults(),
|
||||||
|
groups = {cracky=3,oddly_breakable_by_hand=3},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vitreo incorniciato
|
||||||
|
|
||||||
|
Questa opzione crea un solo bordo lungo tutto l'insieme di nodi, al posto di crearne più per singolo nodo.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_glasslike_framed.png" alt="Bordi vitrei incorniciati">
|
||||||
|
<figcaption>
|
||||||
|
Bordi vitrei incorniciati
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:glass", {
|
||||||
|
description = "Vetro",
|
||||||
|
drawtype = "glasslike_framed",
|
||||||
|
tiles = {"default_glass.png", "default_glass_detail.png"},
|
||||||
|
inventory_image = core.inventorycube("default_glass.png"),
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true, -- Sunlight can shine through block
|
||||||
|
groups = {cracky = 3, oddly_breakable_by_hand = 3},
|
||||||
|
sounds = default.node_sound_glass_defaults()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Puoi inoltre usare il *drawtype* `glasslike_framed_optional` per permettere un opt-in all'utente.
|
||||||
|
|
||||||
|
## Nodi d'aria
|
||||||
|
|
||||||
|
I nodi d'aria (*airlike*) non sono renderizzati e perciò non hanno texture.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miaaria:aria", {
|
||||||
|
description = "Mia Aria",
|
||||||
|
drawtype = "airlike",
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true,
|
||||||
|
|
||||||
|
walkable = false, -- Il giocatore può collidere col nodo
|
||||||
|
pointable = false, -- Non è selezionabile
|
||||||
|
diggable = false, -- Non può essere scavato
|
||||||
|
buildable_to = true, -- Può essere rimpiazzato da altri nodi
|
||||||
|
-- (basta costruire nella stessa coordinata)
|
||||||
|
|
||||||
|
air_equivalent = true,
|
||||||
|
drop = "",
|
||||||
|
groups = {not_in_creative_inventory=1}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Luce e propagazione solare
|
||||||
|
|
||||||
|
La luce di un nodo è salvata in `param1`.
|
||||||
|
Per capire come ombreggiare il lato di un nodo, viene utilizzato il valore di luminosità dei nodi adiacenti.
|
||||||
|
Questo comporta un blocco della luce da parte dei nodi solidi.
|
||||||
|
|
||||||
|
Di base, non viene salvata la luce in nessun nodo né nelle sue istanze.
|
||||||
|
È invece solitamente preferibile farla passare in tipi quali quelli d'aria e vitrei.
|
||||||
|
Per fare ciò, ci sono due proprietà che devono essere definite:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true,
|
||||||
|
```
|
||||||
|
|
||||||
|
La prima riga dice a `param1` di immagazzinare l'indice di luminosità, mentre la seconda permette alla luce del sole di propagarsi attraverso il nodo senza diminuire il proprio valore.
|
||||||
|
|
||||||
|
## Nodi liquidi
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_liquid.png" alt="Drawtype liquido">
|
||||||
|
<figcaption>
|
||||||
|
Drawtype liquido
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Ogni tipo di liquido richiede due definizioni di nodi: una per la sorgente e l'altra per il liquido che scorre.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Alcune proprietà sono state rimosse perché non
|
||||||
|
-- rilevanti per questo capitolo
|
||||||
|
core.register_node("default:water_source", {
|
||||||
|
drawtype = "liquid",
|
||||||
|
paramtype = "light",
|
||||||
|
|
||||||
|
inventory_image = core.inventorycube("default_water.png"),
|
||||||
|
-- ^ questo è necessario per impedire che l'immagine nell'inventario sia animata
|
||||||
|
|
||||||
|
tiles = {
|
||||||
|
{
|
||||||
|
name = "default_water_source_animated.png",
|
||||||
|
animation = {
|
||||||
|
type = "vertical_frames",
|
||||||
|
aspect_w = 16,
|
||||||
|
aspect_h = 16,
|
||||||
|
length = 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
special_tiles = {
|
||||||
|
-- Nuovo stile per il materiale dell'acqua statica (praticamente inutilizzato)
|
||||||
|
{
|
||||||
|
name = "default_water_source_animated.png",
|
||||||
|
animation = {type = "vertical_frames", aspect_w = 16,
|
||||||
|
aspect_h = 16, length = 2.0},
|
||||||
|
backface_culling = false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Comportamento
|
||||||
|
--
|
||||||
|
walkable = false, -- Il giocatore può attraversarlo
|
||||||
|
pointable = false, -- Il giocatore non può selezionarlo
|
||||||
|
diggable = false, -- Il giocatore non può scavarlo
|
||||||
|
buildable_to = true, -- Può essere rimpiazzato da altri nodi
|
||||||
|
|
||||||
|
alpha = 160,
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Proprietà del liquido
|
||||||
|
--
|
||||||
|
drowning = 1,
|
||||||
|
liquidtype = "source",
|
||||||
|
|
||||||
|
liquid_alternative_flowing = "default:water_flowing",
|
||||||
|
-- ^ quando scorre
|
||||||
|
|
||||||
|
liquid_alternative_source = "default:water_source",
|
||||||
|
-- ^ quando è sorgente (statico)
|
||||||
|
|
||||||
|
liquid_viscosity = WATER_VISC,
|
||||||
|
-- ^ quanto veloce
|
||||||
|
|
||||||
|
liquid_range = 8,
|
||||||
|
-- ^ quanto lontano
|
||||||
|
|
||||||
|
post_effect_color = {a=64, r=100, g=100, b=200},
|
||||||
|
-- ^ colore dello schermo quando il player ne è immerso
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
I nodi fluidi hanno una definizione simile, ma con nome e animazione differenti.
|
||||||
|
Guarda default:water_flowing nella mod default di minetest_game per un esempio completo.
|
||||||
|
|
||||||
|
|
||||||
|
## Nodi complessi
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_nodebox.gif" alt="Drawtype complesso">
|
||||||
|
<figcaption>
|
||||||
|
Drawtype complesso
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
I nodi complessi (*nodebox*) ti permettono di creare un nodo che non è cubico, bensì un insieme di più cuboidi.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("stairs:stair_stone", {
|
||||||
|
drawtype = "nodebox",
|
||||||
|
paramtype = "light",
|
||||||
|
node_box = {
|
||||||
|
type = "fixed",
|
||||||
|
fixed = {
|
||||||
|
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
||||||
|
{-0.5, 0, 0, 0.5, 0.5, 0.5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
La parte più importante è la tabella `node_box`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
||||||
|
{-0.5, 0, 0, 0.5, 0.5, 0.5}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ogni riga corrisponde a un cuboide e l'insieme delle righe forma il nodo complesso: i primi tre numeri sono le coordinate (da -0.5 a 0.5) dell'angolo davanti in basso a sinistra, mentre gli altri tre equivalgono all'angolo opposto.
|
||||||
|
Essi sono in formato X, Y, Z, dove Y indica il sopra.
|
||||||
|
|
||||||
|
|
||||||
|
Puoi usare [NodeBoxEditor](https://forum.minetest.net/viewtopic.php?f=14&t=2840) per creare nodi complessi più facilmente, in quanto permette di vedere in tempo reale le modifiche sul nodo che si sta modellando.
|
||||||
|
|
||||||
|
### Nodi complessi a muro
|
||||||
|
|
||||||
|
Certe volte si vogliono avere nodi complessi che cambiano a seconda della loro posizione sul pavimento, sul muro e sul soffitto, come le torce.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:sign_wall", {
|
||||||
|
drawtype = "nodebox",
|
||||||
|
node_box = {
|
||||||
|
type = "wallmounted",
|
||||||
|
|
||||||
|
-- Soffitto
|
||||||
|
wall_top = {
|
||||||
|
{-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Pavimento
|
||||||
|
wall_bottom = {
|
||||||
|
{-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Muro
|
||||||
|
wall_side = {
|
||||||
|
{-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nodi mesh
|
||||||
|
|
||||||
|
Mentre i nodi complessi sono generalmente più semplici da fare, essi sono limitati in quanto possono essere composti solo da cuboidi.
|
||||||
|
I nodi complessi sono anche non ottimizzati: le facce interne, infatti, saranno comunque renderizzate, anche quando completamente nascoste.
|
||||||
|
|
||||||
|
Una faccia è una superficie piatta di una mesh.
|
||||||
|
Una faccia interna appare quando le facce di due nodi complessi si sovrappongono, rendendo invisibili parti del modello ma renderizzandole comunque.
|
||||||
|
|
||||||
|
Puoi registrare un nodo mesh come segue:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:meshy", {
|
||||||
|
drawtype = "mesh",
|
||||||
|
|
||||||
|
-- Contiene le texture di ogni materiale
|
||||||
|
tiles = {
|
||||||
|
"mymod_meshy.png"
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Percorso della mesh
|
||||||
|
mesh = "mymod_meshy.b3d",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Assicurati che la mesh sia presente nella cartella `models`.
|
||||||
|
La maggior parte delle volte la mesh dovrebbe essere nella cartella della tua mod, tuttavia è ok condividere una mesh fornita da un'altra mod dalla quale dipendi.
|
||||||
|
Per esempio, una mod che aggiunge più tipi di mobili potrebbe usfruire di un modello fornito da una mod di mobili base.
|
||||||
|
|
||||||
|
## Nodi insegna
|
||||||
|
|
||||||
|
I nodi insegna (*signlike*) sono nodi piatti che possono essere affissi sulle facce di altri nodi.
|
||||||
|
|
||||||
|
Al contrario del loro nome, i cartelli non rientrano nei nodi insegna bensì in quelli complessi, per fornire un effetto 3D.
|
||||||
|
I tipi insegna tuttavia, sono comunemente usati dalle scale a pioli.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:ladder_wood", {
|
||||||
|
drawtype = "signlike",
|
||||||
|
|
||||||
|
tiles = {"default_ladder_wood.png"},
|
||||||
|
|
||||||
|
-- Necessario: memorizza la rotazione in param2
|
||||||
|
paramtype2 = "wallmounted",
|
||||||
|
|
||||||
|
selection_box = {
|
||||||
|
type = "wallmounted",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nodi pianta
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_plantlike.png" alt="Drawtype pianta">
|
||||||
|
<figcaption>
|
||||||
|
Drawtype pianta
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
I nodi pianta (*plantlike*) raffigurano la loro texture in un pattern a forma di X.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:papyrus", {
|
||||||
|
drawtype = "plantlike",
|
||||||
|
|
||||||
|
-- Viene usata solo una texture
|
||||||
|
tiles = {"default_papyrus.png"},
|
||||||
|
|
||||||
|
selection_box = {
|
||||||
|
type = "fixed",
|
||||||
|
fixed = {-6 / 16, -0.5, -6 / 16, 6 / 16, 0.5, 6 / 16},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nodi fiamma
|
||||||
|
|
||||||
|
I nodi fiamma (*firelike*) sono simili ai pianta, ad eccezione del fatto che sono ideati per avvinghiarsi ai muri e ai soffitti.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_firelike.png" alt="Drawtype fiamma">
|
||||||
|
<figcaption>
|
||||||
|
Drawtype fiamma
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:avvinghiatutto", {
|
||||||
|
drawtype = "firelike",
|
||||||
|
|
||||||
|
-- Viene usata solo una texture
|
||||||
|
tiles = { "miamod:avvinghiatutto" },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Altri drawtype
|
||||||
|
|
||||||
|
Questa non è una lista esaustiva, in quanto ci sono infatti altri tipi di nodi come:
|
||||||
|
|
||||||
|
* Nodi staccionata
|
||||||
|
* Nodi pianta radicata - per quelle acquatiche
|
||||||
|
* Nodi rotaia - per i binari del carrello
|
||||||
|
* Nodi torcia - per nodi 2D su pavimenti/muri/soffitti.
|
||||||
|
Le torce in Minetest Game usano in verità due diverse definizioni dei
|
||||||
|
nodi mesh (default:torch e default:torch_wall).
|
||||||
|
|
||||||
|
Come al solito, consulta la [documentazione sull'API Lua](https://minetest.gitlab.io/minetest/nodes/#node-drawtypes) per l'elenco completo.
|
296
_it/items/nodes_items_crafting.md
Normal file
296
_it/items/nodes_items_crafting.md
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
---
|
||||||
|
title: Nodi, Oggetti e Fabbricazione
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.1
|
||||||
|
description: Impara come registrare nodi, oggetti e ricette di fabbricazione usando register_node, register_item e register_craft.
|
||||||
|
redirect_from: /it/chapters/nodes_items_crafting.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Saper registrare nuovi nodi, oggetti fabbricabili e conseguenti ricette, è un requisito fondamentale per molte mod.
|
||||||
|
|
||||||
|
- [Cosa sono i nodi e gli oggetti?](#cosa-sono-i-nodi-e-gli-oggetti)
|
||||||
|
- [Registrare gli oggetti](#registrare-gli-oggetti)
|
||||||
|
- [Nomi oggetto](#nomi-oggetto)
|
||||||
|
- [Alias](#alias)
|
||||||
|
- [Texture](#texture)
|
||||||
|
- [Registrare un nodo base](#registrare-un-nodo-base)
|
||||||
|
- [Fabbricazione](#fabbricazione)
|
||||||
|
- [Fisse (shaped)](#fisse-shaped)
|
||||||
|
- [Informi (shapeless)](#informi-shapeless)
|
||||||
|
- [Cottura (cooking) e Carburante (fuel)](#cottura-cooking-e-carburante-fuel)
|
||||||
|
- [Gruppi](#gruppi)
|
||||||
|
- [Strumenti, Capacità e Friabilità](#strumenti-capacità-e-friabilità)
|
||||||
|
|
||||||
|
## Cosa sono i nodi e gli oggetti?
|
||||||
|
|
||||||
|
Nodi, oggetti fabbricabili e strumenti sono tutti oggetti.
|
||||||
|
Un oggetto è qualcosa che può essere trovato in un inventario — anche se potrebbe non risultare possibile durante una normale sessione di gioco.
|
||||||
|
|
||||||
|
Un nodo è un oggetto che può essere piazzato o trovato nel mondo.
|
||||||
|
Ogni coordinata nel mondo deve essere occupata da un unico nodo — ciò che appare vuoto è solitamente un nodo d'aria.
|
||||||
|
|
||||||
|
Un oggetto fabbricabile (*craftitem*) non può essere invece piazzato, potendo apparire solo negli inventari o come oggetto rilasciato nel mondo.
|
||||||
|
|
||||||
|
Uno strumento (*tool*) può usurarsi e solitamente non possiede la capacità di scavare.
|
||||||
|
In futuro, è probabile che gli oggetti fabbricabili e gli strumenti verranno fusi in un unico tipo, in quanto la distinzione fra di essi è alquanto artificiosa.
|
||||||
|
|
||||||
|
## Registrare gli oggetti
|
||||||
|
|
||||||
|
Le definizioni degli oggetti consistono in un *nome oggetto* e una *tabella di definizioni*.
|
||||||
|
La tabella di definizioni contiene attributi che influenzano il comportamento dell'oggetto.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("nomemod:nomeoggetto", {
|
||||||
|
description = "Il Mio Super Oggetto",
|
||||||
|
inventory_image = "nomemod_nomeoggetto.png"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nomi oggetto
|
||||||
|
|
||||||
|
Ogni oggetto ha un nome usato per riferirsi a esso, che dovrebbe seguire la seguente struttura:
|
||||||
|
|
||||||
|
nomemod:nomeoggetto
|
||||||
|
|
||||||
|
`nomemod` equivale appunto al nome della mod che registra l'oggetto, e `nomeoggetto` è il nome che si vuole assegnare a quest'ultimo.
|
||||||
|
Esso dovrebbe essere inerente a quello che rappresenta e deve essere unico nella mod.
|
||||||
|
|
||||||
|
### Alias
|
||||||
|
|
||||||
|
Gli oggetti possono anche avere degli *alias* che puntano al loro nome.
|
||||||
|
Un *alias* è uno pseudonimo che dice al motore di gioco di trattarlo come se fosse il nome a cui punta.
|
||||||
|
Ciò è comunemente usato in due casi:
|
||||||
|
|
||||||
|
* Rinominare gli oggetti rimossi in qualcos'altro.
|
||||||
|
Ci potrebbero essere nodi sconosciuti nel mondo e negli inventari se un oggetto viene rimosso da una mod senza nessun codice per gestirlo.
|
||||||
|
* Aggiungere una scorciatoia.
|
||||||
|
`/giveme dirt` è più semplice di `/giveme default:dirt`.
|
||||||
|
|
||||||
|
Registrare un alias è alquanto semplice.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_alias("dirt", "default:dirt")
|
||||||
|
```
|
||||||
|
|
||||||
|
Un buon modo per ricordarne il funzionamento è `da → a`, dove *da*
|
||||||
|
è l'alias e *a* è il nome dell'oggetto a cui punta.
|
||||||
|
|
||||||
|
Le mod devono inoltre assicurarsi di elaborare gli alias prima di occuparsi direttamente del nome dell'oggeto, in quanto l'engine non lo fa di suo.
|
||||||
|
Anche in questo caso non è difficile:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
itemname = core.registered_aliases[itemname] or itemname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Texture
|
||||||
|
|
||||||
|
Per convenzione le texture andrebbero messe nella cartella textures/ con nomi che seguono la struttura `nomemod_nomeoggetto.png`.\\
|
||||||
|
Le immagini in JPEG sono supportate, ma non supportano la trasparenza e sono generalmente di cattiva qualità nelle basse risoluzioni.
|
||||||
|
Si consiglia quindi il formato PNG.
|
||||||
|
|
||||||
|
Le texture su Minetest sono generalmente 16x16 pixel.
|
||||||
|
Possono essere di qualsiasi dimensione, ma è buona norma che rientrino nelle potenze di 2, per esempio 16, 32, 64 o 128.
|
||||||
|
Questo perché dimensioni differenti potrebbero non essere supportate dai vecchi dispositivi, comportando una diminuzione delle performance.
|
||||||
|
|
||||||
|
## Registrare un nodo base
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:diamante", {
|
||||||
|
description = "Diamante alieno",
|
||||||
|
tiles = {"miamod_diamante.png"},
|
||||||
|
is_ground_content = true,
|
||||||
|
groups = {cracky=3, stone=1}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
La proprietà `tiles` è una tabella contenente le texture che il nodo userà.
|
||||||
|
Quando è presente una sola texture, questa sarà applicata su tutte le facce.
|
||||||
|
Per assegnarne invece di diverse, bisogna fornire il nome di 6 texture in quest'ordine:
|
||||||
|
|
||||||
|
sopra (+Y), sotto (-Y), destra (+X), sinistra (-X), dietro (+Z), davanti (-Z).
|
||||||
|
(+Y, -Y, +X, -X, +Z, -Z)
|
||||||
|
|
||||||
|
Ricorda che su Minetest, come nella convenzione della computer grafica 3D, +Y punta verso l'alto.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:diamante", {
|
||||||
|
description = "Diamante alieno",
|
||||||
|
tiles = {
|
||||||
|
"miamod_diamante_up.png", -- y+
|
||||||
|
"miamod_diamante_down.png", -- y-
|
||||||
|
"miamod_diamante_right.png", -- x+
|
||||||
|
"miamod_diamante_left.png", -- x-
|
||||||
|
"miamod_diamante_back.png", -- z+
|
||||||
|
"miamod_diamante_front.png", -- z-
|
||||||
|
},
|
||||||
|
is_ground_content = true,
|
||||||
|
groups = {cracky = 3},
|
||||||
|
drop = "miamod:diamante_frammenti"
|
||||||
|
-- ^ Al posto di far cadere diamanti, fa cadere miamod:diamante_frammenti
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
L'attributo is_ground_content è essenziale per ogni nodo che si vuole far apparire sottoterra durante la generazione della mappa.
|
||||||
|
Le caverne vengono scavate nel mondo dopo che tutti gli altri nodi nell'area sono stati generati.
|
||||||
|
|
||||||
|
## Fabbricazione
|
||||||
|
|
||||||
|
Ci sono diversi tipi di ricette di fabbricazione disponibili, indicate dalla proprietà `type`.
|
||||||
|
|
||||||
|
* shaped - Gli ingredienti devono essere nel giusta posizione.
|
||||||
|
* shapeless - Non importa dove sono gli ingredienti, solo che siano abbastanza.
|
||||||
|
* cooking - Ricette di cottura per la fornace.
|
||||||
|
* fuel - Definisce gli oggetti che possono alimentare il fuoco nella fornace.
|
||||||
|
* tool_repair - Definisce gli oggetti che possono essere riparati.
|
||||||
|
|
||||||
|
Le ricette di fabbricazione non sono oggetti, perciò non usano nomi oggetto per identificare in maniera univoca se stesse.
|
||||||
|
|
||||||
|
### Fisse (shaped)
|
||||||
|
|
||||||
|
Le ricette fisse avvengono quando gli ingredienti devono essere nella forma o sequenza corretta per funzionare.
|
||||||
|
Nell'esempio sotto, i frammenti necessitano di essere in una figura a forma di sedia per poter fabbricare appunto 99 sedie.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shaped",
|
||||||
|
output = "miamod:diamante_sedia 99",
|
||||||
|
recipe = {
|
||||||
|
{"miamod:diamante_frammenti", "", ""},
|
||||||
|
{"miamod:diamante_frammenti", "miamod:diamante_frammenti", ""},
|
||||||
|
{"miamod:diamante_frammenti", "miamod:diamante_frammenti", ""}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Una cosa da tener presente è la colonna vuota sulla parte destra.
|
||||||
|
Questo significa che ci *deve* essere una colonna vuota a destra della forma, altrimenti ciò non funzionerà.
|
||||||
|
Se invece la colonna non dovesse servire, basta ometterla in questo modo:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
output = "miamod:diamante_sedia 99",
|
||||||
|
recipe = {
|
||||||
|
{"miamod:diamante_frammenti", "" },
|
||||||
|
{"miamod:diamante_frammenti", "miamod:diamante_frammenti"},
|
||||||
|
{"miamod:diamante_frammenti", "miamod:diamante_frammenti"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Il campo type non è davvero necessario per le ricette fisse, in quanto sono il tipo di base.
|
||||||
|
|
||||||
|
### Informi (shapeless)
|
||||||
|
|
||||||
|
Le ricette informi sono ricette che vengono usate quando non importa dove sono posizionati gli ingredienti, ma solo che ci siano.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "miamod:diamante 3",
|
||||||
|
recipe = {
|
||||||
|
"miamod:diamante_frammenti",
|
||||||
|
"miamod:diamante_frammenti",
|
||||||
|
"miamod:diamante_frammenti",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cottura (cooking) e carburante (fuel)
|
||||||
|
|
||||||
|
Le ricette di tipo "cottura" non vengono elaborate nella griglia di fabbricazione, bensì nelle fornaci o in qualsivoglia altro strumento di cottura che può essere trovato nelle mod.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "cooking",
|
||||||
|
output = "miamod_diamante_frammenti",
|
||||||
|
recipe = "default:coalblock",
|
||||||
|
cooktime = 10,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
L'unica vera differenza nel codice è che in questo la ricetta non è una tabella (tra parentesi graffe), bensì un singolo oggetto.
|
||||||
|
Le ricette di cottura dispongono anche di un parametro aggiuntivo "cooktime" che indica in secondi quanto tempo ci impiega l'oggetto a cuocersi.
|
||||||
|
Se non è impostato, di base è 3.
|
||||||
|
|
||||||
|
La ricetta qui sopra genera un'unità di frammenti di diamante dopo 10 secondi quando il blocco di carbone (`coalblock`) è nello slot di input, con un qualche tipo di carburante sotto di esso.
|
||||||
|
|
||||||
|
Il tipo "carburante" invece funge da accompagnamento alle ricette di cottura, in quanto definisce cosa può alimentare il fuoco.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "fuel",
|
||||||
|
recipe = "miamod:diamante",
|
||||||
|
burntime = 300,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Esso non ha un output come le altre ricette, e possiede un tempo di arsura (`burntime`) che definisce in secondi per quanto alimenterà la fiamma.
|
||||||
|
In questo caso, 300 secondi!
|
||||||
|
|
||||||
|
## Gruppi
|
||||||
|
|
||||||
|
Gli oggetti possono essere membri di più gruppi, e i gruppi possono avere più membri.
|
||||||
|
Essi sono definiti usando la proprietà `groups` nella tabella di definizione, e possiedono un valore associato.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
groups = {cracky = 3, wood = 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ci sono diverse ragioni per cui usare i gruppi.
|
||||||
|
In primis, vengono utilizzati per descrivere proprietà come friabilità e infiammabilità.
|
||||||
|
In secundis, possono essere usati in una ricetta al posto di un nome oggetto per permettere a qualsiasi oggetto nel gruppo di essere utilizzato.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "miamod:diamante_qualcosa 3",
|
||||||
|
recipe = {"group:wood", "miamod:diamante"}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strumenti, Capacità e Friabilità
|
||||||
|
|
||||||
|
Le friabilità sono dei gruppi particolari utilizzati per definire la resistenza di un nodo quando scavato con un determinato strumento.
|
||||||
|
Una friabilità elevata equivale a una maggior facilità e velocità nel romperlo.
|
||||||
|
È possibile combinarne di più tipi per permettere al nodo di essere distrutto da più tipi di strumento, mentre un nodo senza friabilità non può essere distrutto da nessuno strumento.
|
||||||
|
|
||||||
|
| Gruppo | Miglior strumento | Descrizione |
|
||||||
|
|---------|-------------------|-------------|
|
||||||
|
| crumbly | pala | Terra, sabbia |
|
||||||
|
| cracky | piccone | Cose dure e sgretolabili come la pietra |
|
||||||
|
| snappy | *qualsiasi* | Può essere rotto usando uno strumento adatto;<br>es. foglie, piantine, filo, lastre di metallo |
|
||||||
|
| choppy | ascia | Può essere rotto con dei fendenti; es. alberi, assi di legno |
|
||||||
|
| fleshy | spada | Esseri viventi come animali e giocatori.<br>Potrebbe implicare effetti di sangue al colpire |
|
||||||
|
| explody | ? | Predisposti ad esplodere |
|
||||||
|
| oddly_breakable_by_hand | *qualsiasi* | Torce e simili — molto veloci da rompere |
|
||||||
|
|
||||||
|
|
||||||
|
Ogni strumento possiede poi delle capacità (*capability*).
|
||||||
|
Una capacità include una lista di friabilità supportate, e proprietà associate per ognuna di esse come la velocità di scavata e il livello di usura.
|
||||||
|
Gli strumenti possono anche avere una durezza massima supportata per ogni tipo; ciò serve a prevenire che strumenti più deboli possano rompere nodi meno friabili.
|
||||||
|
È poi molto comune che uno strumento includa tutte le friabilità nelle sue capacità, con quelle meno adatte equivalenti a proprietà inefficienti.
|
||||||
|
Se l'oggetto impugnato dal giocatore non ha una capacità esplicitata, verrà allora usata quella della mano.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_tool("miamod:strumento", {
|
||||||
|
description = "Il mio strumento",
|
||||||
|
inventory_image = "miamod_strumento.png",
|
||||||
|
tool_capabilities = {
|
||||||
|
full_punch_interval = 1.5,
|
||||||
|
max_drop_level = 1,
|
||||||
|
groupcaps = {
|
||||||
|
crumbly = {
|
||||||
|
maxlevel = 2,
|
||||||
|
uses = 20,
|
||||||
|
times = { [1]=1.60, [2]=1.20, [3]=0.80 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
damage_groups = {fleshy=2},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
I gruppi limite (`groupcaps`) sono una lista delle friabilità supportate dallo strumento.
|
||||||
|
I gruppi di danno invece (`damage_groups`) servono a controllare come uno strumento (esterno) danneggia quell'oggetto. Quest'ultimi verranno discussi in seguito nel capitolo Oggetti, Giocatori e Entità.
|
211
_it/map/environment.md
Normal file
211
_it/map/environment.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
title: "Mappa: operazioni base"
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.1
|
||||||
|
description: Operazioni base come set_node e get_node
|
||||||
|
redirect_from: /it/chapters/environment.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
In questo capitolo imparerai come eseguire semplici azioni sulla mappa.
|
||||||
|
|
||||||
|
- [Struttura della mappa](#struttura-della-mappa)
|
||||||
|
- [Lettura](#lettura)
|
||||||
|
- [Lettura dei nodi](#lettura-dei-nodi)
|
||||||
|
- [Ricerca dei nodi](#ricerca-dei-nodi)
|
||||||
|
- [Scrittura](#scrittura)
|
||||||
|
- [Scrittura dei nodi](#scrittura-dei-nodi)
|
||||||
|
- [Rimozione dei nodi](#rimozione-dei-nodi)
|
||||||
|
- [Caricamento blocchi](#caricamento-blocchi)
|
||||||
|
- [Cancellazione blocchi](#cancellazione-blocchi)
|
||||||
|
|
||||||
|
## Struttura della mappa
|
||||||
|
|
||||||
|
La mappa di Minetest è suddivisa in Blocchi Mappa (*MapBlocks*), cubi di 16x16x16 nodi.
|
||||||
|
Man mano che i giocatori si addentrano per la mappa, i Blocchi Mappa vengono creati, caricati e rimossi dalla memoria.
|
||||||
|
Le aree della mappa che non sono ancora caricate sono piene di nodi *ignora*, dei nodi segnaposto che non possono
|
||||||
|
essere né attraversati né selezionati. Gli spazi vuoti delle aree già caricate, invece, sono nodi *d'aria*, dei
|
||||||
|
nodi invisibili e attraversabili.
|
||||||
|
|
||||||
|
Spesso, ci si rifà ai blocchi caricati (attenzione! Blocco non vuol dire nodo, come detto qui sopra!) chiamandoli *blocchi attivi*.
|
||||||
|
I blocchi attivi possono essere letti e sovrascritti dalle mod o dai giocatori, e contenere entità attive.
|
||||||
|
Anche il motore di gioco esegue operazioni sulla mappa, come il calcolare la fisica dei liquidi.
|
||||||
|
|
||||||
|
I Blocchi Mappa possono essere sia caricati dal database del mondo che generati.
|
||||||
|
Essi vengono generati fino al limite di generazione della mappa (`mapgen_limit`), che è impostato di base al suo valore massimo, 31000.
|
||||||
|
I Blocchi Mappa esistenti, tuttavia, ignorano questo limite quando caricati dal database del mondo.
|
||||||
|
|
||||||
|
## Lettura
|
||||||
|
|
||||||
|
### Lettura dei nodi
|
||||||
|
|
||||||
|
Un nodo può essere letto da un mondo fornendone la posizione:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local nodo = core.get_node({ x = 1, y = 3, z = 4 })
|
||||||
|
print(dump(nodo)) --> { name=.., param1=.., param2=.. }
|
||||||
|
```
|
||||||
|
|
||||||
|
Se la posizione è un decimale, verrà arrotondata alle coordinate del nodo.
|
||||||
|
`get_node` ritornerà sempre una tabella contenente le informazioni del nodo:
|
||||||
|
|
||||||
|
* `name` - Il nome del nodo, che sarà `ignore` quando l'area non è caricata.
|
||||||
|
* `param1` - Guarda la definizione dei nodi. È solitamente associato alla luce.
|
||||||
|
* `param2` - Guarda la definizione dei nodi.
|
||||||
|
|
||||||
|
Per vedere se un nodo è caricato si può utilizzare `core.get_node_or_nil`, che ritornerà `nil` se il nome del nodo risulta `ignore`
|
||||||
|
(la funzione non caricherà comunque il nodo).
|
||||||
|
Potrebbe comunque ritornare `ignore` se un blocco contiene effettivamente `ignore`: questo succede ai limiti della mappa.
|
||||||
|
|
||||||
|
### Ricerca dei nodi
|
||||||
|
|
||||||
|
Minetest offre un numero di funzioni d'aiuto per accelerare le azioni più comuni legate alla mappa.
|
||||||
|
Le più frequenti sono quelle per trovare i nodi.
|
||||||
|
|
||||||
|
Per esempio, mettiamo che si voglia creare un certo tipo di pianta che cresce più velocemente vicino alla pietra;
|
||||||
|
si dovrebbe controllare che ogni nodo nei pressi della pianta sia pietra, e modificarne il suo indice di crescita di conseguenza.
|
||||||
|
|
||||||
|
`core.find_node_near` ritornerà il primo nodo trovato in un dato raggio, combaciante con le informazioni passategli (nomi di nodi o gruppi).
|
||||||
|
Nell'esempio che segue, andiamo alla ricerca di un nodo di mese nel raggio di 5 nodi:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local vel_crescita = 1
|
||||||
|
local pos_nodo = core.find_node_near(pos, 5, { "default:stone" })
|
||||||
|
if pos_nodo then
|
||||||
|
core.chat_send_all("Nodo trovato a: " .. dump(pos_nodo))
|
||||||
|
vel_crescita = 2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Mettiamo ora che l'indice di crescita debba incrementare per ogni nodo di pietra nei dintorni.
|
||||||
|
Si dovrebbe quindi usare una funzione in grado di trovare più nodi in un'area:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos1 = vector.subtract(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local pos2 = vector.add(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local lista_pos =
|
||||||
|
core.find_nodes_in_area(pos1, pos2, { "default:stone" })
|
||||||
|
local vel_crescita = 1 + #lista_pos
|
||||||
|
```
|
||||||
|
|
||||||
|
Il codice qui in alto ritorna il numero di nodi in un *volume cuboidale*.
|
||||||
|
Il che è diverso da usare `find_node_near`, il quale usa la distanza dalla posizione data (cioé una *sfera*).
|
||||||
|
Per ovviare a ciò, bisogna controllare l'intervallo manualmente.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos1 = vector.subtract(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local pos2 = vector.add(pos, { x = 5, y = 5, z = 5 })
|
||||||
|
local lista_pos =
|
||||||
|
core.find_nodes_in_area(pos1, pos2, { "default:stone" })
|
||||||
|
local vel_crescita = 1
|
||||||
|
for i=1, #lista_pos do
|
||||||
|
local delta = vector.subtract(lista_pos[i], pos)
|
||||||
|
if delta.x*delta.x + delta.y*delta.y + delta.z*delta.z <= 5*5 then
|
||||||
|
vel_crescita = vel_crescita + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Ora il codice aumenterà correttamente `vel_crescita` basandosi su quanti nodi di pietra ci sono in un intervallo.
|
||||||
|
|
||||||
|
Notare come si sia comparata la distanza al quadrato dalla posizione, invece che calcolarne la radice quadrata per ottenerne la distanza vera e propria.
|
||||||
|
Questo perché i computer trovano le radici quadrate computazionalmente pesanti, quindi dovrebbero essere evitate il più possibile.
|
||||||
|
|
||||||
|
Ci sono altre variazioni delle due funzioni sopracitate, come `find_nodes_with_meta` e `find_nodes_in_area_under_air`, che si comportano in modo simile e sono utili in altre circostanze.
|
||||||
|
|
||||||
|
## Scrittura
|
||||||
|
|
||||||
|
### Scrittura dei nodi
|
||||||
|
|
||||||
|
Puoi usare `set_node` per sovrascrivere nodi nella mappa.
|
||||||
|
Ogni chiamata a `set_node` ricalcolerà la luce e richiamerà i suoi callback, il che significa che `set_node` è alquanto lento quando usato su un elevato numero di nodi.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.set_node({ x = 1, y = 3, z = 4 }, { name = "default:stone" })
|
||||||
|
|
||||||
|
local nodo = core.get_node({ x = 1, y = 3, z = 4 })
|
||||||
|
print(nodo.name) --> default:stone
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_node` rimuoverà ogni metadato e inventario associato a quel nodo: ciò non è sempre desiderabile, specialmente se si stanno usando
|
||||||
|
più definizioni di nodi per rappresentarne concettualmente uno. Un esempio è il nodo fornace: per quanto lo si immagini come un nodo unico,
|
||||||
|
sono in verità due.
|
||||||
|
|
||||||
|
Si può impostare un nuovo nodo senza rimuoverne metadati e inventario con `swap_node`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.swap_node({ x = 1, y = 3, z = 4 }, { name = "default:stone" })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rimozione dei nodi
|
||||||
|
|
||||||
|
Un nodo deve sempre essere presente. Per rimuoverlo, basta impostarlo uguale a `air`.
|
||||||
|
|
||||||
|
Le seguenti due linee di codice sono equivalenti, rimuovendo in entrambi i casi il nodo:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.remove_node(pos)
|
||||||
|
core.set_node(pos, { name = "air" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Infatti, `remove_node` non fa altro che richiamare `set_node` con nome `air`.
|
||||||
|
|
||||||
|
## Caricamento blocchi
|
||||||
|
|
||||||
|
Puoi usare `core.emerge_area` per caricare i blocchi mappa.
|
||||||
|
Questo comando è asincrono, ovvero i blocchi non saranno caricati istantaneamente; al contrario, verranno caricati man mano e il callback associato sarà richiamato a ogni passaggio.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Carica un'area 20x20x20
|
||||||
|
local mezza_dimensione = { x = 10, y = 10, z = 10 }
|
||||||
|
local pos1 = vector.subtract(pos, mezza_dimensione)
|
||||||
|
local pos2 = vector.add (pos, mezza_dimensione)
|
||||||
|
|
||||||
|
local param = {} -- dati persistenti tra un callback e l'altro
|
||||||
|
core.emerge_area(pos1, pos2, mio_callback, param)
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest chiamerà la funzione locale definita qua sotto `mio_callback` ogni volta che carica un blocco, con delle informazioni sul progresso.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function mio_callback(pos, action,
|
||||||
|
calls_remaining, param)
|
||||||
|
-- alla prima chiamata, registra il numero di blocchi
|
||||||
|
if not param.blocchi_totali then
|
||||||
|
param.blocchi_totali = calls_remaining + 1
|
||||||
|
param.blocchi_caricati = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Incrementa il numero di blocchi caricati
|
||||||
|
param.loaded_blocks = param.blocchi_caricati + 1
|
||||||
|
|
||||||
|
-- Invia messaggio indicante il progresso
|
||||||
|
if param.blocchi_totali == param.blocchi_caricati then
|
||||||
|
core.chat_send_all("Ho finito di caricare blocchi!")
|
||||||
|
else
|
||||||
|
local percentuale = 100 * param.blocchi_caricati / param.blocchi_totali
|
||||||
|
local msg = string.format("Caricamento blocchi %d/%d (%.2f%%)",
|
||||||
|
param.blocchi_caricati, param.blocchi_totali, percentuale)
|
||||||
|
core.chat_send_all(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo non è l'unico modo per caricare blocchi; utilizzando un LVM (nel dettaglio nel capitolo 19) si potranno infatti caricare i blocchi selezionati in maniera sincrona.
|
||||||
|
|
||||||
|
## Cancellazione blocchi
|
||||||
|
|
||||||
|
Puoi usare `delete_area` per cancellare una serie di blocchi mappa:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Cancella un'area 20x20x20
|
||||||
|
local mezza_dimensione = { x = 10, y = 10, z = 10 }
|
||||||
|
local pos1 = vector.subtract(pos, mezza_dimensione)
|
||||||
|
local pos2 = vector.add (pos, mezza_dimensione)
|
||||||
|
|
||||||
|
core.delete_area(pos1, pos2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo cancellerà tutti i blocchi mappa in quell'area, anche quelli solo parzialmente selezionati.
|
315
_it/map/objects.md
Normal file
315
_it/map/objects.md
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
---
|
||||||
|
title: Oggetti, giocatori ed entità
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.4
|
||||||
|
description: Alla scopera degli ObjectRef
|
||||||
|
degrad:
|
||||||
|
level: warning
|
||||||
|
title: Gradi e radianti
|
||||||
|
message: La rotazione dell'oggetto figlio è in gradi, mentre quella dell'oggetto è in radianti.
|
||||||
|
Assicurati di usare il metodo di misura corretto.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
In questo capitolo imparerai come manipolare gli oggetti e come definirne di tuoi.
|
||||||
|
|
||||||
|
- [Cosa sono gli oggetti, i giocatori e le entità?](#cosa-sono-gli-oggetti-i-giocatori-e-le-entità)
|
||||||
|
- [Posizione e velocità](#posizione-e-velocità)
|
||||||
|
- [Proprietà degli oggetti](#proprietà-degli-oggetti)
|
||||||
|
- [Entità](#entità)
|
||||||
|
- [Salute e danno](#salute-e-danno)
|
||||||
|
- [Punti vita (HP)](#punti-vita-hp)
|
||||||
|
- [Pugni, Gruppi Danno e Gruppi Armatura](#pugni-gruppi-danno-e-gruppi-armatura)
|
||||||
|
- [Esempi di calcolo del danno](#esempi-di-calcolo-del-danno)
|
||||||
|
- [Oggetti figli](#oggetti-figli)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
## Cosa sono gli oggetti, i giocatori e le entità?
|
||||||
|
|
||||||
|
Giocatori e entità sono entrambi tipi di oggetti (ObjectRef, quindi di nuovo un riferimento). Un oggetto è qualcosa che si può muovere indipendentemente dalla griglia di nodi e che ha proprietà come velocità e scala.
|
||||||
|
Attenzione, tuttavia, a non confonderli con gli oggetti nel senso di "cose che possono essere messe in un inventario" (in inglese hanno infatti nomi diversi: *objects* e *items*), anche perché hanno un sistema di registrazione tutto loro.
|
||||||
|
|
||||||
|
Ci sono alcune differenze tra giocatori ed entità.
|
||||||
|
La più grande è che i primi sono controllati da chi gioca, mentre le seconde sono controllate dalle mod.
|
||||||
|
Ciò significa che, per esempio, la velocità di un giocatore non può essere modificata dalle mod - i giocatori appartengono al lato client, mentre le entità al lato server.
|
||||||
|
Un'altra differenza è che i giocatori fanno caricare i Blocchi Mappa che li circondano, le entità invece no: quest'ultime vengono salvate e diventano inattive quando il Blocco Mappa in cui si trovano viene rimosso dalla memoria.
|
||||||
|
|
||||||
|
Questa distinzione è resa meno chiara dal fatto che le entità sono controllate tramite una Tabella di Entità Lua che vedremo qui sotto.
|
||||||
|
|
||||||
|
## Posizione e velocità
|
||||||
|
|
||||||
|
`get_pos` e `set_pos` permettono di ottenere e impostare la posizione di un oggetto.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local giocatore = core.get_player_by_name("bob")
|
||||||
|
local pos = giocatore:get_pos()
|
||||||
|
giocatore:set_pos({ x = pos.x, y = pos.y + 1, z = pos.z })
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_pos` imposta la posizione seduta stante, senza animazione.
|
||||||
|
Se invece si desidera animare il movimento dell'oggetto verso la nuova posizione, si dovrebbe usare `move_to`.
|
||||||
|
Questo, tuttavia, funziona soltanto per le entità.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
miaentita:move_to({ x = pos.x, y = pos.y + 1, z = pos.z })
|
||||||
|
```
|
||||||
|
|
||||||
|
Una cosa importante da tenere a mente quando si lavora con le entità è la latenza di rete.
|
||||||
|
In un mondo ideale, le informazioni riguardo i movimenti delle entità arriverebbero subito, nell'ordine corretto e a intervalli simili a come sono stati inviati.
|
||||||
|
Tuttavia, a meno che tu non stia giocando in locale, questo non è un mondo ideale.
|
||||||
|
Le informazioni ci mettono un attimo ad arrivare: per esempio i `set_pos` potrebbero non arrivare in ordine, saltando alcune chiamate.
|
||||||
|
O lo spazio da coprire di un `move_to` potrebbe non essere suddiviso perfettamente, rendendo l'animazione meno fluida.
|
||||||
|
Tutto ciò ha come risultato il client che vede cose leggermente diverse dal server, che è una cosa di cui dovresti essere consapevole.
|
||||||
|
|
||||||
|
## Proprietà degli oggetti
|
||||||
|
|
||||||
|
Le proprietà degli oggetti sono usate per comunicare al client come renderizzare e gestire un oggetto.
|
||||||
|
Non è possibile definire delle proprietà personalizzate, perché le proprietà sono per definizione fatte per essere usate dall'engine.
|
||||||
|
|
||||||
|
Al contrario dei nodi, gli oggetti hanno un comportamento dinamico.
|
||||||
|
Si può per esempio cambiare il loro aspetto in qualsiasi momento, aggiornandone le proprietà:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
oggetto:set_properties({
|
||||||
|
visual = "mesh",
|
||||||
|
mesh = "omino.b3d",
|
||||||
|
textures = {"omino_texture.png"},
|
||||||
|
visual_size = {x=1, y=1},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Le proprietà aggiornate verranno inviate a tutti i giocatori nelle vicinanze.
|
||||||
|
Questo è molto utile per avere una vasto ammontare di varietà a basso costo, uno fra tanti l'avere diverse skin per giocatore.
|
||||||
|
|
||||||
|
Come mostrato nella prossima sezione, le entità possono avere delle proprietà iniziali, che andranno dichiarate nella loro definizione.
|
||||||
|
|
||||||
|
## Entità
|
||||||
|
|
||||||
|
Un'entità ha una tabella di definizione che ricorda quella degli oggetti (intesi come *items*).
|
||||||
|
Questa tabella può contenere metodi di callback, proprietà iniziali e membri personalizzati.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local MiaEntita = {
|
||||||
|
initial_properties = {
|
||||||
|
hp_max = 1,
|
||||||
|
physical = true,
|
||||||
|
collide_with_objects = false,
|
||||||
|
collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},
|
||||||
|
visual = "wielditem",
|
||||||
|
visual_size = {x = 0.4, y = 0.4},
|
||||||
|
textures = {""},
|
||||||
|
spritediv = {x = 1, y = 1},
|
||||||
|
initial_sprite_basepos = {x = 0, y = 0},
|
||||||
|
},
|
||||||
|
|
||||||
|
messaggio = "Messaggio predefinito",
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiaEntita:imposta_messaggio(msg)
|
||||||
|
self.messaggio = msg
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Tuttavia, c'è una differenza sostanziale tra entità e oggetti; perché quando un'entità appare (come quando viene creata o caricata) una nuova tabella viene generata per quell'entità, *ereditando* le proprietà dalla tabella originaria tramite una metatabella.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Questa eredità avviene usando una metatabella. Le metatabelle rappresentano un aspetto importante di Lua, che bisogna tenere bene a mente in quanto sono una parte essenziale del linguaggio.
|
||||||
|
|
||||||
|
In parole povere, le metatabelle permettono di controllare come si comporta una tabella quando viene usata una certa sintassi in Lua.
|
||||||
|
Vengono usate soprattutto per la loro abilità di usare un'altra tabella come prototipo, fungendo da valori di base di quest'ultima quando essa non contiene le proprietà e i metodi richiesti.
|
||||||
|
|
||||||
|
Mettiamo che si voglia accedere al campo `x` della tabella `a` (`a.x`).
|
||||||
|
Se la tabella `a` ha quel campo, allora ritornerà normalmente.
|
||||||
|
Tuttavia, se `a.x` non esiste ma esiste una metatabella `b` associata ad `a`, `b` verrà ispezionata alla ricerca di un eventuale `b.x` da ritornare al posto di `nil`.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Sia la tabella di un ObjectRef che quella di un'entità forniscono modi per ottenerne la controparte:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local entita = oggetto:get_luaentity()
|
||||||
|
local oggetto = entita.object
|
||||||
|
print("L'entità si trova a " .. core.pos_to_string(oggetto:get_pos()))
|
||||||
|
```
|
||||||
|
|
||||||
|
Ci sono diversi callback disponibili da usare per le entità.
|
||||||
|
Una lista completa può essere trovata in [lua_api.md](https://minetest.gitlab.io/minetest/minetest-namespace-reference/#registered-definition-tables).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function MiaEntita:on_step(dtime)
|
||||||
|
local pos = self.oggetto:get_pos()
|
||||||
|
local pos_giu = vector.subtract(pos, vector.new(0, 1, 0))
|
||||||
|
|
||||||
|
local delta
|
||||||
|
if core.get_node(pos_giu).name == "air" then
|
||||||
|
delta = vector.new(0, -1, 0)
|
||||||
|
elseif core.get_node(pos).name == "air" then
|
||||||
|
delta = vector.new(0, 0, 1)
|
||||||
|
else
|
||||||
|
delta = vector.new(0, 1, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
delta = vector.multiply(delta, dtime)
|
||||||
|
|
||||||
|
self.oggetto:move_to(vector.add(pos, delta))
|
||||||
|
end
|
||||||
|
|
||||||
|
function MiaEntita:on_punch(hitter)
|
||||||
|
core.chat_send_player(hitter:get_player_name(), self.message)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Ora, se si volesse spawnare e usare questa entità, si noterà che il messaggio andrebbe perduto quando l'entità diventa inattiva per poi ritornare attiva.
|
||||||
|
Questo succede perché il messaggio non è salvato.
|
||||||
|
Al posto di salvare tutto nella tabella dell'entità, Minetest ti permette di scegliere come salvare le cose.
|
||||||
|
Questo succede nella *Staticdata*, una stringa che contiene tutte le informazioni personalizzate che si vogliono ricordare.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function MiaEntita:get_staticdata()
|
||||||
|
return core.write_json({
|
||||||
|
messaggio = self.messaggio,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function MiaEntita:on_activate(staticdata, dtime_s)
|
||||||
|
if staticdata ~= "" and staticdata ~= nil then
|
||||||
|
local data = core.parse_json(staticdata) or {}
|
||||||
|
self:imposta_messaggio(data.messaggio)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest può chiamare `get_staticdata()` quando e quante volte vuole.
|
||||||
|
Questo perché non aspetta che un Blocco Mappa diventi inattivo per salvarlo, in quanto comporterebbe una perdita di informazioni.
|
||||||
|
I Blocchi Mappa sono salvati circa ogni 18 secondi, quindi dovresti notare un simile intervallo per la chiamata a `get_staticdata()`.
|
||||||
|
|
||||||
|
`on_activate()`, d'altro canto, viene chiamato solo quando un'entità diventa attiva o nel Blocco Mappa appena caricato o quando spawna.
|
||||||
|
Questo significa che il suo staticdata inizialmente potrebbe essere vuoto (dato l'intervallo di 18 secondi).
|
||||||
|
|
||||||
|
Infine, c'è bisogno di registrare la tabella usando `register_entity`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_entity("miamod:entita", MiaEntita)
|
||||||
|
```
|
||||||
|
|
||||||
|
L'entità può essere spawnata da una mod nel seguente modo:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos = { x = 1, y = 2, z = 3 }
|
||||||
|
local oggetto = core.add_entity(pos, "miamod:entita", nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
Il terzo parametro è lo staticdata inziale.
|
||||||
|
Per impostare il messaggio, puoi usare la Tabella di Entità Lua:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
oggetto:get_luaentity():imposta_messaggio("ciao!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Salute e danno
|
||||||
|
|
||||||
|
### Punti vita (HP)
|
||||||
|
|
||||||
|
Ogni oggetto ha un valore Punti Vita (HP), che rappresenta la salute attuale.
|
||||||
|
Nei giocatori è inoltre possibile impostare il valore di salute massima tramite la proprietà `hp_max`.
|
||||||
|
Al raggiungere gli 0 HP, un oggetto muore.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local hp = oggetto:get_hp()
|
||||||
|
oggetto:set_hp(hp + 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pugni, Gruppi Danno e Gruppi Armatura
|
||||||
|
|
||||||
|
Il danno è la riduzione degli HP di un oggetto.
|
||||||
|
Quest'ultimo può prendere "a pugni" un altro oggetto per infliggere danno.
|
||||||
|
"A pugni" perché non si parla necessariamente di un pugno vero e proprio: può essere infatti un'esplosione, un fendente, e via dicendo.
|
||||||
|
|
||||||
|
Il danno complessivo è calcolato moltiplicando i Gruppi Danno del pugno con le vulnerabilità dell'obiettivo.
|
||||||
|
Questo è poi eventualmente ridotto a seconda di quanto recente è stato il colpo precedente.
|
||||||
|
Vedremo tra poco nel dettaglio quest'aspetto.
|
||||||
|
|
||||||
|
Proprio come i [Gruppi Danno dei nodi](../items/nodes_items_crafting.html#strumenti-capacità-e-friabilità), questi gruppi possono prendere qualsiasi nome e non necessitano di essere registrati.
|
||||||
|
Tuttavia, si è soliti usare gli stessi nomi di quelli dei nodi.
|
||||||
|
|
||||||
|
La vulnerabilità di un oggetto a un certo tipo di danno dipende dalla sua [proprietà](#proprietà-degli-oggetti) `armor_groups`.
|
||||||
|
Al contrario di quello che potrebbe far intendere il nome, `armor_groups` specifica la percentuale di danno subita da specifici Gruppi Danno, e non la resistenza.
|
||||||
|
Se un Gruppo Danno non è elencato nei Gruppi Armatura di un oggetto, quest'ultimo ne sarà completamente immune.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
obiettivo:set_armor_groups({
|
||||||
|
fleshy = 90,
|
||||||
|
crumbly = 50,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Nell'esempio qui sopra, l'oggetto subirà il 90% di danno `fleshy` e 50% di quello `crumbly`.
|
||||||
|
|
||||||
|
Quando un giocatore prende "a pugni" un oggetto, i Gruppi Danno vengono estrapolati dall'oggetto che ha attualmente il mano.
|
||||||
|
Negli altri casi, saranno le mod a decidere quali Gruppi Danno usare.
|
||||||
|
|
||||||
|
### Esempi di calcolo del danno
|
||||||
|
|
||||||
|
Prendiamo a pugni l'oggetto `target`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local capacita_oggetto = {
|
||||||
|
full_punch_interval = 0.8,
|
||||||
|
damage_groups = { fleshy = 5, choppy = 10 },
|
||||||
|
|
||||||
|
-- Questo è usato solo per scavare nodi, ma è comunque richiesto
|
||||||
|
max_drop_level=1,
|
||||||
|
groupcaps={
|
||||||
|
fleshy={times={[1]=2.5, [2]=1.20, [3]=0.35}, uses=30, maxlevel=2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local tempo_da_ultimo_pugno = capacita_oggetto.full_punch_interval
|
||||||
|
obiettivo:punch(oggetto, tempo_da_ultimo_pugno, capacita_oggetto)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ora, calcoliamo a quanto ammonterà il danno.
|
||||||
|
I Gruppi Danno del pugno sono `fleshy=5` e `choppy=10`, con l'obiettivo che prenderà 90% di danno da fleshy e 0% da choppy.
|
||||||
|
|
||||||
|
Per prima cosa, moltiplichiamo i Gruppi Danno per le vulnerabilità, e ne sommiamo il risultato.
|
||||||
|
Poi, moltiplichiamo per un numero tra 0 e 1 a seconda di `tempo_da_ultimo_pugno`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
= (5*90/100 + 10*0/100) * limit(tempo_da_ultimo_pugno / full_punch_interval, 0, 1)
|
||||||
|
= (5*90/100 + 10*0/100) * 1
|
||||||
|
= 4.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Dato che HP è un intero, il danno è arrotondato a 5 punti.
|
||||||
|
|
||||||
|
## Oggetti figli
|
||||||
|
|
||||||
|
Gli oggetti figli (*attachments*) si muovono quando il genitore - l'oggetto al quale sono legati - viene mosso.
|
||||||
|
Un oggetto può possedere un numero illimitato di figli, ma non più di un genitore.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
figlio:set_attach(parent, bone, position, rotation)
|
||||||
|
```
|
||||||
|
|
||||||
|
Il `get_pos()` di un oggetto ritornerà sempre la sua posizione globale, a prescindere dal fatto che sia figlio o meno.
|
||||||
|
`set_attach` prende invece una posizione relativa, ma non è quello che credi: la posizione del figlio è relativa a quella del genitore *amplificata quest'ultima* di 10 volte.
|
||||||
|
Quindi, `0,5,0` sarà metà nodo in alto rispetto al genitore.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.degrad %}
|
||||||
|
|
||||||
|
Per i modelli 3D animati, il parametro `bone` (osso) è usato per collegare un'entità a un osso.
|
||||||
|
Le animazioni 3D sono basate su degli scheletri - una rete di ossa nel modello dove ogni osso può avere una posizione e rotazione per cambiare il modello, tipo per muovere un braccio.
|
||||||
|
Il collegamento a un osso è utile se si vuole per esempio far impugnare qualcosa al personaggio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
oggetto:set_attach(player,
|
||||||
|
"Braccio destro", -- osso predefinito
|
||||||
|
{x=0.2, y=6.5, z=3}, -- posizione predefinita
|
||||||
|
{x=-100, y=225, z=90}) -- rotazione predefinita
|
||||||
|
```
|
||||||
|
|
||||||
|
## Il tuo turno
|
||||||
|
|
||||||
|
* Fai un mulino combinando dei nodi con un'entità.
|
||||||
|
* Crea un mostro di tua scelta (usando l'API delle entità, e senza usare altre mod).
|
217
_it/map/storage.md
Normal file
217
_it/map/storage.md
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
---
|
||||||
|
title: Storaggio e metadati
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.3
|
||||||
|
description: Scopri come funziona lo spazio d'archiviazione delle mod e come usare i metadati per passare informazioni.
|
||||||
|
redirect_from:
|
||||||
|
- /it/chapters/node_metadata.html
|
||||||
|
- /it/map/node_metadata.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
In questo capitolo imparerai i vari modi per immagazzinare dati.
|
||||||
|
|
||||||
|
- [Metadati](#metadati)
|
||||||
|
- [Cos'è un metadato?](#cosè-un-metadato)
|
||||||
|
- [Ottenere i metadati di un oggetto](#ottenere-i-metadati-di-un-oggetto)
|
||||||
|
- [Lettura e scrittura](#lettura-e-scrittura)
|
||||||
|
- [Chiavi speciali](#chiavi-speciali)
|
||||||
|
- [Immagazzinare tabelle](#immagazzinare-tabelle)
|
||||||
|
- [Metadati privati](#metadati-privati)
|
||||||
|
- [Tabelle Lua](#tabelle-lua)
|
||||||
|
- [Storaggio Mod](#storaggio-mod)
|
||||||
|
- [Database](#database)
|
||||||
|
- [Decidere quale usare](#decidere-quale-usare)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
## Metadati
|
||||||
|
|
||||||
|
### Cos'è un metadato?
|
||||||
|
|
||||||
|
In Minetest, un metadato è una coppia chiave-valore usata per collegare dei dati a qualcosa.
|
||||||
|
Puoi usare i metadati per salvare informazioni nei nodi, nei giocatori o negli ItemStack.
|
||||||
|
|
||||||
|
Ogni tipo di metadato usa la stessa identica API.
|
||||||
|
Ognuno di essi salva i valori come stringhe, ma ci sono comunque dei metodi per convertire e salvare altri tipi di primitivi.
|
||||||
|
|
||||||
|
Per evitare conflitti con altre mod, dovresti usare la nomenclatura convenzionale per le chiavi: `nomemod:nomechiave`.
|
||||||
|
Alcune chiavi hanno invece un significato speciale, come vedremo più in basso.
|
||||||
|
|
||||||
|
Ricorda che i metadati sono dati riguardo altri dati.
|
||||||
|
Il dato in sé, come il tipo di un nodo o la quantità di un ItemStack, non rientra perciò nella definizione.
|
||||||
|
|
||||||
|
### Ottenere i metadati di un oggetto
|
||||||
|
|
||||||
|
Se si conosce la posizione di un nodo, si possono ottenere i suoi metadati:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local meta = core.get_meta({ x = 1, y = 2, z = 3 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Quelli dei giocatori e degli ItemStack invece sono ottenuti tramite `get_meta()`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local p_meta = player:get_meta()
|
||||||
|
local i_meta = pila:get_meta()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lettura e scrittura
|
||||||
|
|
||||||
|
Nella maggior parte dei casi, per leggere e scrivere metadati saranno usati i metodi `get_<type>()` e `set_<type>()`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(meta:get_string("foo")) --> ""
|
||||||
|
meta:set_string("foo", "bar")
|
||||||
|
print(meta:get_string("foo")) --> "bar"
|
||||||
|
```
|
||||||
|
|
||||||
|
Tutti i getter ritorneranno un valore di default se la chiave non esiste, rispettivamente `""` per le stringhe e `0` per gli interi.
|
||||||
|
Si può inoltre usare `get()` per ritornare o una stringa o nil.
|
||||||
|
|
||||||
|
Come gli inventari, anche i metadati sono riferimenti: ogni cambiamento applicato ad essi, cambierà la fonte originale.
|
||||||
|
|
||||||
|
Inoltre, se è possibile convertire un intero in stringa e viceversa, basterà cambiare `get_int`/`get_string` per ottenerne la versione corrispondente:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(meta:get_int("count")) --> 0
|
||||||
|
meta:set_int("count", 3)
|
||||||
|
print(meta:get_int("count")) --> 3
|
||||||
|
print(meta:get_string("count")) --> "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chiavi speciali
|
||||||
|
|
||||||
|
`infotext` è usato nei nodi per mostrare una porzione di testo al passare il mirino sopra il nodo.
|
||||||
|
Questo è utile, per esempio, per mostrare lo stato o il proprietario di un nodo.
|
||||||
|
|
||||||
|
`description` è usato negli ItemStack per sovrascrivere la descrizione al passare il mouse sopra l'oggetto in un formspec (come l'inventario, li vedremo più avanti).
|
||||||
|
È possibile utilizzare `core.colorize()` per cambiarne il colore.
|
||||||
|
|
||||||
|
`owner` è una chiave comune, usata per immagazzinare il nome del giocatore a cui appartiene l'oggetto o il nodo.
|
||||||
|
|
||||||
|
### Immagazzinare tabelle
|
||||||
|
|
||||||
|
Le tabelle devono essere convertite in stringhe prima di essere immagazzinate.
|
||||||
|
Minetest offre due formati per fare ciò: Lua e JSON.
|
||||||
|
|
||||||
|
Quello in Lua tende a essere molto più veloce e corrisponde al formato usato da Lua per le tabelle, mentre JSON è un formato più standard, con una miglior struttura, e che ben si presta per scambiare informazioni con un altro programma.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = { username = "utente1", score = 1234 }
|
||||||
|
meta:set_string("foo", core.serialize(data))
|
||||||
|
|
||||||
|
data = core.deserialize(meta:get_string("foo"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadati privati
|
||||||
|
|
||||||
|
Di base, tutti i metadati dei nodi sono inviati al client. Rendendo le loro chiavi private, questo invece non succede.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
meta:set_string("segreto", "asd34dn")
|
||||||
|
meta:mark_as_private("segreto")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabelle Lua
|
||||||
|
|
||||||
|
Le tabelle possono essere convertite da/a stringhe nei metadati tramite `to_table` e `from_table`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local tmp = meta:to_table()
|
||||||
|
tmp.foo = "bar"
|
||||||
|
meta:from_table(tmp)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storaggio Mod
|
||||||
|
|
||||||
|
Lo spazio d'archiviazione della mod (*storage*) usa la stessa identica API dei metadati, anche se non sono tecnicamente la stessa cosa.
|
||||||
|
Il primo infatti è per mod, e può essere ottenuto solo durante l'inizializzazione - appunto - della mod.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local memoria = core.get_mod_storage()
|
||||||
|
```
|
||||||
|
|
||||||
|
Nell'esempio è ora possibile manipolare lo spazio d'archiviazione come se fosse un metadato:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
memoria:set_string("foo", "bar")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
Se la mod ha buone probabilità di essere usata su un server e tenere traccia di un sacco di dati, è buona norma offrire un database come metodo di storaggio.
|
||||||
|
Dovresti rendere ciò opzionale, separando il come i dati vengono salvati e il dove vengono usati.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local backend
|
||||||
|
if use_database then
|
||||||
|
backend =
|
||||||
|
dofile(core.get_modpath("miamod") .. "/backend_sqlite.lua")
|
||||||
|
else
|
||||||
|
backend =
|
||||||
|
dofile(core.get_modpath("miamod") .. "/backend_storage.lua")
|
||||||
|
end
|
||||||
|
|
||||||
|
backend.get_foo("a")
|
||||||
|
backend.set_foo("a", { score = 3 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Il file `backend_storage.lua` dell'esempio (puoi nominarlo come vuoi) dovrebbe includere l'implementazione del metodo di storaggio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local memoria = core.get_mod_storage()
|
||||||
|
local backend = {}
|
||||||
|
|
||||||
|
function backend.set_foo(key, value)
|
||||||
|
memoria:set_string(key, core.serialize(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
function backend.get_foo(key)
|
||||||
|
return core.deserialize(memoria:get_string(key))
|
||||||
|
end
|
||||||
|
|
||||||
|
return backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Il file `backend_sqlite.lua` dovrebbe fare una cosa simile, ma utilizzando la libreria Lua *lsqlite3* al posto della memoria d'archiviazione interna.
|
||||||
|
|
||||||
|
Usare un database come SQLite richiede l'utilizzo di un ambiente non sicuro (*insecure environment*).
|
||||||
|
Un ambiente non sicuro è una tabella disponibile solamente alle mod con accesso esplicito dato dall'utente, e contiene una copia meno limitata della API Lua, che potrebbe essere abusata da mod con intenzioni malevole.
|
||||||
|
Gli ambienti non sicuri saranno trattati più nel dettaglio nel capitolo sulla [Sicurezza](../quality/security.html).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local amb_nonsicuro = core.request_insecure_environment()
|
||||||
|
assert(amb_nonsicuro, "Per favore aggiungi miamod a secure.trusted_mods nelle impostazioni")
|
||||||
|
|
||||||
|
local _sql = amb_nonsicuro.require("lsqlite3")
|
||||||
|
-- Previene che altre mod usino la libreria globale sqlite3
|
||||||
|
if sqlite3 then
|
||||||
|
sqlite3 = nil
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Spiegare il funzionamento di SQL o della libreria lsqlite non rientra nell'obiettivo di questo libro.
|
||||||
|
|
||||||
|
## Decidere quale usare
|
||||||
|
|
||||||
|
Il tipo di metodo che si sceglie di utilizzare dipende dal tipo di dati trattati, come sono formattati e quanto sono grandi.
|
||||||
|
In linea di massima, i dati piccoli vanno fino ai 10KB, quelli medi 10MB, e quelli grandi oltre i 10MB.
|
||||||
|
|
||||||
|
I metadati dei nodi sono un'ottima scelta quando si vogliono immagazzinare dati relativi al nodo.
|
||||||
|
Inserirne di medi (quindi massimo 10MB) è abbastanza efficiente se si rendono privati.
|
||||||
|
|
||||||
|
Quelli degli oggetti invece dovrebbero essere usati solo per piccole quantità di dati e non è possibile evitare di inviarli al client.
|
||||||
|
I dati, poi, saranno anche copiati ogni volta che la pila viene spostata o ne viene fatto accesso tramite Lua.
|
||||||
|
|
||||||
|
La memoria interna della mod va bene per i dati di medie dimensioni, tuttavia provare a salvarne di grandi potrebbe rivelarsi inefficiente.
|
||||||
|
È meglio usare un database per le grandi porzioni di dati, onde evitare di dover sovrascrivere tutti i dati a ogni salvataggio.
|
||||||
|
|
||||||
|
I database sono fattibili solo per i server a causa della necessità di lasciar passare la mod nell'ambiente non sicuro.
|
||||||
|
Si prestano bene per i grandi ammontare di dati.
|
||||||
|
|
||||||
|
## Il tuo turno
|
||||||
|
|
||||||
|
* Crea un nodo che sparisce dopo essere stato colpito cinque volte.
|
||||||
|
(Usa `on_punch` nella definizione del nodo e `core.set_node`)
|
97
_it/map/timers.md
Normal file
97
_it/map/timers.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Timer dei nodi e ABM
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.2
|
||||||
|
description: Impara come creare ABM e timer per modificare i blocchi.
|
||||||
|
redirect_from:
|
||||||
|
- /it/chapters/abms.html
|
||||||
|
- /it/map/abms.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Eseguire periodicamente una funzione su certi nodi è abbastanza comune.
|
||||||
|
Minetest fornisce due metodi per fare ciò: gli ABM (*Active Block Modifiers*, Modificatori di blocchi attivi) e i timer associati ai nodi.
|
||||||
|
|
||||||
|
Gli ABM scansionano tutti i Blocchi Mappa alla ricerca dei nodi che rientrano nei canoni:
|
||||||
|
essi sono quindi ottimali per quei nodi che si trovano con frequenza in giro per il mondo, come l'erba.
|
||||||
|
Possiedono un alto consumo della CPU, senza invece pressoché impattare sulla memoria e lo spazio d'archiviazione.
|
||||||
|
|
||||||
|
Per i nodi invece non troppo comuni o che già usano metadati, come le fornaci e i macchinari, dovrebbero venire impiegati i timer.
|
||||||
|
I timer dei nodi tengon traccia dei timer accodati in ogni Blocco Mappa, eseguendoli quando raggiungono lo zero.
|
||||||
|
Ciò significa che non hanno bisogno di cercare tra tutti i nodi caricati per trovare un match, bensì, richiedendo un po' più di memoria e spazio d'archiviazione, vanno alla ricerca dei soli nodi con un timer in corso.
|
||||||
|
|
||||||
|
- [Timer dei nodi](#timer-dei-nodi)
|
||||||
|
- [ABM: modificatori di blocchi attivi](#abm-modificatori-di-blocchi-attivi)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
## Timer dei nodi
|
||||||
|
|
||||||
|
A ogni nodo è associato un timer.
|
||||||
|
Questi timer possono essere gestiti ottenendo un oggetto NodeTimerRef (quindi un riferimento, come già visto per gli inventari).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local timer = core.get_node_timer(pos)
|
||||||
|
timer:start(10.5) -- in secondi
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando un timer raggiunge lo zero, viene eseguito il metodo `on_timer`, che va dichiarato dentro la tabella di definizione del nodo.
|
||||||
|
`on_timer` richiede un solo parametro, ovvero la posizione del nodo.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("porteautomatiche:porta_aperta", {
|
||||||
|
on_timer = function(pos)
|
||||||
|
core.set_node(pos, { name = "porteautomatiche:porta_chiusa" })
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Ritornando `true`, il timer ripartirà (con la stessa durata di prima).
|
||||||
|
È inoltre possibile usare `get_node_timer(pos)` all'interno di `on_timer`, basta assicurarsi di ritornare `false` per evitare conflitti.
|
||||||
|
|
||||||
|
|
||||||
|
Potresti aver tuttavia notato una limitazione: per questioni di ottimizzazione, infatti, è possibile avere uno e un solo timer per tipo di nodo, e solo un timer attivo per nodo.
|
||||||
|
|
||||||
|
|
||||||
|
## ABM: modificatori di blocchi attivi
|
||||||
|
|
||||||
|
Erba aliena, a scopo illustrativo del capitolo, è un tipo d'erba che ha una probabilità di apparire vicino all'acqua.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("alieni:erba", {
|
||||||
|
description = "Erba Aliena",
|
||||||
|
light_source = 3, -- Il nodo irradia luce. Min 0, max 14
|
||||||
|
tiles = {"alieni_erba.png"},
|
||||||
|
groups = {choppy=1},
|
||||||
|
on_use = core.item_eat(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_abm({
|
||||||
|
nodenames = {"default:dirt_with_grass"}, -- nodo sul quale applicare l'ABM
|
||||||
|
neighbors = {"default:water_source", "default:water_flowing"}, -- nodi che devono essere nei suoi dintorni (almeno uno)
|
||||||
|
interval = 10.0, -- viene eseguito ogni 10 secondi
|
||||||
|
chance = 50, -- possibilità di partire su un nodo ogni 50
|
||||||
|
action = function(pos, node, active_object_count,
|
||||||
|
active_object_count_wider)
|
||||||
|
local pos = {x = pos.x, y = pos.y + 1, z = pos.z}
|
||||||
|
core.set_node(pos, {name = "alieni:erba"})
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo ABM viene eseguito ogni 10 secondi, e per ogni nodo d'erba (`default:default_with_grass`) c'è una possibilità su 50 che l'ABM parta.
|
||||||
|
Quando ciò accade, un nodo di erba aliena (`alieni:erba`) gli viene piazzato sopra (attenzione, tuttavia, che così facendo, il nodo che c'era prima verrà cancellato, quindi sarebbe meglio controllare prima che sopra ci sia dell'aria)
|
||||||
|
|
||||||
|
Specificare dei vicini (*neighbors*) è opzionale.
|
||||||
|
Se ne vengono specificati più di uno, basterà che uno solo di essi sia presente per soddisfare la condizione.
|
||||||
|
|
||||||
|
Anche le possibilità (*chance*) sono opzionali.
|
||||||
|
Se non vengono specificate, l'ABM verrà sempre eseguito quando le altre condizioni sono soddisfatte.
|
||||||
|
|
||||||
|
## Il tuo turno
|
||||||
|
|
||||||
|
* Tocco di Mida: tramuta l'acqua in oro con una possibilità di 1 su 100, ogni 5 secondi;
|
||||||
|
* Decadimento: fai che il legno diventi terra quando questo confina con dell'acqua.
|
||||||
|
* Al fuoco!: fai prendere fuoco a ogni blocco d'aria (suggerimento: "air" e "fire:basic_flame"). Avvertenza: aspettati un crash del gioco
|
190
_it/players/chat.md
Executable file
190
_it/players/chat.md
Executable file
@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
title: Chat e comandi
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.2
|
||||||
|
description: Come registrare un comando e gestire i messaggi della chat
|
||||||
|
redirect_from: /it/chapters/chat.html
|
||||||
|
cmd_online:
|
||||||
|
level: warning
|
||||||
|
title: I giocatori offline possono eseguire comandi
|
||||||
|
message: |
|
||||||
|
Viene passato il nome del giocatore al posto del giocatore in sé perché le mod possono eseguire comandi in vece di un giocatore offline.
|
||||||
|
Per esempio, il ponte IRC permette ai giocatori di eseguire comandi senza dover entrare in gioco.
|
||||||
|
|
||||||
|
Assicurati quindi di non dar per scontato che un giocatore sia connesso.
|
||||||
|
Puoi controllare ciò tramite `core.get_player_by_name`, per vedere se ritorna qualcosa o meno.
|
||||||
|
|
||||||
|
cb_cmdsprivs:
|
||||||
|
level: warning
|
||||||
|
title: Privilegi e comandi
|
||||||
|
message: |
|
||||||
|
Il privilegio shout non è necessario per far sì che un giocatore attivi questo richiamo.
|
||||||
|
Questo perché i comandi sono implementati in Lua, e sono semplicemente dei messaggi in chat che iniziano con /.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Le mod possono interagire con la chat del giocatore, tra l'inviare messaggi, intercettarli e registrare dei comandi.
|
||||||
|
|
||||||
|
- [Inviare messaggi](#inviare-messaggi)
|
||||||
|
- [A tutti i giocatori](#a-tutti-i-giocatori)
|
||||||
|
- [A giocatori specifici](#a-giocatori-specifici)
|
||||||
|
- [Comandi](#comandi)
|
||||||
|
- [Accettare più argomenti](#accettare-più-argomenti)
|
||||||
|
- [Usare string.split](#usare-stringsplit)
|
||||||
|
- [Usare i pattern Lua](#usare-i-pattern-lua)
|
||||||
|
- [Intercettare i messaggi](#intercettare-i-messaggi)
|
||||||
|
|
||||||
|
## Inviare messaggi
|
||||||
|
|
||||||
|
### A tutti i giocatori
|
||||||
|
|
||||||
|
Per inviare un messaggio a tutti i giocatori connessi in gioco, si usa la funzione `chat_send_all`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.chat_send_all("Questo è un messaggio visualizzabile da tutti")
|
||||||
|
```
|
||||||
|
|
||||||
|
Segue un esempio di come apparirerebbe in gioco:
|
||||||
|
|
||||||
|
<Tizio> Guarda qui
|
||||||
|
Questo è un messaggio visualizzabile da tutti
|
||||||
|
<Caio> Eh, cosa?
|
||||||
|
|
||||||
|
Il messaggio appare su una nuova riga, per distinguerlo dai messaggi dei giocatori.
|
||||||
|
|
||||||
|
### A giocatori specifici
|
||||||
|
|
||||||
|
Per inviare un messaggio a un giocatore in particolare, si usa invece la funzione `chat_send_player`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.chat_send_player("Tizio", "Questo è un messaggio per Tizio")
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo messaggio viene mostrato esattamente come il precedente, ma solo, in questo caso, a Tizio.
|
||||||
|
|
||||||
|
## Comandi
|
||||||
|
|
||||||
|
Per registrare un comando, per esempio `/foo`, si usa `register_chatcommand`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_chatcommand("foo", {
|
||||||
|
privs = {
|
||||||
|
interact = true,
|
||||||
|
},
|
||||||
|
func = function(name, param)
|
||||||
|
return true, "Hai detto " .. param .. "!"
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Nel codice qui in alto, `interact` è elencato come [privilegio](privileges.html) necessario; in altre parole, solo i giocatori che hanno quel privilegio possono usare il comando.
|
||||||
|
|
||||||
|
`param` è una stringa contenente tutto ciò che un giocatore scrive dopo il nome del comando.
|
||||||
|
Per esempio, in `/grantme uno,due,tre`, `param` equivarrà a `uno,due,tre`.
|
||||||
|
|
||||||
|
I comandi ritornano un massimo di due valori, dove il primo è un booleano che indica l'eventuale successo, mentre il secondo è un messaggio da inviare all'utente.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.cmd_online %}
|
||||||
|
|
||||||
|
|
||||||
|
### Accettare più argomenti
|
||||||
|
|
||||||
|
Non è raro che i comandi richiedano più argomenti, come per esempio `/squadra entra <nome_squadra>`.
|
||||||
|
Ci sono due modi per implementare ciò: usare `string.split` o i pattern Lua.
|
||||||
|
|
||||||
|
|
||||||
|
#### Usare string.split
|
||||||
|
|
||||||
|
Una stringa può essere spezzettata in più parti tramite `string.split(" ")`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local parti = param:split(" ")
|
||||||
|
local cmd = parti[1]
|
||||||
|
|
||||||
|
if cmd == "entra" then
|
||||||
|
local nome_squadra = parti[2]
|
||||||
|
squadra.entra(name, nome_squadra)
|
||||||
|
return true, "Sei dentro la squadra!"
|
||||||
|
elseif cmd == "gioc_max" then
|
||||||
|
local nome_squadra = parti[2]
|
||||||
|
local gioc_max = tonumber(parti[3])
|
||||||
|
if nome_squadra and gioc_max then
|
||||||
|
return true, "Giocatori massimi della squadra " .. nome_squadra .. " impostati a " .. gioc_max
|
||||||
|
else
|
||||||
|
return false, "Uso: /squadra gioc_max <nome_squadra> <numero>"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return false, "È necessario un comando"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Usare i pattern Lua
|
||||||
|
|
||||||
|
[I pattern Lua](https://www.lua.org/pil/20.2.html) sono un modo per estrapolare pezzi di testo seguendo delle regole.
|
||||||
|
Sono perfetti in caso di argomenti che contengono spazi, o comunque quando è richiesto un controllo più meticoloso dei parametri catturati.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local a, msg = string.match(param, "^([%a%d_-]+) (.+)$")
|
||||||
|
```
|
||||||
|
|
||||||
|
Il codice sovrastante implementa `/msg <a> <messaggio>`. Vediamo cos'è successo partendo da sinistra:
|
||||||
|
|
||||||
|
* `^` dice di iniziare a combaciare dall'inizio della stringa;
|
||||||
|
* `()` è un gruppo - qualsiasi cosa che combaci con ciò che è contenuto al suo interno verrà ritornato da string.match;
|
||||||
|
* `[]` significa che i caratteri al suo interno sono accettati;
|
||||||
|
* `%a` significa che accetta ogni lettera e `%d` ogni cifra.
|
||||||
|
* `[%a%d_-]` significa che accetta ogni lettera, cifra, `_` e `-`.
|
||||||
|
* `+` dice di combaciare ciò che lo precede una o più volte.
|
||||||
|
* `.` dice di combaciare qualsiasi tipo di carattere.
|
||||||
|
* `$` dice di combaciare la fine della stringa.
|
||||||
|
|
||||||
|
Detto semplicemente, il pattern cerca un nome (una parola fatta di lettere, numeri, trattini o trattini bassi), poi uno spazio e poi il messaggio (uno o più caratteri, qualsiasi essi siano).
|
||||||
|
Vengono poi ritornati nome e messaggio, perché sono inseriti nelle parentesi.
|
||||||
|
|
||||||
|
Questo è come molte mod implementano comandi complessi.
|
||||||
|
Una guida più completa ai pattern è probabilmente quella su [lua-users.org](http://lua-users.org/wiki/PatternsTutorial) o la [documentazione PIL](https://www.lua.org/pil/20.2.html).
|
||||||
|
|
||||||
|
<p class="book_hide">
|
||||||
|
C'è anche una libreria scritta dall'autore di questo libro che può essere usata
|
||||||
|
per creare comandi complessi senza l'utilizzo di pattern:
|
||||||
|
<a href="https://gitlab.com/rubenwardy/ChatCmdBuilder">Chat Command Builder</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Intercettare i messaggi
|
||||||
|
|
||||||
|
Per intercettare un messaggio, si usa `register_on_chat_message`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_chat_message(function(name, message)
|
||||||
|
print(name .. " ha detto " .. message)
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ritornando `false`, si permette al messaggio di essere inviato.
|
||||||
|
In verità `return false` può anche essere omesso in quanto `nil` verrebbe ritornato implicitamente, e nil è trattato come false.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.cb_cmdsprivs %}
|
||||||
|
|
||||||
|
Dovresti assicurarti, poi, che il messaggio potrebbe essere un comando che invia messaggi in chat,
|
||||||
|
o che l'utente potrebbere non avere `shout`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_chat_message(function(name, message)
|
||||||
|
if message:sub(1, 1) == "/" then
|
||||||
|
print(name .. " ha eseguito un comando")
|
||||||
|
elseif core.check_player_privs(name, { shout = true }) then
|
||||||
|
print(name .. " ha detto " .. message)
|
||||||
|
else
|
||||||
|
print(name .. " ha provato a dire " .. message ..
|
||||||
|
" ma non ha lo shout")
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
```
|
330
_it/players/formspecs.md
Normal file
330
_it/players/formspecs.md
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
---
|
||||||
|
title: GUI (Formspec)
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.5
|
||||||
|
description: Tempo di interagire con le finestre
|
||||||
|
redirect_from: /it/chapters/formspecs.html
|
||||||
|
submit_vuln:
|
||||||
|
level: warning
|
||||||
|
title: Client malevoli possono inviare qualsiasi cosa quando più gli piace
|
||||||
|
message: Non dovresti mai fidarti di un modulo di compilazione - anche se non hai mai mostrato loro il formspec.
|
||||||
|
Questo significa che dovresti controllarne i privilegi e assicurarti che dovrebbero effettivamente essere in grado di eseguire quest'azione.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/formspec_example.png" alt="Furnace Inventory">
|
||||||
|
<figcaption>
|
||||||
|
Screenshot del formspec di una fornace e della sua struttura.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
In questo capitolo impareremo come creare un formspec e mostrarlo all'utente.
|
||||||
|
Un formspec è il codice di specifica di un modulo (*form*, da qui *form*-*spec*).
|
||||||
|
In Minetest, i moduli sono delle finestre come l'inventario del giocatore e possono contenere un'ampia gamma di elementi, come le etichette, i pulsanti e i campi.
|
||||||
|
|
||||||
|
Tieni presente che se non si ha bisogno di ricevere input dal giocatore, per esempio quando si vogliono far apparire semplicemente delle istruzioni a schermo, si dovrebbe considerare l'utilizzo di una [HUD (Heads Up Display)](hud.html) piuttosto che quello di un formspec, in quanto le finestre inaspettate (con tanto di mouse che appare) tendono a impattare negativamente sulla giocabilità.
|
||||||
|
|
||||||
|
- [Coordinate reali o datate](#coordinate-reali-o-datate)
|
||||||
|
- [Anatomia di un formspec](#anatomia-di-un-formspec)
|
||||||
|
- [Elementi](#elementi)
|
||||||
|
- [Intestazione](#intestazione)
|
||||||
|
- [Esempio: indovina un numero](#esempio-indovina-un-numero)
|
||||||
|
- [Imbottitura e spaziatura](#imbottitura-e-spaziatura)
|
||||||
|
- [Ricevere i moduli di compilazione](#ricevere-i-moduli-di-compilazione)
|
||||||
|
- [Contesti](#contesti)
|
||||||
|
- [Ricavare un formspec](#ricavare-un-formspec)
|
||||||
|
- [Formspec nei nodi](#formspec-nei-nodi)
|
||||||
|
- [Inventario del giocatore](#inventario-del-giocatore)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
|
||||||
|
## Coordinate reali o datate
|
||||||
|
|
||||||
|
Nelle vecchie versioni di Minetest, i formspec erano incoerenti.
|
||||||
|
Il modo in cui elementi diversi venivano posizionati nel formspec variava in maniere inaspettate; era difficile predirne la collocazione e allinearli correttamente.
|
||||||
|
Da Minetest 5.1.0, tuttavia, è stata introdotta una funzione chiamata Coordinate Reali (*real coordinates*), la quale punta a correggere questo comportamento tramite l'introduzione di un sistema di coordinate coerente.
|
||||||
|
L'uso delle coordinate reali è caldamente consigliato, onde per cui questo capitolo non tratterà di quelle vecchie.
|
||||||
|
|
||||||
|
Usando una versione del formspec maggiore o uguale a 2, esse saranno abilitate di base.
|
||||||
|
|
||||||
|
## Anatomia di un formspec
|
||||||
|
|
||||||
|
### Elementi
|
||||||
|
|
||||||
|
Il formspec è un linguaggio di dominio specifico con un formato insolito.
|
||||||
|
Consiste in un numero di elementi che seguono il seguente schema:
|
||||||
|
|
||||||
|
tipo[param1;param2]
|
||||||
|
|
||||||
|
Viene prima dichiarato il tipo dell'elemento, seguito dai parametri nelle parentesi quadre.
|
||||||
|
Si possono concatenare più elementi, piazzandoli eventualmente su più linee:
|
||||||
|
|
||||||
|
foo[param1]bar[param1]
|
||||||
|
bo[param1]
|
||||||
|
|
||||||
|
Gli elementi sono o oggetti come i campi di testo e i pulsanti, o dei metadati come la grandezza e lo sfondo.
|
||||||
|
Per una lista esaustiva di tutti i possibili elementi, si rimanda a [lua_api.md](https://minetest.gitlab.io/minetest/formspec/).
|
||||||
|
|
||||||
|
### Intestazione
|
||||||
|
|
||||||
|
L'intestazione di un formspec contiene informazioni che devono apparire prima di tutto il resto.
|
||||||
|
Questo include la grandezza del formspec, la posizione, l'ancoraggio, e se il tema specifico del gioco debba venir applicato.
|
||||||
|
|
||||||
|
Gli elementi nell'intestazione devono essere definiti in un ordine preciso, altrimenti ritorneranno un errore.
|
||||||
|
L'ordine è dato nel paragrafo qui in alto e, come sempre, documentato in lua_api.md.
|
||||||
|
|
||||||
|
La grandezza è in caselle formspec - un'unità di misura che è circa 64 pixel, ma varia a seconda della densità dello schermo e delle impostazioni del client.
|
||||||
|
Ecco un formspec di 2x2:
|
||||||
|
|
||||||
|
formspec_version[4]
|
||||||
|
size[2,2]
|
||||||
|
|
||||||
|
Notare come è stata esplicitamente definita la versione del linguaggio: senza di essa, il sistema datato sarebbe stato usato di base - che avrebbe impossibilitato il posizionamento coerente degli elementi e altre nuove funzioni.
|
||||||
|
|
||||||
|
La posizione e l'ancoraggio degli elementi sono usati per collocare il formspec nello schermo.
|
||||||
|
La posizione imposta dove si troverà (con valore predefinito al centro, `0.5,0.5`), mentre l'ancoraggio da dove partire, permettendo di allineare il formspec con i bordi dello schermo.
|
||||||
|
Per esempio, lo si può posizionare ancorato a sinistra in questo modo:
|
||||||
|
|
||||||
|
formspec_version[4]
|
||||||
|
size[2,2]
|
||||||
|
position[0,0.5]
|
||||||
|
anchor[0,0.5]
|
||||||
|
|
||||||
|
Per l'esattezza è stato messo il centro del formspec sul bordo sinistro dello schermo (`position[0, 0.5]`) e poi ne è stato spostato l'ancoraggio in modo da allineare il lato sinistro del formspec con quello dello schermo.
|
||||||
|
|
||||||
|
## Esempio: indovina un numero
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/formspec_guessing.png" alt="Guessing Formspec">
|
||||||
|
<figcaption>
|
||||||
|
Il formspec del gioco dell'indovinare un numero
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Il modo migliore per imparare è sporcarsi le mani, quindi creiamo un gioco.
|
||||||
|
Il principio è semplice: la mod decide un numero, e il giocatore deve tentare di indovinarlo.
|
||||||
|
La mod, poi, comunica se si è detto un numero più alto o più basso rispetto a quello corretto.
|
||||||
|
|
||||||
|
Prima di tutto, costruiamo una funzione per creare il formspec.
|
||||||
|
È buona pratica fare ciò, in quanto rende il riutilizzo più comodo.
|
||||||
|
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
indovina = {}
|
||||||
|
|
||||||
|
function indovina.prendi_formspec(nome)
|
||||||
|
-- TODO: comunicare se il numero del tentativo era più alto o più basso
|
||||||
|
local testo = "Sto pensando a un numero... Prova a indovinare!"
|
||||||
|
|
||||||
|
local formspec = {
|
||||||
|
"formspec_version[4]",
|
||||||
|
"size[6,3.476]",
|
||||||
|
"label[0.375,0.5;", core.formspec_escape(testo), "]",
|
||||||
|
"field[0.375,1.25;5.25,0.8;numero;Numero;]",
|
||||||
|
"button[1.5,2.3;3,0.8;indovina;Indovina]"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- table.concat è più veloce della concatenazione di stringhe - `..`
|
||||||
|
return table.concat(formspec, "")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Nel codice qui sopra abbiamo inserito un'etichetta (*label*), un campo (*field*) e un pulante (*button*).
|
||||||
|
Un campo ci permete di inserire del testo, mentre useremo il pulsante per inviare il modulo.
|
||||||
|
Noterai che gli elementi sono posizionati attentamente per aggiungere imbottitura e spaziatura (*padding* e *spacing*),
|
||||||
|
ma ci arriveremo tra poco.
|
||||||
|
|
||||||
|
Come prossima cosa, vogliamo permettere al giocatore di visualizzare il formspec.
|
||||||
|
Il metodo principale per farlo è usare `show_formspec`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function indovina.mostra_a(nome)
|
||||||
|
core.show_formspec(nome, "indovina:gioco", indovina.prendi_formspec(nome))
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_chatcommand("gioco", {
|
||||||
|
func = function(name)
|
||||||
|
indovina.mostra_a(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
La funzione `show_formspec` prende il nome del giocatore, il nome del formspec e il formspec stesso.
|
||||||
|
Il nome di quest'ultimo dovrebbe seguire il formato del nome degli oggetti, tipo `nomemod:nomeoggetto`.
|
||||||
|
|
||||||
|
### Imbottitura e spaziatura
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/formspec_padding_spacing.png" alt="Padding and spacing">
|
||||||
|
<figcaption>
|
||||||
|
Il formspec del gioco dell'indovinare un numero
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
L'imbottitura (*padding*) è lo spazio che intercorre tra il bordo del formspec e i suoi contenuti, o tra elementi non in relazione fra loro - mostrato in rosso.
|
||||||
|
La spaziatura (*spacing*) è invece lo spazio tra elementi in comune - mostrata in blu.
|
||||||
|
|
||||||
|
È abbastanza uno standard avere un'imbottitura di `0.375` e una spaziatura di `0.25`.
|
||||||
|
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
|
||||||
|
### Ricevere i moduli di compilazione
|
||||||
|
|
||||||
|
Quando `show_formspec` viene chiamato, il formspec viene inviato al client per essere visualizzato.
|
||||||
|
Per far sì che i formspec siano utili, le informazioni devono essere ritornate dal client al server.
|
||||||
|
Il metodo per fare ciò è chiamato Campo di Compilazione (*formspec field submission*), e per `show_formspec` quel campo viene ottenuto usando un callback globale:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player, formname, fields)
|
||||||
|
if formname ~= "indovina:gioco" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if fields.indovina then
|
||||||
|
local p_name = player:get_player_name()
|
||||||
|
core.chat_send_all(p_name .. " ha tentato di indovinare con il numero " .. fields.numero)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
La funzione data in `core.register_on_player_receive_fields` è chiamata ogni volta che un utente invia un modulo.
|
||||||
|
La maggior parte dei callback necessiteranno di controllare il nome fornito alla funzione, e uscire se non è quello esatto; tuttavia, alcuni potrebbero necessitare di operare su più moduli, se non addirittura su tutti.
|
||||||
|
|
||||||
|
Il parametro `fields` è una tabella di tutti i valori inviati dall'utente, indicizzati per stringhe.
|
||||||
|
I nomi degli elementi appariranno nel campo con il loro nome, ma solo se sono rilevanti per l'evento che ha causato l'invio.
|
||||||
|
Per esempio, un elemento "pulsante" apparirà nei campi solo se quel particolare pulsante è stato premuto.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.submit_vuln %}
|
||||||
|
|
||||||
|
Quindi, ora il formspec è stato inviato al client e il client ritorna quelle informazioni.
|
||||||
|
Il prossimo passaggio è generare e ricordare il valore ricevuto, e aggiornare il formspec basandosi sui tentativi.
|
||||||
|
Il modo per fare ciò è usare un concetto chiamato "contesto".
|
||||||
|
|
||||||
|
|
||||||
|
### Contesti
|
||||||
|
|
||||||
|
In molti casi si può desiderare che le informazioni passate da `show_formspec` al callback non raggiungano il client.
|
||||||
|
Ciò potrebbe includere con cosa è stato chiamato un comando via chat, o di cosa tratta la finestra di dialogo.
|
||||||
|
In questo caso, il valore che si necessita di ricordare.
|
||||||
|
|
||||||
|
Un contesto (*context*) è una tabella assegnata a ogni giocatore per immagazzinare informazioni, e i contesti di tutti i giocatori sono
|
||||||
|
salvati in una variabile locale di file:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local _contesti = {}
|
||||||
|
local function prendi_contesto(nome)
|
||||||
|
local contesto = _contesto[nome] or {}
|
||||||
|
_contesti[nome] = contesto
|
||||||
|
return contesto
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
_contexts[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ora abbiamo bisogno di modificare il codice da mostrare, per aggiornare il contesto prima di mostrare il formspec:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function indovina.mostra_a(nome)
|
||||||
|
local contesto = prendi_contesto(nome)
|
||||||
|
contesto.soluzione = contesto.soluzione or math.random(1, 10)
|
||||||
|
|
||||||
|
local formspec = indovina.prendi_formspec(nome, contesto)
|
||||||
|
core.show_formspec(nome, "indovina:gioco", formspec)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Abbiamo anche bisogno di modificare la generazione del formspec per usare il contesto:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function indovina.prendi_formspec(nome, contesto)
|
||||||
|
local testo
|
||||||
|
if not contesto.tentativo then
|
||||||
|
testo = "Sto pensando a un numero... Prova a indovinare!"
|
||||||
|
elseif contesto.tentativo == contesto.soluzione then
|
||||||
|
testo = "Yeee, hai indovinato!"
|
||||||
|
elseif contesto.tentativo > contesto.soluzione then
|
||||||
|
testo = "Troppo alto!"
|
||||||
|
else
|
||||||
|
testo = "Troppo basso!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Tieni a mente che quando si ottiene il formspec è buona norma leggerne il contesto, senza però aggiornalo.
|
||||||
|
Questo può rendere la funzione più semplice, e anche più facile da testare.
|
||||||
|
|
||||||
|
E in ultimo, abbiamo bisogno di aggiornare il contesto con il tentativo del giocatore:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if fields.indovina then
|
||||||
|
local nome = player:get_player_name()
|
||||||
|
local contesto = prendi_contesto(nome)
|
||||||
|
contesto.tentativo = tonumber(fields.numero)
|
||||||
|
indovina.mostra_a(nome)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ricavare un formspec
|
||||||
|
|
||||||
|
Ci sono tre diversi modi per far sì che un formspec sia consegnato al client:
|
||||||
|
|
||||||
|
* [show_formspec](#esempio-indovina-un-numero): il metodo usato qui sopra. I campi sono ottenuti tramite `register_on_player_receive_fields`;
|
||||||
|
* [Metadati di un nodo](#formspec-nei-nodi): si aggiunge il formspec nel nodo tramite metadati, che viene mostrato *immediatamente* al giocatore che preme il nodo col tasto destro.
|
||||||
|
I campi vengono ricevuti attraverso un metodo nella definizione del nodo chiamato `on_receive_fields`.
|
||||||
|
* [Inventario del giocatore](#inventario-del-giocatore): il formspec viene inviato al client in un certo momento, e mostrato immediatamente quando il giocatore preme "I".
|
||||||
|
I campi vengono ricevuti tramite `register_on_player_receive_fields`.
|
||||||
|
|
||||||
|
### Formspec nei nodi
|
||||||
|
|
||||||
|
`core.show_formspec` non è l'unico modo per mostrare un formspec; essi possono infatti essere aggiunti anche ai [metadati di un nodo](../map/storage.html).
|
||||||
|
Per esempio, questo è usato con le casse per permettere tempi più veloci d'apertura - non si ha bisogno di aspettare che il server invii il formspec della cassa al giocatore.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("miamod:tastodestro", {
|
||||||
|
description = "Premimi col tasto destro del mouse!",
|
||||||
|
tiles = {"miamod_tastodestro.png"},
|
||||||
|
groups = {cracky = 1},
|
||||||
|
after_place_node = function(pos, placer)
|
||||||
|
-- Questa funzione è eseguita quando viene piazzato il nodo.
|
||||||
|
-- Il codice che segue imposta il formspec della cassa.
|
||||||
|
-- I metadati sono un modo per immagazzinare dati nel nodo.
|
||||||
|
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("formspec",
|
||||||
|
"formspec_version[4]" ..
|
||||||
|
"size[5,5]"..
|
||||||
|
"label[1,1;Questo è mostrato al premere col destro]"..
|
||||||
|
"field[1,2;2,1;x;x;]")
|
||||||
|
end,
|
||||||
|
on_receive_fields = function(pos, formname, fields, player)
|
||||||
|
if(fields.quit) then return end
|
||||||
|
print(fields.x)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
I formspec impostati in questo modo non innescano lo stesso callback.
|
||||||
|
Per far in modo di ricevere il modulo di input per i formspec nei nodi, bisogna includere una voce `on_receive_fields` al registrare il nodo.
|
||||||
|
|
||||||
|
Questo stile di callback viene innescato al premere invio in un campo, che è possibile grazie a `core.show_formspec`; tuttavia, questi tipi di moduli possono essere mostrati solo
|
||||||
|
tramite il premere col tasto destro del mouse su un nodo. Non è possibile farlo programmaticamente.
|
||||||
|
|
||||||
|
### Inventario del giocatore
|
||||||
|
|
||||||
|
L'inventario del giocatore è un formspec, che viene mostrato al premere "I".
|
||||||
|
Il callback globale viene usato per ricevere eventi dall'inventario, e il suo nome è `""`.
|
||||||
|
|
||||||
|
Ci sono svariate mod che permettono ad altrettante mod di personalizzare l'inventario del giocatore.
|
||||||
|
La mod ufficialmente raccomandata è [SFINV](https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md), ed è inclusa in Minetest Game.
|
||||||
|
|
||||||
|
### Il tuo turno
|
||||||
|
|
||||||
|
* Estendi l'indovina il numero per far in modo che tenga traccia del risultato migliore di ogni giocatore, dove con "risultato migliore" si intende il minor numero di tentativi per indovinare.
|
||||||
|
* Crea un nodo chiamato "Casella delle lettere" dove gli utenti possono aprire un formspec e lasciare messaggi.
|
||||||
|
Questo nodo dovrebbe salvare il nome del mittente come `owner` nei metadati, e dovrebbe usare `show_formspec` per mostrare formspec differenti a giocatori differenti.
|
281
_it/players/hud.md
Normal file
281
_it/players/hud.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
---
|
||||||
|
title: HUD
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.6
|
||||||
|
description: come creare elementi a schermo
|
||||||
|
redirect_from: /it/chapters/hud.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Le HUD (Heads Up Display) ti permettono di mostrare testi, immagini e altri elementi grafici senza interrompere il giocatore.
|
||||||
|
Le HUD, infatti, non accettano input dall'utente, lasciando quel ruolo ai [formspec](formspecs.html).
|
||||||
|
|
||||||
|
- [Posizionamento](#posizionamento)
|
||||||
|
- [Posizione e scostamento](#posizione-e-scostamento)
|
||||||
|
- [Allineamento](#allineamento)
|
||||||
|
- [Esempio: tabellone segnapunti](#esempio-tabellone-segnapunti)
|
||||||
|
- [Elementi di testo](#elementi-di-testo)
|
||||||
|
- [Parametri](#parametri)
|
||||||
|
- [Tornando all'esempio](#tornando-allesempio)
|
||||||
|
- [Elementi immagine](#elementi-immagine)
|
||||||
|
- [Parametri](#parametri-1)
|
||||||
|
- [Tornando all'esempio](#tornando-allesempio-1)
|
||||||
|
- [Cambiare un elemento](#cambiare-un-elemento)
|
||||||
|
- [Salvare gli ID](#salvare-gli-id)
|
||||||
|
- [Altri elementi](#altri-elementi)
|
||||||
|
|
||||||
|
## Posizionamento
|
||||||
|
|
||||||
|
### Posizione e scostamento
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img
|
||||||
|
width="300"
|
||||||
|
src="{{ page.root }}//static/hud_diagram_center.svg"
|
||||||
|
alt="Diagramma che mostra un elemento di testo centrato">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Essendoci schermi di tutte le dimensioni e risoluzioni, per funzionare bene le HUD devono sapersi adattare a ognuno di essi.
|
||||||
|
|
||||||
|
Per ovviare al problema, Minetest specifica il collocamento di un elemento usando sia una posizione in percentuale che uno scostamento (*offset*).
|
||||||
|
La posizione percentuale è relativa alla grandezza dello schermo, dacché per posizionarne un elemento al centro, bisogna fornire un valore di 0.5 (cioè il 50%).
|
||||||
|
|
||||||
|
Lo scostamento è poi usato per - appunto - scostare un elemento in relazione alla sua posizione.
|
||||||
|
|
||||||
|
<div style="clear:both;"></div>
|
||||||
|
|
||||||
|
### Allineamento
|
||||||
|
|
||||||
|
L'allineamento (*alignment*) è dove il risultato della posizione e dello scostamento viene applicato sull'elemento - per esempio, `{x = -1.0, y = 0.0}` allineerà i valori degli altri due parametri sulla sinistra dell'elemento.
|
||||||
|
Questo è particolarmente utile quando si vuole allineare del testo a sinistra, a destra o al centro.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
width="500"
|
||||||
|
src="{{ page.root }}//static/hud_diagram_alignment.svg"
|
||||||
|
alt="Diagramma che mostra i vari tipi di allineamento">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Il diagramma qui in alto mostra mostra tre finestre (in blu), ognuna contenente un elemento HUD (in giallo) con ogni volta un allineamento diverso.
|
||||||
|
La freccia è il risultato del calcolo di posizione e scostamento.
|
||||||
|
|
||||||
|
### Esempio: tabellone segnapunti
|
||||||
|
|
||||||
|
Per capire meglio il capitolo, vedremo come posizionare e aggiornare un pannello contenente dei punti come questo:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_final.png"
|
||||||
|
alt="Screenshot dell'HUD da realizzare">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Nello screenshot sovrastante, tutti gli elementi hanno la stessa posizione percentuale (100%, 50%) - ma scostamenti diversi.
|
||||||
|
Questo permette all'intero pannello di essere ancorato sulla destra della finestra, senza posizioni/scostamenti strani al ridimensionarla.
|
||||||
|
|
||||||
|
## Elementi di testo
|
||||||
|
|
||||||
|
Puoi creare un elemento HUD una volta ottenuto il riferimento al giocatore al quale assegnarla:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local giocatore = core.get_player_by_name("tizio")
|
||||||
|
local idx = giocatore:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 0.5, y = 0.5},
|
||||||
|
offset = {x = 0, y = 0},
|
||||||
|
text = "Ciao mondo!",
|
||||||
|
alignment = {x = 0, y = 0}, -- allineamento centrato
|
||||||
|
scale = {x = 100, y = 100}, -- lo vedremo tra poco
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
La funzione `hud_add` ritorna l'ID di un elemento, che è utile per modificarlo o rimuoverlo.
|
||||||
|
|
||||||
|
### Parametri
|
||||||
|
|
||||||
|
Il tipo dell'elemento è ottenuto usando la proprietà `hud_elem_type` nella tabella di definizione.
|
||||||
|
Cambiando il tipo, cambia il significato di alcune delle proprietà che seguono.
|
||||||
|
|
||||||
|
`scale` sono i limiti del testo; se esce da questo spazio, viene tagliato - nell'esempio `{x=100, y=100}`.
|
||||||
|
|
||||||
|
`number` è il colore del testo, ed è in [formato esadecimale](http://www.colorpicker.com/) - nell'esempio `0xFF0000`.
|
||||||
|
|
||||||
|
|
||||||
|
### Tornando all'esempio
|
||||||
|
|
||||||
|
Andiamo avanti e piazziamo tutto il testo nel nostro pannello (si son tenute le stringhe in inglese per non confondere con l'immagine, NdT):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Ottiene il numero di blocchi scavati e piazzati dallo spazio d'archiviazione; se non esiste è 0
|
||||||
|
local meta = giocatore:get_meta()
|
||||||
|
local digs_testo = "Digs: " .. meta:get_int("punteggio:digs")
|
||||||
|
local places_testo = "Places: " .. meta:get_int("punteggio:places")
|
||||||
|
|
||||||
|
giocatore:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -120, y = -25},
|
||||||
|
text = "Stats",
|
||||||
|
alignment = 0,
|
||||||
|
scale = { x = 100, y = 30},
|
||||||
|
number = 0xFFFFFF,
|
||||||
|
})
|
||||||
|
|
||||||
|
giocatore:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -180, y = 0},
|
||||||
|
text = digs_testo,
|
||||||
|
alignment = -1,
|
||||||
|
scale = { x = 50, y = 10},
|
||||||
|
number = 0xFFFFFF,
|
||||||
|
})
|
||||||
|
|
||||||
|
giocatore:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -70, y = 0},
|
||||||
|
text = places_testo,
|
||||||
|
alignment = -1,
|
||||||
|
scale = { x = 50, y = 10},
|
||||||
|
number = 0xFFFFFF,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Il risultato è il seguente:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_text.png"
|
||||||
|
alt="Screenshot della HUD finora">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
|
||||||
|
## Elementi immagine
|
||||||
|
|
||||||
|
Le immagini sono create in un modo molto simile a quelli del testo:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
player:hud_add({
|
||||||
|
hud_elem_type = "image",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -220, y = 0},
|
||||||
|
text = "punteggio_sfondo.png",
|
||||||
|
scale = { x = 1, y = 1},
|
||||||
|
alignment = { x = 1, y = 0 },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Siamo ora a questo punto:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_background_img.png"
|
||||||
|
alt="Screenshot della HUD finora">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
### Parametri
|
||||||
|
|
||||||
|
Il campo `text` in questo caso viene usato per fornire il nome dell'immagine.
|
||||||
|
|
||||||
|
Se una coordinata in `scale` è positiva, allora è un fattore scalare dove 1 è la grandezza originale, 2 è il doppio e così via.
|
||||||
|
Tuttavia, se una coordinata è negativa, sarà la percentuale della grandezza dello schermo.
|
||||||
|
Per esempio, `x=-100` equivale al 100% della larghezza di quest'ultimo.
|
||||||
|
|
||||||
|
### Tornando all'esempio
|
||||||
|
|
||||||
|
Creiamo la barra di progresso per il nostro tabellone usando `scale`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local percentuale = tonumber(meta:get("punteggio:score") or 0.2)
|
||||||
|
|
||||||
|
giocatore:hud_add({
|
||||||
|
hud_elem_type = "image",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -215, y = 23},
|
||||||
|
text = "barra_punteggio_vuota.png",
|
||||||
|
scale = { x = 1, y = 1},
|
||||||
|
alignment = { x = 1, y = 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
giocatore:hud_add({
|
||||||
|
hud_elem_type = "image",
|
||||||
|
position = {x = 1, y = 0.5},
|
||||||
|
offset = {x = -215, y = 23},
|
||||||
|
text = "barra_punteggio_piena.png",
|
||||||
|
scale = { x = percentuale, y = 1},
|
||||||
|
alignment = { x = 1, y = 0 },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Abbiamo ora una HUD uguale identica a quella della prima immagine!
|
||||||
|
C'è un problema, tuttavia: non si aggiornerà al cambiare delle statistiche.
|
||||||
|
|
||||||
|
## Cambiare un elemento
|
||||||
|
|
||||||
|
Per cambiare un elemento si usa l'ID ritornato dal metodo `hud_add`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = giocatore:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
text = "Ciao mondo!",
|
||||||
|
-- parametri rimossi per brevità
|
||||||
|
})
|
||||||
|
|
||||||
|
giocatore:hud_change(idx, "text", "Nuovo testo")
|
||||||
|
giocatore:hud_remove(idx)
|
||||||
|
```
|
||||||
|
|
||||||
|
Il metodo `hud_change` prende l'ID dell'elemento, la proprietà da cambiare e il nuovo valore.
|
||||||
|
La chiamata qui sopra cambia la proprietà `text` da "Ciao mondo!" a "Nuovo test".
|
||||||
|
|
||||||
|
Questo significa che fare `hud_change` subito dopo `hud_add` è funzionalmente equivalente a
|
||||||
|
fare ciò che segue, in maniera alquanto inefficiente:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = giocatore:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
text = "Nuovo testo",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Salvare gli ID
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local hud_salvate = {}
|
||||||
|
|
||||||
|
function punteggio.aggiorna_hud(giocatore)
|
||||||
|
local nome_giocatore = giocatore:get_player_name()
|
||||||
|
|
||||||
|
-- Ottiene il numero di blocchi scavati e piazzati dallo spazio d'archiviazione; se non esiste è 0
|
||||||
|
local meta = giocatore:get_meta()
|
||||||
|
local digs_testo = "Digs: " .. meta:get_int("punteggio:digs")
|
||||||
|
local places_testo = "Places: " .. meta:get_int("punteggio:places")
|
||||||
|
local percentuale = tonumber(meta:get("punteggio:score") or 0.2)
|
||||||
|
|
||||||
|
local ids = hud_salvate[nome_giocatore]
|
||||||
|
if ids then
|
||||||
|
giocatore:hud_change(ids["places"], "text", places_testo)
|
||||||
|
giocatore:hud_change(ids["digs"], "text", digs_testo)
|
||||||
|
giocatore:hud_change(ids["bar_foreground"],
|
||||||
|
"scale", { x = percentuale, y = 1 })
|
||||||
|
else
|
||||||
|
ids = {}
|
||||||
|
hud_salvate[player_name] = ids
|
||||||
|
|
||||||
|
-- crea gli elementi HUD e imposta gli ID in `ids`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_on_joinplayer(punteggio.aggiorna_hud)
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
hud_salvate[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Altri elementi
|
||||||
|
|
||||||
|
Dai un occhio a [lua_api.md](https://minetest.gitlab.io/minetest/hud/) per una lista completa degli elementi HUD.
|
64
_it/players/player_physics.md
Normal file
64
_it/players/player_physics.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Fisica del giocatore
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.4
|
||||||
|
redirect_from: /it/chapters/player_physics.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
La fisica del giocatore può essere modificata usando le sovrascritture apposite (*physics ovverrides*).
|
||||||
|
Esse sono dei moltiplicatori che servono per impostare la velocità di movimento, del salto, o la gravità del singolo giocatore.
|
||||||
|
Per esempio, un valore di 2 sulla gravità, renderà la gravità di un utente due volte più forte.
|
||||||
|
|
||||||
|
- [Esempio base](#esempio-base)
|
||||||
|
- [Sovrascritture disponibili](#sovrascritture-disponibili)
|
||||||
|
- [Vecchio sistema di movimento](#vecchio-sistema-di-movimento)
|
||||||
|
- [Incompatibilità tra mod](#incompatibilità-tra-mod)
|
||||||
|
- [Il tuo turno](#il-tuo-turno)
|
||||||
|
|
||||||
|
## Esempio base
|
||||||
|
|
||||||
|
Segue l'esempio di un comando di antigravità:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_chatcommand("antigrav", {
|
||||||
|
func = function(name, param)
|
||||||
|
local giocatore = core.get_player_by_name(name)
|
||||||
|
giocatore:set_physics_override({
|
||||||
|
gravity = 0.1, -- imposta la gravità al 10% del suo valore originale
|
||||||
|
-- (0.1 * 9.81)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sovrascritture disponibili
|
||||||
|
|
||||||
|
`set_physics_override()` è una tabella. Stando a [lua_api.md](https://minetest.gitlab.io/minetest/class-reference/#player-only-no-op-for-other-objects), le chiavi possono essere:
|
||||||
|
|
||||||
|
* `speed`: moltiplicatore della velocità di movimento (predefinito: 1)
|
||||||
|
* `jump`: moltiplicatore del salto (predefinito: 1)
|
||||||
|
* `gravity`: moltiplicatore della gravità (predefinito: 1)
|
||||||
|
* `sneak`: se il giocatore può camminare di soppiatto o meno (predefinito: true)
|
||||||
|
|
||||||
|
### Vecchio sistema di movimento
|
||||||
|
|
||||||
|
Il movimento dei giocatori prima della versione 0.4.16 includeva il cosiddetto *sneak glitch*, il quale permetteva vari glitch di movimento come l'abilità di scalare un "ascensore" fatta di certi blocchi premendo shift (la camminata di soppiatto) e salto. Nonostante non fosse una funzionalità voluta, è stata mantenuta nelle sovrascritture dato il suo uso in molti server.
|
||||||
|
|
||||||
|
Per ripristinare del tutto questo comportamento servono due chiavi:
|
||||||
|
|
||||||
|
* `new_move`: se usare o meno il nuovo sistema di movimento (predefinito: true)
|
||||||
|
* `sneak_glitch`: se il giocatore può usare o meno il glitch dell'ascensore (default: false)
|
||||||
|
|
||||||
|
## Incompatibilità tra mod
|
||||||
|
|
||||||
|
Tieni a mente che le mod che sovrascrivono la stessa proprietà fisica di un giocatore tendono a essere incompatibili tra di loro.
|
||||||
|
Quando si imposta una sovrascrittura, sovrascriverà qualsiasi altro suo simile impostato in precedenza: ciò significa che se la velocità di movimento di un giocatore viene cambiata più volte, solo l'ultimo valore verrà preso in considerazione.
|
||||||
|
|
||||||
|
## Il tuo turno
|
||||||
|
|
||||||
|
* **Sonic**: Imposta il moltiplicatore di velocità a un valore elevato (almeno 6) quando un giocatore entra in gioco;
|
||||||
|
* **Super rimbalzo**: Aumenta il valore del salto in modo che il giocatore possa saltare 20 metri (1 cubo = 1 metro);
|
||||||
|
* **Spazio**: Fai in modo che la gravità diminuisca man mano che si sale di altitudine.
|
124
_it/players/privileges.md
Normal file
124
_it/players/privileges.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
title: Privilegi
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.1
|
||||||
|
description: Tu, non puoi, passareee! (Tu invece sì)
|
||||||
|
redirect_from: /it/chapters/privileges.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
I privilegi (*privileges*, solitamente abbreviati in *privs*), danno ai giocatori l'abilità di eseguire certe azioni.
|
||||||
|
I proprietari dei server possono assegnare e revocare i privilegi per controllare quali cose un giocatore può o non può fare.
|
||||||
|
|
||||||
|
- [Privilegi sì e privilegi no](#privilegi-si-e-privilegi-no)
|
||||||
|
- [Dichiarazione](#dichiarazione)
|
||||||
|
- [Controlli](#controlli)
|
||||||
|
- [Ottenere e impostare privilegi](#ottenere-e-impostare-privilegi)
|
||||||
|
- [Aggiungere privilegi a basic_privs](#aggiungere-privilegi-a-basicprivs)
|
||||||
|
|
||||||
|
## Privilegi sì e privilegi no
|
||||||
|
|
||||||
|
I privilegi non sono fatti per indicare classi o status.
|
||||||
|
|
||||||
|
**Privilegi corretti:**
|
||||||
|
|
||||||
|
* interact
|
||||||
|
* shout
|
||||||
|
* noclip
|
||||||
|
* fly
|
||||||
|
* kick
|
||||||
|
* ban
|
||||||
|
* vote
|
||||||
|
* worldedit
|
||||||
|
* area_admin - se si tratta di un privilegio per gli amministratori è ok
|
||||||
|
|
||||||
|
**Privilegi sbagliati:**
|
||||||
|
|
||||||
|
* moderatore
|
||||||
|
* amministratore
|
||||||
|
* elfo
|
||||||
|
* nano
|
||||||
|
|
||||||
|
## Dichiarazione
|
||||||
|
|
||||||
|
Usa `register_privilege` per dichiarare un nuovo privilegio:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_privilege("voto", {
|
||||||
|
description = "Può votare nei sondaggi",
|
||||||
|
give_to_singleplayer = false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`give_to_singleplayer` è di base true, quindi non c'è bisogno di specificarlo se non lo si vuole mettere false.
|
||||||
|
|
||||||
|
## Controlli
|
||||||
|
|
||||||
|
Per controllare velocemente se un giocatore ha tutti i privilegi necessari o meno:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local celo, manca = core.check_player_privs(player_or_name, {
|
||||||
|
interact = true,
|
||||||
|
voto = true })
|
||||||
|
```
|
||||||
|
|
||||||
|
In quest'esempio, `celo` è true se il giocatore ha sia `interact` che `voto`.
|
||||||
|
Se `celo` è false, allora `manca` conterrà una tabella con i privilegi mancanti.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local celo, manca = core.check_player_privs(name, {
|
||||||
|
interact = true,
|
||||||
|
voto = true })
|
||||||
|
|
||||||
|
if celo then
|
||||||
|
print("Il giocatore ha tutti i privilegi!")
|
||||||
|
else
|
||||||
|
print("Al giocatore mancano i seguenti privilegi: " .. dump(manca))
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Se non hai bisogno di controllare i privilegi mancanti, puoi inserire `check_player_privs` direttamente nel costrutto if:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if not core.check_player_privs(name, { interact=true }) then
|
||||||
|
return false, "Hai bisogno del privilegio 'interact' per eseguire quest'azione!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ottenere e impostare privilegi
|
||||||
|
|
||||||
|
Si può accedere o modificare i privilegi di un giocatore anche se quest'ultimo non risulta online.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local privs = core.get_player_privs(name)
|
||||||
|
print(dump(privs))
|
||||||
|
|
||||||
|
privs.voto = true
|
||||||
|
core.set_player_privs(name, privs)
|
||||||
|
```
|
||||||
|
|
||||||
|
I privilegi sono sempre specificati come una tabella chiave-valore, con il loro nome come chiave e true/false come valore.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
fly = true,
|
||||||
|
interact = true,
|
||||||
|
shout = true -- per poter scrivere in chat
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aggiungere privilegi a basic_privs
|
||||||
|
|
||||||
|
I giocatori con il privilegio `basic_privs` sono in grado di assegnare e revocare un set limitato di privilegi.
|
||||||
|
È cosa comune assegnarlo ai moderatori in modo che possano mettere o togliere `interact` e `shout` agli altri giocatori, ma che al tempo stesso non possano assegnare privilegi (a loro stessi o ad altri giocatori) con maggiori possibilità di abuso - come `give` e `server`.
|
||||||
|
|
||||||
|
Per modificare quali sono i privilegi contenuti in `basic_privs`, va cambiata l'omonima opzione.
|
||||||
|
Se di base si ha infatti:
|
||||||
|
|
||||||
|
basic_privs = interact, shout
|
||||||
|
|
||||||
|
Per aggiungere `vote`, basta fare:
|
||||||
|
|
||||||
|
basic_privs = interact, shout, vote
|
4
_it/players/sfinv.md
Normal file
4
_it/players/sfinv.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
sitemap: false
|
||||||
|
redirect_to: "https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md"
|
||||||
|
---
|
209
_it/quality/clean_arch.md
Normal file
209
_it/quality/clean_arch.md
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
title: Introduzione alle architetture pulite
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.4
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Una volta che una mod raggiunge dimensioni considerevoli, sarà sempre più difficile mantenerne il codice pulito e privo di errori.
|
||||||
|
Questo è un grosso problema soprattutto quando si usa un linguaggio dinamico come Lua, considerando che il compilatore restituisce pochissimi aiuti quando si tratta di cose come l'assicurarsi di non aver fatto errori di battitura.
|
||||||
|
|
||||||
|
Questo capitolo si occupa di concetti importanti, necessari per mantenere il codice pulito, e di strutture utili per realizzarli.
|
||||||
|
Tieni a mente che lo scopo del capitolo non è quello di essere LA bibbia, bensì di dare un'idea su come muoversi.
|
||||||
|
Non c'è il modo giusto e il modo sbagliato per ideare una mod, in quanto il loro design è alquanto soggettivo.
|
||||||
|
|
||||||
|
- [Coesione, dipendenze, e separazione degli ambiti](#coesione-dipendenze-e-separazione-degli-ambiti)
|
||||||
|
- [Osservatore](#osservatore)
|
||||||
|
- [Modello-Vista-Controllo](#modello-vista-controllo)
|
||||||
|
- [API-Vista](#api-vista)
|
||||||
|
- [Conclusione](#conclusione)
|
||||||
|
|
||||||
|
|
||||||
|
## Coesione, dipendenze, e separazione degli ambiti
|
||||||
|
|
||||||
|
Senza alcuna pianificazione, il codice di un progetto diverrà man mano un bel fritto misto (o quello che gli inglesi definiscono *spaghetti code*).
|
||||||
|
Ovvero, mancherà di struttura perché ne è stata fatta un'accozzaglia senza chiare delimitazioni.
|
||||||
|
Sul lungo corso il progetto diverrà ingestibile, concludendosi con il suo abbandono.
|
||||||
|
|
||||||
|
Per evitare ciò, è buona cosa ideare il proprio progetto come un insieme di piccole aree di codice che interagiscono tra di loro.
|
||||||
|
|
||||||
|
> All'interno di ogni grande programma, c'è un programma più piccolo che cerca di scappare.
|
||||||
|
>
|
||||||
|
> --C.A.R. Hoare
|
||||||
|
|
||||||
|
Questo dovrebbe essere fatto in modo tale da ottenere una "separazione degli ambiti" (*Separation of Concerns*), dove ogni area dovrebe essere distinta e occuparsi di un bisogno specifico.
|
||||||
|
|
||||||
|
Questi programmi/aree dovrebbero avere le due seguenti proprietà:
|
||||||
|
|
||||||
|
* **Alta coesione** - ciò che succede nell'area dovrebbe essere strettamente legato.
|
||||||
|
* **Basse dipendenze** - mantenere le dipendenze tra un'area e l'altra più basse possibili, ed evitare di affidarsi a implementazioni interne.
|
||||||
|
È davvero ottimo assicurarsi di ciò, in quanto cambiare le API di certe aree risulterà più fattibile.
|
||||||
|
|
||||||
|
Tieni a mente che ciò si applica sia nella relazione tra una mod e l'altra, che in quella tra le varie aree all'interno della stessa mod.
|
||||||
|
|
||||||
|
## Osservatore
|
||||||
|
|
||||||
|
Una maniera semplice per separare le aree di codice è usare lo schema dell'Osservatore (*Observer pattern*).
|
||||||
|
|
||||||
|
Si prenda l'esempio di sbloccare un trofeo quando un giocatore uccide per la prima volta un mostro raro.
|
||||||
|
L'approccio ingenuo è quello di avere il codice del trofeo nella funzione di uccisione del mostro, controllando il suo nome e sbloccando il trofeo se coincide.
|
||||||
|
Questa è però una cattiva idea, in quanto rende il mob dipendente dal codice dei trofei.
|
||||||
|
Se si contiuasse su questa strada - per esempio aggiungendo l'esperienza ottenuta uccidendo il mob - si finirebbe con l'avere un sacco di dipendenze alla rinfusa.
|
||||||
|
|
||||||
|
Lo schema dell'Osservatore dice invece di far esporre alla mod del mostro un modo per far sì che altre aree di codice possano inserire comodamente dei comportamenti extra e ricevere informazioni riguardo all'evento.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mieimob.registered_on_death = {}
|
||||||
|
function mieimob.register_on_death(func)
|
||||||
|
table.insert(mieimob.registered_on_death, func)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- nel codice della morte del mob
|
||||||
|
for i=1, #mieimob.registered_on_death do
|
||||||
|
mieimob.registered_on_death[i](entity, reason)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Quindi l'altro codice registra ciò che gli serve:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mieimob.register_on_death(function(mob, reason)
|
||||||
|
if reason.type == "punch" and reason.object and
|
||||||
|
reason.object:is_player() then
|
||||||
|
trofei.avvisa_morte_mostro(reason.object, mob.name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Potresti star pensando "aspetta un secondo, questo mi sembra terribilmente familiare".
|
||||||
|
E hai ragione! L'API di Minetest è molto incentrata sull'Osservatore, per far in modo che il motore di gioco non debba preoccuparsi di cosa è in ascolto di cosa.
|
||||||
|
|
||||||
|
## Modello-Vista-Controllo
|
||||||
|
|
||||||
|
Nel prossimo capitolo discuteremo di come testare automaticamente il codice, e uno dei problemi che riscontreremo sarà come separare il più possibile la logica (calcoli, cosa bisognerebbe fare) dalle chiamate alle API (`core.*`, altre mod).
|
||||||
|
|
||||||
|
Un modo per fare ciò è pensare a:
|
||||||
|
|
||||||
|
* Che **dati** si hanno;
|
||||||
|
* Che **azioni** si possono eseguire con quei dati;
|
||||||
|
* Come gli **eventi** (come formspec, pugni ecc.) inneschino queste azioni, e come quest'ultime abbiano conseguenze nel motore di gioco.
|
||||||
|
|
||||||
|
Prendiamo come esempio una mod di protezione del terreno.
|
||||||
|
I dati di cui si dispone sono quelli delle aree e i metadati ad esse associati.
|
||||||
|
Le azioni richieste sono `crea`, `modifica` o `cancella`.
|
||||||
|
Gli eventi che richiamano le azioni sono invece comandi via chat e formspec.
|
||||||
|
|
||||||
|
Durante i test sarà possibile assicurarsi che un'azione, quando richiamata, faccia quello che deve fare ai dati.
|
||||||
|
Non c'è bisogno di testare l'evento che chiama l'azione (ciò richiederebbe usare l'API di Minetest, e l'area di codice dovrebbe comunque rimanere quanto più piccola possibile).
|
||||||
|
|
||||||
|
Si dovrebbe scrivere la rappresentazione dei dati usando Lua puro.
|
||||||
|
"Puro" in questo contesto significa che le funzioni potrebbero venir eseguite al di fuori di Minetest - nessuna delle funzioni del motore di gioco vengono chiamate.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Dati
|
||||||
|
function terreno.crea(nome, nome_area)
|
||||||
|
terreno.terreni[nome_area] = {
|
||||||
|
nome = nome_area,
|
||||||
|
owner = nome,
|
||||||
|
-- altre cose
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function terreno.ottieni_da_nome(nome_area)
|
||||||
|
return terreno.terreni[nome_area]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Anche le azioni dovrebbero essere pure, ma chiamare altre funzioni è più accettato che il comportamento qui sopra.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Controllo
|
||||||
|
function terreno.gestore_invio_crea(nome, nome_area)
|
||||||
|
-- processa cose
|
||||||
|
-- (tipo controllare se ci sono sovrapposizioni, controllo dei permessi ecc)
|
||||||
|
|
||||||
|
terreno.crea(nome, nome_area)
|
||||||
|
end
|
||||||
|
|
||||||
|
function terreno.gestore_richiesta_crea(nome)
|
||||||
|
-- questo è un cattivo esempio, come spiegato poco più avanti
|
||||||
|
terreno.mostra_formspec_crea(nome)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I gestori degli eventi dovranno interagire con la API di Minetest.
|
||||||
|
Il numero di calcoli dovrebbero essere minimizzati il più possibile, in quanto non sarà fattibile testare quest'area così facilmente.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Vista
|
||||||
|
function terreno.mostra_formspec_crea(nome)
|
||||||
|
-- nota come qui non ci siano calcoli complessi!
|
||||||
|
return [[
|
||||||
|
size[4,3]
|
||||||
|
label[1,0;Questo è un esempio]
|
||||||
|
field[0,1;3,1;nome_area;]
|
||||||
|
button_exit[0,2;1,1;esci;Esci]
|
||||||
|
]]
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_chatcommand("/land", {
|
||||||
|
privs = { terreno = true },
|
||||||
|
func = function(name)
|
||||||
|
land.gestore_richiesta_crea(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
terreno.gestore_invio_crea(player:get_player_name(),
|
||||||
|
fields.nome_area)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
L'approccio adottato qui in alto è la struttura Modello-Vista-Controllo (MVC).
|
||||||
|
Il modello è un insieme di dati aventi funzioni minime.
|
||||||
|
La vista è un insieme di funzioni che sono in ascolto di eventi per passarli al controllo, e che riceve inoltre chiamate dal controllo per fare qualcosa con l'API di Minetest.
|
||||||
|
Il controllo è dove le decisioni vengono prese e la maggior parte delle operazioni eseguite.
|
||||||
|
|
||||||
|
Il controllo non dovrebbe avere nessuna conoscenza riguardo l'API di Minetest - nota come non ci siano chiamate a Minetest o funzioni nella vista che le ricordino.
|
||||||
|
*NON* dovresti, quindi, avere una funzione come `vista.hud_aggiungi(giocatore, def)`.
|
||||||
|
Al contrario, la vista definisce alcune azioni che il controllo può dirle di fare, come `vista.hud_aggiungi(info)` dove `info` è un valore o una tabella che non è imparentata in alcun modo con l'API di Minetest.
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img
|
||||||
|
width="100%"
|
||||||
|
src="{{ page.root }}/static/mvc_diagram.svg"
|
||||||
|
alt="Diagramma che mostra la struttura MVC (Modello-Vista-Controllo)">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
È importante che ogni area comunichi soltanto con i suoi diretti vicini, per ridurre il più possibile la quantità di codice da cambiare al modificare qualcosa.
|
||||||
|
Per esempio, per cambiare il formspec avrai bisogno di modificare solo la vista.
|
||||||
|
Per cambiare la API della vista, la vista e il controllo - ma non il modello.
|
||||||
|
|
||||||
|
In pratica, questo approccio è raramente utilizzato per via della sua alta complessità e perché non dà molti benefici alla maggior parte delle mod.
|
||||||
|
Al contrario, un approccio più comune e leggermente meno rigido è quello API-Vista.
|
||||||
|
|
||||||
|
|
||||||
|
### API-Vista
|
||||||
|
|
||||||
|
In un mondo ideale, si avrebbero le 3 aree MVC perfettamente separate... ma siamo nel mondo reale.
|
||||||
|
Un buon compromesso è ridurre la mod in due parti:
|
||||||
|
|
||||||
|
* **API** - modello + controllo. Non ci dovrebbe essere nessun uso di `core.` nella API.
|
||||||
|
* **Vista** - la vista, esattamente come quella spiegata sopra.
|
||||||
|
È buona norma strutturare questa parte in file separati per ogni tipo di evento.
|
||||||
|
|
||||||
|
La [crafting mod](https://github.com/rubenwardy/crafting) di rubenwardy segue ben o male questo schema: `api.lua` è quasi tutto puro Lua che gestisce lo spazio d'archiviazione e i calcoli che farebbe il controllo, `gui.lua` mostra e invia i formspec, e `async_crafter.lua` è la vista e il controllo dei timer e i formspec nei nodi.
|
||||||
|
|
||||||
|
Separare la mod in questa maniera permette di testare molto facilmente la API, in quanto non passa per quella di Minetest - come mostrato nel [prossimo capitolo](unit_testing.html).
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusione
|
||||||
|
|
||||||
|
Cosa sia del buon codice è soggettivo, e dipende altamente dal progetto che si vuole realizzare.
|
||||||
|
Come regola generale, cerca di tenere un'alta coesione (parti di codice tra loro connesse vicine) e poche dipendenze.
|
||||||
|
|
||||||
|
Suggerisco caldamente, per chi mastica l'inglese, di leggere il libro [Game Programming Patterns](http://gameprogrammingpatterns.com/).
|
||||||
|
Lo si può leggere gratuitamente [online](http://gameprogrammingpatterns.com/contents.html) e descrive molto più nel dettaglio gli approcci di programmazione da tenere quando si parla di videogiochi.
|
127
_it/quality/common_mistakes.md
Normal file
127
_it/quality/common_mistakes.md
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: Errori comuni
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.1
|
||||||
|
redirect_from: /it/chapters/common_mistakes.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Questo capitolo illustra gli errori più comuni e come evitarli.
|
||||||
|
|
||||||
|
- [Fai attenzione quando salvi gli ObjectRef in delle variabili (giocatori ed entità)](#fai-attenzione-quando-salvi-gli-objectref-in-delle-variabili-giocatori-ed-entità)
|
||||||
|
- [Non fidarti dei campi dei formspec](#non-fidarti-dei-campi-dei-formspec)
|
||||||
|
- [Imposta gli ItemStack dopo averli modificati](#imposta-gli-itemstack-dopo-averli-modificati)
|
||||||
|
|
||||||
|
## Fai attenzione quando salvi gli ObjectRef in delle variabili (giocatori ed entità)
|
||||||
|
|
||||||
|
Un ObjectRef viene invalidato quando il giocatore o l'entità che esso rappresenta abbandona il gioco.
|
||||||
|
Ciò si verifica quando un giocatore si disconnette, o quando un'entità viene rimossa dalla memoria.
|
||||||
|
|
||||||
|
Da Minetest 5.2, i metodi degli ObjectRef ritorneranno sempre `nil` quando non validi.
|
||||||
|
In altre parole, ogni chiamata verrà ignorata.
|
||||||
|
|
||||||
|
Si dovrebbe evitare quanto possibile di immagazzinare gli ObjectRef in delle variabili.
|
||||||
|
Se ciò tuttavia accade, assicurati di controllare se esiste ancora, come illustrato qui di seguito:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- questo codice funziona solo da Minetest 5.2 in poi
|
||||||
|
if obj:get_pos() then
|
||||||
|
-- è valido!
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non fidarti dei campi dei formspec
|
||||||
|
|
||||||
|
Client malevoli possono compilare i campi nei formspec quando vogliono, con qualsiasi contenuto vogliono.
|
||||||
|
|
||||||
|
Per esempio, il seguente codice presenta una vulnerabilità che permette ai giocatori di assegnarsi da soli il privilegio di moderatore:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function show_formspec(name)
|
||||||
|
if not core.check_player_privs(name, { privs = true }) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
core.show_formspec(name, "modman:modman", [[
|
||||||
|
size[3,2]
|
||||||
|
field[0,0;3,1;target;Nome;]
|
||||||
|
button_exit[0,1;3,1;sub;Promuovi]
|
||||||
|
]])
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
-- MALE! Manca il controllo dei privilegi!
|
||||||
|
|
||||||
|
local privs = core.get_player_privs(fields.target)
|
||||||
|
privs.kick = true
|
||||||
|
privs.ban = true
|
||||||
|
core.set_player_privs(fields.target, privs)
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Aggiungi un controllo dei privilegi per ovviare:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
if not core.check_player_privs(name, { privs = true }) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- code
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Imposta gli ItemStack dopo averli modificati
|
||||||
|
|
||||||
|
Se ci si fa caso, nella documentazione si parla di `ItemStack` e non `ItemStackRef`.
|
||||||
|
Questo perché gli ItemStack NON sono un riferimento, bensì una copia.
|
||||||
|
Questo vuol dire che modificando la copia, non si modificherà in automatico anche l'originale.
|
||||||
|
|
||||||
|
Sbagliato:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
local pila = inv:get_stack("main", 1) -- lo copio
|
||||||
|
pila:get_meta():set_string("description", "Un po' smangiucchiato")
|
||||||
|
-- MALE! Le modifiche saranno perse
|
||||||
|
```
|
||||||
|
|
||||||
|
Giusto:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
local pila = inv:get_stack("main", 1) -- lo copio
|
||||||
|
pila:get_meta():set_string("description", "Un po' smangiucchiato")
|
||||||
|
inv:set_stack("main", 1, pila)
|
||||||
|
-- Corretto! L'ItemStack è stato cambiato con la copia
|
||||||
|
```
|
||||||
|
|
||||||
|
Il comportamento dei callback è leggermente più complicato.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_item_eat(function(hp_change, replace_with_item,
|
||||||
|
itemstack, user, pointed_thing)
|
||||||
|
itemstack:get_meta():set_string("description", "Un po' smangiucchiato")
|
||||||
|
-- Quasi corretto! I dati saranno persi se un altro callback annulla questa chiamata
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Se nessun callback cancella l'operazione, la pila sarà impostata e la descrizione aggiornata; ma se un callback effettivamente cancella l'operazione, l'aggiornamento potrebbe andar perduto.
|
||||||
|
|
||||||
|
È meglio quindi fare così:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_item_eat(function(hp_change, replace_with_item,
|
||||||
|
itemstack, user, pointed_thing)
|
||||||
|
itemstack:get_meta():set_string("description", "Un po' smangiucchiato")
|
||||||
|
user:get_inventory():set_stack("main", user:get_wield_index(),
|
||||||
|
itemstack)
|
||||||
|
-- Corretto! La descrizione verrà sempre aggiornata
|
||||||
|
end)
|
||||||
|
```
|
101
_it/quality/luacheck.md
Normal file
101
_it/quality/luacheck.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
title: Controllo automatico degli errori
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.2
|
||||||
|
description: Usa LuaCheck per trovare eventuali errori
|
||||||
|
redirect_from: /it/chapters/luacheck.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
In questo capitolo, imparerai come usare uno strumento chiamato LuaCheck per scansionare automaticamente le tue mod alla ricerca di eventuali errori.
|
||||||
|
LuaCheck può essere usato in combinazione con l'editor per fornire avvertimenti vari.
|
||||||
|
|
||||||
|
- [Installare LuaCheck](#installare-luacheck)
|
||||||
|
- [Windows](#windows)
|
||||||
|
- [Linux](#linux)
|
||||||
|
- [Eseguire LuaCheck](#eseguire-luacheck)
|
||||||
|
- [Configurare LuaCheck](#configurare-luacheck)
|
||||||
|
- [Risoluzione problemi](#risoluzione-problemi)
|
||||||
|
- [Uso nell'editor](#uso-nelleditor)
|
||||||
|
|
||||||
|
## Installare LuaCheck
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Basta scaricare luacheck.exe dall'apposita [pagina delle versioni su Github](https://github.com/mpeterv/luacheck/releases).
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Per prima cosa, avrai bisogno di installare LuaRocks:
|
||||||
|
|
||||||
|
sudo apt install luarocks
|
||||||
|
|
||||||
|
Poi va installato globalmente LuaCheck:
|
||||||
|
|
||||||
|
sudo luarocks install luacheck
|
||||||
|
|
||||||
|
Per controllare che sia stato installato correttamente, fai:
|
||||||
|
|
||||||
|
luacheck -v
|
||||||
|
|
||||||
|
## Eseguire LuaCheck
|
||||||
|
|
||||||
|
La prima volta che si esegue LuaCheck, segnalerà probabilmente un sacco di falsi errori.
|
||||||
|
Questo perché ha ancora bisogno di essere configurato.
|
||||||
|
|
||||||
|
Su Windows, apri la powershell o la bash nella cartella principale del tuo progetto ed esegui `path\to\luacheck.exe .`
|
||||||
|
|
||||||
|
Su Linux, esegui `luacheck .` nella cartella principale del progetto.
|
||||||
|
|
||||||
|
## Configurare LuaCheck
|
||||||
|
|
||||||
|
Crea un file chiamato .luacheckrc nella cartella principale del tuo progetto.
|
||||||
|
Questa può essere quella di un gioco, di un pacchetto mod o di una mod singola.
|
||||||
|
|
||||||
|
Inserisci il seguente codice all'interno:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
unused_args = false
|
||||||
|
allow_defined_top = true
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
"minetest",
|
||||||
|
}
|
||||||
|
|
||||||
|
read_globals = {
|
||||||
|
string = {fields = {"split"}},
|
||||||
|
table = {fields = {"copy", "getn"}},
|
||||||
|
|
||||||
|
-- Builtin
|
||||||
|
"vector", "ItemStack",
|
||||||
|
"dump", "DIR_DELIM", "VoxelArea", "Settings",
|
||||||
|
|
||||||
|
-- MTG
|
||||||
|
"default", "sfinv", "creative",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi, avrai bisogno di assicurarti che funzioni eseguendo LuaCheck: dovresti ottenere molti meno errori questa volta.
|
||||||
|
Partendo dal primo errore, modifica il codice per risolvere il problema, o modifica la configurazione di LuaCheck se il codice è corretto.
|
||||||
|
Dài un occhio alla lista sottostante.
|
||||||
|
|
||||||
|
### Risoluzione problemi
|
||||||
|
|
||||||
|
* **accessing undefined variable foobar** - Se `foobar` dovrebbe essere una variabile globale, aggiungila a `read_globals`.
|
||||||
|
Altrimenti, manca un `local` vicino a `foobar`.
|
||||||
|
* **setting non-standard global variable foobar** - Se `foobar` dovrebbe essere una variabile globale, aggiungila a `globals`.
|
||||||
|
Rimuovila da `read_globals` se presente.
|
||||||
|
Altrimenti, manca un `local` vicino a `foobar`.
|
||||||
|
* **mutating read-only global variable 'foobar'** - Sposta `foobar` da `read_globals` a `globals`, o smetti di modificare `foobar`.
|
||||||
|
|
||||||
|
## Uso nell'editor
|
||||||
|
|
||||||
|
È caldamente consigliato installare un'estensione per il tuo editor di fiducia che ti mostri gli errori senza eseguire alcun comando.
|
||||||
|
Queste sono disponibili nella maggior parte degli editor, come:
|
||||||
|
|
||||||
|
* **VSCode** - Ctrl+P, poi incolla: `ext install dwenegar.vscode-luacheck`;
|
||||||
|
* **Sublime** - Installala usando package-control:
|
||||||
|
[SublimeLinter](https://github.com/SublimeLinter/SublimeLinter),
|
||||||
|
[SublimeLinter-luacheck](https://github.com/SublimeLinter/SublimeLinter-luacheck).
|
26
_it/quality/readmore.md
Normal file
26
_it/quality/readmore.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Per approfondire
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.7
|
||||||
|
redirect_from: /it/chapters/readmore.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lista di risorse
|
||||||
|
|
||||||
|
Dopo aver letto questo libro, se mastichi l'inglese dai un occhio a ciò che segue:
|
||||||
|
|
||||||
|
### Moddaggio di Minetest
|
||||||
|
|
||||||
|
* Riferimento alla API Lua di Minetest - [versione interattiva](https://minetest.gitlab.io/minetest/) |
|
||||||
|
[versione su pagina singola](https://github.com/minetest/minetest/blob/master/doc/lua_api.md).
|
||||||
|
* Spulcia le [mod esistenti](https://forum.minetest.net/viewforum.php?f=11).
|
||||||
|
|
||||||
|
### Programmazione in Lua
|
||||||
|
|
||||||
|
* [Programmazione in Lua (PIL)](http://www.lua.org/pil/).
|
||||||
|
|
||||||
|
### Modellazione 3D
|
||||||
|
|
||||||
|
* [Blender 3D: Noob to pro](https://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro).
|
||||||
|
* [Usare Blender con Minetest](http://wiki.minetest.net/Using_Blender).
|
144
_it/quality/releasing.md
Normal file
144
_it/quality/releasing.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
title: Rilasciare una mod
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.6
|
||||||
|
redirect_from: /it/chapters/releasing.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Rilasciare (o pubblicare) una mod permette ad altre persone di poterne usufruire.
|
||||||
|
Una volta che una mod è stata rilasciata potrebbe venir usata nelle partite locali (a giocatore singolo) o nei server, inclusi quelli pubblici.
|
||||||
|
|
||||||
|
- [Scegliere una licenza](#scegliere-una-licenza)
|
||||||
|
- [LGPL e CC-BY-SA](#lgpl-e-cc-by-sa)
|
||||||
|
- [CC0](#cc0)
|
||||||
|
- [MIT](#mit)
|
||||||
|
- [Impacchettare](#impacchettare)
|
||||||
|
- [README.txt](#readmetxt)
|
||||||
|
- [mod.conf / game.conf](#modconf--gameconf)
|
||||||
|
- [screenshot.png](#screenshotpng)
|
||||||
|
- [Caricare](#caricare)
|
||||||
|
- [Sistemi di Controllo Versione](#sistemi-di-controllo-versione)
|
||||||
|
- [Rilasciare su ContentDB](#rilasciare-su-contentdb)
|
||||||
|
- [Creare la discussione sul forum](#creare-la-discussione-sul-forum)
|
||||||
|
|
||||||
|
## Scegliere una licenza
|
||||||
|
|
||||||
|
Avrai bisogno di specificare una licenza per la tua mod.
|
||||||
|
Questo è importante perché dice alle altre persone cosa possono e non possono fare col tuo lavoro.
|
||||||
|
Se una mod non ha una licenza, la gente non saprà cosa sarà autorizzata a modificare, distribuire o se potrà usarla su un server pubblico.
|
||||||
|
|
||||||
|
Le licenze variano anche a seconda di cosa si vuole tutelare: per esempio, le Creative Commons non dovrebbero essere usate per il codice sorgente, ma sono un'ottima opzione per cose come immagini, testi e modelli 3D.
|
||||||
|
|
||||||
|
Puoi adottare la licenza che preferisci; tuttavia, sappi che le mod con licenze che ne vietano lavori derivati sono bandite dal forum ufficiale di Minetest (in altre parole, per essere consentita sul forum, gli altri sviluppatori devono poter essere in grado di modificarla e rilasciarne la versione modificata).
|
||||||
|
|
||||||
|
Tieni anche a mente che **la licenza di pubblico dominio non è una licenza valida**, perché la sua definizione varia da stato a stato.
|
||||||
|
|
||||||
|
È importante sottolineare che la WTFPL (*do What The Fuck you want to Public License*, la "facci il cazzo che ti pare")
|
||||||
|
[è caldamente *s*consigliata](https://content.minetest.net/help/wtfpl/), e alcune persone potrebbero decidere di non usare la tua mod se ha questa licenza.
|
||||||
|
|
||||||
|
### LGPL e CC-BY-SA
|
||||||
|
|
||||||
|
Questa è la combinazione più comune nella comunità di Minetest, nonché quella usata sia da Minetest che da Minetest Game.
|
||||||
|
Si imposta il codice sotto LGPL 2.1 e i contenuti artistici sotto CC-BY-SA.
|
||||||
|
Ciò significa che:
|
||||||
|
|
||||||
|
* Chiunque può modificare, ridistribuire e vendere versioni modificate e non;
|
||||||
|
* Se qualcuno modifica la tua mod, deve adottare la stessa licenza;
|
||||||
|
* Devono citare l'autore originale.
|
||||||
|
|
||||||
|
### CC0
|
||||||
|
|
||||||
|
Queste licenze possono essere usate sia per il codice che per contenuti artistici, permettendo a chiunque di fare quello che gli va - incluso il non citare l'autore.
|
||||||
|
|
||||||
|
### MIT
|
||||||
|
|
||||||
|
Questa è una licenza comune per il codice.
|
||||||
|
La differenza con la LGPL è che le copie derivate in questo caso non devono per forza essere libere (i primi 2 punti della LGPL/GPL), bensì può essere trasformato in codice proprietario.
|
||||||
|
|
||||||
|
## Impacchettare
|
||||||
|
|
||||||
|
Ci sono alcuni file che è consigliato includere nelle proprie mod e nei propri giochi prima di rilasciarli.
|
||||||
|
|
||||||
|
### README.txt
|
||||||
|
|
||||||
|
Il README dovrebbe dichiarare:
|
||||||
|
|
||||||
|
* Cosa fa la mod/gioco;
|
||||||
|
* Come si usa;
|
||||||
|
* Che licenza ha;
|
||||||
|
* Quali dipendenze richiede;
|
||||||
|
* Come installare la mod;
|
||||||
|
* Versione corrente della mod;
|
||||||
|
* Eventualmente, dove segnalare i problemi o comunque richiedere aiuto.
|
||||||
|
|
||||||
|
### mod.conf / game.conf
|
||||||
|
|
||||||
|
Assicurati di aggiungere una descrizione che spieghi cosa fa la mod o il gioco, usando la chiave `description`.
|
||||||
|
Cerca di essere preciso e coinciso: dovrebbe essere breve perché il contenuto verrà mostrato nell'installer del motore di gioco, che ha uno spazio limitato.
|
||||||
|
|
||||||
|
Per esempio, consigliato:
|
||||||
|
|
||||||
|
description = Aggiunge zuppa, torte, pane e succhi
|
||||||
|
|
||||||
|
Sconsigliato:
|
||||||
|
|
||||||
|
description = Cibo per Minetest
|
||||||
|
|
||||||
|
### screenshot.png
|
||||||
|
|
||||||
|
Gli screenshot dovrebbero essere in proporzione 3:2 e avere una grandezza minima di 300x200px.
|
||||||
|
|
||||||
|
Lo screen verrà mostrato all'interno di Minetest come anteprima del contenuto.
|
||||||
|
|
||||||
|
## Caricare
|
||||||
|
|
||||||
|
Per far sì che un potenziale utente possa scaricare la tua mod, c'è bisogno di caricarla in uno spazio pubblico.
|
||||||
|
Ci sono svariati modi per fare ciò quindi usa l'approccio che ritieni più opportuno; l'importante è che esso rispetti i requisiti qui elencati, ed eventuale richieste aggiuntive dei moderatori del forum:
|
||||||
|
|
||||||
|
* **Stabile** - Il sito che conterrà il file non dovrebbe essere propenso a chiudere i battenti da un momento all'altro senza preavviso;
|
||||||
|
* **Link diretto** - Dovresti essere in grado di cliccare su un link e scaricare il file senza il bisogno di dover passare per altre pagine;
|
||||||
|
* **Senza virus** - Caricamenti su siti sospetti potrebbero contenere materiali non sicuri.
|
||||||
|
|
||||||
|
ContentDB soddisfa questi requisiti, richiedendo giusto un file .zip.
|
||||||
|
|
||||||
|
### Sistemi di Controllo Versione
|
||||||
|
|
||||||
|
Un Sistema di Controllo Versione (VCS, *Version Control System*) è un programma che gestisce i cambiamenti di altri programmi, spesso facilitandone la distribuzione e la collaborazione.
|
||||||
|
|
||||||
|
La maggior parte dei creatori di mod su Minetest usa Git e un sito per ospitare il loro codice come GitHub o GitLab.
|
||||||
|
|
||||||
|
Usare git può essere difficile all'inizio.
|
||||||
|
Se hai bisogno di una mano e mastichi l'inglese, prova a dare un occhio a [Pro Git book](http://git-scm.com/book/en/v1/Getting-Started) - gratis da leggere online.
|
||||||
|
|
||||||
|
## Rilasciare su ContentDB
|
||||||
|
|
||||||
|
ContentDB è il luogo ufficiale dove trovare e distribuire mod, giochi e pacchetti texture.
|
||||||
|
Gli utenti possono manualmente andare alla ricerca di contenuti tramite il sito, o scaricarli e installarli direttamente dall'integrazione presente nel menù principale di Minetest.
|
||||||
|
|
||||||
|
Iscriviti su [ContentDB](https://content.minetest.net) e aggiungi il tuo lavoro.
|
||||||
|
Assicurati di leggere le linee guida (in inglese) nella sezione d'aiuto (*Help*).
|
||||||
|
|
||||||
|
## Creare la discussione sul forum
|
||||||
|
|
||||||
|
Puoi anche creare una discussione sul forum per far in modo che gli utenti possano discutere ciò che hai fatto.
|
||||||
|
|
||||||
|
Per le mod usa la sezione ["WIP Mods"](https://forum.minetest.net/viewforum.php?f=9) (*Work In Progress*), per i giochi invece ["WIP Games"](https://forum.minetest.net/viewforum.php?f=50).
|
||||||
|
|
||||||
|
|
||||||
|
Puoi ora creare la discussione nella sezione ["WIP Mods"](https://forum.minetest.net/viewforum.php?f=9) (WIP sta per *Work In Progress*, lavori in corso).\\
|
||||||
|
Quando ritieni che la tua mod abbia raggiunto la sua prima versione ufficiale, puoi [richiedere (in inglese) che venga spostata](https://forum.minetest.net/viewtopic.php?f=11&t=10418) in "Mod Releases".
|
||||||
|
|
||||||
|
La discussione dovrebbe contenere contenuti simili al README, con giusto un po' più di coinvolgimento e il link per scaricare la mod.
|
||||||
|
È buona cosa aggiungere anche degli eventuali screenshot per far capire al volo cosa fa la mod, se possibile.
|
||||||
|
|
||||||
|
Il titolo della discussione deve seguire uno dei seguenti formati:
|
||||||
|
|
||||||
|
* [Mod] Nome mod [nomemod]
|
||||||
|
* [Mod] Nome mod [numero versione] [nomemod]
|
||||||
|
|
||||||
|
Per esempio:
|
||||||
|
|
||||||
|
* [Mod] Blocchi pazzi [0.1] [blocchipazzi]
|
93
_it/quality/security.md
Normal file
93
_it/quality/security.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Sicurezza
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.3
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
La sicurezza è molto importante per evitare che una mod permetta di far perdere il controllo del server al suo proprietario.
|
||||||
|
|
||||||
|
- [Concetti fondamentali](#concetti-fondamentali)
|
||||||
|
- [Formspec](#formspec)
|
||||||
|
- [Non fidarsi mai dei campi dei formspec](#non-fidarsi-mai-dei-campi-dei-formspec)
|
||||||
|
- [Il momento per controllare non è il momento dell'uso (Time of Check is not Time of Use)](#il-momento-per-controllare-non-è-il-momento-delluso-time-of-check-is-not-time-of-use)
|
||||||
|
- [Ambienti (non sicuri)](#ambienti-non-sicuri)
|
||||||
|
|
||||||
|
## Concetti fondamentali
|
||||||
|
|
||||||
|
Il concetto più importante quando si parla di sicurezza è **non fidarsi mai dell'utente**.
|
||||||
|
Ogni cosa che l'utente può inviare al server deve essere trattata come malevola.
|
||||||
|
Questo significa che dovresti sempre controllare che le informazioni da loro immesse siano valide, che abbiano i privilegi necessari e che siano autorizzati a fare determinate azioni.
|
||||||
|
|
||||||
|
Un'azione malevola non è necessariamente la modifica o la distruzione di dati, ma può essere anche l'accedere a dati sensibili, come gli hash delle password o i messaggi privati.
|
||||||
|
Questo è grave soprattutto se il server possiede informazioni sugli utenti come le loro e-mail o la loro età, che alcuni potrebbero richiedere per questioni di verifica.
|
||||||
|
|
||||||
|
## Formspec
|
||||||
|
|
||||||
|
### Non fidarsi mai dei campi dei formspec
|
||||||
|
|
||||||
|
Qualsiasi utente può inviare qualsiasi formspec con i valori che preferisce quando preferisce.
|
||||||
|
|
||||||
|
Segue del codice trovato realmente in una mod:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
for key, field in pairs(fields) do
|
||||||
|
local x,y,z = string.match(key,
|
||||||
|
"goto_([%d-]+)_([%d-]+)_([%d-]+)")
|
||||||
|
if x and y and z then
|
||||||
|
player:set_pos({ x=tonumber(x), y=tonumber(y),
|
||||||
|
z=tonumber(z) })
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Riesci a vedere il problema? Un utente malintenzionato potrebbe inviare un formspec contenente la propria posizione, permettendogli di venire teletrasportato dovunque vuole.
|
||||||
|
Addirittura il tutto potrebbe essere automatizzato usando modifiche del client per replicare il comportamento di `/teleport` senza aver bisogno di alcun privilegio.
|
||||||
|
|
||||||
|
La soluzione per questo tipo di problematica è usare un [Contesto](../players/formspecs.html#contexts), come mostrato precedentemente nel capitolo dei Formspec.
|
||||||
|
|
||||||
|
### Il momento per controllare non è il momento dell'uso (Time of Check is not Time of Use)
|
||||||
|
|
||||||
|
Qualsiasi utente può inviare qualsiasi formspec con i valori che preferisce quando preferisce, sì: a meno che il motore di gioco non glielo impedisca:
|
||||||
|
|
||||||
|
* L'invio dei formspec di un nodo vengono bloccati se l'utente è troppo distante;
|
||||||
|
* Dalla 5.0 in poi, i formspec con un nome sono bloccati se non sono stati ancora mostrati.
|
||||||
|
|
||||||
|
Questo significa che dovresti controllare che l'utente soddisfi i requisiti per visualizzare il formspec in primis, esattamente come per ogni azione corrispondente.
|
||||||
|
|
||||||
|
La vulnerabilità causata dal controllare i privilegi nel `show_formspec` ma non nella gestione del formspec in primis è chiamata *Time Of Check is not Time Of Use* (Il momento per controllare non è il momento dell'uso), o più brevemente TOCTOU.
|
||||||
|
|
||||||
|
|
||||||
|
## Ambienti (non sicuri)
|
||||||
|
|
||||||
|
Minetest permette alle mod di richiedere ambienti senza limiti, dando loro accesso all'intera API Lua.
|
||||||
|
|
||||||
|
Riesci a individuare la vulnerabilità in questo pezzo di codice??
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ie = core.request_insecure_environment()
|
||||||
|
ie.os.execute(("path/to/prog %d"):format(3))
|
||||||
|
```
|
||||||
|
|
||||||
|
`string.format` è una funzione nella tabella globale condivisa `string`.
|
||||||
|
Una mod malevola potrebbe sovrascrivere questa funzione e passare "cose" a `os.execute`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
string.format = function()
|
||||||
|
return "xdg-open 'http://esempio.com'"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
La mod potrebbe passare qualcosa di molto più malevolo dell'apertura di un sito, come dare il controllo remoto della macchina al malintenzionato in questione.
|
||||||
|
|
||||||
|
Alcune regole per usare un ambiente non sicuro:
|
||||||
|
|
||||||
|
* Tenerlo sempre in una variabile locale e non passarlo mai a una funzione;
|
||||||
|
* Assicurarsi di potersi fidare di qualsiasi input eseguita in una funzione insicura, per evitare il problema sopracitato.
|
||||||
|
Questo significa evitare funzioni globali ridefinibili.
|
153
_it/quality/translations.md
Normal file
153
_it/quality/translations.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
title: Traduzione
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.05
|
||||||
|
marked_text_encoding:
|
||||||
|
level: info
|
||||||
|
title: Marked Text Encoding
|
||||||
|
message: |
|
||||||
|
Non hai davvero bisogno di capire come funziona il testo formattato, ma potrebbe aiutarti a capire meglio.
|
||||||
|
|
||||||
|
```
|
||||||
|
"\27(T@miamod)Hello everyone!\27E"
|
||||||
|
```
|
||||||
|
|
||||||
|
* `\27` è il carattere di escape - è usato per dire a Minetest di far attenzione, in quanto sta per seguire qualcosa di speciale. È usato sia per le traduzioni che per la colorazione del testo.
|
||||||
|
* `(T@miamod)` dice che il testo a seguire è traducibile usando il dominio testuale di `miamod`.
|
||||||
|
* `Hello everyone!` è il testo in inglese da tradurre, passato alla funzione di traduzione.
|
||||||
|
* `\27E` è di nuovo il carattere di escape, dove `E` è usato per segnalare che si è arrivati alla fine.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
Aggiungere il supporto per le traduzioni nelle tue mod e giochi dà la possibilità a più persone di gustarsele.
|
||||||
|
Stando a Google Play, il 64% dei giocatori di Minetest Android non usano l'inglese come prima lingua.
|
||||||
|
Per quanto Minetest non tenga traccia di questo parametro nelle altre piattaforme, vien comunque da sé pensare che una buona parte di giocatrici e giocatori non siano madrelingua inglesi.
|
||||||
|
|
||||||
|
Minetest ti permette di localizzare i tuoi contenuti in tante lingue diverse, chiedendoti il testo base in inglese, seguito da dei file di traduzione che mappano le parole/frasi equivalenti nelle altre lingue. Questo processo di traduzione è fatto a lato client, cosicché ogni giocatore possa vedere il contenuto nella propria lingua (se disponibile).
|
||||||
|
|
||||||
|
|
||||||
|
- [Come funziona la traduzione lato client?](#come-funziona-la-traduzione-lato-client)
|
||||||
|
- [Testo formattato](#testo-formattato)
|
||||||
|
- [File di traduzione](#file-di-traduzione)
|
||||||
|
- [Formattare una stringa](#formattare-una-stringa)
|
||||||
|
- [Buona prassi per una buona traduzione](#buona-prassi-per-una-buona-traduzione)
|
||||||
|
- [Traduzioni lato server](#traduzioni-lato-server)
|
||||||
|
- [Per concludere](#per-concludere)
|
||||||
|
|
||||||
|
|
||||||
|
## Come funziona la traduzione lato client?
|
||||||
|
|
||||||
|
### Testo formattato
|
||||||
|
|
||||||
|
Il server ha bisogno di dire ai client come tradurre il testo.
|
||||||
|
Questo accade grazie alla funzione `core.get_translator(dominiotestuale)`, che abbrevieremo con `S()`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local S = core.get_translator("miamod")
|
||||||
|
|
||||||
|
core.register_craftitem("miamod:oggetto", {
|
||||||
|
description = S("My Item"),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Il primo parametro di `get_translator` è il dominio testuale, che funge da [spazio dei nomi](https://it.wikipedia.org/wiki/Namespace).
|
||||||
|
Piuttosto che avere tutte le traduzioni di una lingua salvate nello stesso file, queste possono essere suddivise in domini testuali (un file per dominio per lingua).
|
||||||
|
È buona norma assegnare al dominio testuale lo stesso nome della mod, onde evitare conflitti tra mod diverse.
|
||||||
|
|
||||||
|
Il testo formattato può essere usato nella maggior parte dei casi dove è richiesto un testo fatto per gli esseri umani - come i formspec, i campi di definizioni di un oggetto, `infotext` ecc.
|
||||||
|
Nel caso dei formspec, tieni presente che dovrai usare la funzione di escape `core.formspec_escape` per una corretta visualizzazione.
|
||||||
|
|
||||||
|
Quando il client incontra del testo formattato, come quello passato in `description`, ne andrà a cercare il corrispettivo nel file di traduzione della lingua del giocatore. Se la ricerca non avrà avuto esito positivo, ritornerà quello in inglese.
|
||||||
|
|
||||||
|
Nel testo formattato sono presenti il testo sorgente in inglese, il dominio testuale, e qualsivoglia altro parametro passato a `S()`.
|
||||||
|
Essenzialmente, è una codifica testuale della chiamata a `S` che contiene tutte le informazioni necessarie.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.marked_text_encoding %}
|
||||||
|
|
||||||
|
|
||||||
|
### File di traduzione
|
||||||
|
|
||||||
|
I file di traduzione sono file che possono essere trovati nella cartella `locale` di ogni mod.
|
||||||
|
Al momento, l'unico formato supportato è `.tr`, ma è probabile che altri formati più tipici saranno aggiunti in futuro.
|
||||||
|
I file di traduzione devono essere nominati nel seguente modo: `[dominiotestuale].[codicelingua].tr`.
|
||||||
|
|
||||||
|
I file `.tr` iniziano con un commento che ne specifica il dominio, seguito poi da righe che mappano il testo originale in inglese nella lingua del file.
|
||||||
|
|
||||||
|
Per esempio, `miamod.it.tr`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# textdomain: miamod
|
||||||
|
Hello everyone!=Ciao a tutti!
|
||||||
|
I like grapefruit=Mi piace il pompelmo
|
||||||
|
```
|
||||||
|
|
||||||
|
Dovresti creare dei file di traduzione basati sul codice sorgente delle tue mod/giochi, usando uno strumento come [update_translations](https://github.com/minetest-tools/update_translations).
|
||||||
|
Questo cercherà tutte le occorrenze di `S(` nel tuo codice Lua, creando in automatico un modello che traduttrici e traduttori potranno usare per tradurre nella loro lingua.
|
||||||
|
Inoltre, si prenderà cura di aggiornare i file di traduzione ogniqualvolta verranno effettuate modifiche al codice.
|
||||||
|
|
||||||
|
|
||||||
|
## Formattare una stringa
|
||||||
|
|
||||||
|
Non è raro dover inserire una variabile dentro una stringa da tradurre.
|
||||||
|
È importante che il testo non sia semplicemente concatenato, in quanto impedirebbe a chi traduce di cambiare l'ordine delle variabili all'interno della frase.
|
||||||
|
Al contrario, dovresti usare il seguente sistema di formattazione:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_joinplayer(function(player)
|
||||||
|
core.chat_send_all(S("Everyone, say hi to @1!", player:get_player_name()))
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Se vuoi scrivere letteralmente `@` nella tua frase, dovrai usare una sequenza di escape scrivendo `@@`.
|
||||||
|
|
||||||
|
Dovresti evitare di concatenare stringhe *all'interno* di una frase; piuttosto, sarebbe meglio concatenare più frasi come da esempio, in quanto permette a chi traduce di lavorare su stringhe più piccole:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
S("Hello @1!", player_name) .. " " .. S("You have @1 new messages.", #msgs)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Buona prassi per una buona traduzione
|
||||||
|
|
||||||
|
* Evita di concatenare il testo, optando invece per formattare le stringhe. Questo permette a chi traduce di avere pieno controllo sull'ordine degli elementi;
|
||||||
|
* Crea i file di traduzione in automatico usando [update_translations](https://github.com/minetest-tools/update_translations);
|
||||||
|
* È cosa comune che le variabili cambino il testo circostante, per esempio tramite genere e numero. Risulta spesso difficile trovare qualcosa che si sposi bene in tutti i casi, perciò si tende a cambiare la struttura della frase in modo che risulti sempre corretta ("Hai ottenuto 3 mele" -> "Hai ottenuto mela (x3)");
|
||||||
|
* Le traduzioni potrebbero essere molto più lunghe o molto più corte rispetto all'originale. Assicurati di lasciare sempre un po' di respiro;
|
||||||
|
* Non tutte le lingue scrivono i numeri nella stessa maniera, come per esempio `1.000` e `1'000`;
|
||||||
|
* Non dar per scontato che le altre lingue usino le maiscuole nella stessa maniera della tua.
|
||||||
|
|
||||||
|
|
||||||
|
## Traduzioni lato server
|
||||||
|
|
||||||
|
Certe volte ti capiterà di voler sapere quale traduzione di una tal stringa stia venendo visualizzata dal giocatore.
|
||||||
|
Puoi usare `get_player_information` per ottenere la lingua utilizzata e `get_translated_string` per tradurne il testo formattato.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local list = {
|
||||||
|
S("Hello world!"),
|
||||||
|
S("Potato")
|
||||||
|
}
|
||||||
|
|
||||||
|
core.register_chatcommand("find", {
|
||||||
|
func = function(name, param)
|
||||||
|
local info = core.get_player_information(name)
|
||||||
|
local lingua = info and info.language or "en"
|
||||||
|
|
||||||
|
for _, riga in ipairs(lista) do
|
||||||
|
local trad = core.get_translated_string(language, riga)
|
||||||
|
if trad:contains(query) then
|
||||||
|
return riga
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per concludere
|
||||||
|
|
||||||
|
Se ben gestita, l'API per le traduzioni permette di rendere mod e giochi più accessibili.
|
||||||
|
|
||||||
|
Si tenga comunque conto che Minetest è in continua evoluzione e che l'API verrà probabilmente ampliata in futuro.
|
||||||
|
Per esempio, il supporto per i file di traduzione *gettext* permetterà l'utilizzo di piattaforme e strumenti consolidati come Weblate, mentre nel frattempo si sta lavorando al supporto per il genere e il numero.
|
160
_it/quality/unit_testing.md
Normal file
160
_it/quality/unit_testing.md
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
title: Testing d'unità automatici
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.5
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduzione <!-- omit in toc -->
|
||||||
|
|
||||||
|
I testing d'unità sono uno strumento essenziale nell'assicurarsi che il codice sia corretto.
|
||||||
|
Questo capitolo ti mostrerà come scrivere questi per le mod e i giochi di Minetest usando Busted.
|
||||||
|
Scrivere i testing d'unità per le funzioni dove vengono chiamate quelle di Minetest è alquanto difficile, ma per fortuna abbiamo già discusso [nel capitolo precedente](clean_arch.html) come strutturare il codice in modo da non complicarci la vita.
|
||||||
|
|
||||||
|
- [Installare Busted](#installare-busted)
|
||||||
|
- [Il tuo primo test](#il-tuo-primo-test)
|
||||||
|
- [init.lua](#initlua)
|
||||||
|
- [api.lua](#apilua)
|
||||||
|
- [tests/api_spec.lua](#testsapi_speclua)
|
||||||
|
- [Simulare: usare funzioni esterne](#simulare-usare-funzioni-esterne)
|
||||||
|
- [Conclusione](#conclusione)
|
||||||
|
|
||||||
|
## Installare Busted
|
||||||
|
|
||||||
|
Prima di tutto, c'è bisogno di installare LuaRocks.
|
||||||
|
|
||||||
|
* Windows: segui le istruzioni sulla [wiki di LuaRocks](https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows).
|
||||||
|
* Debian/Ubuntu Linux: `sudo apt install luarocks`
|
||||||
|
|
||||||
|
Poi, dovresti installare Busted a livello globale:
|
||||||
|
|
||||||
|
sudo luarocks install busted
|
||||||
|
|
||||||
|
Infine, controlla che sia installato:
|
||||||
|
|
||||||
|
busted --version
|
||||||
|
|
||||||
|
|
||||||
|
## Il tuo primo test
|
||||||
|
|
||||||
|
Busted è il quadro strutturale (o *framework*) per eccellenza di Lua.
|
||||||
|
Quello che fa è cercare i file Lua con il nome che termina in `_spec`, eseguendoli poi in un ambiente Lua a sé stante.
|
||||||
|
|
||||||
|
miamod/
|
||||||
|
├── init.lua
|
||||||
|
├── api.lua
|
||||||
|
└── test
|
||||||
|
└── api_spec.lua
|
||||||
|
|
||||||
|
|
||||||
|
### init.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
miamod = {}
|
||||||
|
|
||||||
|
dofile(core.get_modpath("miamod") .. "/api.lua")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### api.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function miamod.somma(x, y)
|
||||||
|
return x + y
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### tests/api_spec.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Cerca le cose necessarie in package.path = "../?.lua;" .. package.path
|
||||||
|
|
||||||
|
-- Imposta la globale miamod per far sì che l'API possa scriverci sopra
|
||||||
|
_G.mymod = {} --_
|
||||||
|
-- Esegue il file api.lua
|
||||||
|
require("api")
|
||||||
|
|
||||||
|
-- Test vari
|
||||||
|
describe("somma", function()
|
||||||
|
it("aggiunge", function()
|
||||||
|
assert.equals(2, miamod.somma(1, 1))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("supporta valori negativi", function()
|
||||||
|
assert.equals(0, miamod.somma(-1, 1))
|
||||||
|
assert.equals(-2, miamod.somma(-1, -1))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Puoi ora eseguire i vari test aprendo un terminale nella cartella della mod ed eseguendo `busted .`.
|
||||||
|
|
||||||
|
È importante che il file dell'API non crei da sé la tabella, in quanto le variabili globali su Busted funzionano diversamente.
|
||||||
|
Ogni variabile che dovrebbe essere globale su Minetest è invece un file locale su Busted.
|
||||||
|
Sarebbe stato un modo migliore per Minetest di gestire le cose, ma è ormai troppo tardi per renderlo realtà.
|
||||||
|
|
||||||
|
Un'altra cosa da notare è che qualsiasi file si stia testando, bisognerebbe evitare che chiami funzioni al di fuori di esso.
|
||||||
|
Si tende infatti a scrivere i test che controllino un solo file alla volta.
|
||||||
|
|
||||||
|
|
||||||
|
## Simulare: usare funzioni esterne
|
||||||
|
|
||||||
|
Il simulare (*mocking*) è la pratica di sostituire le funzioni dalle quali la parte di codice da testare è dipendente.
|
||||||
|
Questo può avere due obiettivi: il primo, la funzione potrebbe non essere disponibile nell'area di testing; il secondo, si potrebbero voler catturare le chiamate alla funzione e gli argomenti da essa passati.
|
||||||
|
|
||||||
|
Se si sono seguiti i consigli nel capitolo delle [Architetture pulite](clean_arch.html), si avrà già un file bello pronto da testare, anche se si dovrà comunque simulare le cose non contenute nell'area di testing (per esempio, la vista quando si testa il controllo/API).
|
||||||
|
Se invece si è deciso di lasciar perdere quella parte, allora le cose sono un po' più complicate in quanto ci sarà da simulare anche la API di Minetest.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- come prima, crea una tabella
|
||||||
|
_G.minetest = {}
|
||||||
|
|
||||||
|
-- Definisce la funzione simulata
|
||||||
|
local chiamate_chat_send_all = {}
|
||||||
|
function core.chat_send_all(name, message)
|
||||||
|
table.insert(chiamate_chat_send_all, { nome = name, messaggio = message })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test
|
||||||
|
describe("elenca_aree", function()
|
||||||
|
it("ritorna una riga per ogni area", function()
|
||||||
|
chiamate_chat_send_all = {} -- resetta la tabella
|
||||||
|
|
||||||
|
miamod.elenca_aree_chat("singleplayer", "singleplayer")
|
||||||
|
|
||||||
|
assert.equals(2, #chiamate_chat_send_all)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("invia al giocatore giusto", function()
|
||||||
|
chiamate_chat_send_all = {} -- resetta la tabella
|
||||||
|
|
||||||
|
miamod.elenca_aree_chat("singleplayer", "singleplayer")
|
||||||
|
|
||||||
|
for _, chiamata in pairs(chiamate_chat_send_all) do --_
|
||||||
|
assert.equals("singleplayer", chiamata.nome)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- I due test qui in alto in verità sono inutili in quanto
|
||||||
|
-- questo li esegue entrambi
|
||||||
|
it("ritorna correttamente", function()
|
||||||
|
chiamate_chat_send_all = {} -- resetta la tabella
|
||||||
|
|
||||||
|
miamod.elenca_aree_chat("singleplayer", "singleplayer")
|
||||||
|
|
||||||
|
local previsto = {
|
||||||
|
{ nome = "singleplayer", messaggio = "Town Hall (2,43,63)" },
|
||||||
|
{ nome = "singleplayer", messaggio = "Airport (43,45,63)" },
|
||||||
|
}
|
||||||
|
assert.same(previsto, chiamate_chat_send_all)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusione
|
||||||
|
|
||||||
|
I testing d'unità aumenteranno notevolmente la qualità e l'affidabilità di un progetto se usati adeguatamente, ma ti richiederanno di strutturare il codice in maniera diversa dal solito.
|
||||||
|
|
||||||
|
Per un esempio di mod con molti testing d'unità, vedere la mod [*crafting* di rubenwardy](https://github.com/rubenwardy/crafting).
|
56
_layouts/base.html
Normal file
56
_layouts/base.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
layout: compress
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
|
||||||
|
{% assign pathsplit = page.url | split: '/' %}
|
||||||
|
{% assign language = pathsplit[1] %}
|
||||||
|
{% assign language_info = site.data.languages | where: "code", language %}
|
||||||
|
{% if language_info %}
|
||||||
|
<html lang="{{ language }}">
|
||||||
|
{% else %}
|
||||||
|
<html>
|
||||||
|
{% endif %}
|
||||||
|
<head>
|
||||||
|
<title>{% if page.homepage %}{% else %}{{ page.title }} - {% endif %}Luanti / Minetest Modding Book</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta name="author" content="rubenwardy">
|
||||||
|
<meta name="flattr:id" content="gl763e">
|
||||||
|
<link rel="canonical" href="https://rubenwardy.com/minetest_modding_book{{ page.url }}">
|
||||||
|
<meta name="og:url" content="https://rubenwardy.com/minetest_modding_book{{ page.url }}">
|
||||||
|
<meta name="og:title" content="{{ page.title | escape }}">
|
||||||
|
<meta name="og:author" content="rubenwardy">
|
||||||
|
<meta name="og:site_name" content="Luanti Modding Book (formerly Minetest)">
|
||||||
|
{% if page.description %}
|
||||||
|
<meta name="og:description" content="{{ page.description | escape | strip }}">
|
||||||
|
<meta name="description" content="{{ page.description | escape | strip }}">
|
||||||
|
{% endif %}
|
||||||
|
{% if page.image %}
|
||||||
|
<meta name="og:image" content="{{ page.image | absolute_url }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% assign oldSegment = "/" | append: language | append: "/" %}
|
||||||
|
{% for other_lang in site.data.languages %}
|
||||||
|
{% unless other_lang.code == language %}
|
||||||
|
{% assign newSegment = "/" | append: other_lang.code | append: "/" %}
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="{{ other_lang.code }}"
|
||||||
|
href="{{ page.url | replace: oldSegment, newSegment | relative_url }}">
|
||||||
|
{% endunless %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page.noindex %}
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<style>body,html,nav{background:#333}nav,nav li,nav li a{display:block}body,html,main,nav li{margin:0;padding:0}main,nav{position:absolute;top:0}body,html{font-size:17px;color:#000}#container{width:100%;max-width:1100px;margin:auto;position:relative}nav{left:0;width:280px;list-style:none;color:#fff}nav li a{padding:5px;color:#ccc;text-decoration:none}main{left:280px;right:0}article{background:#fff;padding:0 20px 20px}</style>
|
||||||
|
<link rel="stylesheet" href="{{ page.root }}/static/style.css?v=4">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
9
_layouts/compress.html
Normal file
9
_layouts/compress.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
# Jekyll layout that compresses HTML
|
||||||
|
# v2.0.0
|
||||||
|
# http://jch.penibelst.de/
|
||||||
|
# © 2014–2015 Anatol Broder
|
||||||
|
# MIT License
|
||||||
|
---
|
||||||
|
|
||||||
|
{% if site.compress_html.ignore.envs contains jekyll.environment %}{{ content }}{% else %}{% capture _content %}{{ content }}{% endcapture %}{% assign _profile = site.compress_html.profile %}{% if site.compress_html.endings == "all" %}{% assign _endings = "html head body li dt dd p rt rp optgroup option colgroup caption thead tbody tfoot tr td th" | split: " " %}{% else %}{% assign _endings = site.compress_html.endings %}{% endif %}{% for _element in _endings %}{% capture _end %}</{{ _element }}>{% endcapture %}{% assign _content = _content | remove: _end %}{% endfor %}{% if _profile and _endings %}{% assign _profile_endings = _content | size | plus: 1 %}{% endif %}{% assign _pre_befores = _content | split: "<pre" %}{% assign _content = "" %}{% for _pre_before in _pre_befores %}{% assign _pres = _pre_before | split: "</pre>" %}{% case _pres.size %}{% when 2 %}{% capture _content %}{{ _content }}<pre{{ _pres.first }}</pre>{{ _pres.last | split: " " | join: " " }}{% endcapture %}{% when 1 %}{% capture _content %}{{ _content }}{{ _pres.last | split: " " | join: " " }}{% endcapture %}{% endcase %}{% endfor %}{% if _profile %}{% assign _profile_collapse = _content | size | plus: 1 %}{% endif %}{% if site.compress_html.comments == "all" %}{% assign _comments = "<!-- -->" | split: " " %}{% else %}{% assign _comments = site.compress_html.comments %}{% endif %}{% if _comments.size == 2 %}{% assign _comment_befores = _content | split: _comments.first %}{% for _comment_before in _comment_befores %}{% assign _comment_content = _comment_before | split: _comments.last | first %}{% if _comment_content %}{% capture _comment %}{{ _comments.first }}{{ _comment_content }}{{ _comments.last }}{% endcapture %}{% assign _content = _content | remove: _comment %}{% endif %}{% endfor %}{% if _profile %}{% assign _profile_comments = _content | size | plus: 1 %}{% endif %}{% endif %}{% if site.compress_html.clippings == "all" %}{% assign _clippings = "html head title base link meta style body article section nav aside h1 h2 h3 h4 h5 h6 hgroup header footer address p hr blockquote ol ul li dl dt dd figure figcaption main div table caption colgroup col tbody thead tfoot tr td th" | split: " " %}{% else %}{% assign _clippings = site.compress_html.clippings %}{% endif %}{% for _element in _clippings %}{% assign _edges = " <e;<e; </e>;</e>;</e> ;</e>" | replace: "e", _element | split: ";" %}{% assign _content = _content | replace: _edges[0], _edges[1] | replace: _edges[2], _edges[3] | replace: _edges[4], _edges[5] %}{% endfor %}{% if _profile and _clippings %}{% assign _profile_clippings = _content | size | plus: 1 %}{% endif %}{{ _content }}{% if _profile %} <table id="compress_html_profile_{{ site.time | date: "%Y%m%d" }}" class="compress_html_profile"> <thead> <tr> <td>Step <td>Bytes <tbody> <tr> <td>raw <td>{{ content | size }}{% if _profile_endings %} <tr> <td>endings <td>{{ _profile_endings }}{% endif %}{% if _profile_collapse %} <tr> <td>collapse <td>{{ _profile_collapse }}{% endif %}{% if _profile_comments %} <tr> <td>comments <td>{{ _profile_comments }}{% endif %}{% if _profile_clippings %} <tr> <td>clippings <td>{{ _profile_clippings }}{% endif %} </table>{% endif %}{% endif %}
|
74
_layouts/default.html
Normal file
74
_layouts/default.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: base
|
||||||
|
---
|
||||||
|
|
||||||
|
{% assign pathsplit = page.path | split: '/' %}
|
||||||
|
|
||||||
|
{% assign language = pathsplit[0] %}
|
||||||
|
|
||||||
|
{% if language == "_it" %}
|
||||||
|
{% assign language = "it" %}
|
||||||
|
{% assign links = site.it %}
|
||||||
|
{% else %}
|
||||||
|
{% assign language = "en" %}
|
||||||
|
{% assign links = site.en %}
|
||||||
|
{% endif %}
|
||||||
|
{% assign links = links | where_exp: "item", "item.sitemap != false" | sort: "idx" %}
|
||||||
|
|
||||||
|
{% assign num = 0 %}
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{% for link in links %}
|
||||||
|
{% assign idsplit = link.id | split: '/' %}
|
||||||
|
{% assign section = idsplit[2] %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ page.root }}{{ link.url }}"
|
||||||
|
class="{% if page.title == link.title %}selected{% endif %}{% if section != last_section and section != 'index' %} hr {% endif %}">
|
||||||
|
{% if section != "index" %}{{ num }} - {% endif %}
|
||||||
|
{{ link.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% assign last_section = section %}
|
||||||
|
{% assign num = num | plus:1 %}
|
||||||
|
{% endfor %}
|
||||||
|
<li><a href="https://github.com/rubenwardy/minetest_modding_book/archive/examples.zip" class="hr">Download Examples</a></li>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<article {% if page.homepage %}class="homepage"{% endif %}>
|
||||||
|
<a href="{{ page.root }}/languages.html" class="language-switcher">
|
||||||
|
<img src="{{ page.root }}/static/languages.svg" alt="Choose a language">
|
||||||
|
<span>{{ language }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if page.no_header %}{% else %}<h1>{{ page.title }}</h1>{% endif %}
|
||||||
|
{{ content }}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% for link in links %}
|
||||||
|
{% if link.title == page.title %}
|
||||||
|
{% unless forloop.first %}
|
||||||
|
{% assign prev = tmpprev %}
|
||||||
|
{% endunless %}
|
||||||
|
{% unless forloop.last %}
|
||||||
|
{% assign next = links[forloop.index] %}
|
||||||
|
{% endunless %}
|
||||||
|
{% endif %}
|
||||||
|
{% assign tmpprev = link %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<ul class="prevnext">
|
||||||
|
<li>{% if prev %}<a href="{{ page.root }}{{ prev.url}}">< {{ prev.title }}</a>{% endif %}</li>
|
||||||
|
<li>{% if next %}<a href="{{ page.root }}{{ next.url}}">{{ next.title }} ></a>{% endif %}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
© 2014-{{ site.time | date: '%Y' }}
|
||||||
|
{% if language == "en" %}
|
||||||
|
| Helpful? Consider
|
||||||
|
<a href="https://rubenwardy.com/donate/">donating</a>
|
||||||
|
to support my work.
|
||||||
|
{% endif %}
|
||||||
|
</footer>
|
||||||
|
</main>
|
83
_sass/_code.scss
Normal file
83
_sass/_code.scss
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
code {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0.25em;
|
||||||
|
margin: 2px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
white-space: pre;
|
||||||
|
max-width: 95%;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* https://raw.githubusercontent.com/richleland/pygments-css/master/friendly.css */
|
||||||
|
|
||||||
|
.highlight .hll { background-color: #ffffcc }
|
||||||
|
.highlight .c { color: #60a0b0; font-style: italic } /* Comment */
|
||||||
|
.highlight .err { border: 1px solid #FF0000 } /* Error */
|
||||||
|
.highlight .k { color: #007020; font-weight: bold } /* Keyword */
|
||||||
|
.highlight .o { color: #666666 } /* Operator */
|
||||||
|
.highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */
|
||||||
|
.highlight .cp { color: #007020 } /* Comment.Preproc */
|
||||||
|
.highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */
|
||||||
|
.highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */
|
||||||
|
.highlight .gd { color: #A00000 } /* Generic.Deleted */
|
||||||
|
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||||
|
.highlight .gr { color: #FF0000 } /* Generic.Error */
|
||||||
|
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||||
|
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||||
|
.highlight .go { color: #808080 } /* Generic.Output */
|
||||||
|
.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
|
||||||
|
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||||
|
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||||
|
.highlight .gt { color: #0040D0 } /* Generic.Traceback */
|
||||||
|
.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */
|
||||||
|
.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
|
||||||
|
.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
|
||||||
|
.highlight .kp { color: #007020 } /* Keyword.Pseudo */
|
||||||
|
.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
|
||||||
|
.highlight .kt { color: #902000 } /* Keyword.Type */
|
||||||
|
.highlight .m { color: #40a070 } /* Literal.Number */
|
||||||
|
.highlight .s { color: #4070a0 } /* Literal.String */
|
||||||
|
.highlight .na { color: #4070a0 } /* Name.Attribute */
|
||||||
|
.highlight .nb { color: #007020 } /* Name.Builtin */
|
||||||
|
.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
|
||||||
|
.highlight .no { color: #60add5 } /* Name.Constant */
|
||||||
|
.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */
|
||||||
|
.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */
|
||||||
|
.highlight .ne { color: #007020 } /* Name.Exception */
|
||||||
|
.highlight .nf { color: #06287e } /* Name.Function */
|
||||||
|
.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */
|
||||||
|
.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
|
||||||
|
.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */
|
||||||
|
.highlight .nv { color: #bb60d5 } /* Name.Variable */
|
||||||
|
.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */
|
||||||
|
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||||
|
.highlight .mf { color: #40a070 } /* Literal.Number.Float */
|
||||||
|
.highlight .mh { color: #40a070 } /* Literal.Number.Hex */
|
||||||
|
.highlight .mi { color: #40a070 } /* Literal.Number.Integer */
|
||||||
|
.highlight .mo { color: #40a070 } /* Literal.Number.Oct */
|
||||||
|
.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */
|
||||||
|
.highlight .sc { color: #4070a0 } /* Literal.String.Char */
|
||||||
|
.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
|
||||||
|
.highlight .s2 { color: #4070a0 } /* Literal.String.Double */
|
||||||
|
.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
|
||||||
|
.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */
|
||||||
|
.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
|
||||||
|
.highlight .sx { color: #c65d09 } /* Literal.String.Other */
|
||||||
|
.highlight .sr { color: #235388 } /* Literal.String.Regex */
|
||||||
|
.highlight .s1 { color: #4070a0 } /* Literal.String.Single */
|
||||||
|
.highlight .ss { color: #517918 } /* Literal.String.Symbol */
|
||||||
|
.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
|
||||||
|
.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */
|
||||||
|
.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */
|
||||||
|
.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */
|
||||||
|
.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */
|
115
_sass/_content.scss
Normal file
115
_sass/_content.scss
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
a {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.credit {
|
||||||
|
display: block;
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_image {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
margin: 0 0 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_image img {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_image figcaption {
|
||||||
|
padding: 0 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link, .anchor {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #bbb;
|
||||||
|
padding: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link:hover {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
margin: 30px 0 10px 0;
|
||||||
|
display: block;
|
||||||
|
padding: 0 0 5px 0;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 105%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 30px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc > ul > li > ul > li > ul {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.prevnext {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
background: #eee;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li:first-child {
|
||||||
|
border-right: 2px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child {
|
||||||
|
border-left: 2px solid white;
|
||||||
|
}
|
||||||
|
}
|
121
_sass/_feedback.scss
Normal file
121
_sass/_feedback.scss
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
.feedback {
|
||||||
|
background: white;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
input[name='username'], label[for='username'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
border: none;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
--color-primary-dark: #007DB8;
|
||||||
|
--color-primary-dark-highlight: #06aed5;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
margin: 0.25rem 0.25rem 0.25rem 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: white;
|
||||||
|
transition: color 0.15s ease-in-out, filter 0.15s ease-in-out,
|
||||||
|
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||||
|
box-shadow 0.15s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.icon {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-primary-dark);
|
||||||
|
border-color: var(--color-primary-dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-dark-highlight);
|
||||||
|
border-color: var(--color-primary-dark-highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button, select, optgroup, textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(1.5em + 1.5rem + 2px);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #52575C;
|
||||||
|
background-color: #fff;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #7A8288 !important;
|
||||||
|
}
|
||||||
|
.form-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
small, .small {
|
||||||
|
font-size: 80%;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user