diff --git a/mods/player_monoids/COPYING b/mods/player_monoids/COPYING new file mode 100644 index 00000000..8034ed73 --- /dev/null +++ b/mods/player_monoids/COPYING @@ -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. \ No newline at end of file diff --git a/mods/player_monoids/README.md b/mods/player_monoids/README.md new file mode 100644 index 00000000..245174ab --- /dev/null +++ b/mods/player_monoids/README.md @@ -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. \ No newline at end of file diff --git a/mods/player_monoids/bower.json b/mods/player_monoids/bower.json new file mode 100644 index 00000000..830c0d49 --- /dev/null +++ b/mods/player_monoids/bower.json @@ -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" + ] +} diff --git a/mods/player_monoids/description.txt b/mods/player_monoids/description.txt new file mode 100644 index 00000000..87dacddc --- /dev/null +++ b/mods/player_monoids/description.txt @@ -0,0 +1,2 @@ +player_monoids is a library for managing global player state, such as physics +overrides or player visual size. diff --git a/mods/player_monoids/init.lua b/mods/player_monoids/init.lua new file mode 100644 index 00000000..ed34bdf5 --- /dev/null +++ b/mods/player_monoids/init.lua @@ -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") diff --git a/mods/player_monoids/mod.conf b/mods/player_monoids/mod.conf new file mode 100644 index 00000000..2ff945ae --- /dev/null +++ b/mods/player_monoids/mod.conf @@ -0,0 +1 @@ +name=player_monoids \ No newline at end of file diff --git a/mods/player_monoids/standard_monoids.lua b/mods/player_monoids/standard_monoids.lua new file mode 100644 index 00000000..34d76589 --- /dev/null +++ b/mods/player_monoids/standard_monoids.lua @@ -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, +}) diff --git a/mods/player_monoids/test.lua b/mods/player_monoids/test.lua new file mode 100644 index 00000000..eff14911 --- /dev/null +++ b/mods/player_monoids/test.lua @@ -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, +})