Skip to content

Instantly share code, notes, and snippets.

@Therosin
Last active March 31, 2024 01:05
Show Gist options
  • Select an option

  • Save Therosin/f321a4be0696339514bd7053b8781e68 to your computer and use it in GitHub Desktop.

Select an option

Save Therosin/f321a4be0696339514bd7053b8781e68 to your computer and use it in GitHub Desktop.
LuaWorldState
-- Copyright (C) 2024 Theros <https://github.com/therosin>
--
-- This file is part of LuaWorldStateSystem.
--
-- LuaWorldStateSystem is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- LuaWorldStateSystem is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with LuaWorldStateSystem. If not, see <https://www.gnu.org/licenses/>.
local random = require("utils-random")
local uuidv4 = random.uuidv4
---@class GameWorldEvent
---@field protected _id string Unique identifier for the event
---@field protected _parent GameWorldEventManager|nil Reference to the event manager that holds this event
---@field name string Name of the event
---@field conditions table[] Conditions needed to trigger the event
---@field actions function[] Actions to be executed when the event is triggered
---@field linkedEvents string[] Optional: IDs of other events linked to this event
---@field autoTrigger boolean Automatically trigger the event when conditions are met (default: true)
Event = {}
Event.__index = Event
--- Create a new event object.
---@param name string Name of the event
---@param conditions table[] Conditions needed to trigger the event
---@param actions function[] Actions to be executed when the event is triggered
---@param linkedEvents string[] Optional: IDs of other events linked to this event
---@param id? string Optional: Unique identifier for the event (will be auto-generated if not provided)
---@param parent? GameWorldEventManager|nil Optional: Reference to the event manager that holds this event
function Event.new(name, conditions, actions, linkedEvents, id, parent)
local self = setmetatable({}, Event)
self._id = id or uuidv4()
self._parent = parent
self.name = name
self.conditions = conditions
self.actions = actions or {}
self.linkedEvents = linkedEvents or {}
self.autoTrigger = false
return self
end
function Event:id()
return self._id
end
function Event:parent()
return self._parent
end
function Event:meets_conditions(playerData)
for _, condition in ipairs(self.conditions) do
if condition.type == "attribute" then
if playerData["attributes"][condition.name] < condition.value then
return false
end
elseif condition.type == "relationship" then
if playerData["relationships"][condition.name] < condition.value then
return false
end
elseif condition.type == "loreKey" then
if playerData["loreKeys"][condition.name] ~= condition.value then
return false
end
else
error("Invalid condition type")
end
end
return true
end
function Event:trigger(playerData, withLinkedEvents)
for _, action in ipairs(self.actions) do
action(playerData)
end
if withLinkedEvents and self.parent then
for _, linkedEventId in ipairs(self.linkedEvents) do
-- trigger linked events
local events = self:parent().events
local linkedEvent = events[linkedEventId]
if linkedEvent and linkedEvent:meets_conditions(playerData) then
linkedEvent:trigger(playerData, withLinkedEvents)
end
end
end
end
-- Adds a new action to the event
function Event:define_action(func)
if type(func) == "function" then
table.insert(self.actions, func)
else
error("define_action expects a function")
end
end
---@class GameWorldEventManager
---@field events table<string, GameWorldEvent> Collection of events
---@field activeEvents string[] Collection of active event IDs
EventManager = {
events = {},
activeEvents = {},
}
---@class GameWorldEventOptions
---@field name string Name of the event
---@field conditions table[] Conditions needed to trigger the event
---@field actions function[] Actions to be executed when the event is triggered
---@field linkedEvents string[] Optional: IDs of other events linked to this event
---@field id? string Optional: Unique identifier for the event (will be auto-generated if not provided)
---@field autoTrigger? boolean Optional: Automatically trigger the event when conditions are met (default: true)
--- Create a new event and add it to the event manager.
---@param event GameWorldEventOptions | GameWorldEvent Event object or options to create a new event
---@return GameWorldEvent
function EventManager:add_event(event)
if type(event.id) == "function" and type(event.trigger) == "function" then
---@cast event GameWorldEvent
event._parent = self ---@diagnostic disable-line
self.events[event:id()] = event
return event
else
---@cast event GameWorldEventOptions
local id = event.id or uuidv4()
local newEvent = Event.new(event.name, event.conditions, event.actions, event.linkedEvents, id, self)
self.events[newEvent:id()] = newEvent
return newEvent
end
end
--- Activate an event by ID.
--- this will add the event to the activeEvents list, allowing it to be checked and triggered.
---@param eventId string ID of the event to activate
function EventManager.activate_event(eventId)
if not table.contains(EventManager.activeEvents, eventId) then
table.insert(EventManager.activeEvents, eventId)
end
end
--- Check all events that meet their conditions.
--- this will trigger events that have autoTrigger set to true.
---@param playerData table<string, any> Player data to check against event conditions
---@param linkedEvents boolean Whether linked events should also be triggered (if conditions are met, default: false)
---@return table[] Events that meet their conditions
function EventManager.check_events(playerData, linkedEvents)
local events = {}
for _, eventId in ipairs(EventManager.activeEvents) do
local event = EventManager.events[eventId]
if event and event:meets_conditions(playerData) then
local event_meta = {
id = eventId,
name = event.name,
triggered = false
}
if event.autoTrigger then
event:trigger(playerData, linkedEvents)
event_meta.triggered = true
end
table.insert(events, event_meta)
end
end
return events
end
--- Trigger all events that meet their conditions.
--- this will trigger all events that meet their conditions, regardless of the autoTrigger flag.
---@param playerData table<string, any> Player data to check against event conditions
---@param linkedEvents boolean Whether linked events should also be triggered (if conditions are met, default: false)
---@return string[] IDs of triggered events
function EventManager.trigger_events(playerData, linkedEvents)
local events = {}
for _, eventId in ipairs(EventManager.activeEvents) do
local event = EventManager.events[eventId]
if event and event:meets_conditions(playerData) then
event:trigger(playerData, linkedEvents)
table.insert(events, eventId)
end
end
return events
end
--- Retrieves a list of potential events based on the given player data.
--- @param playerData table The player data used to determine if an event meets the conditions.
--- @return table A table containing the potential events, each represented by a table with 'id' and 'name' fields.
function EventManager.get_potential_events(playerData)
local potentialEvents = {}
for id, event in pairs(EventManager.events) do
if event:meets_conditions(playerData) then
local event_data = {
id = id,
name = event.name
}
table.insert(potentialEvents, event_data)
end
end
return potentialEvents
end
-- Utility function to check table containment
function table.contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end
--- EventManager.
local exports = setmetatable({
CreateEvent = Event.new,
}, { __index = EventManager })
return exports
-- Copyright (C) 2024 Theros <https://github.com/therosin>
--
-- This file is part of LuaWorldStateSystem.
--
-- LuaWorldStateSystem is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- LuaWorldStateSystem is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with LuaWorldStateSystem. If not, see <https://www.gnu.org/licenses/>.
package.path = package.path .. ";../?.lua"
local PlayerData = require("PlayerData")
local EventManager = require("EventManager")
-- Create a new player with some initial attributes, relationships, and loreKeys
local player = PlayerData.new({
strength = 3,
dexterity = 1,
constitution = 1,
intelligence = 0,
wisdom = 0,
charisma = 2,
generosity = 0,
honesty = 0,
}, {
Alex = 2,
Jordan = -2,
}, {
discoveredSecretCave = false,
})
player:set_debug_mode(true)
-- Define a new event using EventManager's CreateEvent function
local event1 = EventManager.CreateEvent(
"event1",
{ { type = "attribute", name = "strength", value = 3 } },
{
function(playerData)
print("Event1 Triggered: Strength condition met.")
-- Increase player's charisma as a reward
playerData:update_attribute("charisma", 1)
end
},
{}
)
event1.autoTrigger = true -- Automatically trigger the event when conditions are met (default: true)
-- Add the newly created event to the EventManager
EventManager:add_event(event1)
-- Alternatively, you can directly add an event to the EventManager
local event2 = EventManager:add_event({
name = "event2",
conditions = { { type = "attribute", name = "charisma", value = 3 } },
actions = {
function(playerData)
print("Event2 Triggered: Charisma condition met.")
-- Increase player's generosity as a reward
playerData:update_attribute("generosity", 1)
end
},
linkedEvents = {}
})
event2.autoTrigger = true
table.insert(event1.linkedEvents, event2:id())
-- Simulate game logic where player discovers a secret cave, affecting loreKeys
player:set_lore_key("discoveredSecretCave", true)
-- If necessary, directly activate a specific event based on gameplay events
EventManager.activate_event(event1:id())
-- In a game loop or specific update function, continuously check for potential events
local potentialEvents = EventManager.get_potential_events(player)
print("Potential events based on the current state: ", #potentialEvents)
for _, event in ipairs(potentialEvents) do
print(string.format("Event: %s - id: %s", event.name, event.id))
end
-- Check and trigger events, assuming the game has progressed and conditions might be met
EventManager.check_events(player, true)

LuaWorldStateSystem

LuaWorldStateSystem is a versatile Lua library designed to manage the state of a game world, including player attributes, relationships, and various game events triggered by specific conditions. This system provides a robust framework for designing dynamic game experiences where player actions influence the game world and its narrative.

Features

  • Player Data Management: Efficiently manage player attributes, relationships with NPCs, and key lore events that shape the game's narrative.

  • Event Management: Define custom game events triggered by specific player conditions, such as reaching attribute thresholds, achieving milestones, or interacting with NPCs.

  • Flexible Condition Evaluation: Evaluate a variety of conditions, including player attributes, relationship levels, and lore events, to determine when events should be triggered.

  • Debugging Support: Enable debug mode to track changes in player data and event triggers for testing and troubleshooting.

Installation

  1. Clone the LuaWorldStateSystem repository to your project directory:

    git clone https://gist.github.com/f321a4be0696339514bd7053b8781e68.git LuaWorldStateSystem
  2. Import the necessary modules into your Lua project:

    local GamePlayerData = require("LuaWorldStateSystem.PlayerData")
    local GameEventManager = require("LuaWorldStateSystem.EventManager")
  3. Start using the system to manage player data and events in your game.

Usage

Player Data Management

-- Create a new player with initial attributes, relationships, and loreKeys
local player = GamePlayerData.new({
    strength = 3,
    dexterity = 1,
    constitution = 1,
    intelligence = 0,
    wisdom = 0,
    charisma = 2,
    generosity = 0,
    honesty = 0,
}, {
    Alex = 2,
    Jordan = -2,
}, {
    discoveredSecretCave = false,
})

-- Update player attributes and relationships
player:update_attribute("generosity", 5)
player:update_relationship("Alex", 3)

-- Set a lore key
player:set_lore_key("discoveredSecretCave", true)

Event Management

-- Define a new event
-- Define a new event using EventManager's CreateEvent function
local event1 = EventManager.CreateEvent(
    "event1",
    { { type = "attribute", name = "strength", value = 3 } },
    {
        function(playerData)
            print("Event1 Triggered: Strength condition met.")
            -- Increase player's charisma as a reward
            playerData:update_attribute("charisma", 1)
        end
    },
    {}
)
event1.autoTrigger = true -- Automatically trigger the event when conditions are met (default: true)

-- Add the newly created event to the EventManager
EventManager:add_event(event1)
-- Alternatively, you can directly add an event to the EventManager
local event2 = EventManager:add_event({
    name = "event2",
    conditions = { { type = "attribute", name = "charisma", value = 3 } },
    actions = {
        function(playerData)
            print("Event2 Triggered: Charisma condition met.")
            -- Increase player's generosity as a reward
            playerData:update_attribute("generosity", 1)
        end
    },
    linkedEvents = {}
})
event2.autoTrigger = true
table.insert(event1.linkedEvents, event2:id())

-- Simulate game logic where player discovers a secret cave, affecting loreKeys
player:set_lore_key("discoveredSecretCave", true)

-- If necessary, directly activate a specific event based on gameplay events
EventManager.activate_event(event1:id())

-- In a game loop or specific update function, continuously check for potential events
local potentialEvents = EventManager.get_potential_events(player)
print("Potential events based on the current state: ", #potentialEvents)
for _, event in ipairs(potentialEvents) do
    print(string.format("Event: %s - id: %s", event.name, event.id))
end


-- Check and trigger events, assuming the game has progressed and conditions might be met
EventManager.check_events(player, true)

Output:

Change event: loreKey discoveredSecretCave changed from false to true
Potential events based on the current state: 1
Event: event1 - id: fcb57462-4854-48ec-a73f-0ec71
Event1 Triggered: Strength condition met.
Change event: attribute charisma changed from 2 to 3
Event2 Triggered: Charisma condition met.
Change event: attribute generosity changed from 0 to 1
  • event1 was triggered because the player's strength condition was met.
  • event2 was subsequently triggered because its condition (charisma reaching 3) was met after event1 triggered, thanks to the linkage established between the events.

Debug mode activated for the player, allowing us to see the changes in attributes (charisma and generosity) and the lore key (discoveredSecretCave) as they occurred during test.

-- Copyright (C) 2024 Theros <https://github.com/therosin>
--
-- This file is part of LuaWorldStateSystem.
--
-- LuaWorldStateSystem is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- LuaWorldStateSystem is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with LuaWorldStateSystem. If not, see <https://www.gnu.org/licenses/>.
--- Deep copy a table, including its metatable
--- avoids shared references between the original and the copy
---@param orig table original to copy
---@return table copy of the table
function table.deepcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[table.deepcopy(orig_key)] = table.deepcopy(orig_value)
end
setmetatable(copy, table.deepcopy(getmetatable(orig)))
else
copy = orig
end
return copy
end
---------------------- Constants ----------------------
--- Player Attribute, e.g. luck, strength, etc.
local DATA_TYPE_ATTRIBUTE = "attribute"
--- Player Relationship, e.g. with other characters, higher value means better relationship
--- recommended to use character names as keys, values could be 0 = neutral, negat
local DATA_TYPE_RELATIONSHIP = "relationship"
local DATA_TYPE_LORE_KEY = "loreKey"
---------------------- PlayerData Class ----------------------
--- Base Attributes Table
--- The default attribute range is from -5 to +5, with 0 being average human capability.
--- Players start with a fixed number of points to allocate up to a soft cap of +3 during character creation.
--- Attributes beyond +3, up to +5, represent levels of proficiency and mastery earned through gameplay achievements.
--- This is just an example; attributes can be added or removed as needed.
--- The default aims to provide a basic set common to RPG systems inspired by D&D 5e or similar frameworks.
-- Note: This system encourages a balanced approach to character creation, with the opportunity to develop and specialize as players progress in the game.
local base_attributes = {
strength = 0, -- Physical power
dexterity = 0, -- Agility and speed
constitution = 0, -- Health and stamina
intelligence = 0, -- Mental acuity and magic
wisdom = 0, -- Insight and perception
charisma = 0, -- Social influence
luck = 0, -- Chance events
agility = 0, -- Physical reflexes and evasion
endurance = 0, -- Sustained physical activity
perception = 0, -- Environmental awareness
magic = 0, -- Magical power
technology = 0, -- Technological skill
}
--- Base Relationships Table
--- Default Relationship Levels and Their Meanings:
--- -10 to -7: Hatred. NPCs with this level of disdain will actively seek to harm the player or obstruct their goals. Recovery from this state requires significant effort.
--- -6 to -4: Strong Dislike. Characters at this level are openly hostile or uncooperative, refusing to assist the player and may spread negative information.
--- -3 to -1: Distrust. These NPCs are skeptical of the player's intentions and offer limited help, requiring persuasion for even minor assistance.
--- 0: Neutral. The baseline relationship status. NPCs are indifferent towards the player, interactions are basic and transactional.
--- +1 to +3: Acquaintance. Initial positive impressions fall here. NPCs are slightly more open and may offer tips or minor assistance.
--- +4 to +6: Friendly. NPCs at this level like the player and will provide help within their capabilities, including sharing information or resources.
--- +7 to +8: Trusted Ally. These NPCs have a high level of trust in the player, willing to support them in risky situations and may share critical secrets.
--- +9: Close Companion. Beyond just allies, NPCs at this level have a deep personal connection with the player, ready to join in their battles and follow them into danger, showing considerable sacrifices.
--- +10: Deep Bond. The highest level of relationship, indicating a profound, possibly romantic or familial bond. NPCs here are committed to the player above all else, potentially laying down their life for them.
local base_relationships = {}
--- Base LoreKeys Table
--- LoreKeys are flags that represent significant player actions, achievements, or discoveries within the game. They serve as a versatile tool for tracking progress and branching the game's narrative or mechanics based on player choices.
--- Examples of LoreKeys include missions completed, secrets uncovered, alliances formed, enemies made, and pivotal decisions taken. These keys can unlock new storylines, affect relationships, open or close paths, and influence the world dynamically.
--- LoreKeys can be used as conditions for various in-game logic, such as unlocking dialogue options with NPCs, altering NPC behavior, triggering events, or revealing hidden areas and items.
--- This system allows for a rich, personalized gaming experience, where the consequences of player actions are tangible and meaningful.
local base_loreKeys = {
-- Example structure; actual lore keys would be dynamically added as players progress through the game.
-- Example: discoveredSecretCave = true, -- Indicates the player has discovered a secret cave.
-- Example: formedAllianceWithElves = true, -- Represents an alliance formed with the elves, affecting relations with other factions.
-- Example: defeatedAncientDragon = true, -- A significant achievement that might be recognized by characters within the game world.
-- Add more keys as needed to reflect the breadth of player actions and world states.
}
PlayerData = {}
PlayerData.__index = PlayerData
-- Method to get base table copies
function PlayerData.get_base_meta()
return table.deepcopy(base_attributes), table.deepcopy(base_relationships), table.deepcopy(base_loreKeys)
end
--- Create a new PlayerData instance
---@param initialAttributes? table -- Initial attributes table
---@param initialRelationships? table -- Initial relationships table
---@param initialLoreKeys? table -- Initial lore keys table
---@return table
function PlayerData.new(initialAttributes, initialRelationships, initialLoreKeys)
local self = setmetatable({}, PlayerData)
self.attributes = initialAttributes or table.deepcopy(base_attributes)
self.relationships = initialRelationships or table.deepcopy(base_relationships)
self.loreKeys = initialLoreKeys or table.deepcopy(base_loreKeys)
self.listeners = {} -- List to store change listeners
self.debug = false -- Set debug flag to false by default
return self
end
--- Update a relationship value
---@param relationship_name string -- Name of the relationship
---@param value number -- Value to add to the relationship (positive or negative integer to increase or decrease the relationship)
function PlayerData:update_relationship(relationship_name, value)
self.relationships[relationship_name] = (self.relationships[relationship_name] or 0) + value
self:trigger_change_callbacks(DATA_TYPE_RELATIONSHIP, relationship_name, self.relationships[relationship_name] - value, self.relationships[relationship_name])
end
--- Update an attribute value
---@param attribute_name string -- Name of the attribute
---@param value number -- Value to add to the attribute (positive or negative integer to increase or decrease the attribute)
function PlayerData:update_attribute(attribute_name, value)
self.attributes[attribute_name] = (self.attributes[attribute_name] or 0) + value
self:trigger_change_callbacks(DATA_TYPE_ATTRIBUTE, attribute_name, self.attributes[attribute_name] - value, self.attributes[attribute_name])
end
--- Set a lore key (will create a new key if it doesn't exist)
---@param key_name string -- Name of the lore key
---@param value boolean -- Value to set for the lore key
function PlayerData:set_lore_key(key_name, value)
self.loreKeys[key_name] = value
self:trigger_change_callbacks(DATA_TYPE_LORE_KEY, key_name, not value, value)
end
--- Trigger change callbacks
---@param data_type string -- Type of data (attribute, relationship, loreKey)
---@param key string -- Key name
---@param old_value any -- Old value
---@param new_value any -- New values
function PlayerData:trigger_change_callbacks(data_type, key, old_value, new_value)
for _, listener in ipairs(self.listeners) do
listener(data_type, key, old_value, new_value)
end
end
--- Add a change listener
---@param listener fun(data_type: string, key: string, old_value: any, new_value: any) -- Listener function
function PlayerData:add_change_listener(listener)
table.insert(self.listeners, listener)
end
local function log_change(data_type, key, old_value, new_value)
print(string.format("Change event: %s %s changed from %s to %s", data_type, key, tostring(old_value), tostring(new_value)))
end
--- Set debug mode
--- When debug mode is enabled, a debug listener is added to log all changes to attributes, relationships, and lore keys.
---@param debug boolean -- Debug mode flag
function PlayerData:set_debug_mode(debug)
self.debug = debug
if debug then
self:add_change_listener(log_change)
else
-- remove the listener if debug is false
for i, listener in ipairs(self.listeners) do
if listener == log_change then
table.remove(self.listeners, i)
break
end
end
end
end
--- Check if the player meets the criteria for a set of events
---@param eventCriteria table -- Criteria for each event
---@return table -- Results for each event
function PlayerData:meets_criteria(eventCriteria)
local eventResults = {}
for eventName, criteriaList in pairs(eventCriteria) do
local allCriteriaMet = true
for _, condition in ipairs(criteriaList) do
if condition.type == "attribute" and (not self.attributes[condition.name] or self.attributes[condition.name] < condition.value) then
allCriteriaMet = false
break
elseif condition.type == "relationship" and (not self.relationships[condition.name] or self.relationships[condition.name] < condition.value) then
allCriteriaMet = false
break
elseif condition.type == "loreKey" and (not self.loreKeys[condition.name] or self.loreKeys[condition.name] ~= condition.value) then
allCriteriaMet = false
break
end
end
eventResults[eventName] = allCriteriaMet
end
return eventResults
end
---------------------- Example Usage ----------------------
-- -- setup base attributes, relationships, and loreKeys
-- local base_attributes, base_relationships, base_loreKeys = PlayerData.get_base_meta()
-- -- set some initial values for the player
-- base_attributes.strength = 2
-- base_attributes.dexterity = 1
-- base_attributes.constitution = 1
-- base_attributes.intelligence = 0
-- base_attributes.wisdom = 0
-- base_attributes.charisma = 2
-- -- add a new attribute
-- base_attributes.generosity = 0
-- base_attributes.honesty = 0
-- -- set some initial relationships
-- base_relationships.Alex = 2
-- base_relationships.Jordan = -2
-- local player = PlayerData.new(base_attributes, base_relationships, base_loreKeys)
-- player:set_debug_mode(true)
-- -- Updating attributes and relationships
-- player:update_attribute("generosity", 5)
-- player:update_relationship("Alex", 3)
-- -- Setting a lore key
-- player:set_lore_key("discovered_secret", true)
-- -- Defining criteria for multiple events
-- local eventCriteria = {
-- event1 = {
-- {type = "attribute", name = "generosity", value = 5},
-- {type = "relationship", name = "Alex", value = 3}
-- },
-- event2 = {
-- {type = "relationship", name = "Jordan", value =3},
-- {type = "loreKey", name = "discovered_secret", value = true}
-- },
-- event3 = {
-- {type = "attribute", name = "honesty", value = 4},
-- {type = "loreKey", name = "joined_guild", value = true}
-- }
-- }
-- -- Evaluate criteria for events
-- local eventResults = player:meets_criteria(eventCriteria)
-- for eventName, met in pairs(eventResults) do
-- print(eventName .. ": " .. tostring(met))
-- end
return PlayerData
-- Copyright (C) 2024 Theros <https://github.com/therosin>
--
-- This file is part of LuaWorldStateSystem.
--
-- LuaWorldStateSystem is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- LuaWorldStateSystem is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with LuaWorldStateSystem. If not, see <https://www.gnu.org/licenses/>.
local function positionalHashing(hexString)
local finalNumber = 0
for i = 1, #hexString do
local char = hexString:sub(i, i)
local numValue = tonumber(char, 16) or 0
finalNumber = (finalNumber + (numValue * i)) % (2 ^ 53)
end
return finalNumber
end
local function createEntropyGenerator(length, outputAsHex)
local lastOutput = nil -- Store the last output to feed into the next generation
return coroutine.wrap(function()
while true do
local entropyChunk = "" -- Accumulate entropy here
while #entropyChunk < length do
local timeSource = os.clock() + os.time()
local randomFactor = math.random()
-- Incorporate the last output into the entropy source, if available
local combinedEntropy = timeSource * randomFactor * 10000
if lastOutput then
if type(lastOutput) == 'string' then
combinedEntropy = combinedEntropy + positionalHashing(lastOutput)
elseif type(lastOutput) == 'number' then
combinedEntropy = combinedEntropy + lastOutput
end
end
-- Generate entropy chunk
local entropyHex = string.format("%x", math.floor(combinedEntropy))
entropyChunk = entropyChunk .. entropyHex -- Concatenate to accumulate
-- Ensure lastOutput updates even within the loop for each chunk added
lastOutput = entropyHex
end
-- Now, entropyChunk is at least 'length' long. Trim if it exceeds.
local outputEntropy = entropyChunk:sub(1, length)
-- Update lastOutput for next generation cycle
if outputAsHex then
lastOutput = outputEntropy
coroutine.yield(outputEntropy)
else
-- Convert the hex string to a number for numeric output
local numericOutput = positionalHashing(outputEntropy)
lastOutput = outputEntropy
coroutine.yield(numericOutput)
end
end
end)
end
local entropyGenerator = createEntropyGenerator(24, true)
--- Generates a random UUIDv4 string
--- NOTE: in the template, 'x' is replaced with a random hex digit (0-9, a-f) and 'y' is replaced with a random hex digit (8-b) this generally conforms to the UUIDv4 spec
--- any other character is left as is. The default template is 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
---@param template? string Optional: a template to generate the UUID from.
---@param debug? boolean Optional: print debug information
---@return string
local function uuidv4(template, debug)
template = template or 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
local hexEntropy = entropyGenerator() -- collect entropy from the generator - 24 hex digits at a time.
local counter = 1 -- To iterate through the entropy hex string
local id = string.gsub(template, '[xy]', function(c)
local v = hexEntropy:sub(counter, counter)
counter = counter + 1
if debug then
print('Replacing', c, 'with', v)
end
return v
end)
return id
end
-- testing
local function test(count)
print("Hex Entropy Generator")
local entropy = createEntropyGenerator(24, true)
for i = 1, 5 do
print(i, entropy())
end
print("Numeric Entropy Generator")
local numEntropy = createEntropyGenerator(24, false)
for i = 1, 5 do
print(i, numEntropy())
end
count = count or 1000
local generated = {}
for i = 1, count do
local id = uuidv4(nil, false)
table.insert(generated, id)
end
local collisions = {}
for i = 1, #generated do
for j = i + 1, #generated do
if generated[i] == generated[j] then
table.insert(collisions, { i, j })
end
end
end
for i = 1, #generated do
print(string.format('Generated ID: %s', generated[i]))
end
if #collisions == 0 then
print('No collisions found')
else
print(string.format('Found %d collisions', #collisions))
for i = 1, #collisions do
local collision = collisions[i]
print(string.format('Collision found: [index: %d, id: %s] [index: %d, id: %s]', collision[1],
generated[collision[1]], collision[2], generated[collision[2]]))
end
end
end
-- test(10000) -- Test with 10,000 generated IDs (no collisions expected, default: 1000)
return setmetatable({
createEntropyGenerator = createEntropyGenerator,
uuidv4 = uuidv4,
test = test
}, {
positionalHashing = positionalHashing
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment