Skip to content

Commit

Permalink
Keys: Allow easy sharing of access without commands
Browse files Browse the repository at this point in the history
This code adds the key concept to minetest_game, and integrates it
with lockable nodes. Currently supported lockable items are the Steel
Door, the Steel Trapdoor, and the Locked Chest.

The goal of this modification is to introduce a fine-grained multi-
player permission system that is intuitive and usable without any
console or chat commands, and doesn't require extra privileges to
be granted or setup. Keys can also physically be conveyed to other
players, adding to gameplay and adding some personality that is
preferable to console commands or editing formspecs.

A skeleton key can be crafted with 1 gold ingot. Skeleton keys can
then be matched to a lockable node by right-clicking the skeleton
key on a lockable node, which changes the skeleton key to a "key".

Gold was chosen as it's currently a not-so very useful item, and
therefore it's likely that players have some, but aren't really
using it for any purpose.

This key can subsequently used by any player to open or access that
lockable node, including retrieving items from Locked Chests, or
putting items in them.

They key is programmed to fit only the particular locked node it is
programmed to. This is achieved by storing a secret value in both
key and locked node. If this secret value doesn't match, the key
will not open the locked node. This allows many keys to be created
for one chest or door, but a key will only fit one node ever. The
secrets are stored in node, and item meta for the key.

If a locked node is removed, all keys that opened it are no longer
valid. Even if a new door/chest is placed in exactly the same spot,
the old keys will no longer fit that node.

Keys can be smelted back in gold ingots if they are no longer useful.

The method of storing a secret in nodemeta and itemstackmeta is secure
as there is no way for the client to create new items on the server
with a particular secret metadata value. Even if you could possible
create such an itemstack on the client, the server does not ever read
itemstackmeta from a client package.

The patch adds an API that allows other nodes and nodes added by
mods to use the same keys as well. The method how to implement this
is described in game_api.txt. The mod should add 2 callbacks to it's
node definition. Example code is given.

Textures are from PixelBOX, thanks to Gambit.
  • Loading branch information
sofar authored and paramat committed Nov 25, 2016
1 parent b0ae488 commit e4b1c93
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 5 deletions.
58 changes: 58 additions & 0 deletions game_api.txt
Expand Up @@ -672,3 +672,61 @@ Carts
like speed, acceleration, player attachment. The handler will
likely be called many times per second, so the function needs
to make sure that the event is handled properly.

Key API
-------

The key API allows mods to add key functionality to nodes that have
ownership or specific permissions. Using the API will make it so
that a node owner can use skeleton keys on their nodes to create keys
for that node in that location, and give that key to other players,
allowing them some sort of access that they otherwise would not have
due to node protection.

To make your new nodes work with the key API, you need to register
two callback functions in each nodedef:


`on_key_use(pos, player)`
* Is called when a player right-clicks (uses) a normal key on your
* node.
* `pos` - position of the node
* `player` - PlayerRef
* return value: none, ignored

The `on_key_use` callback should validate that the player is wielding
a key item with the right key meta secret. If needed the code should
deny access to the node functionality.

If formspecs are used, the formspec callbacks should duplicate these
checks in the metadata callback functions.


`on_skeleton_key_use(pos, player, newsecret)`

* Is called when a player right-clicks (uses) a skeleton key on your
* node.
* `pos` - position of the node
* `player` - PlayerRef
* `newsecret` - a secret value(string)
* return values:
* `secret` - `nil` or the secret value that unlocks the door
* `name` - a string description of the node ("a locked chest")
* `owner` - name of the node owner

The `on_skeleton_key_use` function should validate that the player has
the right permissions to make a new key for the item. The newsecret
value is useful if the node has no secret value. The function should
store this secret value somewhere so that in the future it may compare
key secrets and match them to allow access. If a node already has a
secret value, the function should return that secret value instead
of the newsecret value. The secret value stored for the node should
not be overwritten, as this would invalidate existing keys.

Aside from the secret value, the function should retun a descriptive
name for the node and the owner name. The return values are all
encoded in the key that will be given to the player in replacement
for the wielded skeleton key.

if `nil` is returned, it is assumed that the wielder did not have
permissions to create a key for this node, and no key is created.
2 changes: 2 additions & 0 deletions mods/default/README.txt
Expand Up @@ -177,6 +177,8 @@ Gambit (CC BY-SA 3.0):
default_snow.png
default_snow_side.png
default_snowball.png
default_key.png
default_key_skeleton.png

asl97 (CC BY-SA 3.0):
default_ice.png
Expand Down
21 changes: 21 additions & 0 deletions mods/default/crafting.lua
Expand Up @@ -352,6 +352,13 @@ minetest.register_craft({
}
})

minetest.register_craft({
output = 'default:skeleton_key',
recipe = {
{'default:gold_ingot'},
}
})

minetest.register_craft({
output = 'default:chest',
recipe = {
Expand Down Expand Up @@ -781,6 +788,20 @@ minetest.register_craft({
recipe = "default:clay_lump",
})

minetest.register_craft({
type = 'cooking',
output = 'default:gold_ingot',
recipe = 'default:skeleton_key',
cooktime = 5,
})

minetest.register_craft({
type = 'cooking',
output = 'default:gold_ingot',
recipe = 'default:key',
cooktime = 5,
})

--
-- Fuels
--
Expand Down
55 changes: 52 additions & 3 deletions mods/default/nodes.lua
Expand Up @@ -1619,16 +1619,30 @@ local function get_locked_chest_formspec(pos)
end
local function has_locked_chest_privilege(meta, player)
local name = ""
if player then
if minetest.check_player_privs(player, "protection_bypass") then
return true
end
name = player:get_player_name()
else
return false
end
-- is player wielding the right key?
local item = player:get_wielded_item()
if item:get_name() == "default:key" then
local key_meta = minetest.parse_json(item.get_metadata())
local secret = meta:get_string("key_lock_secret")
if secret ~= key_meta.secret then
return false
end
return true
end
if name ~= meta:get_string("owner") then
if player:get_player_name() ~= meta:get_string("owner") then
return false
end
return true
end
Expand Down Expand Up @@ -1748,6 +1762,41 @@ minetest.register_node("default:chest_locked", {
return itemstack
end,
on_blast = function() end,
on_key_use = function(pos, player)
local secret = minetest.get_meta(pos):get_string("key_lock_secret")
local itemstack = player:get_wielded_item()
local key_meta = minetest.parse_json(itemstack:get_metadata())
if secret ~= key_meta.secret then
return
end
minetest.show_formspec(
player:get_player_name(),
"default:chest_locked",
get_locked_chest_formspec(pos)
)
end,
on_skeleton_key_use = function(pos, player, newsecret)
local meta = minetest.get_meta(pos)
local owner = meta:get_string("owner")
local name = player:get_player_name()
-- verify placer is owner of lockable chest
if owner ~= name then
minetest.record_protection_violation(pos, name)
minetest.chat_send_player(name, "You do not own this chest.")
return nil
end
local secret = meta:get_string("key_lock_secret")
if secret == "" then
secret = newsecret
meta:set_string("key_lock_secret", secret)
end
return secret, "a locked chest", owner
end,
})
Expand Down
Binary file added mods/default/textures/default_key.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added mods/default/textures/default_key_skeleton.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions mods/default/tools.lua
Expand Up @@ -378,3 +378,75 @@ minetest.register_tool("default:sword_diamond", {
},
sound = {breaks = "default_tool_breaks"},
})

minetest.register_tool("default:skeleton_key", {
description = "Skeleton Key",
inventory_image = "default_key_skeleton.png",
groups = {key = 1},
on_place = function(itemstack, placer, pointed_thing)
if pointed_thing.type ~= "node" then
return itemstack
end

local pos = pointed_thing.under
local node = minetest.get_node(pos)

if not node then
return itemstack
end

local on_skeleton_key_use = minetest.registered_nodes[node.name].on_skeleton_key_use
if on_skeleton_key_use then
-- make a new key secret in case the node callback needs it
local random = math.random
local newsecret = string.format(
"%04x%04x%04x%04x",
random(2^16) - 1, random(2^16) - 1,
random(2^16) - 1, random(2^16) - 1)

local secret, _, _ = on_skeleton_key_use(pos, placer, newsecret)

if secret then
-- finish and return the new key
itemstack:take_item()
itemstack:add_item("default:key")
itemstack:set_metadata(minetest.write_json({
secret = secret
}))
return itemstack
end
end
return nil
end
})

minetest.register_tool("default:key", {
description = "Key",
inventory_image = "default_key.png",
groups = {key = 1, not_in_creative_inventory = 1},
stack_max = 1,
on_place = function(itemstack, placer, pointed_thing)
if pointed_thing.type ~= "node" then
return itemstack
end

local pos = pointed_thing.under
local node = minetest.get_node(pos)

if not node or node.name == "ignore" then
return itemstack
end

local ndef = minetest.registered_nodes[node.name]
if not ndef then
return itemstack
end

local on_key_use = ndef.on_key_use
if on_key_use then
on_key_use(pos, placer)
end

return nil
end
})
70 changes: 68 additions & 2 deletions mods/doors/init.lua
Expand Up @@ -140,8 +140,17 @@ function _doors.door_toggle(pos, node, clicker)
end

if clicker and not minetest.check_player_privs(clicker, "protection_bypass") then
-- is player wielding the right key?
local item = clicker:get_wielded_item()
local owner = meta:get_string("doors_owner")
if owner ~= "" then
if item:get_name() == "default:key" then
local key_meta = minetest.parse_json(item:get_metadata())
local secret = meta:get_string("key_lock_secret")
if secret ~= key_meta.secret then
return false
end

elseif owner ~= "" then
if clicker:get_player_name() ~= owner then
return false
end
Expand Down Expand Up @@ -371,6 +380,30 @@ function doors.register(name, def)
if def.protected then
def.can_dig = can_dig_door
def.on_blast = function() end
def.on_key_use = function(pos, player)
local door = doors.get(pos)
door:toggle(player)
end
def.on_skeleton_key_use = function(pos, player, newsecret)
local meta = minetest.get_meta(pos)
local owner = meta:get_string("doors_owner")
local pname = player:get_player_name()

-- verify placer is owner of lockable door
if owner ~= pname then
minetest.record_protection_violation(pos, pname)
minetest.chat_send_player(pname, "You do not own this locked door.")
return nil
end

local secret = meta:get_string("key_lock_secret")
if secret == "" then
secret = newsecret
meta:set_string("key_lock_secret", secret)
end

return secret, "a locked door", owner
end
else
def.on_blast = function(pos, intensity)
minetest.remove_node(pos)
Expand Down Expand Up @@ -491,9 +524,18 @@ end
function _doors.trapdoor_toggle(pos, node, clicker)
node = node or minetest.get_node(pos)
if clicker and not minetest.check_player_privs(clicker, "protection_bypass") then
-- is player wielding the right key?
local item = clicker:get_wielded_item()
local meta = minetest.get_meta(pos)
local owner = meta:get_string("doors_owner")
if owner ~= "" then
if item:get_name() == "default:key" then
local key_meta = minetest.parse_json(item:get_metadata())
local secret = meta:get_string("key_lock_secret")
if secret ~= key_meta.secret then
return false
end

elseif owner ~= "" then
if clicker:get_player_name() ~= owner then
return false
end
Expand Down Expand Up @@ -546,6 +588,30 @@ function doors.register_trapdoor(name, def)
end

def.on_blast = function() end
def.on_key_use = function(pos, player)
local door = doors.get(pos)
door:toggle(player)
end
def.on_skeleton_key_use = function(pos, player, newsecret)
local meta = minetest.get_meta(pos)
local owner = meta:get_string("doors_owner")
local pname = player:get_player_name()

-- verify placer is owner of lockable door
if owner ~= pname then
minetest.record_protection_violation(pos, pname)
minetest.chat_send_player(pname, "You do not own this trapdoor.")
return nil
end

local secret = meta:get_string("key_lock_secret")
if secret == "" then
secret = newsecret
meta:set_string("key_lock_secret", secret)
end

return secret, "a locked trapdoor", owner
end
else
def.on_blast = function(pos, intensity)
minetest.remove_node(pos)
Expand Down

0 comments on commit e4b1c93

Please sign in to comment.