Add files via upload

This commit is contained in:
thunderdog1138 2019-12-15 17:50:59 -05:00 committed by GitHub
parent 1045d88d8a
commit 3ae5819427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 565 additions and 0 deletions

View File

@ -0,0 +1,13 @@
Copyright 2015-2016 raymoo
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,205 @@
# Player Monoids
This is a small library for managing global player state, so that changes made
from different mods do not result in unexpected behavior. The README gives an
introduction to the mod, but you might want to reference API.md along the way.
This mod, combined with playereffects, deprecates monoidal_effects.
Global Player State
===================
Players have behavior-affecting state that can be modified through mods. A couple
examples are physics overrides and armor groups. If more than one mod tries to
change them, it can result in unexpected results.
For example, a player could be
under a speed boost effect from a playereffects effect, and then sleep in a bed.
The bed sets the player's speed to 0, and sets it back to 1 when they get out.
Because the beds mod had no way of knowing that the player was supposed to have
a speed boost, it effectively removed it. One hack to "fix" it would be to save
the player's speed and restore it on wakeup, but this would have its own problems
if the effect wears off in bed. The beds mod would restore the boosted speed,
which wouldn't be removed, since the effect already went away. Thus an exploit
allowing a permanent (until log out) speed boost is introduced.
Player Monoids manages this by creating layers (monoids) on top of player state,
which can keep track of different changes and combine them usefully.
Monoids
=======
Creation
--------
A monoid in Player Monoids is an interface to one piece of player state. For
example, you could have one monoid covering physics overrides, and another
covering fly privilege. You could define a speed monoids like this:
```
-- The values in my speed monoid must be speed multipliers (numbers).
mymod.speed_monoid = player_monoids.make_monoid({
combine = function(speed1, speed2)
return speed1 * speed2
end,
fold = function(tab)
local res = 1
for _, speed in pairs(tab) do
res = res * speed
end
end,
identity = 1,
apply = function(speed, player)
local override = player:get_physics_override()
override.speed = speed
player:set_physics_override(override)
end,
on_change = function() return end,
})
```
This says that two speed multipliers can be combined by multiplication, that
1 can be used as a neutral element, and that the "interpretation" of the speed
multiplier is to set the player's speed physics override to that value. It also
says that nothing in particular needs to happen when the speed changes, other
than applying the new speed multiplier.
Use
---
To add or remove change through a monoid, you must use the ```add_change```
and ```del_change``` methods. For example, you could speed the player up
temporarily like this:
```
-- Zoom!
local zoom_id = mymod.speed_monoid:add_change(some_player, 10)
minetest.after(5,function() mymod.speed_monoid:del_change(some_player, zoom_id) end)
```
You could also specify a string ID to use, instead of the numerical one that
is automatically provided:
```
-- Zoom Mk. II
mymod.speed_monoid:add_change(some_player, 10, "mymod:zoom")
minetest.after(5,function() mymod.speed_monoid:del_change(some_player, "mymod:zoom") end)
```
Reading Values
--------------
You can use ```monoid:value(player)``` to read the current value of the monoid,
for that player. This could be useful if it doesn't just represent built-in
player state. For example, it could represent gardening skill, and you might use
it to calculate the chance of success when harvesting spices.
Nesting Monoids
---------------
You may have already noticed one limitation of this design. That is, for each
kind of player state, you can only combine state changes in one way. If the
standard speed monoid combines speed multipliers by multiplication, you cannot
change it to instead choose the highest speed multiplier. Unfortunately, there
is currently no way change this - you will have to hope that the given monoid
combines in a useful way. However, it is possible to manage a subset of the
values in a custom way.
Suppose that a speed monoid (```mymod.speed_monoid```) already exists, using
multiplication, but you want to write a mod with speed boosts, and only apply
the strongest boost. Most of it could be done the same way:
```
-- My speed boosts monoid takes speed multipliers (numbers) that are at least 1.
newmod.speed_boosts = player_monoids.make_monoid({
combine = function(speed1, speed2)
return speed1 * speed2
end,
fold = function(tab)
local res = 1
for _, speed in pairs(tab) do
res = res * speed
end
end,
identity = 1,
apply = ???
on_change = function() return end,
})
```
But we cannot just change the player speed in ```apply```, otherwise we will
break compatibility with the original speed monoid! The trick here is to use
the original monoid as a proxy for our effects.
```
apply = function(speed, player)
mymod.speed_monoid:add_change(player, speed, "newmod:speed_boosts")
end
```
This means the speed boosts we control can be limited to the strongest boost, but
the resulting boost will still play nice with speed effects from other mods.
You could even add another "nested monoid" just for speed maluses, that takes
the worst speed drain and applies it as a multiplier.
Standard Monoids
================
In the spirit of compatibility, this mod provides some canonical monoids for
commonly used player state. They combine values in a way that should allow
different mods to affect player state fairly. If you make another monoid handling
the same state as one of these, you will break compatibility with any mods using
the standard monoid.
Physics Overrides
-----------------
These monoids set the multiplier of the override they are named after. All three
take non-negative numbers as values and combine them with multiplication. They
are:
* ```player_monoids.speed```
* ```player_monoids.jump```
* ```player_monoids.gravity```
Privileges
----------
These monoids set privileges that affect the player's ordinary gameplay. They
take booleans as input and combine them with logical or. They are:
* ```player_monoids.fly```
* ```player_monoids.noclip```
Other
-----
* ```player_monoids.collisionbox``` - Sets the player's collisionbox. Values are
3D multiplier vectors, which are combined with component-wise multiplication.
* ```player_monoids.visual_size``` - Sets the player's collisionbox. Values are
2D multiplier vectors (x and y), which are combined with component-wise
multiplication.
Use with playereffects
======================
Player Monoids does not provide anything special for persistent effects with
limited lifetime. By using monoids with Wuzzy's playereffects, you can easily
create temporary effects that stack with each other. As an example, an effect
that gives the player 2x speed:
```
local speed = player_monoids.speed
local function apply(player)
speed:add_change(player, 2, "mymod:2x_speed")
end
local function cancel(player)
speed:del_change(player, "mymod:2x_speed")
end
local groups = { "mymod:2x_speed" }
playereffects.register_effect_type("mymod:2x_speed", "2x Speed", groups, apply, cancel)
```
Note that this effect does NOT use the "speed" effect group. As long as other
speed effects use the speed monoid, we do not want them to be cancelled, since
the goal is to combine the effects together. It does use a singleton group to
prevent multiple instances of the same effect. I think that playereffects require
effects to belong to at least one group, but I am not sure.
Caveats
=======
* If the global state managed by a monoid is modified by something other than
the monoid, you will have the same problem as when two mods both independently
try to modify global state without going through a monoid.
* This includes playereffects effects that affect global player state without
going through a monoid.
* You will also get problems if you use multiple monoids to manage the same
global state.
* The order that different effects get combined together is based on key order,
which may not be predictable. So you should try to make your monoids commutative
in addition to associative, or at least not care if the order of two changes
is swapped.

View File

@ -0,0 +1,19 @@
{
"name": "player_monoids",
"description": "Library for making player state changes combinable\n",
"keywords": [
"player_monoids",
"monoid",
"monoids",
"effect",
"playereffects"
],
"homepage": "https://github.com/raymoo/player_monoids",
"forum": "https://forum.minetest.net/viewtopic.php?f=9&t=14895",
"screenshots": [
"https://example.com/screenshot1.png"
],
"authors": [
"raymoo"
]
}

View File

@ -0,0 +1,2 @@
player_monoids is a library for managing global player state, such as physics
overrides or player visual size.

View File

@ -0,0 +1,118 @@
-- Copyright (c) raymoo 2016
-- Licensed under Apache 2.0 license. See COPYING for details.
-- Any documentation here are internal details, please avoid using them in your
-- mod.
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/"
player_monoids = {}
local mon_meta = {}
mon_meta.__index = mon_meta
local nop = function() end
-- A monoid object is a table with the following fields:
-- def: The monoid definition
-- player_map: A map from player names to their effect tables. Effect tables
-- are maps from effect IDs to values.
-- value_cache: A map from player names to the cached value for the monoid.
-- next_id: The next unique ID to assign an effect.
local function monoid(def)
local mon = {}
local actual_def = {}
for k, v in pairs(def) do
actual_def[k] = v
end
if not actual_def.apply then
actual_def.apply = nop
end
if not actual_def.on_change then
actual_def.on_change = nop
end
mon.def = actual_def
local p_map = {}
mon.player_map = p_map
mon.next_id = 1
local v_cache = {}
mon.value_cache = v_cache
setmetatable(mon, mon_meta)
minetest.register_on_leaveplayer(function(player)
local p_name = player:get_player_name()
p_map[p_name] = nil
v_cache[p_name] = nil
end)
return mon
end
player_monoids.monoid = monoid
function mon_meta:add_change(player, value)
local p_name = player:get_player_name()
local def = self.def
local p_effects = self.player_map[p_name]
if p_effects == nil then
p_effects = {}
self.player_map[p_name] = p_effects
end
local actual_id
if id then
actual_id = id
else
actual_id = self.next_id
self.next_id = actual_id + 1
end
local old_total = self.value_cache[p_name]
p_effects[actual_id] = value
local new_total = def.fold(p_effects)
self.value_cache[p_name] = new_total
def.apply(new_total, player)
def.on_change(old_total, new_total, player)
return actual_id
end
function mon_meta:del_change(player, id)
local p_name = player:get_player_name()
local def = self.def
local p_effects = self.player_map[p_name]
if p_effects == nil then return end
local old_total = self.value_cache[p_name]
p_effects[id] = nil
local new_total = def.fold(p_effects)
self.value_cache[p_name] = new_total
def.apply(new_total, player)
def.on_change(old_total, new_total, player)
end
function mon_meta:value(player)
local p_name = player:get_player_name()
return self.value_cache[p_name] or self.def.identity
end
dofile(modpath .. "standard_monoids.lua")
dofile(modpath .. "test.lua")

View File

@ -0,0 +1 @@
name=player_monoids

View File

@ -0,0 +1,180 @@
-- Standard effect monoids, to provide canonicity.
local function mult(x, y) return x * y end
local function mult_fold(elems)
local tot = 1
for k,v in pairs(elems) do
tot = tot * v
end
return tot
end
local function v_mult(v1, v2)
local res = {}
for k, v in pairs(v1) do
res[k] = v * v2[k]
end
return res
end
local function v_mult_fold(identity)
return function(elems)
local tot = identity
for k, v in pairs(elems) do
tot = v_mult(tot, v)
end
return tot
end
end
local monoid = player_monoids.monoid
-- Speed monoid. Effect values are speed multipliers. Must be nonnegative
-- numbers.
player_monoids.speed = monoid({
combine = function(x, y) return x * y end,
fold = function(elems)
local res = 1
for k, v in pairs(elems) do
res = res * v
end
return res
end,
identity = 1,
apply = function(mult, player)
local ov = player:get_physics_override()
ov.speed = mult
player:set_physics_override(ov)
end,
})
-- Jump monoid. Effect values are jump multipliers. Must be nonnegative
-- numbers.
player_monoids.jump = monoid({
combine = function(x, y) return x * y end,
fold = function(elems)
local res = 1
for k, v in pairs(elems) do
res = res * v
end
return res
end,
identity = 1,
apply = function(mult, player)
local ov = player:get_physics_override()
ov.jump = mult
player:set_physics_override(ov)
end,
})
-- Gravity monoid. Effect values are gravity multipliers.
player_monoids.gravity = monoid({
combine = function(x, y) return x * y end,
fold = function(elems)
local res = 1
for k, v in pairs(elems) do
res = res * v
end
return res
end,
identity = 1,
apply = function(mult, player)
local ov = player:get_physics_override()
ov.gravity = mult
player:set_physics_override(ov)
end,
})
-- Fly ability monoid. The values are booleans, which are combined by or. A true
-- value indicates having the ability to fly.
player_monoids.fly = monoid({
combine = function(p, q) return p or q end,
fold = function(elems)
for k, v in pairs(elems) do
if v then return true end
end
return false
end,
identity = false,
apply = function(can_fly, player)
local p_name = player:get_player_name()
local privs = minetest.get_player_privs(p_name)
if can_fly then
privs.fly = true
else
privs.fly = nil
end
minetest.set_player_privs(p_name, privs)
end,
})
-- Noclip ability monoid. Works the same as fly monoid.
player_monoids.noclip = monoid({
combine = function(p, q) return p or q end,
fold = function(elems)
for k, v in pairs(elems) do
if v then return true end
end
return false
end,
identity = false,
apply = function(can_noclip, player)
local p_name = player:get_player_name()
local privs = minetest.get_player_privs(p_name)
if can_noclip then
privs.noclip = true
else
privs.noclip = nil
end
minetest.set_player_privs(p_name, privs)
end,
})
local def_col_scale = { x=0.3, y=1, z=0.3 }
-- Collisionbox scaling factor. Values are a vector of x, y, z multipliers.
player_monoids.collisionbox = monoid({
combine = v_mult,
fold = v_mult_fold({x=1, y=1, z=1}),
identity = {x=1, y=1, z=1},
apply = function(multiplier, player)
local v = vector.multiply(def_col_scale, multiplier)
player:set_properties({
collisionbox = { -v.x, -v.y, -v.z, v.z, v.y, v.z }
})
end,
})
player_monoids.visual_size = monoid({
combine = v_mult,
fold = v_mult_fold({x=1, y=1}),
identity = {x=1, y=1},
apply = function(multiplier, player)
player:set_properties({
visual_size = multiplier
})
end,
})

View File

@ -0,0 +1,27 @@
local speed = player_monoids.speed
minetest.register_privilege("monoid_master", {
description = "Allows testing of player monoids.",
give_to_singleplayer = false,
})
local function test(player)
local ch_id = speed:add_change(player, 10)
local p_name = player:get_player_name()
minetest.chat_send_player(p_name, "Your speed is: " .. speed:value(player))
minetest.after(3, function()
speed:del_change(player, ch_id)
minetest.chat_send_player(p_name, "Your speed is: " .. speed:value(player))
end)
end
minetest.register_chatcommand("test_monoids", {
description = "Runs a test on monoids",
privs = { monoid_master = true },
func = function(p_name)
test(minetest.get_player_by_name(p_name))
end,
})