StatesAPI ~ For any project

--!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