Branching system (#10)

This commit enhances the player_monoids library by adding branching support and an improved testing system to make sure it works, as proposed in #9 . These changes enable isolated player state management and improve library reliability. This PR focuses on implementing the branching proposal and adding automated tests.
This commit is contained in:
Gio
2025-05-17 18:10:30 +02:00
committed by GitHub
parent 8d6f1ade93
commit ee81c59f2c
5 changed files with 1164 additions and 288 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.