mirror of
https://github.com/minetest-mods/player_monoids.git
synced 2026-01-11 19:45:28 +01:00
Compare commits
2 Commits
pr_11_docs
...
pr_12_code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19df095028 | ||
|
|
deddbfd740 |
@@ -3,9 +3,12 @@
|
||||
ignore = { "212" }
|
||||
|
||||
read_globals = {
|
||||
table = {fields = {"copy"}},
|
||||
|
||||
"core",
|
||||
"minetest",
|
||||
"vector",
|
||||
"dump",
|
||||
}
|
||||
|
||||
globals = {
|
||||
|
||||
291
API.md
291
API.md
@@ -1,218 +1,169 @@
|
||||
# Player Monoids Library Documentation
|
||||
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
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 (`ObjectRef:set_physics_override`), while another mod reduces it. Without a structured way to combine these changes, mods can overwrite each other's effects, leading to unpredictable behavior.
|
||||
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.
|
||||
|
||||
**Undocumented functions or variables may change at any time without notice.**
|
||||
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.
|
||||
|
||||
### Use Cases and Types
|
||||
|
||||
### Terminology
|
||||
Monoids are useful for managing both built-in player attributes and custom mod-defined states. For example:
|
||||
|
||||
- **Monoid**: Represents one aspect of the player state
|
||||
- Examples: movement speed factor, corruption level, reputation level, the `fast` privilege,
|
||||
environmental effects
|
||||
- **Branch**: A situation or time-based context of the player.
|
||||
- Monoids of the active branch are applied to the player. This allows context-based overriding.
|
||||
- Use-cases: freezing the player (sleeping in bed), increased armor groups in PvP,
|
||||
increased jump height while playing a minigame.
|
||||
|
||||
|
||||
## Monoids
|
||||
- **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.
|
||||
|
||||
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).
|
||||
|
||||
- **Multiplicative**: Combine values using multiplication (e.g., speed multipliers).
|
||||
- **Additive**: Combine values using addition (e.g., armor bonuses).
|
||||
- **Custom Logic**: Use custom logic to combine values (e.g., vectors for directional effects).
|
||||
---
|
||||
|
||||
### Definition Structure
|
||||
|
||||
### Built-in monoids
|
||||
|
||||
[standard_monoids.lua](standard_monoids.lua) provides the following *monoids*:
|
||||
|
||||
- Player physics overrides: (multipliers)
|
||||
- `player_monoids.speed`: movement speed
|
||||
- `player_monoids.jump`: jump speed
|
||||
- `player_monoids.gravity`: acceleration.
|
||||
- Privilege management (*OR*-combined): `fly`, `noclip`
|
||||
- Player appearance: (multipliers)
|
||||
- `player_monoids.collisionbox`: Scales the player’s collision box with component-wise multiplication.
|
||||
- `player_monoids.visual_size`: Scales the player’s visual size as a 2D multiplier vector.
|
||||
|
||||
|
||||
### Monoid Definition
|
||||
|
||||
A *monoid* is defined as a Lua table. See also: `player_monoids.make_monoid`.
|
||||
You may find an example below.
|
||||
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
|
||||
{
|
||||
identity = value, -- Neutral/default value
|
||||
combine = function(value1, value2), -- Combines two values
|
||||
fold = function({values, ...}), -- Combines multiple elements
|
||||
apply = function(value, player), -- Applies the combined value to the player
|
||||
on_change = function(old_value, new_value, player, branch_name), -- Optional callback for value changes
|
||||
listen_to_all_changes = boolean -- Optional; enables callbacks across branches
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
- `identity`
|
||||
- This is the base onto which all values of the active monoids are applied to
|
||||
. As a rule of thumb:
|
||||
- If `combine` is multiplicative: `identity = 1.0`
|
||||
- If `combine` is additive: `identity = 0`
|
||||
- `combine = function(value1, value2)`
|
||||
- Combines two values, originating from separate `monoid:add_change`.
|
||||
- Return value: output value (same type as `value1` and `value2`)
|
||||
- This function *must* be associative. Hence these two expressions must be equal:
|
||||
- `combine(a, combine(b, c))`
|
||||
- `combine(combine(a, b), c)`
|
||||
- `fold = function({ value1, value2, ... })`
|
||||
- Identical logic as in `combine`, but accepting more values.
|
||||
- `apply = function(value, player)`
|
||||
- `player` (ObjectRef): Target to apply the `value`
|
||||
- Example: `player:set_physics_override({ speed = value })`
|
||||
- `on_change = function(old_value, new_value, player, branch)`
|
||||
- Optional. This callback is run after a new value was evaluated.
|
||||
- `branch` (table): This is equal to `monoid:get_active_branch(player)`.
|
||||
- `listen_to_all_changes = boolean`
|
||||
- When set to `true`, `on_change` will be called on *every change*, even if `branch` is not the currently active branch.
|
||||
- Default: `false`.
|
||||
- `on_branch_created(monoid, player, branch_name)`
|
||||
- Optional. Called when a new branch is created.
|
||||
- `on_branch_deleted(monoid, player, branch_name)`
|
||||
- Optional. Called when a branch is deleted.
|
||||
Each field plays a specific role in defining the behavior of the monoid:
|
||||
|
||||
**Types of the parameters above:**
|
||||
|
||||
- `branch_name`: a `string`. e.g. `"main"` (default branch)
|
||||
- `monoid`: see [Monoid Definition]
|
||||
- `player`: an `ObjectRef` instance
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
In order to overwrite the `speed` physics override (a built-in monoid), the following
|
||||
definition could be used:
|
||||
- **`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
|
||||
{
|
||||
identity = 1,
|
||||
combine = function(a, b)
|
||||
-- Scale linearly, based on the default 1.
|
||||
return a * b
|
||||
end,
|
||||
fold = function(t)
|
||||
-- Same as `combine` but for more elements
|
||||
local result = 1
|
||||
for _, v in pairs(t) do
|
||||
result = result * v
|
||||
end
|
||||
return result
|
||||
end,
|
||||
apply = function(multiplier, player)
|
||||
player:set_physics_override({speed = multiplier})
|
||||
end,
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
## Branches
|
||||
- **`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.
|
||||
For each player exactly one branch is active at a time.
|
||||
|
||||
- Default branch: `"main"`
|
||||
- Default values: as given by `monoid.identity`
|
||||
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
|
||||
|
||||
**Branch management and assignemnt:**
|
||||
|
||||
- New branch: `monoid:new_branch(player, name)`
|
||||
- Switch to branch: `monoid:checkout_branch(player, name)`
|
||||
- Get branch: `monoid:get_branch(name)`
|
||||
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)).
|
||||
|
||||
*Optional arguments are denoted with `[, arg1]`.
|
||||
---
|
||||
|
||||
Notes:
|
||||
#### `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.
|
||||
|
||||
- `branch_name`: a `string`
|
||||
- `id`: a `number` or a unique mod-defined `string` to identify the change, e.g. `my_boots_mod:speedy_boots`.
|
||||
- `monoid`: see [Monoid Definition]
|
||||
- `player`: an `ObjectRef` instance
|
||||
---
|
||||
|
||||
#### `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 API
|
||||
---
|
||||
|
||||
- `player_monoids.make_monoid(monoid_def)`
|
||||
- Initializes and returns a new monoid based on `monoid_def`.
|
||||
- `monoid_def` (table): see [Monoid Definition]
|
||||
- `monoid:add_change(player, value[, id, branch_name])`
|
||||
- Applies a change represented by `value` to the player.
|
||||
- If the change `id` already exists in the branch, it will be overwritten.
|
||||
- If no `branch_name` is provided, `"main"` will be used.
|
||||
- Returns `id` or a generated unique ID of the change.
|
||||
- `monoid:del_change(player, id[, branch_name])`
|
||||
- Removes the change represented by `id` from the player.
|
||||
- If no `branch_name` is provided, `"main"` will be used.
|
||||
- `monoid:value(player[, branch_name])`
|
||||
- Gets the value of this monoid for the specified player and branch.
|
||||
- If no `branch_name` is provided, the **active branch** will be used.
|
||||
- Returns the combined value for this monoid.
|
||||
#### `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.
|
||||
|
||||
---
|
||||
|
||||
### Branch API
|
||||
#### `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.
|
||||
|
||||
- `monoid:new_branch(player, branch_name)`
|
||||
- Creates a new branch for a player, but does not switch to it.
|
||||
- Returns a [Branch Handler] object.
|
||||
- `mononid:checkout_branch(player, branch_name)`
|
||||
- Switches the player's active branch to the specified one, creating it if it doesn't exist.
|
||||
- The player's state is immediately updated to reflect the combined value of the new active branch.
|
||||
- Returns a [Branch Handler] object.
|
||||
- `monoid:get_active_branch(player)`
|
||||
- Gets a [Branch Handler] object representing the player's currently active branch.
|
||||
- `monoid:get_branch(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:
|
||||
- Key: string, branch name
|
||||
- Value: [Branch Handler] object
|
||||
- `monoid:reset_branch(player[, branch_name])`
|
||||
- Clears all changes associated with a player's branch.
|
||||
- If no branch name is provided, `"main"` will be used.
|
||||
---
|
||||
|
||||
#### `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.
|
||||
|
||||
#### Branch Handler
|
||||
---
|
||||
|
||||
The branch handler (table) provides methods for managing changes specific to the branch.
|
||||
#### `monoid:get_active_branch(player)`
|
||||
Gets a handler object representing the player's currently active branch.
|
||||
|
||||
**Functions:**
|
||||
---
|
||||
|
||||
- `branch:get_name()`
|
||||
- Retrieves this branch’s name as a string.
|
||||
- `branch:add_change(player, value[, id])`:
|
||||
- Adds a change to this branch.
|
||||
- Wrapper for `monoid:add_change`
|
||||
- `branch:del_change(player, id)`
|
||||
- Removes a change from this branch by its ID.
|
||||
- Wrapper for `monoid:del_change`
|
||||
- `branch:value(player)`
|
||||
- Evaluates and returns this branch’s current combined value for a specific player.
|
||||
- Wrapper for `monoid:value`
|
||||
- `branch:reset(player)`
|
||||
- Discards all changes (`id` + `value`) belonging to this branch and player.
|
||||
- `branch: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"`.
|
||||
- The `"main"` branch cannot be deleted.
|
||||
#### `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.
|
||||
|
||||
34
README.md
34
README.md
@@ -1,10 +1,10 @@
|
||||
# Player Monoids Library
|
||||
|
||||
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](API.md)**.
|
||||
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**.
|
||||
|
||||
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.
|
||||
|
||||
Additionally, the mod 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.
|
||||
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.
|
||||
|
||||
## Global Player State
|
||||
|
||||
@@ -14,9 +14,7 @@ For example, a player could be under a speed boost effect from a `playereffects`
|
||||
|
||||
Player Monoids prevents this issue by allowing changes to be layered and combined correctly using monoids and branch-based state management.
|
||||
|
||||
## [Monoids](API.md)
|
||||
|
||||
The functions documentation can be found in the **[API.md](API.md)** file.
|
||||
## Monoids
|
||||
|
||||
### Creation
|
||||
|
||||
@@ -133,6 +131,28 @@ end
|
||||
|
||||
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. 
|
||||
|
||||
## Predefined monoids
|
||||
|
||||
### Physics Overrides
|
||||
|
||||
These monoids modify physics properties using multipliers:
|
||||
|
||||
- `player_monoids.speed`
|
||||
- `player_monoids.jump`
|
||||
- `player_monoids.gravity`
|
||||
|
||||
### Privileges
|
||||
|
||||
These monoids toggle player privileges, using boolean logic:
|
||||
|
||||
- `player_monoids.fly`
|
||||
- `player_monoids.noclip`
|
||||
|
||||
### Other
|
||||
|
||||
- `player_monoids.collisionbox` - Adjusts the player’s collision box with component-wise multiplication.
|
||||
- `player_monoids.visual_size` - Modifies the player’s 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.
|
||||
@@ -141,3 +161,7 @@ This ensures that our boost calculation stays separate while still being compati
|
||||
- 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**.
|
||||
309
init.lua
309
init.lua
@@ -1,6 +1,8 @@
|
||||
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
||||
|
||||
player_monoids = {}
|
||||
player_monoids = {
|
||||
api_version = 2
|
||||
}
|
||||
|
||||
local mon_meta = {}
|
||||
mon_meta.__index = mon_meta
|
||||
@@ -11,50 +13,39 @@ local nop = function() end
|
||||
-- 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.
|
||||
]]
|
||||
|
||||
player_monoids.make_monoid = function(def)
|
||||
local mon = {}
|
||||
assert(type(def) == "table")
|
||||
|
||||
-- Clone the definition to avoid mutating the original
|
||||
local actual_def = {}
|
||||
local actual_def = {
|
||||
-- Default values of optional fields
|
||||
apply = nop,
|
||||
on_change = nop,
|
||||
on_branch_created = nop,
|
||||
on_branch_deleted = nop,
|
||||
listen_to_all_changes = false,
|
||||
}
|
||||
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
|
||||
-- Mandatory fields
|
||||
assert(actual_def.identity ~= nil)
|
||||
-- (combine is unused)
|
||||
assert(type(actual_def.fold) == "function")
|
||||
assert(type(actual_def.apply) == "function")
|
||||
|
||||
-- Optional fields
|
||||
assert(type(actual_def.on_change) == "function")
|
||||
assert(type(actual_def.listen_to_all_changes) == "boolean")
|
||||
assert(type(actual_def.on_branch_created) == "function")
|
||||
assert(type(actual_def.on_branch_deleted) == "function")
|
||||
|
||||
local mon = {}
|
||||
mon.def = actual_def
|
||||
|
||||
mon.player_map = {} -- p_name -> { active_branch="main", branches={ branch_name={ effects={}, value=...} } }
|
||||
mon.value_cache = {} -- p_name -> numeric or table
|
||||
mon.player_map = {} -- Contains the branch data
|
||||
mon.next_id = 1
|
||||
|
||||
setmetatable(mon, mon_meta)
|
||||
@@ -63,51 +54,60 @@ player_monoids.make_monoid = function(def)
|
||||
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)
|
||||
|
||||
-- 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
|
||||
end
|
||||
|
||||
local function init_player_branches_if_missing(self, p_name)
|
||||
if not self.player_map[p_name] then
|
||||
self:init_branches(p_name)
|
||||
|
||||
--- @brief Gets or initializes the player data of the current monoid
|
||||
--- @param p_name string
|
||||
--- @param do_reset boolean, whether to reset all player data. Default: false
|
||||
--- @return A table, the player data.
|
||||
function mon_meta:_get_player_data(p_name, do_reset)
|
||||
local p_data
|
||||
if not do_reset then
|
||||
p_data = self.player_map[p_name]
|
||||
if p_data then
|
||||
return p_data
|
||||
end
|
||||
end
|
||||
|
||||
p_data = {
|
||||
active_branch = "main",
|
||||
branches = {}
|
||||
}
|
||||
self.player_map[p_name] = p_data
|
||||
|
||||
-- Create the main branch
|
||||
local bdata = self:_get_branch_data(p_name, "main")
|
||||
p_data.last_value = bdata.value -- for 'on_change'
|
||||
|
||||
return p_data
|
||||
end
|
||||
|
||||
-- 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)
|
||||
--- @brief Gets or initializes the givne branch
|
||||
function mon_meta:_get_branch_data(p_name, branch_name)
|
||||
local branches = self.player_map[p_name].branches
|
||||
local existing_branch = branches[branch_name]
|
||||
local branch = branches[branch_name]
|
||||
if branch then
|
||||
return branch
|
||||
end
|
||||
|
||||
if not existing_branch then
|
||||
branches[branch_name] = {
|
||||
effects = {},
|
||||
value = self.def.identity
|
||||
}
|
||||
-- Create
|
||||
branch = {
|
||||
effects = {},
|
||||
value = table.copy(self.def.identity)
|
||||
}
|
||||
branches[branch_name] = branch
|
||||
|
||||
existing_branch = branches[branch_name]
|
||||
|
||||
local player = minetest.get_player_by_name(p_name)
|
||||
if branch_name ~= "main" then
|
||||
local player = core.get_player_by_name(p_name)
|
||||
if player then
|
||||
self.def.on_branch_created(self, player, branch_name)
|
||||
end
|
||||
end
|
||||
|
||||
return existing_branch
|
||||
return branch
|
||||
end
|
||||
|
||||
-- decide if to call on_change for this change based on listen_to_all_changes
|
||||
@@ -118,62 +118,67 @@ function mon_meta:call_on_change(old_value, new_value, player, branch_name)
|
||||
end
|
||||
end
|
||||
|
||||
function mon_meta:add_change(player, value, id, branch_name)
|
||||
--- @brief Internal function to change (or remove) an effect
|
||||
--- @param player ObjectRef
|
||||
--- @param value new effect value (may be nil)
|
||||
--- @param id string/integer, effect ID
|
||||
--- @param branch_name string, branch to modify
|
||||
function mon_meta:_set_change(player, value, id, branch_name)
|
||||
assert(value == nil or type(value) == type(self.def.identity))
|
||||
|
||||
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
|
||||
-- TODO: 'new_branch' and 'checkout_branch' should be used instead to create the branch!
|
||||
local p_branch_data = self:_get_branch_data(p_name, branch_name)
|
||||
|
||||
local old_total = p_branch_data.value
|
||||
p_effects[actual_id] = value
|
||||
|
||||
local new_total = self.def.fold(p_effects)
|
||||
p_branch_data.effects[id] = value
|
||||
local new_total = self.def.fold(p_branch_data.effects)
|
||||
p_branch_data.value = new_total
|
||||
|
||||
if self.player_map[p_name].active_branch == branch then
|
||||
if self.player_map[p_name].active_branch == branch_name then
|
||||
self.def.apply(new_total, player)
|
||||
end
|
||||
|
||||
self:call_on_change(old_total, new_total, player, branch)
|
||||
return actual_id
|
||||
self:call_on_change(old_total, new_total, player, branch_name)
|
||||
end
|
||||
|
||||
function mon_meta:add_change(player, value, id, branch_name)
|
||||
local p_name = player:get_player_name()
|
||||
branch_name = branch_name or "main"
|
||||
|
||||
-- Create if not existing
|
||||
self:_get_player_data(p_name)
|
||||
self:_get_branch_data(p_name, branch_name)
|
||||
|
||||
if not id then
|
||||
id = self.next_id
|
||||
self.next_id = self.next_id + 1
|
||||
end
|
||||
|
||||
self:_set_change(player, value, id, branch_name)
|
||||
return 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 p_data = self:_get_player_data(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)
|
||||
branch_name = branch_name or "main"
|
||||
local p_branch_data = p_data.branches[branch_name]
|
||||
if not p_branch_data then
|
||||
return
|
||||
end
|
||||
|
||||
self:call_on_change(old_total, new_total, player, branch)
|
||||
self:_set_change(player, nil, id, branch_name)
|
||||
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 p_data = self:_get_player_data(p_name)
|
||||
|
||||
local branch = branch_name or "main"
|
||||
local bdata = self.player_map[p_name].branches[branch]
|
||||
branch_name = branch_name or "main"
|
||||
local bdata = p_data.branches[branch_name]
|
||||
if not bdata then
|
||||
return -- Branch doesn't exist, nothing to reset
|
||||
end
|
||||
@@ -182,25 +187,27 @@ function mon_meta:reset_branch(player, branch_name)
|
||||
|
||||
-- Clear effects and recalc
|
||||
bdata.effects = {}
|
||||
local new_total = self.def.fold({})
|
||||
local new_total = table.copy(self.identity)
|
||||
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]
|
||||
self.value_cache[p_name] = active_branch_data.value
|
||||
self.def.apply(active_branch_data.value, player)
|
||||
local active_branch = p_data.active_branch or "main"
|
||||
if branch_name == active_branch then
|
||||
-- Apply the new values
|
||||
p_data.last_value = bdata.value
|
||||
self.def.apply(bdata.value, player)
|
||||
end
|
||||
|
||||
-- Fire on_change for the branch being reset
|
||||
self:call_on_change(old_total, new_total, player, branch)
|
||||
self:call_on_change(old_total, new_total, player, branch_name)
|
||||
end
|
||||
|
||||
-- new method: create a branch for a player, but do NOT check it out
|
||||
-- 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)
|
||||
-- Create if not existing
|
||||
self:_get_player_data(p_name)
|
||||
self:_get_branch_data(p_name, branch_name)
|
||||
|
||||
return self:get_branch(branch_name)
|
||||
end
|
||||
@@ -228,32 +235,7 @@ function mon_meta:get_branch(branch_name)
|
||||
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)
|
||||
return monoid:delete_branch(player, branch_name)
|
||||
end,
|
||||
}
|
||||
end
|
||||
@@ -266,62 +248,71 @@ end
|
||||
|
||||
function mon_meta:get_branches(player)
|
||||
local p_name = player:get_player_name()
|
||||
init_player_branches_if_missing(self, p_name)
|
||||
local p_data = self:_get_player_data(p_name)
|
||||
|
||||
local branch_map = self.player_map[p_name].branches or {}
|
||||
local result = {}
|
||||
for b_name, _ in pairs(branch_map) do
|
||||
for b_name, _ in pairs(p_data.branches) 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)
|
||||
local p_name = player:get_player_name()
|
||||
local player_data = self:_get_player_data(p_name)
|
||||
|
||||
if not b then
|
||||
local existing_branch = player_data.branches[branch_name]
|
||||
if not existing_branch or branch_name == "main" then
|
||||
return false
|
||||
end
|
||||
|
||||
b:delete(player)
|
||||
-- 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 = self:value(player, "main")
|
||||
|
||||
player_data.last_value = new_main_total
|
||||
self.def.apply(new_main_total, player)
|
||||
end
|
||||
|
||||
-- Remove the branch
|
||||
player_data.branches[branch_name] = nil
|
||||
|
||||
self.def.on_branch_deleted(self, player, branch_name)
|
||||
return true
|
||||
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())
|
||||
local p_name = player:get_player_name()
|
||||
for _, monoid in pairs(player_monoids) do
|
||||
if type(monoid) == "table" and monoid._get_player_data then
|
||||
monoid:_get_player_data(p_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 p_data = self:_get_player_data(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
|
||||
branch_name = branch_name or p_data.active_branch
|
||||
local bdata = p_data.branches[branch_name]
|
||||
return bdata and bdata.value or table.copy(self.def.identity)
|
||||
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 p_data = self:_get_player_data(p_name)
|
||||
|
||||
local old_total = self.value_cache[p_name] or self.def.identity
|
||||
local old_total = p_data.last_value
|
||||
local checkout_branch = self:new_branch(player, branch_name)
|
||||
|
||||
self.player_map[p_name].active_branch = branch_name
|
||||
p_data.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)
|
||||
p_data.last_value = new_total
|
||||
self.def.apply(new_total, player)
|
||||
self:call_on_change(old_total, new_total, player, branch_name)
|
||||
|
||||
return checkout_branch
|
||||
end
|
||||
|
||||
6
test.lua
6
test.lua
@@ -424,12 +424,16 @@ local function test_on_branch_create_delete(player)
|
||||
end
|
||||
|
||||
local del_branch = speed:checkout_branch(player, "my_del_branch")
|
||||
local retval
|
||||
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)
|
||||
retval = del_branch:delete(player)
|
||||
minetest.chat_send_player(p_name, ((retval == true) and "PASS" or "FAIL") .. " - branch:delete(...) #1")
|
||||
end
|
||||
retval = del_branch:delete(player)
|
||||
minetest.chat_send_player(p_name, ((retval == false) and "PASS" or "FAIL") .. " - branch:delete(...) #2")
|
||||
|
||||
if created_count == 0 then
|
||||
minetest.chat_send_player(p_name, "[OnBranchCreateDelete] FAIL: on_branch_created not called.")
|
||||
|
||||
Reference in New Issue
Block a user