# Types

PlayerState uses TypeScript-style type annotations for better code safety and IntelliSense support.

## Core Data Types

### PlayerData

The main data structure for player information, based on your DefaultData module.

lua

    export type PlayerData = typeof(DefaultData)

1

**Example DefaultData structure:**

lua

        Coins = 0,
        Level = 1,
        Experience = 0,
        Inventory = {},
        Settings = {
            MusicEnabled = true,
            SoundEnabled = true,
        },
        Plot = {
            Likes = 0,
            Size = 1,
            Buildings = {},
        },
        Stats = {
            TotalPlayTime = 0,
            GamesPlayed = 0,
            HighScore = 0,
        },
        Achievements = {},

        -- NEW: Leaderstats configuration
        leaderstats = {
            ["💰 Coins"] = "Coins",
            ["⭐ Level"] = "Level",
            ["👍 Likes"] = "Plot.Likes"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

## Path Types

### ValuePath

String type for dot-separated data paths.

lua

    export type ValuePath = string

1

**Examples:**

lua

    "Coins"              -- Top-level access
    "Plot.Likes"         -- Nested access
    "Settings.MusicEnabled"  -- Deep nesting
    "Stats.HighScore"    -- Any depth
    "Plot.Buildings.House.Level"  -- Complex nesting

1
2
3
4
5

### ArrayPath

String type for paths to array/list data structures.

lua

    export type ArrayPath = string

1

**Examples:**

lua

    "Inventory"          -- Top-level array
    "Plot.Buildings"     -- Nested array
    "Achievements"       -- List of achievements
    "Stats.RecentScores" -- Nested score array

1
2
3
4

### DictPath

String type for paths to dictionary/table data structures.

lua

    export type DictPath = string

1

**Examples:**

lua

    "Settings"           -- Settings dictionary
    "Plot.Buildings"     -- Building configuration
    "Stats"              -- Player statistics
    "Inventory.Equipped" -- Equipment slots

1
2
3
4

## Batch Operation Types

### BatchOperation

Type for individual batch operations used in `BatchSetValues()`.

lua

    export type BatchOperation = {
        path: ValuePath,
        value: any,

1
2
3
4

**Example:**

lua

    local operations: {BatchOperation} = {
        {path = "Coins", value = 1000},
        {path = "Level", value = 5},
        {path = "Plot.Likes", value = 50},
        {path = "Settings.MusicEnabled", value = true}

1
2
3
4
5
6

### BatchUpdateOperation

Type for increment/decrement operations used in `BatchUpdateValues()`.

lua

    export type BatchUpdateOperation = {
        path: ValuePath,
        operation: "Increment" | "Decrement",
        amount: number?,

1
2
3
4
5

**Example:**

lua

    local operations: {BatchUpdateOperation} = {
        {path = "Coins", operation = "Increment", amount = 100},
        {path = "Lives", operation = "Decrement"},
        {path = "Plot.Likes", operation = "Increment", amount = 5}

1
2
3
4
5

### LeaderboardEntry

Type for leaderboard entries returned by leaderboard functions.

lua

    export type LeaderboardEntry = {
        userId: number,
        score: number,
        rank: number,

1
2
3
4
5

**Example:**

lua

    local leaderboardEntry: LeaderboardEntry = {
        userId = 123456789,
        score = 1500,
        rank = 1

    -- Leaderboard array
    local leaderboard: {LeaderboardEntry} = {
        {userId = 123456789, score = 1500, rank = 1},
        {userId = 987654321, score = 1200, rank = 2},
        {userId = 555666777, score = 1000, rank = 3}

    ### LeaderboardInfo

    Type for the local player’s leaderboard info synced to the client.

    ```lua
    export type LeaderboardInfo = {
        rank: number?,
        score: number?,
        statName: string,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

**Notes:**

  * This is derived from the replicated `_LeaderboardRanks` table.
  * It is **temporary display data** and is not intended to be saved into player data.

## Change Information Types

### ChangeInfo

Enhanced type for change information in client listeners.

lua

    export type ChangeInfo = {
        action: "Set" | "SetValues" | "TableInsert" | "TableRemove",
        path: {string},
        index: number?,

1
2
3
4
5

**Action Types:**

  * `"Set"`: Single value change
  * `"SetValues"`: Multiple values changed
  * `"TableInsert"`: Item added to array
  * `"TableRemove"`: Item removed from array

**Properties:**

  * `action`: The type of change that occurred
  * `path`: Array of path segments that changed
  * `index`: Optional index for array operations

**Example Usage:**

lua

    PlayerState.OnChanged(".", function(newValue, oldValue, info)
        if info and info.action == "TableInsert" then
            print(`Item added at index {info.index} to {table.concat(info.path, ".")}`)
        end
    end)

1
2
3
4
5

## Server Interface Types

### PlayerStateServer

The enhanced server module interface with all available functions.

lua

    export type PlayerStateServer = {
        -- Initialization
        Init: (player: Player, existingData: PlayerData?) -> boolean,

        -- Basic operations
        Set: (player: Player, key: string, value: any) -> boolean,
        Get: (player: Player, key: string) -> any,

        -- Path operations
        SetPath: (player: Player, path: ValuePath, value: any) -> boolean,
        GetPath: (player: Player, path: ValuePath) -> any,

        -- Batch operations
        SetValues: (player: Player, values: {[string]: any}) -> boolean,
        BatchSetValues: (player: Player, operations: {BatchOperation}) -> boolean,
        FlushBatch: (player: Player) -> boolean,

        -- Array operations
        AddToArray: (player: Player, arrayPath: ArrayPath, item: any) -> boolean,
        RemoveFromArray: (player: Player, arrayPath: ArrayPath, index: number) -> boolean,
        UpdateArrayItem: (player: Player, arrayPath: ArrayPath, index: number, newItem: any) -> boolean,

        -- Dictionary operations
        SetInDict: (player: Player, dictPath: DictPath, key: string, value: any) -> boolean,
        GetFromDict: (player: Player, dictPath: DictPath, key: string | number) -> any,
        RemoveFromDict: (player: Player, dictPath: DictPath, key: string) -> boolean,

        -- Cache management
        ClearPathCache: () -> (),

        -- Utility functions
        GetReplica: (player: Player) -> ReplicaInstance?,
        GetProfile: (player: Player) -> ProfileStoreProfile?,
        GetAll: (player: Player) -> PlayerData?,
        SaveData: (player: Player) -> boolean,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

### ProfileStoreProfile

Enhanced ProfileStore profile interface for data persistence.

lua

    export type ProfileStoreProfile = {
        Data: PlayerData,
        OnSessionEnd: RBXScriptSignal,
        AddUserId: (self: ProfileStoreProfile, userId: number) -> (),
        Reconcile: (self: ProfileStoreProfile) -> (),
        EndSession: (self: ProfileStoreProfile) -> (),
        IsActive: (self: ProfileStoreProfile) -> boolean,

1
2
3
4
5
6
7
8

**Key Properties:**

  * `Data`: The actual player data
  * `OnSessionEnd`: Event fired when profile session ends
  * `AddUserId()`: Associates user ID with profile
  * `Reconcile()`: Merges template changes with existing data
  * `EndSession()`: Manually end the profile session
  * `IsActive()`: Check if profile is still active

**Notes:**

  * **NEW:** Enhanced validation for active status
  * **NEW:** Better session management
  * **NEW:** Automatic cleanup on session end

## Client Interface Types

### PlayerStateClient

The enhanced client module interface with all available functions.

lua

    export type PlayerStateClient = {
        -- Data access
        Get: (key: string) -> any,
        GetPath: (path: ValuePath) -> any,
        GetFromDict: (dictPath: ValuePath, key: string | number) -> any,
        Clone: (value: any, deep: boolean?) -> any,
        GetAll: () -> PlayerData?,
        GetLeaderboardInfo: (statName: string) -> LeaderboardInfo?,
        GetLeaderboard: (statName: string, limit: number?) -> {LeaderboardEntry},

        -- Status checking
        IsReady: () -> boolean,

        -- Cache management
        ClearCache: () -> (),

        -- Change listeners
        OnChanged: (pathOrKey: string, callback: (newValue: any, oldValue: any, changeInfo: ChangeInfo | {string}?) -> ()) -> ReplicaClient.Connection?,

        -- Advanced access
        GetReplica: () -> ReplicaInstance?,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

**Key Changes:**

  * **NEW:** `IsReady()` for status checking
  * **NEW:** `ClearCache()` for manual cache management
  * **NEW:** Enhanced `OnChanged()` with `ChangeInfo` support
  * **NEW:** Better error handling and validation

### Enhanced Change Callback

Type for change listener callback functions with enhanced information.

lua

    type ChangeCallback = (newValue: any, oldValue: any, changeInfo: ChangeInfo | {string}?) -> ()

1

**Parameters:**

  * `newValue`: The new value after the change
  * `oldValue`: The previous value before the change
  * `changeInfo`: Enhanced change information with action details OR simple path array

**Example with Action Handling:**

lua

    local connection = PlayerState.OnChanged("Inventory", function(newValue, oldValue, info)
        if info and typeof(info) == "table" and info.action then
            -- Enhanced change info
            if info.action == "TableInsert" then
                print(`Item added at index {info.index}`)
            elseif info.action == "TableRemove" then
                print(`Item removed from index {info.index}`)
            end
        else
            -- Simple path info
            print("Inventory changed")
        end
    end)

1
2
3
4
5
6
7
8
9
10
11
12
13

## Replica System Types

### ReplicaInstance

The Replica object used for real-time synchronization.

lua

    export type ReplicaInstance = ReplicaServer.Replica -- or ReplicaClient.Replica

1

**Common Properties:**

  * `Data`: The synchronized data
  * `Tags`: Metadata tags (UserId, PlayerName, etc.)
  * `OnChange`: Advanced change listener
  * `Subscribe/Unsubscribe`: Client subscription management
  * **NEW:** Enhanced validation methods
  * **NEW:** Better error handling

### ReplicaConnection

Connection object returned by change listeners.

lua

    export type ReplicaConnection = ReplicaClient.Connection

1

**Methods:**

  * `Disconnect()`: Remove the listener and clean up

**Notes:**

  * **NEW:** Enhanced connection validation
  * **NEW:** Automatic cleanup on disconnect
  * **NEW:** Better memory management

## Configuration Types

### Enhanced Profile Configuration

lua

    type ProfileConfig = {
        Key: string,        -- Base key for ProfileStore
        Template: PlayerData, -- Default data template
        GlobalUpdates: boolean, -- Enable global updates

1
2
3
4
5

### Enhanced DataStore Configuration

lua

    type DataStoreConfig = {
        Name: string,       -- DataStore name
        Scope: "Production" | "Testing", -- Environment scope

1
2
3
4

### Complete Configuration with New Options

lua

    type PlayerStateConfig = {
        Profile: ProfileConfig,
        DataStore: DataStoreConfig,
        CleanExtraFields: boolean,  -- Remove fields not in template during reconciliation
        DataWaitTimeout: number,    -- Seconds to wait for player data before timeout
        BatchDelay: number,         -- Batch processing delay in seconds
        BatchSize: number,          -- Auto-flush batch when this many operations are queued
        MaxCacheSize: number,       -- Maximum number of cached path keys
        Leaderboard: {              -- Leaderboard configuration
            Enabled: boolean,           -- Enable/disable leaderboard functionality
            DataStoreName: string,      -- DataStore for leaderboard data
            TrackedStats: {string},     -- Stats to track on leaderboards
            SyncInterval: number,       -- Leaderboard sync frequency in seconds
            UpdateOnPlayerLeave: boolean, -- Update leaderboards when players leave
        },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

**Default Configuration:**

lua

    -- PlayerStateConfig.lua
    local Config = {
        Server = {
            Profile = {
                Key = "PlayerData",
                Template = DefaultData,
            },
            DataStore = {
                Name = "PlayerData_0.01",
                Scope = "Production",
            },
            CleanExtraFields = false,
            DataWaitTimeout = 10,
            BatchDelay = 0.03,      -- 30ms batch delay
            BatchSize = 20,         -- Auto-flush at 20 operations
            MaxCacheSize = 1000,    -- Maximum cache entries

            Leaderboard = {
                Enabled = true,
                DataStoreName = "PlayerState_Leaderboards",
                TrackedStats = {
                    "Gems",
                    "Coins",
                    -- Add stat names here to create leaderboards
                },
                SyncInterval = 60,
                UpdateOnPlayerLeave = true,
            },
        },
        Client = {
            CacheDuration = 0.1,
            CacheCleanupInterval = 30,
            MaxCacheSize = 1000,
            MaxPathCacheSize = 1000,
        },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

## Leaderstats Types

### LeaderstatsConfig

Configuration type for automatic leaderstats integration.

lua

    export type LeaderstatsConfig = {
        [string]: ValuePath  -- Display name -> data path

1
2
3

**Example:**

lua

    local leaderstatsConfig: LeaderstatsConfig = {
        ["💰 Coins"] = "Coins",
        ["⭐ Level"] = "Level",
        ["👍 Likes"] = "Plot.Likes",
        ["🏆 High Score"] = "Stats.HighScore"

1
2
3
4
5
6

**Notes:**

  * Keys are the display names shown in leaderstats
  * Values are paths to the actual data
  * Supports nested paths with dot notation
  * Numbers automatically use IntValue, others use StringValue

## Type-Safe Usage Examples

### Server Type Safety

lua

    local PlayerState: PlayerStateServer = require(...)

    -- Type-safe function with proper return types
    local function awardCoins(player: Player, amount: number, source: string): boolean
        local currentCoins: number = PlayerState.Get(player, "Coins") or 0

        if amount <= 0 then
            warn(`Invalid coin amount: {amount}`)
            return false
        end

        local success = PlayerState.Set(player, "Coins", currentCoins + amount)
        if success then
            print(`Awarded {amount} coins to {player.Name} from {source}`)
        end

        return success
    end

    -- Type-safe batch operations
    local function levelUpPlayer(player: Player): boolean
        local operations: {BatchOperation} = {
            {path = "Level", value = (PlayerState.Get(player, "Level") or 1) + 1},
            {path = "Experience", value = 0},
            {path = "Coins", value = (PlayerState.Get(player, "Coins") or 0) + 500}

        return PlayerState.BatchSetValues(player, operations)
    end

    -- Type-safe data retrieval
    local function getPlayerStats(player: Player): {Level: number, Experience: number, Coins: number}?
        local allData: PlayerData? = PlayerState.GetAll(player)

        if not allData then
            return nil
        end

        return {
            Level = allData.Level or 1,
            Experience = allData.Experience or 0,
            Coins = allData.Coins or 0
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

### Client Type Safety

lua

    local PlayerState: PlayerStateClient = require(...)

    -- Type-safe change listeners with enhanced info
    local function setupCoinDisplay(label: TextLabel): ReplicaConnection?
        return PlayerState.OnChanged("Coins", function(newValue: any, oldValue: any, info: ChangeInfo | {string}?)
            local coins: number = newValue or 0
            label.Text = tostring(coins)

            -- Handle enhanced change info
            if info and typeof(info) == "table" and info.action then
                if oldValue and coins > oldValue then
                    showCoinGainEffect(coins - oldValue)
                end
            end
        end)
    end

    -- Type-safe global change listener
    local function setupGlobalListener(): ReplicaConnection?
        return PlayerState.OnChanged(".", function(newValue: any, oldValue: any, info: ChangeInfo | {string}?)
            if info and typeof(info) == "table" and info.action then
                local changeInfo = info :: ChangeInfo
                local pathString = table.concat(changeInfo.path, ".")

                if changeInfo.action == "TableInsert" then
                    print(`Item added to {pathString} at index {changeInfo.index}`)
                elseif changeInfo.action == "TableRemove" then
                    print(`Item removed from {pathString} at index {changeInfo.index}`)
                end
            end
        end)
    end

    -- Type-safe data access with readiness check
    local function getInventoryCount(): number?
        if not PlayerState.IsReady() then
            return nil
        end

        local inventory: {any} = PlayerState.GetPath("Inventory") or {}
        return #inventory
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

## Custom Type Extensions

### Custom Item Types

You can extend these types for your specific use case:

lua

    -- Custom item type for inventory
    export type InventoryItem = {
        Id: string,
        Name: string,
        Rarity: "Common" | "Rare" | "Epic" | "Legendary",
        Level: number,
        Attributes: {[string]: any}?,
        AcquiredTime: number?,
        UpgradedTime: number?,

    -- Custom plot building type
    export type PlotBuilding = {
        Type: string,
        Level: number,
        Position: Vector3,
        Rotation: number,
        Upgrades: {string},
        BuiltTime: number,
        Health: number,

    -- Extended player data with custom types
    export type ExtendedPlayerData = PlayerData & {
        Inventory: {InventoryItem},
        Plot: {
            Likes: number,
            Size: number,
            Buildings: {[string]: PlotBuilding},
        },
        Stats: {
            TotalPlayTime: number,
            GamesPlayed: number,
            HighScore: number,
            RecentScores: {number},

    -- Batch operation with custom validation
    export type ValidatedBatchOperation = BatchOperation & {
        validate: () -> boolean,
        description: string,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

## Type Guards and Validation

### Runtime Type Checking

Utility functions for runtime type checking:

lua

    -- Check if value is a valid inventory item
    local function isInventoryItem(value: any): boolean
        return typeof(value) == "table"
            and typeof(value.Id) == "string"
            and typeof(value.Name) == "string"
            and typeof(value.Rarity) == "string"
            and typeof(value.Level) == "number"
    end

    -- Check if change info is enhanced
    local function isChangeInfo(value: any): boolean
        return typeof(value) == "table"
            and typeof(value.action) == "string"
            and typeof(value.path) == "table"
    end

    -- Type-safe array access
    local function getInventoryItem(index: number): InventoryItem?
        if not PlayerState.IsReady() then
            return nil
        end

        local inventory = PlayerState.GetPath("Inventory")

        if typeof(inventory) ~= "table" or not inventory[index] then
            return nil
        end

        local item = inventory[index]
        return isInventoryItem(item) and item or nil
    end

    -- Validate batch operations
    local function validateBatchOperations(operations: {BatchOperation}): boolean
        for _, operation in operations do
            if typeof(operation.path) ~= "string" or operation.value == nil then
                return false
            end

            -- Check for leaderstats paths
            if operation.path == "leaderstats" or string.sub(operation.path, 1, 12) == "leaderstats." then
                warn(`Invalid batch operation: cannot modify leaderstats path {operation.path}`)
                return false
            end
        end

        return true
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

## Performance Type Considerations

### Caching Types

lua

    -- Cache entry types
    type CacheEntry<T> = {
        value: T,
        timestamp: number,

    type PathCache = {[string]: {string}}
    type ValueCache<T> = {[string]: CacheEntry<T>}

1
2
3
4
5
6
7
8

### Connection Management Types

lua

    -- Connection manager types
    type ConnectionManager = {
        connections: {[string]: ReplicaConnection},
        add: (name: string, connection: ReplicaConnection) -> (),
        remove: (name: string) -> (),
        cleanup: () -> (),

1
2
3
4
5
6
7

These enhanced types provide better IntelliSense support, catch potential errors at development time, and make the codebase more maintainable with clear interfaces and expectations.


# Server Functions

Server-side PlayerState API for data management and persistence.

Server Only - Security

**Server scripts only.** No built-in RemoteEvents - create your own with validation.
**Never trust client data!**

Studio: ViewedUser

When using **ViewedUser** in PlayerStateConfig (Studio-only), setting **ReadOnly** to `true` causes all write APIs (`Set`, `SetPath`, `SetValues`, batch ops, `Increment`, `Decrement`, array/dict writes, `WipePlayerData`, etc.) to return `false` and warn. Reads and Replica subscriptions still work. See [Setup — ViewedUser](https://playerstate.netlify.app/setup#vieweduser-studio-only).

## Initialization Functions

### Init()

`Init(player, existingData?)`

Initializes PlayerState for a player. Must be called when a player joins.

**Parameters:**

  * `player: Player` \- The player to initialize
  * `existingData?: PlayerData` \- Optional existing data (not currently supported)

**Returns:** `boolean` \- Success status

Example

lua

    local success = PlayerState.Init(player)
    if success then
        print("Player data loaded successfully")
    else
        warn("Failed to load player data")
        -- Player will be kicked automatically
    end

1
2
3
4
5
6
7

**Notes:**

  * Automatically loads data from ProfileStore and creates Replica for client sync
  * Handles ProfileStore session management with automatic leaderstats integration
  * Enhanced data validation and cleanup
  * Player will be kicked if data fails to load

## Basic Data Functions

### Set()

`Set(player, key, value) → boolean`

Sets a top-level data value with enhanced validation.

**Parameters:** `player: Player`, `key: string`, `value: any`
**Returns:** `boolean` \- Success status (false if leaderstats modification attempted)

Example

lua

    local success = PlayerState.Set(player, "Coins", 1000)
    if success then
        print("Coins updated successfully")
    else
        warn("Failed to update coins")
    end

1
2
3
4
5
6

Note

Use `SetPath()` for nested paths instead of dot notation

### Get()

`Get(player, key) → any`

Gets a top-level data value with validation. Automatically clones tables when `AutoCloneTables` is enabled in configuration.

**Parameters:** `player: Player`, `key: string`
**Returns:** `any` \- The value, or `nil` if not found. Tables are cloned if `AutoCloneTables` is enabled.

Example

lua

    local coins = PlayerState.Get(player, "Coins")
    local level = PlayerState.Get(player, "Level")
    print(`Player {player.Name} has {coins or 0} coins and is level {level or 1}`)

    -- With AutoCloneTables enabled, tables are automatically cloned
    local inventory = PlayerState.Get(player, "Inventory") -- Safe to modify
    table.insert(inventory, newItem) -- Won't affect original if AutoCloneTables is true

1
2
3
4
5
6
7

Note

  * When `AutoCloneTables` is enabled in config, table values are automatically deep cloned
  * Prevents accidental modification of internal data structures
  * Configure in `PlayerStateConfig.Server.AutoCloneTables`

## Path-based Functions

### SetPath()

`SetPath(player, path, value) → boolean`

Sets a nested data value using dot notation with enhanced validation.

**Parameters:** `player: Player`, `path: ValuePath`, `value: any`
**Returns:** `boolean` \- Success status

Example

lua

    local success = PlayerState.SetPath(player, "Plot.Likes", 50)
    if success then
        print("Plot likes updated")
    end

    PlayerState.SetPath(player, "Settings.MusicEnabled", false)
    PlayerState.SetPath(player, "Stats.HighScore", 1200)

1
2
3
4
5
6
7

Note

Returns success status with optimized path parsing and caching

### GetPath()

`GetPath(player, path) → any`

Gets a nested data value using dot notation with optimized performance. Automatically clones tables when `AutoCloneTables` is enabled in configuration.

**Parameters:** `player: Player`, `path: ValuePath`
**Returns:** `any` \- The value, or `nil` if not found. Tables are cloned if `AutoCloneTables` is enabled.

Example

lua

    local likes = PlayerState.GetPath(player, "Plot.Likes")
    local musicEnabled = PlayerState.GetPath(player, "Settings.MusicEnabled")
    local highScore = PlayerState.GetPath(player, "Stats.HighScore")

    -- With AutoCloneTables enabled, tables are automatically cloned
    local settings = PlayerState.GetPath(player, "Settings") -- Safe to modify
    settings.NewOption = true -- Won't affect original if AutoCloneTables is true

1
2
3
4
5
6
7

Note

  * Optimized nested value retrieval with enhanced path caching system
  * When `AutoCloneTables` is enabled in config, table values are automatically deep cloned
  * Prevents accidental modification of internal data structures
  * Configure in `PlayerStateConfig.Server.AutoCloneTables`

## Change Subscription Functions

### OnChanged()

`OnChanged(player, pathOrKey, callback) → Connection?`

Subscribes to changes in a specific player's profile.

**Parameters:**

  * `player: Player` \- The player to watch
  * `pathOrKey: string` \- A top-level key (e.g. `"Coins"`) or a dot-separated path (e.g. `"Inventory.Slots"`)
  * `callback: (newValue, oldValue, info) -> ()` \- Fired when data at the path changes

**Returns:** `Connection?` \- Connection to disconnect when done, or `nil` if invalid (player invalid, path not a string, or callback not a function)

The callback receives `(newValue, oldValue, info)` where `info` includes:

  * `action: string` \- `"Set"`, `"SetValues"`, `"TableInsert"`, or `"TableRemove"`
  * `path` \- Path of the change
  * `index` (optional) - For array operations

Using the root path `"."` subscribes to every change on that profile. Not recommended except for debugging, since it fires on every mutation and can hurt performance. Prefer listening on the narrowest path you need (e.g. `"Coins"` or `"Mailbox.UnreadCount"`).

Example

lua

    local connection = PlayerState.OnChanged(player, "Coins", function(newValue, oldValue, info)
        print(`Coins: {oldValue} -> {newValue} ({info.action})`)
    end)

    PlayerState.OnChanged(player, "Inventory.Slots", function(newValue, oldValue, info)
        if info.action == "TableInsert" then
            print(`Item added at index {info.index}`)
        end
    end)

    game.Players.PlayerRemoving:Connect(function(leavingPlayer)
        if leavingPlayer == player and connection then
            connection:Disconnect()
        end
    end)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Disconnect When Done

Call `connection:Disconnect()` when the listener is no longer needed (e.g. when the player leaves or the feature is destroyed) to avoid leaks and unnecessary work.

## Numeric Operation Functions

### Increment()

`Increment(player, key, amount?) → boolean`

Increments a numeric value with validation. Works with both top-level keys and nested paths.

**Parameters:** `player: Player`, `key: string`, `amount: number?` (defaults to 1)
**Returns:** `boolean` \- Success status

Example

lua

    -- Increment top-level values
    local success = PlayerState.Increment(player, "Coins", 100)
    if success then
        print("Coins increased by 100")
    end

    -- Increment by 1 (default)
    PlayerState.Increment(player, "Level")

    -- Increment nested values using paths
    PlayerState.Increment(player, "Plot.Likes", 5)
    PlayerState.Increment(player, "Stats.HighScore", 250)
    PlayerState.Increment(player, "Stats.GamesPlayed") -- defaults to +1

1
2
3
4
5
6
7
8
9
10
11
12
13

Note

Only works with numeric values - warns if value is not a number

Numeric Values Only

`Increment()` only works with numeric values. Attempting to increment a non-numeric value will return `false` and show a warning.

### Decrement()

`Decrement(player, key, amount?) → boolean`

Decrements a numeric value with validation. Works with both top-level keys and nested paths.

**Parameters:** `player: Player`, `key: string`, `amount: number?` (defaults to 1)
**Returns:** `boolean` \- Success status

Example

lua

    -- Spend coins (top-level)
    local success = PlayerState.Decrement(player, "Coins", 50)
    if success then
        print("Coins decreased by 50")
    else
        warn("Failed to spend coins")
    end

    -- Decrement by 1 (default)
    PlayerState.Decrement(player, "Lives")

    -- Decrement nested values using paths
    PlayerState.Decrement(player, "Plot.Health", 10)
    PlayerState.Decrement(player, "Stats.Attempts", 1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

Note

Internally uses `Increment()` with negative amounts

Performance & Safety

Use `Increment()` and `Decrement()` for numeric operations instead of `Get()` \+ `Set()` patterns. They're safer and more efficient!

lua

    -- ❌ Less efficient and less safe
    local coins = PlayerState.Get(player, "Coins")
    PlayerState.Set(player, "Coins", coins + 100)

    -- ✅ Better approach
    PlayerState.Increment(player, "Coins", 100)

1
2
3
4
5
6

**Dictionary Operations:** `SetInDict()` and `RemoveFromDict()` now use efficient nested path updates to prevent race conditions and minimize network traffic.

## Batch Operation Functions

### SetValues()

`SetValues(player, values) → boolean`

Sets multiple top-level values at once with leaderstats protection.

**Parameters:** `player: Player`, `values: {[string]: any}`
**Returns:** `boolean` \- Success status

Example

lua

    local success = PlayerState.SetValues(player, {
        Coins = 1000,
        Level = 5,
        Experience = 750
    })

    if success then
        print("Batch update successful")
    end

1
2
3
4
5
6
7
8
9

Note

Returns success status and prevents leaderstats modification in batch

Performance

Use `SetValues()` instead of multiple `Set()` calls for better performance!

### BatchSetValues()

`BatchSetValues(player, operations) → boolean`

Queues multiple path-based operations for optimized batch processing.

**Parameters:** `player: Player`, `operations: {BatchOperation}`
**Returns:** `boolean` \- Success status

Basic BatchComplex Batch

lua

    local success = PlayerState.BatchSetValues(player, {
        {path = "Coins", value = 1000},
        {path = "Level", value = 5},
        {path = "Plot.Likes", value = 50}
    })

1
2
3
4
5

lua

    local operations = {
        {path = "Coins", value = 2000},
        {path = "Experience", value = 500},
        {path = "Plot.Likes", value = 100},
        {path = "Settings.MusicEnabled", value = true},
        {path = "Stats.GamesPlayed", value = 25}

    PlayerState.BatchSetValues(player, operations)

1
2
3
4
5
6
7
8
9

**Notes:**

  * High-performance batch processing system with automatic optimization
  * Configurable batch delay and size limits with auto-flush
  * Prevents leaderstats modification

Batch Performance

  * Operations are grouped by path structure for optimal performance
  * Root-level operations are batched together
  * Nested operations are intelligently grouped
  * Auto-flush occurs at 20 operations or 0.03s delay

### BatchUpdateValues()

`BatchUpdateValues(player, operations) → boolean`

Applies multiple increment/decrement operations at path-based locations in a single batch. Validates that each target value is numeric, then commits all updates via `BatchSetValues()`.

**Parameters:** `player: Player`, `operations: {BatchUpdateOperation}`
**Returns:** `boolean` \- Success status (`false` if validation fails or any path is non-numeric)

Each `BatchUpdateOperation` has:

  * `path: string` \- Dot-notation path to a numeric value
  * `operation: "Increment" | "Decrement"`
  * `amount: number?` \- Optional; defaults to `1`

Example

lua

    local success = PlayerState.BatchUpdateValues(player, {
        {path = "Coins", operation = "Increment", amount = 100},
        {path = "Plot.Likes", operation = "Increment", amount = 5},
        {path = "Lives", operation = "Decrement"},  -- amount defaults to 1
        {path = "Stats.GamesPlayed", operation = "Increment"}
    })

    if success then
        print("Batch update applied")
    else
        warn("BatchUpdateValues failed (invalid player, missing replica, or non-numeric path)")
    end

1
2
3
4
5
6
7
8
9
10
11
12

Numeric paths only

If any path points to a non-numeric value, the function warns and returns `false`; no changes are applied.

### FlushBatch()

`FlushBatch(player) → boolean`

Immediately processes all queued batch operations for a player.

**Parameters:** `player: Player`
**Returns:** `boolean` \- Success status

Example

lua

    -- Queue operations
    PlayerState.BatchSetValues(player, {
        {path = "Coins", value = 1000},
        {path = "Level", value = 5}
    })

    -- Force immediate processing
    PlayerState.FlushBatch(player)

1
2
3
4
5
6
7
8

Note

Manual batch processing control for critical operations

## Array Operation Functions

### AddToArray()

`AddToArray(player, arrayPath, item) → boolean`

Adds an item to the end of an array with validation.

**Parameters:** `player: Player`, `arrayPath: ArrayPath`, `item: any`
**Returns:** `boolean` \- Success status

Example

lua

    -- Add item to inventory
    local success = PlayerState.AddToArray(player, "Inventory", {
        Id = "sword_001",
        Name = "Iron Sword",
        Rarity = "Common"
    })

    if success then
        print("Item added to inventory")
    end

    -- Add achievement
    PlayerState.AddToArray(player, "Achievements", "first_kill")

1
2
3
4
5
6
7
8
9
10
11
12
13

Note

Enhanced array operation with success return and validation

Array Indexing

Arrays are 1-indexed in Lua. Index 1 is the first item!

### RemoveFromArray()

`RemoveFromArray(player, arrayPath, index) → boolean`

Removes an item from an array by index with bounds checking.

**Parameters:** `player: Player`, `arrayPath: ArrayPath`, `index: number` (1-based)
**Returns:** `boolean` \- Success status

Example

lua

    -- Remove first item from inventory
    local success = PlayerState.RemoveFromArray(player, "Inventory", 1)
    if not success then
        warn("Failed to remove item")
    end

    -- Remove specific achievement
    PlayerState.RemoveFromArray(player, "Achievements", 3)

1
2
3
4
5
6
7
8

Note

Safe index-based removal with bounds checking and validation

### UpdateArrayItem()

`UpdateArrayItem(player, arrayPath, index, newItem) → boolean`

Updates an item in an array by index with validation.

**Parameters:** `player: Player`, `arrayPath: ArrayPath`, `index: number` (1-based), `newItem: any`
**Returns:** `boolean` \- Success status

Example

lua

    -- Update inventory item
    local success = PlayerState.UpdateArrayItem(player, "Inventory", 1, {
        Id = "sword_002",
        Name = "Steel Sword",
        Rarity = "Rare",
        Enchantments = {"Sharpness", "Durability"}
    })

    if success then
        print("Item upgraded successfully")
    end

1
2
3
4
5
6
7
8
9
10
11

Note

Safe array item modification with index validation and bounds checking

## Dictionary Operation Functions

### SetInDict()

`SetInDict(player, dictPath, key, value) → boolean`

Sets a value in a dictionary/table with efficient nested path updates and automatic dictionary creation.

**Parameters:** `player: Player`, `dictPath: DictPath`, `key: string`, `value: any`
**Returns:** `boolean` \- Success status

Example

lua

    -- Set building data (creates dictionary if needed)
    local success = PlayerState.SetInDict(player, "Plot.Buildings", "House", {
        Level = 2,
        Material = "Wood",
        Position = Vector3.new(10, 0, 10)
    })

    -- Set game setting
    PlayerState.SetInDict(player, "Settings", "GraphicsQuality", "High")

    -- Set player stat
    PlayerState.SetInDict(player, "Stats", "BestTime", 120.5)

1
2
3
4
5
6
7
8
9
10
11
12

Note

Efficient nested path updates with automatic dictionary creation and race condition prevention

### GetFromDict()

`GetFromDict(player, dictPath, key) → any`

Gets a value from a dictionary using a key with automatic type conversion. Automatically clones tables when `AutoCloneTables` is enabled in configuration.

**Parameters:** `player: Player`, `dictPath: DictPath`, `key: string | number`
**Returns:** `any` \- The value, or `nil` if not found. Tables are cloned if `AutoCloneTables` is enabled.

Example

lua

    -- Get building data
    local houseData = PlayerState.GetFromDict(player, "Plot.Buildings", "House")
    if houseData then
        print("House level:", houseData.Level)
        print("House material:", houseData.Material)
    end

    -- Get with numeric key (automatically converted to string)
    local item = PlayerState.GetFromDict(player, "Inventory.Equipped", 1)

    -- Get setting
    local quality = PlayerState.GetFromDict(player, "Settings", "GraphicsQuality")
    print("Graphics quality:", quality or "Default")

1
2
3
4
5
6
7
8
9
10
11
12
13

Note

  * Automatically converts number keys to strings for consistency
  * When `AutoCloneTables` is enabled in config, table values are automatically deep cloned
  * Prevents accidental modification of internal data structures

### RemoveFromDict()

`RemoveFromDict(player, dictPath, key) → boolean`

Removes a key from a dictionary using efficient nested path updates.

**Parameters:** `player: Player`, `dictPath: DictPath`, `key: string`
**Returns:** `boolean` \- Success status

Example

lua

    -- Remove building
    local success = PlayerState.RemoveFromDict(player, "Plot.Buildings", "OldHouse")
    if success then
        print("Building removed")
    end

    -- Remove obsolete setting
    PlayerState.RemoveFromDict(player, "Settings", "ObsoleteOption")

1
2
3
4
5
6
7
8

Note

Efficient nested path removal with race condition prevention and minimal network traffic

## Cache Management Functions

### ClearPathCache()

`ClearPathCache() → void`

Clears the global path parsing cache for memory management.

**Parameters:** None
**Returns:** Nothing

Example

lua

    -- Clear path cache during server maintenance
    PlayerState.ClearPathCache()

    -- Useful for debugging path-related issues

1
2
3
4

Note

Global cache management for path parsing with automatic size management (max 1000 entries)

## Events

Data Manipulation Guidelines

**BeforeSave & ProfileLoaded:** Use PlayerState functions for data changes (recommended) as they provide validation, leaderstats sync, and proper error handling. Direct data manipulation via the `data` parameter bypasses these safeguards.

**ProfileUnloaded:** PlayerState functions are disabled. Only use direct data access via the `data` parameter for read-only operations or external system synchronization.

### BeforeSave()

`BeforeSave:Connect(listener)`

Fired before player data is saved, allowing for final data modifications.

**Parameters:** `listener: (player: Player, data: PlayerData) -> ()`
**Returns:** `Connection` \- Event connection for disconnecting

Example

lua

    PlayerState.BeforeSave:Connect(function(player, data)
        -- Use PlayerState functions for data changes (recommended)
        PlayerState.Set(player, "LastSaveTime", os.time())
        PlayerState.SetPath(player, "Stats.TotalPlayTime", data.Stats.PlayTime or 0)

        -- Direct data manipulation bypasses validation (use sparingly)
        data.FinalCoins = data.Coins -- Direct access for read-only operations

        print(`Saving data for {player.Name}`)
    end)

1
2
3
4
5
6
7
8
9
10

Note

Fired before automatic saves with PlayerState functions working normally. Use PlayerState functions for data changes (recommended) as they provide validation and leaderstats sync.

### ProfileLoaded()

`ProfileLoaded:Connect(listener)`

Fired when a player's profile data has been loaded and is ready for use.

**Parameters:** `listener: (player: Player, data: PlayerData) -> ()`
**Returns:** `Connection` \- Event connection for disconnecting

Example

lua

    PlayerState.ProfileLoaded:Connect(function(player, data)
        -- Use PlayerState functions for data changes (recommended)
        PlayerState.Set(player, "JoinTime", os.time())
        PlayerState.SetPath(player, "Stats.Logins", (data.Stats.Logins or 0) + 1)

        -- Read data for initialization logic
        print(`{player.Name}'s data loaded with {data.Coins} coins`)

        -- Setup player-specific features
        setupPlayerUI(player)
    end)

1
2
3
4
5
6
7
8
9
10
11

Note

Fired after `PlayerState.Init()` completes successfully. PlayerState functions work normally - use them for data changes (recommended) for validation and proper sync.

### ProfileUnloaded()

`ProfileUnloaded:Connect(listener)`

Fired when a player's profile session ends and data is being unloaded.

**Parameters:** `listener: (player: Player, data: PlayerData) -> ()`
**Returns:** `Connection` \- Event connection for disconnecting

Example

lua

    PlayerState.ProfileUnloaded:Connect(function(player, data)
        -- Cleanup player-specific systems
        print(`{player.Name}'s profile unloaded`)

        -- Direct data access only - PlayerState functions don't work here
        local finalCoins = data.Coins
        local playTime = data.Stats.PlayTime or 0

        -- Save to external systems if needed
        saveToAnalytics(player.UserId, finalCoins, playTime)
    end)

1
2
3
4
5
6
7
8
9
10
11

Note

Fired when profile session ends. PlayerState functions do NOT work during this event - only direct data access via the `data` parameter is available for read-only operations.

### MigrationResult

`MigrationResult:Connect(listener)`

Fired when legacy migration completes for a player. Use for logging, analytics, or debugging migration behavior.

**Parameters:** `listener: (player: Player, result: MigrationResult, data: PlayerData) -> ()`
**Returns:** `Connection` \- Event connection for disconnecting

**Result fields:** `applied`, `legacyFound`, `markerWritten`, `keyUsed`, `strategy`, `markerPath`, `continuous`, `reason`

Example

lua

    PlayerState.MigrationResult:Connect(function(player, result, data)
        print("Migration for", player.Name, result.applied, result.reason, result.strategy, result.keyUsed)
    end)

1
2
3

See [Legacy Data Migration](https://playerstate.netlify.app/migration#migrationresult-signal) for full documentation.

## Utility Functions

### GetAll()

`GetAll(player) → PlayerData?`

Gets all player data with validation.

**Parameters:** `player: Player`
**Returns:** `PlayerData?` \- Complete data table, or `nil` if not found

Example

lua

    local allData = PlayerState.GetAll(player)
    if allData then
        print("Player has", allData.Coins, "coins")
        -- Safely iterate through data
        for key, value in pairs(allData) do
            print(`{key}: {value}`)
        end
    end

1
2
3
4
5
6
7
8

Note

Returns reference to actual data (be careful with modifications)

### GetReplica()

`GetReplica(player) → ReplicaInstance?`

Gets the raw Replica instance for advanced operations.

**Parameters:** `player: Player`
**Returns:** `ReplicaInstance?` \- Replica instance, or `nil` if not found

Example

lua

    local replica = PlayerState.GetReplica(player)
    if replica then
        -- Advanced Replica operations
        replica:OnChange(function(action, path, ...)
            print(`Data changed: {action} at {table.concat(path, ".")}`)
        end)

        -- Check replica status
        print("Replica is active:", replica:IsActive())
    end

1
2
3
4
5
6
7
8
9
10

Note

Enhanced validation ensures replica is active

### GetProfile()

`GetProfile(player) → ProfileStoreProfile?`

Gets the raw ProfileStore profile for advanced operations.

**Parameters:** `player: Player`
**Returns:** `ProfileStoreProfile?` \- Profile instance, or `nil` if not found

Example

lua

    local profile = PlayerState.GetProfile(player)
    if profile then
        -- Advanced ProfileStore operations
        print("Profile is active:", profile:IsActive())

        -- Access profile metadata
        print("Profile tags:", profile.Tags)
    end

1
2
3
4
5
6
7
8

Note

Enhanced validation ensures profile is active

### Clone()

`Clone(value, deep?) → any`

Clones a value, with optional deep cloning for tables. Useful for safely copying data without modifying the original.

**Parameters:** `value: any`, `deep: boolean?` (defaults to `false`)
**Returns:** `any` \- Cloned value (tables are cloned, other types returned as-is)

Example

lua

    -- Shallow clone (default)
    local originalTable = {coins = 100, items = {sword = true}}
    local clonedTable = PlayerState.Clone(originalTable)
    clonedTable.coins = 200 -- Only modifies clone
    clonedTable.items.sword = false -- Modifies original too (shallow clone)

    -- Deep clone (nested tables are also cloned)
    local deepCloned = PlayerState.Clone(originalTable, true)
    deepCloned.coins = 300 -- Only modifies clone
    deepCloned.items.sword = false -- Only modifies clone (deep clone)

    -- Clone non-table values (returns as-is)
    local number = PlayerState.Clone(100) -- Returns 100
    local string = PlayerState.Clone("test") -- Returns "test"

1
2
3
4
5
6
7
8
9
10
11
12
13
14

Note

  * Non-table values are returned unchanged
  * Shallow clone (default) uses `table.clone()` for performance
  * Deep clone recursively clones all nested tables
  * Use deep clone when you need to modify nested structures safely

### GetOfflineData()

`GetOfflineData(userId) → PlayerData?`

Retrieves player data for offline users from ProfileStore. Automatically checks if the player is currently in the server first for better performance.

**Parameters:** `userId: number`
**Returns:** `PlayerData?` \- Player data, or `nil` if not found or invalid

Example

lua

    local offlineData = PlayerState.GetOfflineData(123456789)
    if offlineData then
        print("Offline player has", offlineData.Coins, "coins")
        print("Level:", offlineData.Level)
    else
        print("No offline data found for user")
    end

1
2
3
4
5
6
7

Note

  * Automatically checks if player is in server first (uses live data if available)
  * Falls back to ProfileStore for offline players
  * More efficient than previous implementation

### SetOfflineData()

`SetOfflineData(userId, path, value) → boolean`

Sets data for offline users in ProfileStore. Automatically checks if the player is currently in the server first and uses live data operations when available.

**Parameters:** `userId: number`, `path: string`, `value: any`
**Returns:** `boolean` \- Success status

Example

lua

    -- Set top-level data
    local success = PlayerState.SetOfflineData(123456789, "Coins", 5000)
    if success then
        print("Offline coins updated successfully")
    end

    -- Set nested data using paths
    PlayerState.SetOfflineData(123456789, "Plot.Likes", 100)
    PlayerState.SetOfflineData(123456789, "Settings.MusicEnabled", false)

1
2
3
4
5
6
7
8
9

Note

  * Automatically checks if player is in server first (uses live PlayerState functions if available)
  * Falls back to ProfileStore messaging for offline players with active sessions
  * Creates new session if player is completely offline
  * More efficient handling of online vs offline players

Session Paths Blocked

`SetOfflineData` blocks runtime-only paths such as `session.*`. Writes to those paths will warn or fail. Session data is never persisted; use `SetPath` when the player is in-game.

### SaveData()

`SaveData(player) → boolean`

Forces data save validation check.

**Parameters:** `player: Player`
**Returns:** `boolean` \- Profile active status

Example

lua

    -- Check if save will succeed before critical operation
    local canSave = PlayerState.SaveData(player)
    if canSave then
        -- Proceed with critical operation
        PlayerState.Set(player, "ImportantData", value)
    else
        warn("Cannot save data - profile inactive")
    end

1
2
3
4
5
6
7
8

Note

Returns profile active status for validation

### WipePlayerData()

`WipePlayerData(player) → boolean`

Wipes all player data and resets it to the template values.

**Parameters:** `player: Player`
**Returns:** `boolean` \- Success status

Example

lua

    -- Reset player data to template values
    local success = PlayerState.WipePlayerData(player)
    if success then
        print(`{player.Name}'s data has been reset`)
    else
        warn(`Failed to reset {player.Name}'s data`)
    end

1
2
3
4
5
6
7

Note

Resets all data to CONFIG.Profile.Template values and kicks player

### WipeOfflinePlayerData()

`WipeOfflinePlayerData(userId) → boolean`

Wipes data for offline players and resets it to template values.

**Parameters:** `userId: number`
**Returns:** `boolean` \- Success status

Example

lua

    -- Reset offline player data
    local success = PlayerState.WipeOfflinePlayerData(123456789)
    if success then
        print("Offline player data reset successfully")
    else
        warn("Failed to reset offline player data")
    end

1
2
3
4
5
6
7

Note

Works with offline players not currently in the game

## 🏆 Global Leaderboard

Configuration Required & Runtime Validation

**Leaderboard functionality requires proper configuration in PlayerStateConfig!**

Make sure your `PlayerStateConfig.Server.Leaderboard` section is properly configured:

lua

    Server = {
        Leaderboard = {
            Enabled = true,
            DataStoreName = "PlayerState_Leaderboards",
            TrackedStats = {"Coins", "Level", "HighScore"},
            SyncInterval = 60,
            UpdateOnPlayerLeave = true,
        },

1
2
3
4
5
6
7
8
9

**Runtime Validation:** Functions now validate configuration at runtime and will warn/return appropriate values if misconfigured:

  * `GetLeaderboard()` returns empty array `{}`
  * `GetPlayerRank()` returns `nil`
  * `UpdateLeaderboard()` returns `false`

### Client-synced rank/score (for UI)

When leaderboards are enabled, PlayerState syncs each player’s current leaderboard display info to the client via a replicated `_LeaderboardRanks` table.

**What this gives you:**

  * **Reliable UI data**: the client can read `{rank, score}` for tracked stats without re-deriving it.
  * **Instant-feeling score**: when a tracked stat changes (via `Set`, `SetPath`, `SetValues`, etc.), the synced `score` updates right away.
  * **Rank on refresh**: `rank` updates on the normal server refresh cycle (based on `SyncInterval`).

**Important:**

  * `_LeaderboardRanks` is **temporary display data** and is treated as non-persistent (it will not be saved into player data).
  * On the client, use `PlayerStateClient.GetLeaderboardInfo(statName)` to read this value.

### GetLeaderboard()

`GetLeaderboard(statName, topCount) → {LeaderboardEntry}`

Retrieves the top players for a specific leaderboard statistic.

**Parameters:** `statName: string`, `topCount: number`
**Returns:** `{LeaderboardEntry}` \- Array of leaderboard entries sorted by score (highest first), or empty array if configuration issues

Example

lua

    -- Get top 10 coin leaders
    local coinLeaderboard = PlayerState.GetLeaderboard("Coins", 10)
    for i, entry in ipairs(coinLeaderboard) do
        print(`#{i}: User {entry.userId} - {entry.score} coins`)
    end

    -- Get top 100 high scores
    local highScores = PlayerState.GetLeaderboard("HighScore", 100)
    if #highScores > 0 then
        print(`Highest score: {highScores[1].score} by User {highScores[1].userId}`)
    end

1
2
3
4
5
6
7
8
9
10
11

Note

Results are cached for 15 seconds to improve performance.

Configuration Validation

**This function validates configuration at runtime:**

  * Returns **empty array `{}`** and warns if `CONFIG.Leaderboard.Enabled = false`
  * Returns **empty array `{}`** and warns if the `statName` is not in `CONFIG.Leaderboard.TrackedStats`
  * Requires the stat name to be properly configured before calling

### GetPlayerRank()

`GetPlayerRank(player, statName) → number?`

Gets a player's current rank on a specific leaderboard.

**Parameters:** `player: Player`, `statName: string`
**Returns:** `number?` \- Player's rank (1-based), or `nil` if not ranked, invalid player, or configuration issues

Example

lua

    -- Get player's coin rank
    local coinRank = PlayerState.GetPlayerRank(player, "Coins")
    if coinRank then
        print(`You're ranked #{coinRank} for coins!`)
    else
        print("You're not yet ranked for coins")
    end

    -- Check multiple ranks
    local levelRank = PlayerState.GetPlayerRank(player, "Level")
    local scoreRank = PlayerState.GetPlayerRank(player, "HighScore")

    if levelRank and scoreRank then
        print(`Level Rank: #{levelRank}, Score Rank: #{scoreRank}`)
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Note

Returns `nil` if player has no score for this stat or if the player is invalid.

Configuration Validation

**This function validates configuration at runtime:**

  * Returns **`nil`** and warns if `CONFIG.Leaderboard.Enabled = false`
  * Returns **`nil`** and warns if the `statName` is not in `CONFIG.Leaderboard.TrackedStats`
  * Returns **`nil`** if the player is invalid or not found
  * Requires the stat name to be properly configured before calling

### UpdateLeaderboard()

`UpdateLeaderboard(player, statName, score) → boolean`

Manually updates a player's leaderboard entry for a specific statistic.

**Parameters:** `player: Player`, `statName: string`, `score: number`
**Returns:** `boolean` \- Success status, or `false` if configuration or validation issues

Example

lua

    -- Update player's coin leaderboard
    local success = PlayerState.UpdateLeaderboard(player, "Coins", 5000)
    if success then
        print("Leaderboard updated successfully")
    else
        warn("Failed to update leaderboard")
    end

    -- Update multiple stats
    PlayerState.UpdateLeaderboard(player, "Level", 25)
    PlayerState.UpdateLeaderboard(player, "HighScore", 1200)

1
2
3
4
5
6
7
8
9
10
11

Note

This function immediately updates the leaderboard. Most stats are automatically updated when data changes, but you can use this for manual updates.

Configuration Validation

**This function validates configuration at runtime:**

  * Returns **`false`** and warns if `CONFIG.Leaderboard.Enabled = false`
  * Returns **`false`** and warns if the `statName` is not in `CONFIG.Leaderboard.TrackedStats`
  * Returns **`false`** if the player is invalid
  * Requires the stat name to be properly configured before calling
  * Clears the cache for the updated stat to ensure fresh data

## Leaderstats Integration

### 🏆 Automatic Leaderstats

PlayerState now automatically creates and manages leaderstats based on your data configuration:

lua

    -- In your DefaultData module, add leaderstats configuration:
    local DefaultData = {
        Coins = 0,
        Level = 1,
        Experience = 0,
        Plot = {
            Likes = 0
        },

        -- Leaderstats configuration
        leaderstats = {
            ["💰 Coins"] = "Coins",
            ["⭐ Level"] = "Level",
            ["👍 Likes"] = "Plot.Likes"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

**Features:**

  * **Automatic Creation:** Leaderstats are created when player data loads
  * **Real-time Sync:** Updates automatically when data changes
  * **Type-Safe:** Numbers use IntValue, others use StringValue
  * **Path Support:** Supports nested paths like `"Plot.Likes"`

Leaderstats Protection

Direct modification of leaderstats paths is prevented. Change the source data instead!

lua

    -- ❌ This will fail
    PlayerState.Set(player, "leaderstats", {...})

    -- ✅ This updates the leaderstat automatically
    PlayerState.Set(player, "Coins", 1000)

1
2
3
4
5

## Usage Examples

### 💰 Currency Management with Validation

Example

lua

    -- Award coins (validation handled internally)
    local function awardCoins(player, amount, source)
        local currentCoins = PlayerState.Get(player, "Coins") or 0
        local success = PlayerState.Set(player, "Coins", currentCoins + amount)

        if success then
            print(`Awarded {amount} coins to {player.Name} from {source}`)
            return true
        else
            warn(`Failed to award coins to {player.Name}`)
            return false
        end
    end

    -- Spend coins with validation
    local function spendCoins(player, amount, item)
        local currentCoins = PlayerState.Get(player, "Coins") or 0
        if currentCoins < amount then
            return false, "Insufficient coins"
        end

        local success = PlayerState.Set(player, "Coins", currentCoins - amount)
        if success then
            print(`{player.Name} spent {amount} coins on {item}`)
            return true, "Purchase successful"
        else
            return false, "Transaction failed"
        end
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

### 📈 Level System with Batch Operations

Example

lua

    -- Level up player with batch operations
    local function levelUp(player)
        local currentLevel = PlayerState.Get(player, "Level") or 1
        local currentCoins = PlayerState.Get(player, "Coins") or 0
        local bonusCoins = currentLevel * 100

        local success = PlayerState.BatchSetValues(player, {
            {path = "Level", value = currentLevel + 1},
            {path = "Experience", value = 0},
            {path = "Coins", value = currentCoins + bonusCoins}
        })

        if success then
            print(`{player.Name} leveled up to {currentLevel + 1}!`)
            return true
        end

        return false
    end

    -- Add experience with level up check
    local function addExperience(player, amount)
        local currentExp = PlayerState.Get(player, "Experience") or 0
        local currentLevel = PlayerState.Get(player, "Level") or 1
        local expNeeded = currentLevel * 100

        local newExp = currentExp + amount

        if newExp >= expNeeded then
            -- Level up
            levelUp(player)
        else
            -- Just add experience
            PlayerState.Set(player, "Experience", newExp)
        end
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

### 🎒 Enhanced Inventory Management

Example

lua

    local MAX_INVENTORY_SIZE = 50

    -- Add item with validation and limits
    local function addItem(player, item)
        local inventory = PlayerState.GetPath(player, "Inventory") or {}
        if #inventory >= MAX_INVENTORY_SIZE then
            return false, "Inventory full"
        end

        -- Add timestamp to item
        item.AcquiredTime = os.time()

        local success = PlayerState.AddToArray(player, "Inventory", item)
        if success then
            print(`Added {item.Name} to {player.Name}'s inventory`)
            return true, "Item added"
        end

        return false, "Failed to add item"
    end

    -- Remove item by ID
    local function removeItemById(player, itemId)
        local inventory = PlayerState.GetPath(player, "Inventory") or {}

        for index, item in ipairs(inventory) do
            if item.Id == itemId then
                local success = PlayerState.RemoveFromArray(player, "Inventory", index)
                if success then
                    print(`Removed {item.Name} from {player.Name}'s inventory`)
                    return true
                end
                break
            end
        end

        return false
    end

    -- Upgrade item in inventory
    local function upgradeItem(player, itemIndex, newStats)
        local inventory = PlayerState.GetPath(player, "Inventory") or {}
        local item = inventory[itemIndex]

        if not item then
            return false, "Item not found"
        end

        -- Merge new stats
        for key, value in pairs(newStats) do
            item[key] = value
        end

        -- Update upgraded time
        item.UpgradedTime = os.time()

        local success = PlayerState.UpdateArrayItem(player, "Inventory", itemIndex, item)
        if success then
            print(`Upgraded {item.Name} for {player.Name}`)
            return true
        end

        return false, "Upgrade failed"
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

### 🏆 Leaderboard Management

Example

lua

    -- Server: Comprehensive leaderboard system with validation
    local function setupLeaderboardSystem()
        -- Configuration reminder
        -- Make sure PlayerStateConfig.Server.Leaderboard is properly configured!

        -- ✅ Get top 10 players for coins (works if configured)
        local topCoins = PlayerState.GetLeaderboard("Coins", 10)
        if #topCoins > 0 then
            for rank, entry in ipairs(topCoins) do
                print(`#{rank}: User {entry.userId} - {entry.score} coins`)
            end
        else
            print("No coin leaderboard data available (check configuration)")
        end

        -- ✅ Get player's current rank (returns nil if not ranked)
        local playerRank = PlayerState.GetPlayerRank(player, "Coins")
        if playerRank then
            print(`You're currently ranked #{playerRank} for coins!`)
        else
            print("You're not yet ranked for coins")
        end

        -- ✅ Manual leaderboard update (for special cases)
        local success = PlayerState.UpdateLeaderboard(player, "HighScore", 2500)
        if success then
            print("High score leaderboard updated!")
        else
            print("Failed to update leaderboard (check configuration)")
        end
    end

    -- Safe leaderboard usage with error handling
    local function safeLeaderboardUsage()
        -- Always check if leaderboard is available before heavy usage
        local testBoard = PlayerState.GetLeaderboard("Coins", 1)
        if #testBoard == 0 then
            warn("Leaderboard not configured properly - check PlayerStateConfig")
            return false
        end

        -- Proceed with leaderboard operations
        local playerRank = PlayerState.GetPlayerRank(player, "Coins")
        return PlayerState.UpdateLeaderboard(player, "Coins", 100)
    end

    -- Get multiple leaderboard stats at once
    local function getPlayerStatsSummary(player)
        local stats = {
            coinRank = PlayerState.GetPlayerRank(player, "Coins"),
            levelRank = PlayerState.GetPlayerRank(player, "Level"),
            scoreRank = PlayerState.GetPlayerRank(player, "HighScore"),

        -- Display player's rankings
        for statName, rank in pairs(stats) do
            if rank then
                print(`{statName}: Rank #{rank}`)
            else
                print(`{statName}: Not ranked yet`)
            end
        end

        return stats
    end

    -- Leaderboard UI data
    local function getLeaderboardUIData(statName, maxEntries)
        local leaderboard = PlayerState.GetLeaderboard(statName, maxEntries or 50)
        local uiData = {}

        for i, entry in ipairs(leaderboard) do
            table.insert(uiData, {
                rank = entry.rank,
                userId = entry.userId,
                score = entry.score,
                displayName = "Loading...", -- You'll need to get this from Player service
            })
        end

        return uiData
    end

    -- Periodic leaderboard announcements
    local function announceTopPlayers()
        local topPlayers = PlayerState.GetLeaderboard("Coins", 3)

        if #topPlayers >= 3 then
            print("🏆 TOP 3 RICHEST PLAYERS 🏆")
            for i, entry in ipairs(topPlayers) do
                local medal = i == 1 and "🥇" or i == 2 and "🥈" or "🥉"
                print(`{medal} #{i}: User {entry.userId} - {entry.score} coins`)
            end
        end
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

### 🏠 Plot Building System

Example

lua

    -- Build structure on plot (efficient nested updates)
    local function buildStructure(player, buildingType, position)
        local buildingData = {
            Type = buildingType,
            Level = 1,
            Position = position,
            BuiltTime = os.time(),
            Health = 100

        local buildingId = `{buildingType}_{os.time()}`
        local success = PlayerState.SetInDict(player, "Plot.Buildings", buildingId, buildingData)

        if success then
            print(`{player.Name} built {buildingType} at {position}`)
            return true, buildingId
        end

        return false, "Build failed"
    end

    -- Upgrade building (efficient single-key update)
    local function upgradeBuilding(player, buildingId)
        local building = PlayerState.GetPath(player, `Plot.Buildings.{buildingId}`)
        if not building then
            return false, "Building not found"
        end

        building.Level = building.Level + 1
        building.UpgradedTime = os.time()

        local success = PlayerState.SetInDict(player, "Plot.Buildings", buildingId, building)
        if success then
            print(`{player.Name} upgraded {building.Type} to level {building.Level}`)
            return true
        end

        return false, "Upgrade failed"
    end

    -- Remove building (efficient key removal)
    local function removeBuilding(player, buildingId)
        local success = PlayerState.RemoveFromDict(player, "Plot.Buildings", buildingId)
        if success then
            print(`{player.Name} removed building {buildingId}`)
            return true
        end

        return false, "Removal failed"
    end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

### 🔄 Event-Driven Data Management

Example

lua

    -- Setup event handlers for comprehensive data management
    local function setupPlayerStateEvents()
        -- When player data loads
        PlayerState.ProfileLoaded:Connect(function(player, data)
            -- Welcome back message
            local loginCount = (data.Stats.Logins or 0) + 1
            PlayerState.SetPath(player, "Stats.Logins", loginCount)
            PlayerState.Set(player, "LastJoinTime", os.time())

            -- Check for daily rewards
            local lastReward = data.Stats.LastDailyReward or 0
            local currentDay = math.floor(os.time() / 86400) -- Days since epoch

            if currentDay > lastReward then
                PlayerState.SetPath(player, "Stats.LastDailyReward", currentDay)
                PlayerState.Increment(player, "Coins", 100)
                print(`{player.Name} received daily reward!`)
            end

            -- Initialize player systems
            setupPlayerUI(player)
            setupPlayerEffects(player)
        end)

        -- Before data saves
        PlayerState.BeforeSave:Connect(function(player, data)
            -- Final cleanup and validation
            data.LastSaveTime = os.time()

            -- Calculate session playtime
            local joinTime = data.LastJoinTime or os.time()
            local sessionTime = os.time() - joinTime
            local totalPlayTime = (data.Stats.TotalPlayTime or 0) + sessionTime

            PlayerState.SetPath(player, "Stats.TotalPlayTime", totalPlayTime)
            PlayerState.SetPath(player, "Stats.LastSessionTime", sessionTime)

            -- Validate data integrity
            if data.Coins and data.Coins < 0 then
                warn(`{player.Name} had negative coins, resetting to 0`)
                PlayerState.Set(player, "Coins", 0)
            end

            print(`Saving {player.Name}'s data - Session: {sessionTime}s, Total: {totalPlayTime}s`)
        end)

        -- When profile unloads
        PlayerState.ProfileUnloaded:Connect(function(player, data)
            -- Analytics and external system updates
            local finalStats = {
                userId = player.UserId,
                coins = data.Coins or 0,
                level = data.Level or 1,
                totalPlayTime = data.Stats.TotalPlayTime or 0,
                logins = data.Stats.Logins or 0

            -- Send to analytics (direct data access only)
            sendToAnalytics(finalStats)

            -- Cleanup player-specific systems
            cleanupPlayerUI(player)
            cleanupPlayerEffects(player)

            print(`{player.Name} profile unloaded - Final coins: {finalStats.coins}`)
        end)
    end

    -- Call this during server startup
    setupPlayerStateEvents()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

## Performance Tips

Optimization

**Batch Operations:** Use `BatchSetValues()` for multiple changes instead of individual calls **Cache Management:** Call `ClearPathCache()` periodically for memory optimization **Return Values:** Always check success returns for error handling **Path Reuse:** Reuse path strings when possible for better performance

Best Practices

**Automatic Validation:** Functions validate automatically - no manual checks needed **Leaderstats Protection:** Change source data instead of direct leaderstats modification **Error Handling:** Use proper error handling and provide user feedback **Cache Monitoring:** Monitor cache performance and clear when necessary


# Stoway - Advanced Roblox Inventory System (V2.4.1)

Stoway is a data-driven inventory and hotbar framework for Roblox. It uses Fusion for reactive UI, server-authoritative inventory state, and delta replication for efficient updates. Use it as a base for RPG, survival, or utility-heavy inventory systems.
Download the latest packaged release from Releases.

## Table of Contents
- [Features](#features)
- [Getting Started](#getting-started)
- [Configuration](#configuration)
- [Debug Commands](#debug-commands)
- [Documentation](#documentation)
- [Credits](#credits)

## Features
### Hotbar
- Drag and drop between hotbar and storage.
- Supports static slots and dynamic display behavior.
- Keybind support for quick equip.

### Storage
- Items are tracked as data (UUID-based), not persistent tool instances.
- Configurable stacking, limits, sorting, and filtering.
- Optional backpack enable/disable modes.

### Systems
- Reactive UI with Fusion.
- Server-authoritative state with delta replication and correction sync paths.
- Console mode support, selection mode, and drop UI.
- Multi-skin UI support via settings.

## Getting Started
1. Download the latest `.rbxm` from [Releases](https://github.com/Zyn-ic/Stoway/releases).
2. Place server module(s) in `ServerScriptService`.
3. Place client module(s) in `StarterPlayerScripts`.
4. Place shared modules (`Settings`, `Types`, `RarityValues`, `Binds`) in `ReplicatedStorage/Shared`.
5. Ensure required packages (such as Fusion) are available in `ReplicatedStorage/Packages`.
6. Initialize on server:

```lua
local StowayServer = require(game.ServerScriptService.StowayServerV1_2)
StowayServer.Init()

## Configuration
Primary settings are in `src/shared/Settings.luau`.

```lua
local Settings = {}

Settings.Hotbar = {
    Type = "Static", -- "Static" | "Dynamic"
    MaxSlots = 10,

Settings.Storage = {
    Limit = 15,        -- 0 = infinite
    CanStack = true,
    Sorting = true,
    MaxStackSize = 5,
    BackpackEnabled = true,
    SortOrder = "None" -- "None" | "Name" | "Rarity" | "ItemType"

Settings.Gameplay = {
    Droppable = true,
    DropDistance = 10,
    MouseScroll = true,

return Settings

## Debug Commands
Built-in chat commands for testing (do not leave unrestricted in production):
- `/inv`
- `/add [itemId] [amount?]`
- `/addback [itemId] [amount?]`
- `/remove [uuid] [amount?]`
- `/swap [Hotbar/Storage] [slot] [Hotbar/Storage] [slot]`
- `/drop [Hotbar/Storage] [slot] [amount?]`
- `/set_limit [n]`
- `/set_hotbar_slots [n]`
- `/setui [UiType]`

## Documentation
Full docs: [https://zyn-ic.github.io/Stoway/](https://zyn-ic.github.io/Stoway/)

Basic Load Flow: [MermaidLiveView](https://shorturl.at/lkznn)

Cross Compatability Flow: [MermaidLiveView](https://shorturl.at/C41x2)

Different Console Modes Flow: [MermaidLiveView](https://shorturl.at/ALPBR)

## Credits
1.   [**HowToRoblox**](https://www.youtube.com/watch?v=kqa4u_9mSTQ&list=WL&index=6) - sourced a lot of core mechanics from him for backpack/hotbar in my ealier versions.
2.  [**Knineteen19**](https://www.youtube.com/watch?v=d2tdaRmqGXI&list=WL&index=8) - his series helped me get a good understanding for inventory/hotbar, presistance, and stacking.
3.  [**Avafe**](https://devforum.roblox.com/t/neohotbar-a-modern-customizable-backpack/2738850) - learned about the existance of [fusion and react](https://www.youtube.com/watch?v=OHqMLEL5QnY&t=1730s) on roblox from this custom hotbar and backpack system.
4.  [**Illusion**](https://devforum.roblox.com/t/illusions-inputactionsystem-currently-the-best-input-manager/4071242) for creating a module around roblox's new InputActionService *thank you this saved me a lot of time* 😭


⚡ Headless Backend Architecture

# Stoway Docs

A production-ready, server-authoritative inventory system for Roblox with O(1) lookups, full persistence support, and cross-platform input (mouse, touch, gamepad).

[► 5-Min Quick Start](https://zyn-ic.github.io/Stoway/quickstart.html) [📚 Browse Guides](https://zyn-ic.github.io/Stoway/guides/index.html) [📝 Copy Templates](https://zyn-ic.github.io/Stoway/templates/index.html)

[ 💻 ### Server Backend Server-authoritative inventory management, operations, and anti-exploit protection. → ](https://zyn-ic.github.io/Stoway/server/index.html) [ 🎨 ### Client Frontend Fusion reactive UI, hook system for customization, and console/gamepad support. → ](https://zyn-ic.github.io/Stoway/client/index.html)

## Key Concepts

Understand the core ideas behind Stoway

🔒

### Server-Authoritative

All operations validated server-side. Single RemoteEvent with per-player operation locking prevents exploit spam.

[Learn more →](https://zyn-ic.github.io/Stoway/server/architecture.html)

🔗

### Context-Based Input

IllusionIAS contexts prevent bind conflicts. Same keys work differently in Combat vs Inventory mode.

[Learn more →](https://zyn-ic.github.io/Stoway/client/input/index.html)

📚

### Hooks System

Client-side customization without modifying core code. Sound effects, animations, analytics via event hooks.

[Learn more →](https://zyn-ic.github.io/Stoway/client/hooks/index.html)

🎨

### GUI Skins

Template-based UI system. Multiple skins with different layouts, all using the same slot factory.

[Learn more →](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html)

⚙

### Settings Layers

Global defaults in Settings.luau, per-player overrides in state.Settings. Runtime modifiable.

[Learn more →](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html)

🎮

### Console Support

Gamepad-aware UI with selection states, visual mode indicators, and hybrid input switching.

[Learn more →](https://zyn-ic.github.io/Stoway/client/input/console-modes.html)

## Project Structure

Quick reference to source locations

Server Code Location

    src/server/StowayServerV1_2/
    - init.luau              # Entry point & API
    - Core/
    -   InventoryState.luau  # Per-player data container
    -   SlotManager.luau     # Hotbar/Storage slot logic
    -   LimitChecker.luau    # Weight validation
    - Operations/
    -   AddOperation.luau    # Add items to inventory
    -   RemoveOperation.luau # Remove items
    -   SwapOperation.luau   # Swap/move items
    -   EquipOperation.luau  # Equip/Unequip items
    -   DropOperation.luau   # Drop to world
    - Replication/           # Delta updates to clients
    - Utils/                 # UUID, MetadataParser, etc.
    - Debug/                 # Chat commands

Client Code Location

    src/client/StowayClientv1_2/
    - init.luau              # Entry point & UI builder
    - CoreState/
    -   InventoryStore.luau  # Fusion reactive state
    - Input/
    -   InputManager.luau    # Context-based binds
    -   InteractionManager.luau # Mouse/Gamepad switching
    -   ConsoleModeManager.luau # Combat/Inventory modes
    -   SelectionManager.luau   # Selection state machine
    -   ConsoleDropUI.luau  # Drop amount UI
    - Hook/
    -   HookManager.luau     # Event hook system
    - Operations/
    -   InventoryController.luau # Client operations
    - DragFunctions/
    -   DragHandler.luau     # Drag-and-drop logic
    - SlotCreation/
    -   SlotFactory.luau     # Slot UI component
    - NetworkFunctions/
    -   NetworkHandler.luau  # Server sync

## Popular Topics

Common questions and tasks

[ + #### Add a New Server Operation Step-by-step guide to extending the inventory system ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ 🎨 #### Create a GUI Skin Design custom inventory interfaces with templates ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 🔢 #### Understand the Binds Module Context-based input system explained ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) [ ⚙ #### Configure Settings Global vs per-player settings distinction ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 💬 #### Use Hooks for Customization Practical examples for UI feedback ](https://zyn-ic.github.io/Stoway/client/hooks/index.html) [ 🎮 #### Console Mode System Combat vs Inventory modes and state flow ](https://zyn-ic.github.io/Stoway/client/input/console-modes.html)


# + Add Server Features

Learn how to extend Stoway by creating new operations, adding API methods, and wiring them to the network protocol.

## Overview

How the operations system works

Stoway uses a modular operation system. Each operation (Add, Remove, Swap, Equip, Drop) is a self-contained module that handles a specific inventory action. Adding a new feature involves:

  1. **Create the operation module** \- Handle the core logic
  2. **Add an API method** \- Expose the operation in InventoryService
  3. **Wire to network protocol** \- Allow clients to trigger it
  4. **Add replication (optional)** \- Notify clients of changes

Architecture Overview Diagram

    Client Request → InventoryAction (RemoteEvent)
        → OperationHandlers[operation]
        → OperationModule.Execute()
        → InventoryState updates
        → Replicator.Send*() (optional)
        → Client update

## Step 1: Create the Operation Module

Write the core logic for your operation

Create a new file in `src/server/StowayServerV1_2/Operations/`. Use the template below:

MyOperation.luau Luau

    -- src/server/StowayServerV1_2/Operations/MyOperation.luau
    -- Description: What your operation does

    local MyOperation = {}

    -- Type definitions for clarity
    export type MyResult = {
        success: boolean,
        reason: string?,
        -- Add your result fields here

    -- Main execute function
    function MyOperation.Execute(
        state: InventoryStateType,
        -- Add your parameters
        param1: string,
        param2: number
    ): MyResult
        -- 1. Validate inputs
        if not param1 then
            return { success = false, reason = "INVALID_PARAM" }
        end

        -- 2. Perform the operation
        -- Modify state.Items, state.Hotbar, state.Storage as needed

        -- 3. Return result
        return {
            success = true,
            -- Add result fields
    end

    return MyOperation

**💡 Best Practices:**

  * Always validate inputs before modifying state
  * Use the existing helpers (SlotManager, LimitChecker, etc.)
  * Return descriptive error messages
  * Keep the operation focused on a single task

## Step 2: Add API Method

Expose your operation in InventoryService

Open `src/server/StowayServerV1_2/init.luau` and add:

init.luau - Imports Luau

    -- At the top with other operation imports
    local MyOperation = require(script.Operations.MyOperation)

init.luau - API Method Luau

    -- Add in the PUBLIC API section

    function InventoryService.MyOperation(
        player: Player,
        param1: string,
        param2: number,
        REPLICATE: boolean
    )
        local state = PlayerInventories[player]
        if not state then return { success = false, reason = "NO_INVENTORY" } end

        local result = MyOperation.Execute(state, param1, param2)

        if result.success and REPLICATE then
            -- Add replication if needed
            -- Replicator.SendSomething(player, ...)
        end

        return result
    end

## Step 3: Wire to Network Protocol

Allow clients to trigger the operation

init.luau - Operation Types Luau

    -- In the Operations table
    local Operations = {
        SWAP = "Swap",
        EQUIP = "Equip",
        UNEQUIP = "Unequip",
        REMOVE = "Remove",
        DROP = "Drop",
        STACK = "Stack",
        -- Add your new operation type
        MY_OPERATION = "MyOperation",

init.luau - Operation Handlers Luau

    -- In the OperationHandlers table
    local OperationHandlers = {
        -- ... existing handlers ...

        -- Add your handler
        [Operations.MY_OPERATION] = function(player, args)
            return InventoryService.MyOperation(
                player,
                args.param1,
                args.param2,
                false  -- Replication handled by API method
            )
        end,

## Step 4: Add Replication (Optional)

Notify clients of changes

If your operation changes visible state, add a replication method. See `Replicator.luau` for patterns:

Replicator.luau Pattern Luau

    -- Add to Replicator module

    function Replicator.SendMyOperation(player, -- your data)
        local payload = {
            Operation = "MyOperation",
            -- Your payload data
        InventoryActionRemote:FireClient(player, payload)
    end

## Example: Craft Operation

Complete working example

Here's a simplified craft operation that consumes ingredients and produces an output item:

CraftOperation.luau Luau

    -- src/server/StowayServerV1_2/Operations/CraftOperation.luau

    local SlotManager = require(script.Parent.Parent.Core.SlotManager)
    local RemoveOperation = require(script.Parent.Parent.Operations.RemoveOperation)
    local AddOperation = require(script.Parent.Parent.Operations.AddOperation)

    local CraftOperation = {}

    export type CraftResult = {
        success: boolean,
        reason: string?,
        outputItem: string?,

    function CraftOperation.Execute(
        state: InventoryStateType,
        recipeId: string
    ): CraftResult
        -- Define your recipes (could also be in a separate module)
        local recipes = {
            sword = {
                ingredients = { wood = 2, iron = 1 },
                output = "sword"
            },
            potion = {
                ingredients = { herb = 3, water = 1 },
                output = "potion"

        local recipe = recipes[recipeId]
        if not recipe then
            return { success = false, reason = "UNKNOWN_RECIPE" }
        end

        -- Check ingredients exist
        for itemId, amount in pairs(recipe.ingredients) do
            local count = state.ItemsByID[itemId] and #state.ItemsByID[itemId] or 0
            if count < amount then
                return { success = false, reason = "MISSING_INGREDIENTS" }
            end
        end

        -- Consume ingredients
        for itemId, amount in pairs(recipe.ingredients) do
            for i = 1, amount do
                local uuid = state.ItemsByID[itemId][1]
                RemoveOperation.Execute(state, uuid, 1)
            end
        end

        -- Add output
        local addResult = AddOperation.Execute(state, recipe.output, 1, nil)

        return {
            success = true,
            outputItem = recipe.output
    end

    return CraftOperation

## Next Steps

Related documentation

[ 💻 #### API Reference Full InventoryService documentation ](https://zyn-ic.github.io/Stoway/server/inventory-service.html) [ ✖ #### Remove Features Learn to disable features ](https://zyn-ic.github.io/Stoway/guides/remove-features.html) [ 💾 #### Add Persistence Save inventory state ](https://zyn-ic.github.io/Stoway/guides/persistence.html) [ 📝 #### Templates Copy operation template ](https://zyn-ic.github.io/Stoway/templates/index.html)


# ⚔ Console Modes

Gamepad users have two distinct modes: Combat and Inventory. Each mode enables different binds and controls, with automatic switching based on user interaction.

## Mode Overview

Understanding Combat vs Inventory modes

⚔

### Combat Mode

Default

Character controls are active. Inventory interaction is disabled. Quick hotbar access for equipping items during gameplay.

#### Enabled Binds

1-9: Hotbar slots R1+X: Slot 1 R1+Y: Slot 2 L1+X: Slot 5

#### Disabled Features

✖ Selection navigation ✖ Swap/Drop actions ✖ Drag-and-drop

🎒

### Inventory Mode

Toggle

Character controls are suppressed. UI navigation is active. Full inventory management with selection-based actions.

#### Enabled Binds

D-Pad: Navigate slots Y: Swap X: Drop A: Quick Move B: Cancel

#### Disabled Features

✖ Hotbar slot shortcuts ✖ Character movement

## Mode Switching

How to transition between modes

🔬

#### Long Press L3

Hold the left stick (L3) for 0.5 seconds to toggle between modes.

↔

📍

#### Context Switch

ConsoleModeManager enables/disables IllusionIAS contexts automatically.

↔

🟠

#### Visual Indicator

Badge appears showing current mode with color coding.

Long Press Configuration Luau

    -- src/client/StowayClientv1_2/Input/ConsoleModeManager.luau

    -- Long press duration (seconds)
    local LONG_PRESS_DURATION = 0.5

    -- Detection logic
    switchAction.Activated:Connect(function(active, pressed)
        if pressed then
            isPressed = true
            -- Schedule toggle after duration
            longPressTask = task.delay(LONG_PRESS_DURATION, function()
                if isPressed then
                    ConsoleModeManager.ToggleMode()
                end)
        else
            isPressed = false
            if longPressTask then
                task.cancel(longPressTask)
                longPressTask = nil
            end
        end
    end)

## Visual Mode Indicator

HUD badge showing current mode

A small badge appears in the bottom-right corner showing the current mode:

⚔ COMBAT

#### Combat Mode

Reddish badge, character controls active

🎒 SELECTING

#### Inventory Mode

Greenish badge, shows selection state

Mode Indicator Creation Luau

    function CreateModeIndicator()
        local player = game.GetService("Players").LocalPlayer

        modeIndicatorGui = Instance.new("ScreenGui")
        modeIndicatorGui.Name = "StowayConsoleHUD"
        modeIndicatorGui.Parent = player:WaitForChild("PlayerGui")

        local bg = Instance.new("Frame")
        bg.Name = "ModeBadge"
        bg.Size = UDim2.fromOffset(150, 40)
        bg.Position = UDim2.new(0.95, -160, 0.85, 0)
        bg.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
        bg.Parent = modeIndicatorGui

        modeTextLabel = Instance.new("TextLabel")
        modeTextLabel.Text = "COMBAT"
        modeTextLabel.TextColor3 = Color3.fromRGB(255, 100, 100)  -- Red
        modeTextLabel.Parent = bg
    end

## Mode Transition Behavior

What happens when switching modes

Action | Switching to Combat | Switching to Inventory
---|---|---
`InputManager` | Combat context enabled | Inventory context enabled
`SelectionManager` | Disabled, clears selection | Enabled
`GuiService` | GuiNavigationEnabled = false | GuiNavigationEnabled = true
`Mode Indicator` | "COMBAT" (red) | "SELECTING" (green)
`Hooks` | OnDeselect fired | -

## Code Structure

ConsoleModeManager implementation

ConsoleModeManager.luau Luau

    local ConsoleModeManager = {}
    local CurrentMode = ""  -- "Combat" | "Inventory"

    -- Set mode directly
    function ConsoleModeManager.SetMode(modeName)
        if CurrentMode == modeName then return end
        CurrentMode = modeName

        if modeName == "Combat" then
            InputManager.SetCombatEnabled(true)
            InputManager.SetInventoryEnabled(false)
            SelectionManager.SetEnabled(false)
            GuiService.GuiNavigationEnabled = false
            GuiService.SelectedObject = nil

            if modeTextLabel then
                modeTextLabel.Text = "⚔ COMBAT"
                modeTextLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
            end

        elseif modeName == "Inventory" then
            InputManager.SetCombatEnabled(false)
            InputManager.SetInventoryEnabled(true)
            SelectionManager.SetEnabled(true)
            GuiService.GuiNavigationEnabled = true

            if modeTextLabel then
                modeTextLabel.Text = "🎒 SELECTING"
                modeTextLabel.TextColor3 = Color3.fromRGB(100, 255, 150)
            end
        end
    end

    -- Toggle between modes
    function ConsoleModeManager.ToggleMode()
        if CurrentMode == "Combat" then
            ConsoleModeManager.SetMode("Inventory")
        else
            ConsoleModeManager.SetMode("Combat")
        end
    end

    return ConsoleModeManager

## State Callback

React to mode changes

SelectionManager provides a callback that updates the mode indicator text when selection state changes:

State Change Callback Luau

    -- Connected in SetMode when entering Inventory mode
    SelectionManager.OnStateChanged = function(newDesc)
        if modeTextLabel then
            modeTextLabel.Text = "🎒 " .. newDesc
        end
    end

    -- Returns descriptions like:
    -- "🖼 SELECTING"     (idle)
    -- "🔄 SWAP - SELECT TARGET" (source selected)
    -- "🗑 DROPPING ITEM"  (dropping)

## Customization

Modifying mode behavior

[ Reference 🔢 ### Change Switch Bind Modify the L3 long press to a different key. ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) [ Reference ✓ ### Selection States Customize the state descriptions shown in inventory mode. ](https://zyn-ic.github.io/Stoway/client/input/selection-states.html) [ Reference 📍 ### Interaction Manager How gamepad detection triggers console mode. ](https://zyn-ic.github.io/Stoway/client/input/interaction-manager.html)

## Related Documentation

See also

[ 🎮 #### Input System Complete overview ](https://zyn-ic.github.io/Stoway/client/input/index.html) [ ✓ #### Selection States Inventory mode actions ](https://zyn-ic.github.io/Stoway/client/input/selection-states.html) [ 🔢 #### Binds Module Input configuration ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) [ 💬 #### Hooks OnSelect/OnDeselect hooks ](https://zyn-ic.github.io/Stoway/client/hooks/index.html)


# 🎨 Client Frontend

Reactive UI built with Fusion, featuring a robust Hook system for customization, and full console/gamepad support.

[ 📚 ### Hooks System Event-driven customization for UI interactions. → ](https://zyn-ic.github.io/Stoway/client/hooks/index.html) [ 🎨 ### GUI System Slot factory and template-based UI building. → ](https://zyn-ic.github.io/Stoway/client/index.html) [ 🎮 ### Input & Console Mouse/touch and gamepad support with mode switching. → ](https://zyn-ic.github.io/Stoway/client/input/index.html)

## Key Features

Client-side capabilities

💡

### Fusion Reactive UI

Reactive state management with automatic UI updates.

📚

### Hook System

Customize behavior without modifying core code.

🎮

### Console Support

Gamepad navigation with Combat/Inventory modes.

🔢

### Context Binds

IllusionIAS contexts prevent bind conflicts.

🎨

### GUI Skins

Multiple UI templates with the same logic.

👁

### Drag & Drop

Intuitive item management with visual feedback.

## Quick Links

Common tasks and references

[ 🎨 #### Create a GUI Skin Design custom inventory interfaces ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 📚 #### Use Hooks Customize client behavior ](https://zyn-ic.github.io/Stoway/guides/use-hooks.html) [ 🔢 #### Binds Module Configure input bindings ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html)