Added branching mechanism

Implements #9
This commit is contained in:
Giov4 2025-02-04 16:06:32 +01:00
parent 8d6f1ade93
commit df7147c1f8
5 changed files with 1197 additions and 321 deletions

221
API.md
View File

@ -1,76 +1,169 @@
#Player Monoids
# Player Monoids Library Documentation
The idea behind this library is that global player state (physics overrides,
armor values, etc.) changes from multiple mods should mesh nicely with each
other. This means they must be combinable in a sane way.
Monoids
=======
A player monoid covers a single kind of player state a mod might want to change.
These can be built-in player state, like speed multipliers or fly permissions,
or could be custom state introduced by mods, such as corruption or reputation
level. When you make a player monoid, you must choose some type of value to
represent state changes - for example, numbers for speed multipliers, or vectors
for "lucky direction". Each mod can contribute different changes, represented
by this type of value, and they are all combined together. This combined value
is interpreted and converted into actual effects on the player's state.
Privileges could be set, physics overrides would be used to effect speed
changes, and a mod might change some value to match the monoid.
Definition
----------
A player monoid definition is a table with the following:
## Table of Contents
1. [Introduction](#introduction)
- 1.1 [Use Cases and Types](#use-cases-and-types)
- 1.2 [Definition Structure](#definition-structure)
2. [Branch System](#branch-system)
3. [API Reference](#api-reference)
* ```combine(elem1, elem2)``` - An associative binary operation
* ```fold({elems})``` - Equivalent to combining a whole list with ```combine```
* ```identity``` - An identity element for ```combine```
* ```apply(value, player)``` - Apply the effect represented by ```value```
to ```player```
* ```on_change(val1, val2, player)``` - Do something when the value on a
player changes. (optional)
Additionally, you should document what values are valid representatives of
your monoid's effects. When something says that a value is "in a monoid", it
means that value is a valid representative of your monoid's effects.
combine and fold
----------------
```combine``` should take two values in your monoid and produce a third value in
your monoid. It should also be an associative operation. ```fold`` should take a
table containing elements of your monoid as input and combine them together in
key order. It should be equivalent to using ```combine``` to combine all the
values together. For example, ```combine``` could multiply two speed multipliers
together, and ```fold``` could multiply every value together.
## Introduction
identity
--------
```identity```, when combined with any other value, should result in the other
value. It also represents the "default" or "neutral" state of the player, and
will be used when there are no status effects active for a particular monoid.
For example, the identity of a speed monoid could be the multiplier ```1```.
The **Player Monoids Library** is designed to solve the problem of conflicting player state changes in Luanti when multiple mods are involved. For example, one mod might want to increase a player's speed, while another mod reduces it. Without a structured way to combine these changes, mods can overwrite each other's effects, leading to unpredictable behavior.
apply
-----
```apply``` is the function that interprets a value in your monoid to do
something to the player's state. For example, you could set a speed multiplier
as the speed physics override for the player.
This library introduces **monoids**, which represent specific aspects of the player state, such as speed modifiers, jump height, or even custom states like corruption levels or reputation systems. Monoids allow changes from multiple mods to be combined consistently and predictably. Additionally, the library supports **branches**, which isolate changes into separate contexts. This makes it possible to maintain different states for different scenarios, such as minigames or alternate dimensions.
Functions
=========
```player_monoids.make_monoid(monoid_def)``` - Creates a new monoid that can be
used to make changes to the player state. Returns a monoid.
### Use Cases and Types
Monoid Methods
--------------
```monoid:add_change(player, value[, "id"])``` - Applies the change represented
by ```value``` to ```player```. Returns an ID for the change. If the optional
string argument ```"id"``` is supplied, that is used as the ID instead, and any
existing change with that ID is removed. IDs are only guaranteed to be unique
per-player. Conversely, you are allowed to make multiple changes with the same
ID as long as they are all on different players.
Monoids are useful for managing both built-in player attributes and custom mod-defined states. For example:
```monoid:del_change(player, id)``` - Removes the change with the given ID, from
the given player, if it exists.
- **Built-in Attributes**: Monoids can manage physics overrides like speed multipliers, jump height modifiers, or gravity changes. They can also handle privilege management (e.g., enabling or disabling fly or noclip combining booleans with the *or* operator) or armor values.
- **Custom Mod States**: Mods can define their own monoids for features like corruption levels, reputation systems, or environmental effects. For instance, you could create a monoid that tracks "lucky directions" as vectors.
```monoid:value(player)``` - The current combined value of the monoid for the
given player.
Monoids can be categorized based on how they combine values:
- **Multiplicative Monoids**: Combine values using multiplication (e.g., speed multipliers).
- **Additive Monoids**: Combine values using addition (e.g., armor bonuses).
- **Custom Logic Monoids**: Use custom logic to combine values (e.g., vectors for directional effects).
---
### Definition Structure
A monoid is defined as a Lua table that specifies how values are combined, applied to the player, and managed. The structure includes the following fields:
```lua
{
  combine = function(elem1, elem2),  -- Combines two elements (must be associative)
  fold = function({elems}),          -- Combines multiple elements
  identity = value,                  -- Neutral/default value
  apply = function(value, player),   -- Applies the combined value to the player
  on_change = function(old, new, player, branch),  -- Optional callback for value changes
  listen_to_all_changes = boolean    -- Optional; enables branch-wide callbacks
}
```
Each field plays a specific role in defining the behavior of the monoid:
- **`combine`** defines how two values are merged. The function must be associative, meaning that `combine(a, combine(b, c))` should be equivalent to `combine(combine(a, b), c)`. For example, in a speed multiplier monoid:
```lua
combine = function(a, b) return a * b end
```
- **`fold`** combines multiple values at once by applying `combine` iteratively. It processes a table of values and merges them into one:
```lua
fold = function(t)
local result = 1
for _, v in pairs(t) do result = result * v end
return result
end
```
- **`identity`** is the neutral default value that will be used when there are no status effects active for a particular monoid. When combined with any other value, it leaves it unchanged. For example:
- Speed multipliers: `identity = 1.0`
- Additive bonuses: `identity = 0`
- **`apply`** translates the combined monoid value into actual effects on the player's state:
```lua
apply = function(multiplier, player)
player:set_physics_override({speed = multiplier})
end
```
- **`on_change`** is an optional callback triggered whenever the monoid's value changes for a player:
```lua
on_change = function(old_val, new_val, player, branch)
local branch_name = branch:get_name()
core.log("Speed changed from " .. old_val .. " to " .. new_val .. " on branch " .. branch_name)
end
```
- **`listen_to_all_changes`**, when set to `true`, ensures that `on_change` is triggered for all branch updates instead of just the active branch.
- **`on_branch_created(monoid, player, branch_name)`**: Optional callback, called when a new branch is created.
- **`on_branch_deleted(monoid, player, branch_name)`**: Optional callback, called when a branch is deleted.
---
## Branch System
Branches allow mods to isolate state changes into separate contexts without interfering with each other. Each branch maintains its own set of modifiers and can be activated independently.
By default, every player starts on the `"main"` branch. This branch represents their normal state and is created automatically when a monoid is initialized. Additional branches can be created and accessed in three ways:
- Using `monoid:new_branch(player, name)` to create a new branch without activating it
- Using `monoid:checkout_branch(player, name)` to switch the player's active branch, creating it if needed
- Using `monoid:get_branch(name)` to get a wrapper for managing the branch at any time, or false if it doesn't exist
When switching branches with `checkout_branch`, the player's state is immediately updated to reflect the combined value of the new active branch.
The inactive branches can still be modified in the background, but their combined values won't affect the player's state until they get activated.
---
## API Reference
#### `player_monoids.make_monoid(monoid_def)`
The `monoid` object mentioned in this API's methods has to first be created using this function. `monoid_def` is a table defining the monoids behavior (see [Definition Structure](#definition-structure)).
---
#### `monoid:add_change(player, value[, id, branch_name])`
Applies a change represented by `value` to the player. Takes a `player` object, a `value` parameter that must be valid for this monoid, an optional *branch-unique* string `id` (if not provided, a random one will be generated), and an optional `branch_name` parameter (if not provided, the `"main"` branch will be used). Returns the ID of the added change.
---
#### `monoid:del_change(player, id[, branch_name])`
Removes the change represented by `id` from the player. If `branch_name` is not provided, the `"main"` branch will be used.
---
#### `monoid:value(player[, branch_name])`
Gets the value of this monoid for a specific player and branch. Takes a player object and an optional `branch_name` parameter. If `branch_name` is not provided, the **active branch** will be used. Returns the combined value for this monoid.
---
#### `monoid:new_branch(player, branch_name)`
Creates a new branch for a player, but does not switch to it. Returns the handler object for the new branch.
- The returned handler provides methods for managing changes specific to that branch:
- **`add_change(player, value[, id])`**: adds a change to this specific branch.
- **`del_change(player, id)`**: removes a change from this specific branch by its ID.
- **`value(player)`**: gets this branchs current combined value for a specific player.
- **`get_name()`**: retrieves this branchs name as a string.
- **`reset(player)`**: clears all changes on this branch for the specified player.
- **`delete(player)`**: deletes this branch for the specified player. If the deleted branch is the active branch, the active branch will be switched to `"main"`. You can't delete the main branch.
---
#### `mononid:checkout_branch(player, name)`
Switches the player's active branch to the specified one, creating it if it doesn't exist. Returns the handler object for the new branch.
---
#### `monoid:get_active_branch(player)`
Gets a handler object representing the player's currently active branch.
---
#### `monoid:get_branch(name)`
Retrieves a handler object for the specified branch. Returns `false` if the branch does not exist.
---
#### `monoid:get_branches(player)`
Returns a table of branch wrappers, keyed by branch name, for all branches associated with the player.
---
#### `monoid:reset_branch(player[, branch_name])`
Clears all changes associated with a player's branch. If no branch name is provided, it resets `"main"` by default.

274
README.md
View File

@ -1,38 +1,26 @@
# Player Monoids
# Player Monoids Library
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.
This is a small library for managing global player state in Luanti, ensuring that multiple mods can modify player attributes without conflicts. The README provides an overview of the mod's purpose and functionality. For a detailed breakdown of available functions and usage, refer to **API.md**.
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.
This mod introduces **monoids**, which represent specific aspects of player state, such as speed modifiers, jump height, or even custom attributes like corruption levels or reputation systems. Monoids allow multiple mods to apply effects in a structured manner, preventing unintended overrides.
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.
Additionally, the mod now includes **branches**, which allow different states to exist independently. This is useful for features like minigames, temporary effects, or alternate player states that should not interfere with the main game.
Player Monoids manages this by creating layers (monoids) on top of player state,
which can keep track of different changes and combine them usefully.
## Global Player State
Monoids
=======
Player state consists of various properties such as physics overrides, privileges, and other custom attributes. These properties are often modified by different mods, leading to unintended side effects. For example, a mod that grants a temporary speed boost might be overridden when another mod resets the players movement speed, inadvertently removing the boost.
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:
```
For example, a player could be under a speed boost effect from a `playereffects` mod and then sleep in a bed. If the bed mod resets the players speed, it might remove the boost entirely. Without a structured approach, the interaction between these mods can be unpredictable, potentially leading to exploits such as permanent speed boosts.
Player Monoids prevents this issue by allowing changes to be layered and combined correctly using monoids and branch-based state management.
## Monoids
### Creation
A monoid in Player Monoids is an abstraction over a specific piece of player state. Examples include physics overrides (like speed and gravity), privilege toggles (fly, noclip), and custom attributes (e.g., status effects, corruption levels). You define a monoid like this:
```lua
-- The values in my speed monoid must be speed multipliers (numbers).
mymod.speed_monoid = player_monoids.make_monoid({
combine = function(speed1, speed2)
@ -47,161 +35,133 @@ mymod.speed_monoid = player_monoids.make_monoid({
end,
identity = 1,
apply = function(speed, player)
local override = player:get_physics_override()
override.speed = speed
player:set_physics_override(override)
player:set_physics_override({ speed = speed })
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.
This defines how speed multipliers combine, the identity value (`1`, meaning no change), and how the monoid applies its effects to the player.
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)
### Use
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")
You modify player state using the `add_change` and `del_change` methods:
minetest.after(5,function() mymod.speed_monoid:del_change(some_player, "mymod:zoom") end)
```lua
-- Increase player speed temporarily
local zoom_id = mymod.speed_monoid:add_change(some_player, 2)
minetest.after(5, function() mymod.speed_monoid:del_change(some_player, zoom_id) 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.
You can also specify a custom string identifier:
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:
```lua
-- Speed boost with named identifier
mymod.speed_monoid:add_change(some_player, 2, "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 is useful when the monoid represents a derived attribute rather than a direct player state value.
### Branch System
Branches allow state changes to be contained within separate contexts, preventing interference between unrelated modifications. Every player starts in the `"main"` branch, but additional branches can be created and managed separately.
For example:
```lua
local speed_branch = mymod.speed_monoid:new_branch(some_player, "minigame")
speed_branch:add_change(some_player, 2)
```
When switching branches, the new branchs state is immediately applied, while the previous one is preserved but inactive:
```lua
mymod.speed_monoid:checkout_branch(some_player, "minigame")
```
To return to the normal game state:
```lua
mymod.speed_monoid:checkout_branch(some_player, "main")
```
### 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 to 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.
If you want to manage subsets of a monoid's values separately, you can create a nested monoid that modifies only a portion of the state while keeping compatibility with the parent monoid.
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:
```lua
-- 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 math.max(speed1, speed2)
end,
fold = function(tab)
local res = 1
for _, speed in pairs(tab) do
res = math.max(res, speed)
end
return res
end,
identity = 1,
apply = ???
on_change = function() return end,
combine = function(speed1, speed2)
return math.max(speed1, speed2)
end,
fold = function(tab)
local res = 1
for _, speed in pairs(tab) do
res = math.max(res, speed)
end
return res
end,
identity = 1,
apply = function(speed, player)
mymod.speed_monoid:add_change(player, speed, "newmod:speed_boosts")
end,
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.
```
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.
However, we cannot just change the player speed directly 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.
```lua
apply = function(speed, player)
mymod.speed_monoid:add_change(player, speed, "newmod:speed_boosts")
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.
This ensures that our boost calculation stays separate while still being compatible with other modifications. You could also introduce another nested monoid for handling slow effects, ensuring only the most significant reduction takes effect. 
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```
## Predefined monoids
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```
### Physics Overrides
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.
These monoids modify physics properties using multipliers:
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
- `player_monoids.speed`
- `player_monoids.jump`
- `player_monoids.gravity`
local function apply(player)
speed:add_change(player, 2, "mymod:2x_speed")
end
### Privileges
local function cancel(player)
speed:del_change(player, "mymod:2x_speed")
end
These monoids toggle player privileges, using boolean logic:
local groups = { "mymod:2x_speed" }
- `player_monoids.fly`
- `player_monoids.noclip`
playereffects.register_effect_type("mymod:2x_speed", "2x Speed", groups, apply, cancel)
```
### Other
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.
- `player_monoids.collisionbox` - Adjusts the players collision box with component-wise multiplication.
- `player_monoids.visual_size` - Modifies the players visual size as a 2D multiplier vector.
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.
## 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.
- Mods should account for the fact that the active branch may change at any time - they should not assume that their effects will always be applied to the player.
- If a mod wants to make sure to always be working with the main branch values, it should be doing that through the optional branch_name parameter in the monoid functions (such as `monoid:value(player, "main")`, and/or by implementing branch checks in `on_change()`).
---
For more details, including function signatures and advanced usage, refer to **API.md**.

379
init.lua
View File

@ -1,118 +1,337 @@
-- 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.
-- def: The monoid definition.
-- player_map: A map from player names to their branch maps. Branch maps
-- contain branches, and each branch holds an 'effects' table.
-- value_cache: A map from player names to the cached value for the monoid.
-- next_id: The next unique ID to assign an effect.
--[[
In def, you can optionally define:
- apply(new_value, player)
- on_change(old_value, new_value, player, branch)
- listen_to_all_changes (bool)
- on_branch_created(monoid, player, branch_name)
- on_branch_deleted(monoid, player, branch_name)
These hooks allow you to respond to monoid changes, branch creation, and branch deletion.
]]
local function monoid(def)
local mon = {}
local mon = {}
local actual_def = {}
-- Clone the definition to avoid mutating the original
local actual_def = {}
for k, v in pairs(def) do
actual_def[k] = v
end
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
if not actual_def.on_branch_created then
actual_def.on_branch_created = nop
end
if not actual_def.on_branch_deleted then
actual_def.on_branch_deleted = nop
end
if actual_def.listen_to_all_changes == nil then
actual_def.listen_to_all_changes = false
end
if not actual_def.apply then
actual_def.apply = nop
end
mon.def = actual_def
if not actual_def.on_change then
actual_def.on_change = nop
end
mon.player_map = {} -- p_name -> { active_branch="main", branches={ branch_name={ effects={}, value=...} } }
mon.value_cache = {} -- p_name -> numeric or table
mon.next_id = 1
mon.def = actual_def
setmetatable(mon, mon_meta)
local p_map = {}
mon.player_map = p_map
-- Clear out data when player leaves
minetest.register_on_leaveplayer(function(player)
local p_name = player:get_player_name()
mon.player_map[p_name] = nil
mon.value_cache[p_name] = nil
end)
mon.next_id = 1
-- Initialize branches for the monoid
function mon:init_branches(player_name)
self.player_map[player_name] = {
active_branch = "main",
branches = {
main = {
effects = {},
value = def.identity
}
}
}
end
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
return mon
end
player_monoids.make_monoid = monoid
function mon_meta:add_change(player, value, 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
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
local function init_player_branches_if_missing(self, p_name)
if not self.player_map[p_name] then
self:init_branches(p_name)
end
end
function mon_meta:del_change(player, id)
local p_name = player:get_player_name()
-- Create or return existing branch. If a new one is created, fire on_branch_created.
local function get_or_create_branch_data(self, p_name, branch_name)
local branches = self.player_map[p_name].branches
local existing_branch = branches[branch_name]
local def = self.def
if not existing_branch then
branches[branch_name] = {
effects = {},
value = self.def.identity
}
local p_effects = self.player_map[p_name]
if p_effects == nil then return end
existing_branch = branches[branch_name]
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
local player = minetest.get_player_by_name(p_name)
if player then
self.def.on_branch_created(self, player, branch_name)
end
end
def.apply(new_total, player)
def.on_change(old_total, new_total, player)
return existing_branch
end
function mon_meta:value(player)
local p_name = player:get_player_name()
return self.value_cache[p_name] or self.def.identity
-- decide if to call on_change for this change based on listen_to_all_changes
function mon_meta:call_on_change(old_value, new_value, player, branch_name)
local p_name = player:get_player_name()
if self.def.listen_to_all_changes or (self.player_map[p_name].active_branch == branch_name) then
self.def.on_change(old_value, new_value, player, self:get_branch(branch_name))
end
end
function mon_meta:add_change(player, value, id, branch_name)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
local branch = branch_name or "main"
local p_branch_data = get_or_create_branch_data(self, p_name, branch)
local p_effects = p_branch_data.effects
local actual_id = id or self.next_id
if not id then
self.next_id = actual_id + 1
end
local old_total = p_branch_data.value
p_effects[actual_id] = value
local new_total = self.def.fold(p_effects)
p_branch_data.value = new_total
if self.player_map[p_name].active_branch == branch then
self.def.apply(new_total, player)
end
self:call_on_change(old_total, new_total, player, branch)
return actual_id
end
function mon_meta:del_change(player, id, branch_name)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
local branch = branch_name or "main"
local p_branch_data = get_or_create_branch_data(self, p_name, branch)
if not p_branch_data then return end
local p_effects = p_branch_data.effects
local old_total = p_branch_data.value
p_effects[id] = nil
local new_total = self.def.fold(p_effects)
p_branch_data.value = new_total
if self.player_map[p_name].active_branch == branch then
self.def.apply(new_total, player)
end
self:call_on_change(old_total, new_total, player, branch)
end
function mon_meta:reset_branch(player, branch_name)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
local branch = branch_name or "main"
local bdata = self.player_map[p_name].branches[branch]
if not bdata then
return -- Branch doesn't exist, nothing to reset
end
local old_total = bdata.value
-- Clear effects and recalc
bdata.effects = {}
local new_total = self.def.fold({})
bdata.value = new_total
-- Update active branch
local active_branch = self.player_map[p_name].active_branch or "main"
local active_branch_data = self.player_map[p_name].branches[active_branch]
local active_branch = self.player_map[p_name].active_branch or "main"
local active_branch_data = self.player_map[p_name].branches[active_branch]
self.value_cache[p_name] = active_branch_data.value
self.def.apply(active_branch_data.value, player)
-- Fire on_change for the branch being reset
self:call_on_change(old_total, new_total, player, branch)
end
-- new method: create a branch for a player, but do NOT check it out
function mon_meta:new_branch(player, branch_name)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
get_or_create_branch_data(self, p_name, branch_name)
return self:get_branch(branch_name)
end
function mon_meta:get_branch(branch_name)
if not branch_name then
return false
end
local monoid = self
return {
add_change = function(_, player, value, id)
return monoid:add_change(player, value, id, branch_name)
end,
del_change = function(_, player, id)
return monoid:del_change(player, id, branch_name)
end,
value = function(_, player)
return monoid:value(player, branch_name)
end,
reset = function(_, player)
return monoid:reset_branch(player, branch_name)
end,
get_name = function(_)
return branch_name
end,
delete = function(_, player)
local p_name = player:get_player_name()
init_player_branches_if_missing(monoid, p_name)
local player_data = monoid.player_map[p_name]
if not player_data then
return
end
local existing_branch = player_data.branches[branch_name]
if not existing_branch or branch_name == "main" then
return
end
-- If it's the active branch, switch to main
if player_data.active_branch == branch_name then
player_data.active_branch = "main"
local new_main_total = monoid:value(player, "main")
monoid.value_cache[p_name] = new_main_total
monoid.def.apply(new_main_total, player)
end
-- Remove the branch
player_data.branches[branch_name] = nil
monoid.def.on_branch_deleted(monoid, player, branch_name)
end,
}
end
function mon_meta:get_active_branch(player)
local p_name = player:get_player_name()
local active = self.player_map[p_name] and self.player_map[p_name].active_branch or "main"
return self:get_branch(active)
end
function mon_meta:get_branches(player)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
local branch_map = self.player_map[p_name].branches or {}
local result = {}
for b_name, _ in pairs(branch_map) do
result[b_name] = self:get_branch(b_name)
end
return result
end
function mon_meta:delete_branch(player, branch_name)
local b = self:get_branch(branch_name)
if not b then
return false
end
b:delete(player)
end
minetest.register_on_joinplayer(function(player)
for _, monoid_instance in pairs(player_monoids) do
if type(monoid_instance) == "table" and monoid_instance.init_branches then
monoid_instance:init_branches(player:get_player_name())
end
end
end)
function mon_meta:value(player, branch_name)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
local chosen_branch = branch_name or self.player_map[p_name].active_branch or "main"
local p_data = self.player_map[p_name]
local bdata = p_data.branches[chosen_branch]
if not bdata then
return self.def.identity
end
local calculated_value = self.def.fold(bdata.effects)
return calculated_value
end
function mon_meta:checkout_branch(player, branch_name)
local p_name = player:get_player_name()
init_player_branches_if_missing(self, p_name)
local old_total = self.value_cache[p_name] or self.def.identity
local checkout_branch = self:new_branch(player, branch_name)
self.player_map[p_name].active_branch = branch_name
local new_total = self:value(player)
self.value_cache[p_name] = new_total
self:call_on_change(old_total, new_total, player, branch_name)
self.def.apply(new_total, player)
return checkout_branch
end
-- Finally, load the additional files
dofile(modpath .. "standard_monoids.lua")
dofile(modpath .. "test.lua")

View File

@ -98,7 +98,6 @@ player_monoids.fly = monoid({
end
minetest.set_player_privs(p_name, privs)
end,
})
@ -124,7 +123,6 @@ player_monoids.noclip = monoid({
end
minetest.set_player_privs(p_name, privs)
end,
})

642
test.lua
View File

@ -1,30 +1,636 @@
-- monoids.lua
-- This file defines a set of testing chatcommands for the monoids system.
local speed = player_monoids.speed
local jump = player_monoids.jump
minetest.register_privilege("monoid_master", {
description = "Allows testing of player monoids.",
give_to_singleplayer = false,
give_to_admin = true,
description = "Allows testing of player monoids.",
give_to_singleplayer = false,
give_to_admin = true,
})
local function test(player)
local ch_id = speed:add_change(player, 10)
local p_name = player:get_player_name()
--------------------------------------------------------------------------------
-- Helper: reset branches for both speed and jump
--------------------------------------------------------------------------------
local function reset_all_monoid_branches(player)
local p_name = player:get_player_name()
minetest.chat_send_player(p_name, "Your speed is: " .. speed:value(player))
for _, monoid in ipairs({speed, jump}) do
if monoid and monoid.player_map and monoid.player_map[p_name] then
local pm = monoid.player_map[p_name]
local to_delete = {}
for bn, _ in pairs(pm.branches) do
table.insert(to_delete, bn)
end
for _, bn in ipairs(to_delete) do
local br = monoid:get_branch(bn)
if br then
br:reset(player)
br:delete(player)
end
end
monoid:get_branch("main")
monoid:checkout_branch(player, "main")
end
end
end
minetest.after(3, function()
local player = minetest.get_player_by_name(p_name)
if not player then return end
speed:del_change(player, ch_id)
minetest.chat_send_player(p_name, "Your speed is: " .. speed:value(player))
end)
--------------------------------------------------------------------------------
-- 1) Test Speed Add/Remove
--------------------------------------------------------------------------------
local function test_speed_add_remove(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local before = speed:value(player)
local ch_id = speed:add_change(player, 10)
local after_add = speed:value(player)
if after_add == before then
minetest.chat_send_player(p_name, "[Add/Remove] FAIL: no speed change.")
else
minetest.chat_send_player(p_name, "[Add/Remove] PASS: speed " .. before .. " -> " .. after_add)
end
minetest.after(2, function()
local again = minetest.get_player_by_name(p_name)
if not again then return end
speed:del_change(again, ch_id)
local after_del = speed:value(again)
if math.abs(after_del - before) < 0.0001 then
minetest.chat_send_player(p_name, "[Add/Remove] PASS: speed returned to " .. before)
else
minetest.chat_send_player(p_name, "[Add/Remove] FAIL: final " .. after_del .. " != initial " .. before)
end
end)
end
--------------------------------------------------------------------------------
-- 2) Test Branch Isolation
--------------------------------------------------------------------------------
local function test_branch_isolation(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local init = speed:value(player)
speed:checkout_branch(player, "arena")
speed:add_change(player, 0.5, "arena_slowdown", "arena")
local arena_spd = speed:value(player)
if arena_spd >= init then
minetest.chat_send_player(p_name, "[BranchIsolation] FAIL: arena slowdown not effective.")
else
minetest.chat_send_player(p_name, "[BranchIsolation] PASS: arena slow " .. init .. " -> " .. arena_spd)
end
minetest.after(2, function()
speed:checkout_branch(player, "main")
speed:add_change(player, 2, "speed_boost")
local main_spd = speed:value(player)
if main_spd <= init then
minetest.chat_send_player(p_name, "[BranchIsolation] FAIL: main speedup not effective.")
else
minetest.chat_send_player(p_name, "[BranchIsolation] PASS: main speed " .. init .. " -> " .. main_spd)
end
end)
minetest.after(4, function()
speed:checkout_branch(player, "arena")
local arena2 = speed:value(player)
minetest.chat_send_player(p_name, "[BranchIsolation] re-check => " .. arena2)
end)
end
--------------------------------------------------------------------------------
-- 3) Test Branch Concurrent
--------------------------------------------------------------------------------
local function test_branch_concurrent(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local init = speed:value(player)
local arena_branch = speed:checkout_branch(player, "arena")
arena_branch:add_change(player, 0.5, "arena_slowdown")
local arena_spd = speed:value(player)
if arena_spd >= init then
minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: arena slowdown didn't reduce speed.")
else
minetest.chat_send_player(p_name, "[BranchConcurrent] PASS: arena from " .. init .. " -> " .. arena_spd)
end
minetest.after(2, function()
local mining_branch = speed:checkout_branch(player, "mining")
if not mining_branch then
minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: 'mining' branch could not be created.")
return
end
mining_branch:add_change(player, 0.3, "mining_slowdown")
local main_branch = speed:get_branch("main")
if not main_branch then
minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: 'main' branch doesn't exist?")
return
end
main_branch:add_change(player, 2, "main_speedup_concurrent")
local mining_spd = speed:value(player)
local main_spd = main_branch:value(player)
if mining_spd >= init then
minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: mining slowdown not effective.")
else
minetest.chat_send_player(p_name, "[BranchConcurrent] PASS: mining slow => " .. mining_spd)
end
if main_spd <= init then
minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: main concurrent speedup not effective.")
else
minetest.chat_send_player(p_name, "[BranchConcurrent] PASS: main speed => " .. main_spd)
end
end)
minetest.after(4, function()
local arena_b = speed:get_branch("arena")
if arena_b then
arena_b:reset(player)
speed:checkout_branch(player, "arena")
local reset_spd = speed:value(player)
minetest.chat_send_player(p_name, "[BranchConcurrent] arena reset => " .. reset_spd)
else
minetest.chat_send_player(p_name, "[BranchConcurrent] FAIL: 'arena' branch not found?")
end
end)
minetest.after(6, function()
speed:checkout_branch(player, "main")
local main_spd = speed:value(player)
minetest.chat_send_player(p_name, "[BranchConcurrent] final main => " .. main_spd)
end)
end
--------------------------------------------------------------------------------
-- 4) Test OnChange ListenAll
--------------------------------------------------------------------------------
local function test_onchange_listen_all(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local call_count = 0
speed.def.listen_to_all_changes = true
local old_on_change = speed.def.on_change
speed.def.on_change = function(old, new, plyr, branch)
call_count = call_count + 1
end
speed:add_change(player, 1, "active_change")
speed:add_change(player, 0.5, "arena_slowdown", "arena")
minetest.after(2, function()
speed.def.on_change = old_on_change
speed.def.listen_to_all_changes = false
if call_count == 0 then
minetest.chat_send_player(p_name, "[OnChangeAll] FAIL: on_change not triggered.")
else
minetest.chat_send_player(p_name, "[OnChangeAll] PASS: on_change called " .. call_count .. " times.")
end
end)
end
minetest.register_chatcommand("test_listen_all", {
description = "Test on_change across all branches.",
privs = {monoid_master = true},
func = function(p_name)
local player = minetest.get_player_by_name(p_name)
if player then
test_onchange_listen_all(player)
end
end,
})
--------------------------------------------------------------------------------
-- 5) Test OnChange ListenActive
--------------------------------------------------------------------------------
local function test_onchange_listen_active(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local call_count = 0
speed.def.listen_to_all_changes = false
local old_on_change = speed.def.on_change
speed.def.on_change = function(old, new, plyr, branch)
call_count = call_count + 1
end
speed:add_change(player, 1, "active_change")
speed:add_change(player, 0.5, "arena_slowdown", "arena")
minetest.after(2, function()
speed.def.on_change = old_on_change
speed.def.listen_to_all_changes = false
if call_count == 0 then
minetest.chat_send_player(p_name, "[OnChangeActive] FAIL: on_change not called.")
else
minetest.chat_send_player(p_name, "[OnChangeActive] PASS: on_change triggered " .. call_count .. " times.")
end
end)
end
minetest.register_chatcommand("test_listen_active", {
description = "Test on_change only in active branch.",
privs = {monoid_master = true},
func = function(p_name)
local player = minetest.get_player_by_name(p_name)
if player then
test_onchange_listen_active(player)
end
end,
})
--------------------------------------------------------------------------------
-- 6) Test BranchName
--------------------------------------------------------------------------------
local function test_branch_name_check(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
speed:checkout_branch(player, "arena")
local branch = speed:get_branch("arena")
if not branch then
minetest.chat_send_player(p_name, "[BranchNameCheck] FAIL: get_branch('arena') is nil?")
return
end
local got = branch:get_name()
if got == "arena" then
minetest.chat_send_player(p_name, "[BranchNameCheck] PASS: got 'arena'")
else
minetest.chat_send_player(p_name, "[BranchNameCheck] FAIL: expected 'arena', got '" .. (got or "nil") .. "'")
end
end
minetest.register_chatcommand("test_branch_name", {
description = "Test branch:get_name() method.",
privs = {monoid_master = true},
func = function(p_name)
local player = minetest.get_player_by_name(p_name)
if player then
test_branch_name_check(player)
end
end,
})
--------------------------------------------------------------------------------
-- 7) Test ActiveBranchGet
--------------------------------------------------------------------------------
local function test_active_branch_check(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
speed:checkout_branch(player, "arena")
local active = speed:get_active_branch(player)
local got = active and active:get_name() or "(nil)"
if got == "arena" then
minetest.chat_send_player(p_name, "[ActiveBranchGet] PASS: 'arena' active")
else
minetest.chat_send_player(p_name, "[ActiveBranchGet] FAIL: expected 'arena', got '" .. got .. "'")
end
end
minetest.register_chatcommand("test_active_branch", {
description = "Test monoid:get_active_branch.",
privs = {monoid_master = true},
func = function(p_name)
local player = minetest.get_player_by_name(p_name)
if player then
test_active_branch_check(player)
end
end,
})
--------------------------------------------------------------------------------
-- 8) Test BranchDelete
--------------------------------------------------------------------------------
local function test_branch_delete_check(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local main_branch = speed:get_branch("main")
local before_main = main_branch:value(player)
local del_branch = speed:checkout_branch(player, "delete_test")
del_branch:add_change(player, 0.2, "delete_test_slowdown")
local delete_spd = speed:value(player)
if delete_spd == before_main then
minetest.chat_send_player(p_name, "[BranchDelete] FAIL: no slowdown?")
else
minetest.chat_send_player(p_name, "[BranchDelete] PASS: speed from " .. before_main .. " to " .. delete_spd)
end
minetest.after(2, function()
del_branch:delete(player)
if speed.player_map[p_name].branches["delete_test"] then
minetest.chat_send_player(p_name, "[BranchDelete] FAIL: branch still exists.")
else
local active_b = speed.player_map[p_name].active_branch
local after_main = speed:value(player)
if active_b ~= "main" then
minetest.chat_send_player(p_name, "[BranchDelete] FAIL: active branch is " .. active_b)
elseif math.abs(after_main - before_main) > 0.0001 then
minetest.chat_send_player(p_name, "[BranchDelete] FAIL: main speed not restored.")
else
minetest.chat_send_player(p_name, "[BranchDelete] PASS: 'delete_test' gone, main restored.")
end
end
end)
end
minetest.register_chatcommand("test_branch_delete_monoids", {
description = "Runs a test on monoids branch deletion.",
privs = {monoid_master = true},
func = function(p_name)
local player = minetest.get_player_by_name(p_name)
if player then
test_branch_delete_check(player)
end
end,
})
--------------------------------------------------------------------------------
-- 9) Test GetBranches
--------------------------------------------------------------------------------
local function test_get_branches(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local br_a = speed:checkout_branch(player, "testA")
if br_a then
br_a:add_change(player, 2, "testA_boost")
else
minetest.chat_send_player(p_name, "[GetBranches] FAIL: unable to create 'testA'.")
end
local br_b = speed:new_branch(player, "testB")
if br_b then
br_b:add_change(player, 0.3, "testB_slow")
else
minetest.chat_send_player(p_name, "[GetBranches] FAIL: new_branch('testB') returned nil.")
end
local br_map = speed:get_branches(player)
if br_map["testA"] and br_map["testB"] then
minetest.chat_send_player(p_name, "[GetBranches] PASS: 'testA' and 'testB' found.")
else
minetest.chat_send_player(p_name, "[GetBranches] FAIL: missing 'testA' or 'testB' in get_branches.")
end
end
--------------------------------------------------------------------------------
-- 10) Test on_branch_created / on_branch_deleted
--------------------------------------------------------------------------------
local function test_on_branch_create_delete(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local created_count = 0
local deleted_count = 0
local old_on_branch_created = speed.def.on_branch_created
local old_on_branch_deleted = speed.def.on_branch_deleted
speed.def.on_branch_created = function(monoid, plyr, branch_name)
created_count = created_count + 1
end
speed.def.on_branch_deleted = function(monoid, plyr, branch_name)
deleted_count = deleted_count + 1
end
local new_branch = speed:checkout_branch(player, "my_new_branch")
if not new_branch then
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: checkout_branch returned nil.")
else
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] Created 'my_new_branch'.")
end
local del_branch = speed:checkout_branch(player, "my_del_branch")
if not del_branch then
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: couldn't create 'my_del_branch'.")
else
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] Created 'my_del_branch'. Deleting...")
del_branch:delete(player)
end
if created_count == 0 then
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: on_branch_created not called.")
else
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] PASS: on_branch_created called " .. created_count .. " time(s).")
end
if deleted_count == 0 then
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: on_branch_deleted not called.")
else
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] PASS: on_branch_deleted called " .. deleted_count .. " time(s).")
end
speed.def.on_branch_created = old_on_branch_created
speed.def.on_branch_deleted = old_on_branch_deleted
end
--------------------------------------------------------------------------------
-- 11) Test monoid:new_branch
--------------------------------------------------------------------------------
local function test_new_branch_method(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local init_speed = speed:value(player)
local custom_branch = speed:new_branch(player, "custom_new_branch")
if not custom_branch then
minetest.chat_send_player(p_name, "[NewBranchMethod] FAIL: new_branch returned nil")
return
end
custom_branch:add_change(player, 0.4, "custom_slow")
local after_new = speed:value(player)
if math.abs(after_new - init_speed) > 0.0001 then
minetest.chat_send_player(p_name, "[NewBranchMethod] FAIL: Speed changed even though new branch not active.")
else
minetest.chat_send_player(p_name, "[NewBranchMethod] PASS: Speed unchanged = " .. after_new)
end
speed:checkout_branch(player, "custom_new_branch")
local after_checkout = speed:value(player)
if after_checkout == init_speed then
minetest.chat_send_player(p_name, "[NewBranchMethod] FAIL: Speed not changed after activation.")
else
minetest.chat_send_player(p_name, "[NewBranchMethod] PASS: Speed changed from " .. init_speed .. " to " .. after_checkout)
end
end
--------------------------------------------------------------------------------
-- 12) Test main branch cannot be deleted
--------------------------------------------------------------------------------
local function test_main_branch_cant_delete(player)
local p_name = player:get_player_name()
reset_all_monoid_branches(player)
local main_br = speed:get_branch("main")
if not main_br then
minetest.chat_send_player(p_name, "[MainBranchCantDelete] FAIL: main branch does not exist?")
return
end
main_br:delete(player)
local still_main = speed.player_map[p_name] and speed.player_map[p_name].branches["main"]
if still_main then
minetest.chat_send_player(p_name, "[MainBranchCantDelete] PASS: main not deleted.")
else
minetest.chat_send_player(p_name, "[MainBranchCantDelete] FAIL: main branch was deleted!")
end
end
--------------------------------------------------------------------------------
-- 13) Test using speed + jump together in the same branch
--------------------------------------------------------------------------------
local function test_speed_and_jump_together(player)
local p_name = player:get_player_name()
if not jump then
minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: 'player_monoids.jump' not defined!")
return
end
reset_all_monoid_branches(player)
-- Grab initial speed + jump
local init_speed = speed:value(player)
local init_jump = jump:value(player)
-- Create or checkout a test branch that affects both
speed:checkout_branch(player, "double_test")
jump:checkout_branch(player, "double_test")
local sp_ch_id = speed:add_change(player, 2, "double_spd", "double_test")
local jp_ch_id = jump:add_change(player, 1.5, "double_jmp", "double_test")
local after_speed = speed:value(player)
local after_jump = jump:value(player)
local speed_changed = after_speed ~= init_speed
local jump_changed = after_jump ~= init_jump
if speed_changed and jump_changed then
minetest.chat_send_player(p_name, "[SpeedJumpTogether] PASS: Speed changed " .. init_speed .. "->" .. after_speed .. ", Jump changed " .. init_jump .. "->" .. after_jump)
else
minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: Speed changed=" .. tostring(speed_changed) .. ", Jump changed=" .. tostring(jump_changed))
end
-- Remove only speed change, see if jump remains.
speed:del_change(player, sp_ch_id, "double_test")
local sp2 = speed:value(player)
local jp2 = jump:value(player)
if math.abs(sp2 - init_speed) < 0.0001 and math.abs(jp2 - after_jump) < 0.0001 then
minetest.chat_send_player(p_name, "[SpeedJumpTogether] PASS: Speed reverted to " .. init_speed .. ", jump remains " .. jp2)
else
minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: partial revert mismatch. Speed=" .. sp2 .. ", jump=" .. jp2)
end
-- Remove jump change, confirm both are back to init
jump:del_change(player, jp_ch_id, "double_test")
local sp3 = speed:value(player)
local jp3 = jump:value(player)
if math.abs(sp3 - init_speed) < 0.0001 and math.abs(jp3 - init_jump) < 0.0001 then
minetest.chat_send_player(p_name, "[SpeedJumpTogether] PASS: both speed/jump back to init.")
else
minetest.chat_send_player(p_name, "[SpeedJumpTogether] FAIL: final mismatch. Speed=" .. sp3 .. ", jump=" .. jp3)
end
end
--------------------------------------------------------------------------------
-- 14) Test monoid:value() with or without a branch param
-- to confirm it uses the active branch if omitted.
--------------------------------------------------------------------------------
local function test_value_api(player)
reset_all_monoid_branches(player)
local p_name = player:get_player_name()
local init_val = speed:value(player) -- active=main
-- new branch and add a slowdown
local br_test = speed:new_branch(player, "value_api_test")
br_test:add_change(player, 0.5, "slower")
-- the newly created branch is not active yet, so speed:value(player) remains init_val
local current_active_val = speed:value(player)
local named_branch_val = speed:value(player, "value_api_test")
if math.abs(current_active_val - init_val) < 0.0001 and named_branch_val < init_val then
minetest.chat_send_player(p_name, "[ValueAPI] PASS: value(player) used 'main'; value(player,'value_api_test') reflected slowdown.")
else
minetest.chat_send_player(p_name, "[ValueAPI] FAIL: mismatch. active=" .. current_active_val .. ", named=" .. named_branch_val .. ", init=" .. init_val)
return
end
-- now switch to that branch, confirm speed:value() uses that branch by default
speed:checkout_branch(player, "value_api_test")
local active_switched_val = speed:value(player)
if math.abs(active_switched_val - named_branch_val) < 0.0001 then
minetest.chat_send_player(p_name, "[ValueAPI] PASS: after checkout, value(player) matches 'value_api_test' branch value.")
else
minetest.chat_send_player(p_name, "[ValueAPI] FAIL: after checkout mismatch. got=" .. active_switched_val .. " vs named=" .. named_branch_val)
end
end
--------------------------------------------------------------------------------
-- /test_monoids runs all tests in sequence
--------------------------------------------------------------------------------
local all_tests = {
{name = "AddRemove", func = test_speed_add_remove, delay = 2},
{name = "BranchIsolation", func = test_branch_isolation, delay = 2},
{name = "BranchConcurrent", func = test_branch_concurrent, delay = 4},
{name = "OnChangeAll", func = test_onchange_listen_all, delay = 2},
{name = "OnChangeActive", func = test_onchange_listen_active, delay = 2},
{name = "BranchNameCheck", func = test_branch_name_check, delay = 1},
{name = "ActiveBranchGet", func = test_active_branch_check, delay = 1},
{name = "BranchDelete", func = test_branch_delete_check, delay = 2},
{name = "GetBranches", func = test_get_branches, delay = 1},
{name = "OnBranchCreateDelete", func = test_on_branch_create_delete, delay = 2},
{name = "NewBranchMethod", func = test_new_branch_method, delay = 2},
{name = "MainBranchCantDelete", func = test_main_branch_cant_delete, delay = 1},
{name = "SpeedJumpTogether", func = test_speed_and_jump_together, delay = 3},
{name = "ValueAPI", func = test_value_api, delay = 2},
}
local function run_tests_sequentially(player, index)
local p_name = player:get_player_name()
if index > #all_tests then
minetest.chat_send_player(p_name, "All tests completed!")
return
end
local info = all_tests[index]
minetest.chat_send_player(p_name, "\n>>> " .. index .. "/" .. #all_tests .. " Running: " .. info.name .. "...")
info.func(player)
minetest.after(info.delay, function()
local again = minetest.get_player_by_name(p_name)
if not again then return end
run_tests_sequentially(again, index + 1)
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,
description = "Runs ALL monoid tests in sequence.",
privs = {monoid_master = true},
func = function(p_name)
local player = minetest.get_player_by_name(p_name)
if player then
minetest.chat_send_player(p_name, "Starting all monoid tests...")
run_tests_sequentially(player, 1)
end
end,
})