mirror of
https://github.com/minetest-mods/player_monoids.git
synced 2025-02-08 14:20:19 +01:00
parent
8d6f1ade93
commit
df7147c1f8
221
API.md
221
API.md
@ -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
|
## Table of Contents
|
||||||
----------
|
1. [Introduction](#introduction)
|
||||||
A player monoid definition is a table with the following:
|
- 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
|
## Introduction
|
||||||
----------------
|
|
||||||
```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.
|
|
||||||
|
|
||||||
identity
|
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.
|
||||||
--------
|
|
||||||
```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```.
|
|
||||||
|
|
||||||
apply
|
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.
|
||||||
-----
|
|
||||||
```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.
|
|
||||||
|
|
||||||
Functions
|
### Use Cases and Types
|
||||||
=========
|
|
||||||
```player_monoids.make_monoid(monoid_def)``` - Creates a new monoid that can be
|
|
||||||
used to make changes to the player state. Returns a monoid.
|
|
||||||
|
|
||||||
Monoid Methods
|
Monoids are useful for managing both built-in player attributes and custom mod-defined states. For example:
|
||||||
--------------
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```monoid:del_change(player, id)``` - Removes the change with the given ID, from
|
- **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.
|
||||||
the given player, if it exists.
|
- **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
|
Monoids can be categorized based on how they combine values:
|
||||||
given player.
|
- **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 monoid’s 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 branch’s current combined value for a specific player.
|
||||||
|
- **`get_name()`**: retrieves this branch’s 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.
|
||||||
|
240
README.md
240
README.md
@ -1,38 +1,26 @@
|
|||||||
# Player Monoids
|
# Player Monoids Library
|
||||||
|
|
||||||
This is a small library for managing global player state, so that changes made
|
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**.
|
||||||
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
|
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.
|
||||||
===================
|
|
||||||
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
|
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.
|
||||||
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,
|
## Global Player State
|
||||||
which can keep track of different changes and combine them usefully.
|
|
||||||
|
|
||||||
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 player’s movement speed, inadvertently removing the boost.
|
||||||
=======
|
|
||||||
|
|
||||||
Creation
|
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 player’s 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.
|
||||||
--------
|
|
||||||
A monoid in Player Monoids is an interface to one piece of player state. For
|
Player Monoids prevents this issue by allowing changes to be layered and combined correctly using monoids and branch-based state management.
|
||||||
example, you could have one monoid covering physics overrides, and another
|
|
||||||
covering fly privilege. You could define a speed monoids like this:
|
## 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).
|
-- The values in my speed monoid must be speed multipliers (numbers).
|
||||||
mymod.speed_monoid = player_monoids.make_monoid({
|
mymod.speed_monoid = player_monoids.make_monoid({
|
||||||
combine = function(speed1, speed2)
|
combine = function(speed1, speed2)
|
||||||
@ -47,61 +35,68 @@ mymod.speed_monoid = player_monoids.make_monoid({
|
|||||||
end,
|
end,
|
||||||
identity = 1,
|
identity = 1,
|
||||||
apply = function(speed, player)
|
apply = function(speed, player)
|
||||||
local override = player:get_physics_override()
|
player:set_physics_override({ speed = speed })
|
||||||
override.speed = speed
|
|
||||||
player:set_physics_override(override)
|
|
||||||
end,
|
end,
|
||||||
on_change = function() return end,
|
on_change = function() return end,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
This says that two speed multipliers can be combined by multiplication, that
|
This defines how speed multipliers combine, the identity value (`1`, meaning no change), and how the monoid applies its effects to the player.
|
||||||
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
|
### 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)
|
|
||||||
|
|
||||||
|
You modify player state using the `add_change` and `del_change` methods:
|
||||||
|
|
||||||
|
```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)
|
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 can also specify a custom string identifier:
|
||||||
|
|
||||||
|
```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)
|
minetest.after(5, function() mymod.speed_monoid:del_change(some_player, "mymod:zoom") end)
|
||||||
```
|
```
|
||||||
|
|
||||||
Reading Values
|
### 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 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.
|
||||||
---------------
|
|
||||||
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
|
### Branch System
|
||||||
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:
|
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 branch’s 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.
|
-- My speed boosts monoid takes speed multipliers (numbers) that are at least 1.
|
||||||
newmod.speed_boosts = player_monoids.make_monoid({
|
newmod.speed_boosts = player_monoids.make_monoid({
|
||||||
combine = function(speed1, speed2)
|
combine = function(speed1, speed2)
|
||||||
@ -115,93 +110,58 @@ newmod.speed_boosts = player_monoids.make_monoid({
|
|||||||
return res
|
return res
|
||||||
end,
|
end,
|
||||||
identity = 1,
|
identity = 1,
|
||||||
apply = ???
|
apply = function(speed, player)
|
||||||
|
mymod.speed_monoid:add_change(player, speed, "newmod:speed_boosts")
|
||||||
|
end,
|
||||||
on_change = function() return 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
|
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.
|
||||||
the original monoid as a proxy for our effects.
|
|
||||||
```
|
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)
|
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
|
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
|
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. 
|
||||||
================
|
|
||||||
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
|
## Predefined monoids
|
||||||
-----------------
|
|
||||||
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
|
### Physics Overrides
|
||||||
----------
|
|
||||||
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
|
These monoids modify physics properties using multipliers:
|
||||||
-----
|
|
||||||
* ```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.speed`
|
||||||
======================
|
- `player_monoids.jump`
|
||||||
Player Monoids does not provide anything special for persistent effects with
|
- `player_monoids.gravity`
|
||||||
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)
|
### Privileges
|
||||||
speed:add_change(player, 2, "mymod:2x_speed")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function cancel(player)
|
These monoids toggle player privileges, using boolean logic:
|
||||||
speed:del_change(player, "mymod:2x_speed")
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
- `player_monoids.collisionbox` - Adjusts the player’s collision box with component-wise multiplication.
|
||||||
speed effects use the speed monoid, we do not want them to be cancelled, since
|
- `player_monoids.visual_size` - Modifies the player’s visual size as a 2D multiplier vector.
|
||||||
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
|
## Caveats
|
||||||
=======
|
|
||||||
* If the global state managed by a monoid is modified by something other than
|
- 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.
|
||||||
the monoid, you will have the same problem as when two mods both independently
|
- This includes `playereffects` effects that affect global player state without going through a monoid.
|
||||||
try to modify global state without going through a monoid.
|
- You will also get problems if you use multiple monoids to manage the same global state.
|
||||||
* This includes playereffects effects that affect global player state without
|
- 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.
|
||||||
going through a monoid.
|
- 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.
|
||||||
* You will also get problems if you use multiple monoids to manage the same
|
- 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()`).
|
||||||
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
|
For more details, including function signatures and advanced usage, refer to **API.md**.
|
||||||
is swapped.
|
|
321
init.lua
321
init.lua
@ -1,31 +1,36 @@
|
|||||||
-- 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()) .. "/"
|
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
||||||
|
|
||||||
player_monoids = {}
|
player_monoids = {}
|
||||||
|
|
||||||
local mon_meta = {}
|
local mon_meta = {}
|
||||||
|
|
||||||
mon_meta.__index = mon_meta
|
mon_meta.__index = mon_meta
|
||||||
|
|
||||||
local nop = function() end
|
local nop = function() end
|
||||||
|
|
||||||
-- A monoid object is a table with the following fields:
|
-- A monoid object is a table with the following fields:
|
||||||
-- def: The monoid definition
|
-- def: The monoid definition.
|
||||||
-- player_map: A map from player names to their effect tables. Effect tables
|
-- player_map: A map from player names to their branch maps. Branch maps
|
||||||
-- are maps from effect IDs to values.
|
-- contain branches, and each branch holds an 'effects' table.
|
||||||
-- value_cache: A map from player names to the cached value for the monoid.
|
-- value_cache: A map from player names to the cached value for the monoid.
|
||||||
-- next_id: The next unique ID to assign an effect.
|
-- 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 function monoid(def)
|
||||||
local mon = {}
|
local mon = {}
|
||||||
|
|
||||||
|
-- Clone the definition to avoid mutating the original
|
||||||
local actual_def = {}
|
local actual_def = {}
|
||||||
|
|
||||||
for k, v in pairs(def) do
|
for k, v in pairs(def) do
|
||||||
actual_def[k] = v
|
actual_def[k] = v
|
||||||
end
|
end
|
||||||
@ -33,86 +38,300 @@ local function monoid(def)
|
|||||||
if not actual_def.apply then
|
if not actual_def.apply then
|
||||||
actual_def.apply = nop
|
actual_def.apply = nop
|
||||||
end
|
end
|
||||||
|
|
||||||
if not actual_def.on_change then
|
if not actual_def.on_change then
|
||||||
actual_def.on_change = nop
|
actual_def.on_change = nop
|
||||||
end
|
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
|
||||||
|
|
||||||
mon.def = actual_def
|
mon.def = actual_def
|
||||||
|
|
||||||
local p_map = {}
|
mon.player_map = {} -- p_name -> { active_branch="main", branches={ branch_name={ effects={}, value=...} } }
|
||||||
mon.player_map = p_map
|
mon.value_cache = {} -- p_name -> numeric or table
|
||||||
|
|
||||||
mon.next_id = 1
|
mon.next_id = 1
|
||||||
|
|
||||||
local v_cache = {}
|
|
||||||
mon.value_cache = v_cache
|
|
||||||
|
|
||||||
setmetatable(mon, mon_meta)
|
setmetatable(mon, mon_meta)
|
||||||
|
|
||||||
|
-- Clear out data when player leaves
|
||||||
minetest.register_on_leaveplayer(function(player)
|
minetest.register_on_leaveplayer(function(player)
|
||||||
local p_name = player:get_player_name()
|
local p_name = player:get_player_name()
|
||||||
p_map[p_name] = nil
|
mon.player_map[p_name] = nil
|
||||||
v_cache[p_name] = nil
|
mon.value_cache[p_name] = nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
return mon
|
return mon
|
||||||
end
|
end
|
||||||
|
|
||||||
player_monoids.make_monoid = monoid
|
player_monoids.make_monoid = monoid
|
||||||
|
|
||||||
function mon_meta:add_change(player, value, id)
|
local function init_player_branches_if_missing(self, p_name)
|
||||||
local p_name = player:get_player_name()
|
if not self.player_map[p_name] then
|
||||||
|
self:init_branches(p_name)
|
||||||
local def = self.def
|
end
|
||||||
|
|
||||||
local p_effects = self.player_map[p_name]
|
|
||||||
if p_effects == nil then
|
|
||||||
p_effects = {}
|
|
||||||
self.player_map[p_name] = p_effects
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local actual_id
|
-- 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]
|
||||||
|
|
||||||
if id then
|
if not existing_branch then
|
||||||
actual_id = id
|
branches[branch_name] = {
|
||||||
else
|
effects = {},
|
||||||
actual_id = self.next_id
|
value = self.def.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_branch = branches[branch_name]
|
||||||
|
|
||||||
|
local player = minetest.get_player_by_name(p_name)
|
||||||
|
if player then
|
||||||
|
self.def.on_branch_created(self, player, branch_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return existing_branch
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
self.next_id = actual_id + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local old_total = self.value_cache[p_name]
|
local old_total = p_branch_data.value
|
||||||
p_effects[actual_id] = value
|
p_effects[actual_id] = value
|
||||||
local new_total = def.fold(p_effects)
|
|
||||||
self.value_cache[p_name] = new_total
|
|
||||||
|
|
||||||
def.apply(new_total, player)
|
local new_total = self.def.fold(p_effects)
|
||||||
def.on_change(old_total, new_total, player)
|
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
|
return actual_id
|
||||||
end
|
end
|
||||||
|
|
||||||
function mon_meta:del_change(player, id)
|
function mon_meta:del_change(player, id, branch_name)
|
||||||
local p_name = player:get_player_name()
|
local p_name = player:get_player_name()
|
||||||
|
init_player_branches_if_missing(self, p_name)
|
||||||
|
|
||||||
local def = self.def
|
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 = self.player_map[p_name]
|
local p_effects = p_branch_data.effects
|
||||||
if p_effects == nil then return end
|
local old_total = p_branch_data.value
|
||||||
|
|
||||||
local old_total = self.value_cache[p_name]
|
|
||||||
p_effects[id] = nil
|
p_effects[id] = nil
|
||||||
local new_total = def.fold(p_effects)
|
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.value_cache[p_name] = new_total
|
||||||
|
|
||||||
def.apply(new_total, player)
|
self:call_on_change(old_total, new_total, player, branch_name)
|
||||||
def.on_change(old_total, new_total, player)
|
self.def.apply(new_total, player)
|
||||||
|
|
||||||
|
return checkout_branch
|
||||||
end
|
end
|
||||||
|
|
||||||
function mon_meta:value(player)
|
-- Finally, load the additional files
|
||||||
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 .. "standard_monoids.lua")
|
||||||
dofile(modpath .. "test.lua")
|
dofile(modpath .. "test.lua")
|
||||||
|
@ -98,7 +98,6 @@ player_monoids.fly = monoid({
|
|||||||
end
|
end
|
||||||
|
|
||||||
minetest.set_player_privs(p_name, privs)
|
minetest.set_player_privs(p_name, privs)
|
||||||
|
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -124,7 +123,6 @@ player_monoids.noclip = monoid({
|
|||||||
end
|
end
|
||||||
|
|
||||||
minetest.set_player_privs(p_name, privs)
|
minetest.set_player_privs(p_name, privs)
|
||||||
|
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
624
test.lua
624
test.lua
@ -1,5 +1,8 @@
|
|||||||
|
-- monoids.lua
|
||||||
|
-- This file defines a set of testing chatcommands for the monoids system.
|
||||||
|
|
||||||
local speed = player_monoids.speed
|
local speed = player_monoids.speed
|
||||||
|
local jump = player_monoids.jump
|
||||||
|
|
||||||
minetest.register_privilege("monoid_master", {
|
minetest.register_privilege("monoid_master", {
|
||||||
description = "Allows testing of player monoids.",
|
description = "Allows testing of player monoids.",
|
||||||
@ -7,24 +10,627 @@ minetest.register_privilege("monoid_master", {
|
|||||||
give_to_admin = true,
|
give_to_admin = true,
|
||||||
})
|
})
|
||||||
|
|
||||||
local function test(player)
|
--------------------------------------------------------------------------------
|
||||||
local ch_id = speed:add_change(player, 10)
|
-- Helper: reset branches for both speed and jump
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
local function reset_all_monoid_branches(player)
|
||||||
local p_name = player:get_player_name()
|
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()
|
--------------------------------------------------------------------------------
|
||||||
|
-- 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)
|
local player = minetest.get_player_by_name(p_name)
|
||||||
if not player then return end
|
if player then
|
||||||
speed:del_change(player, ch_id)
|
test_onchange_listen_all(player)
|
||||||
minetest.chat_send_player(p_name, "Your speed is: " .. speed:value(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)
|
||||||
end
|
end
|
||||||
|
|
||||||
minetest.register_chatcommand("test_monoids", {
|
minetest.register_chatcommand("test_monoids", {
|
||||||
description = "Runs a test on monoids",
|
description = "Runs ALL monoid tests in sequence.",
|
||||||
privs = {monoid_master = true},
|
privs = {monoid_master = true},
|
||||||
func = function(p_name)
|
func = function(p_name)
|
||||||
test(minetest.get_player_by_name(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,
|
end,
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user