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