--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local trove = require(script.trove) -- Change this to the path of your trove if you have one already, or change the code if you have a different garbage collector, otherwise ignore this.
local Signal = require(ReplicatedStorage.Packages.Signal)
export type Key = Player|Model
export type User = Player|Model -- Model is NPC, Player is Player.
export type State = string
export type Value = true | string
local API = {}
local StateTokens = {} :: {
[User]: {[State]: string}
}
API.StateAdded = Signal.new()
API.StateRemoved = Signal.new()
API.UserStatesRemoved = Signal.new()
API.Registry = {} :: {
[User]: {[State]: Value}
}
local function DeepCopy(tbl): any
if type(tbl) ~= "table" then return tbl end
local clone = {}
for k, v in pairs(tbl) do
clone[DeepCopy(k)] = DeepCopy(v)
end
return clone
end
local function GetUser(Key: Key): User?
if Key then
return (Key:IsA("Player")) and Key or (Players:GetPlayerFromCharacter(Key)) and Players:GetPlayerFromCharacter(Key) or (Key:IsA("Model") and Key:FindFirstChild("Humanoid")) and Key
end
return
end
local function GenerateToken(): string
return HttpService:GenerateGUID()
end
--Reconciles a user's states, used for when the states are nil for a user.
function API.ReconcileUser(self: API, Key: Key, EventSignal: RBXScriptSignal?)
local User = GetUser(Key)
if not User then warn("API.ReconcileUser: User not found") return end
if not User.Parent then warn("API.ReconcileUser: User's parent not found") return end
self.Registry[User] = self.Registry[User] or {}
StateTokens[User] = StateTokens[User] or {}
if EventSignal then
EventSignal:Once(function()
self:Clean(User)
end)
end
end
--Returns the given state of a user (or nil).
function API.hasState(self: API, Key: Key, State: State?): boolean
local User = GetUser(Key)
if not User then warn("API.hasState: User not given") return false end
if not User.Parent then warn("API.hasState: User's parent not found") return false end
if not State then warn("API.hasState: State not given") return false end
if not self.Registry[User] then
self:ReconcileUser(User)
end
return self.Registry[User][State] and true or false
end
--Returns a copy of the user's states.
function API.fetchUserStates(self: API, Key: Key): {[State]: Value}?
local User = GetUser(Key)
if not User then warn("API.fetchUserStates: User not given") return end
if not User.Parent then warn("API.fetchUserStates: User's parent not found") return end
if not self.Registry[User] then
self:ReconcileUser(User)
end
return DeepCopy(self.Registry[User]) :: {[State]: Value}
end
--Requires an array of states, the method returns all the states that the user has, as a dictionary.
function API.validateStates(self: API, Key: Key, States: {string}): {[State]: boolean}?
local User = GetUser(Key)
if not User then warn("API.validateStates: User not given") return end
if not User.Parent then warn("API.validateStates: User's parent not found") return end
if not self.Registry[User] then
self:ReconcileUser(User)
end
local areValid: {[State]: boolean} = {}
for _,State in States do
if self.Registry[User][State] then
areValid[State] = true
end
end
return areValid
end
--Adds a given state to the user with a given value or true.
--Also fires the StateAdded signal.
--Supports duration.
--Can give a signal that onced first will Remove the state with a safety net.
function API.AddState(self: API, Key: Key, State: State?, Value: Value?, Duration: number?, EventSignal: RBXScriptSignal?)
local User = GetUser(Key)
if not User then warn("API.AddState: User not given") return end
if not User.Parent then warn("API.AddState: User's parent not found") return end
if not State then warn("API.AddState: State not given") return end
if not self.Registry[User] then
self:ReconcileUser(User)
end
local Token = GenerateToken()
StateTokens[User][State] = Token
self.Registry[User][State] = Value or true
self.StateAdded:Fire(User,State,Value or true)
if EventSignal then
local CleanSignals = trove.new()
CleanSignals:Add(EventSignal:Once(function()
if StateTokens[User] and StateTokens[User][State] == Token then
self:RemoveState(User, State)
end
CleanSignals:Destroy()
end))
CleanSignals:Add(self.StateRemoved:Connect(function(_User, _State)
if (User :: Instance) == (_User :: Instance) and State == _State then
CleanSignals:Destroy()
end
end))
CleanSignals:Add(self.UserStatesRemoved:Connect(function(_User, _States)
if (User :: Instance) == (_User :: Instance) then
CleanSignals:Destroy()
end
end))
end
if Duration then
task.delay(Duration,function()
if StateTokens[User] and StateTokens[User][State] == Token then
StateTokens[User][State] = nil
self:RemoveState(User,State)
end
end)
end
end
--Removes a given state from the user.
--Also fires the StateRemoved signal.
function API.RemoveState(self: API, Key: Key, State: State)
local User = GetUser(Key)
if not User then warn("API.RemoveState: User not given") return end
if not State then warn("API.RemoveState: State not given") return end
if not self.Registry[User] then return end
if not self.Registry[User][State] then return end
if StateTokens[User] then
StateTokens[User][State] = nil
end
self.Registry[User][State] = nil
self.StateRemoved:Fire(User,State)
end
--Sets the user's states to an empty table.
--Fires the UserStatesRemoved signal that contains the user and all states that got removed
function API.clearUserStates(self: API, Key: Key)
local User = GetUser(Key)
if not User then warn("API.clearUserStates: User not given") return end
if not User.Parent then warn("API.clearUserStates: User's parent not found") return end
local States = self:fetchUserStates(User)
self.UserStatesRemoved:Fire(User,States)
self.Registry[User] = {}
StateTokens[User] = {}
end
--Removes the user from the API.
function API.Clean(self: API, Key: Key)
local User = GetUser(Key)
if not User then warn("API.Clean: User not given") return end
local States = self.Registry[User]
if States then
self.UserStatesRemoved:Fire(User, DeepCopy(States) :: {[State]: Value})
end
self.Registry[User] = nil
StateTokens[User] = nil
end
type API = typeof(API)
return API
--Keep in mind that the API does not support client-server communication. This means that a State on the server will not be replicated to the client and vice-versa.
Update 1.0
- The API now supports characters. i.e that is a player’s character can now be passed down as a key
- The player instance can also be passed down
- The API will treat the character the same as the player
- This means that even if you Reconcile with a Character as the key, the API will register it with the player
https://create.roblox.com/store/asset/72473631873859/StatesAPI