# ⚙ Settings Configuration

Understand the two-layer settings system: global defaults and per-player overrides. Learn when to use each and how to modify them at runtime.

## The Two Layers

Global defaults vs per-player overrides

Stoway uses a two-layer settings system to provide flexibility:

🌎

### Global Settings

File: Settings.luau

Default values applied to all players. Set once, affects everyone.

  * Game-wide defaults
  * Static configuration
  * Changed in code
  * Requires server restart to apply

→

👤

### Per-Player Settings

Property: state.Settings

Individual overrides per player. Can change at runtime.

  * Player-specific values
  * Runtime modifiable
  * Synced to client automatically
  * VIP, permissions, game modes

## Global Settings

Configuration in src/shared/Settings.luau

Settings.luau Structure Luau

    -- src/shared/Settings.luau

    -- Hotbar Configuration
    Settings.Hotbar = {
        Type = "Static",      -- Static or Dynamic
        MaxSlots = 10,            -- Number of hotbar slots

    -- Storage/Backpack Configuration
    Settings.Storage = {
        Limit = 15,              -- Weight limit (0 = infinite)
        CanStack = true,         -- Enable stacking
        Sorting = true,          -- Enable sorting
        MaxStackSize = 5,        -- Max items per stack
        SortOrder = "None"        -- Name, Rarity, ItemType, None

    -- Stacking Rules
    Settings.Stacking = {
        RequiredFields = { "Rarity", "Type" },  -- Must match to stack
        Blacklist = {                          -- Never stack these
            ["Legendary"] = true,
            ["Mythic"] = true,
        },
        Rarity = true,            -- Show rarity colors

    -- Gameplay Settings
    Settings.Gameplay = {
        Droppable = true,        -- Can items be dropped
        DropDistance = 10,        -- Drop range

    -- UI Configuration
    Settings.DifferentUIs = {
        Default = {
            FolderName = "StowayGui",
            GuiName = "Stoway",
            HookPresetName = "DefaultHooks"
        },
        Admin = {
            FolderName = "StowayAdmin",
            GuiName = "AdminGui",
            HookPresetName = "AdminHooks"

Setting | Type | Default | Description
---|---|---|---
`Hotbar.MaxSlots` | number | 10 | Number of fixed hotbar slots (1-9 standard)
`Storage.Limit` | number | 15 | Weight limit, 0 = unlimited
`Storage.CanStack` | boolean | true | Enable item stacking
`Storage.MaxStackSize` | number | 5 | Max items per stack
`Gameplay.Droppable` | boolean | true | Allow dropping items to world

## Per-Player Settings

Runtime configuration per player

Each player has a `state.Settings` table that inherits from global defaults but can be overridden:

InventoryState.luau Luau

    -- src/server/StowayServerV1_2/Core/InventoryState.luau

    export type PlayerSettings = {
        Limit: number,             -- Weight limit (0 = infinite)
        CanStack: boolean,         -- Allow stacking
        MaxStackSize: number,      -- Max items per stack
        MaxHotbarSlots: number,   -- Number of hotbar slots
        Droppable: boolean,        -- Allow dropping
        UiType: string,            -- Interface version

    -- Constructor applies defaults
    function InventoryState.new(player: Player): InventoryStateType
        -- ...
        self.Settings = {
            Droppable = Settings.Gameplay.Droppable,
            Limit = Settings.Storage.Limit,
            CanStack = Settings.Storage.CanStack,
            MaxStackSize = Settings.Storage.MaxStackSize,
            MaxHotbarSlots = Settings.Hotbar.MaxSlots,
            UiType = "Default"
        return self
    end

## Modifying Per-Player Settings

Change settings at runtime

1

### Direct Modification

Modify `state.Settings` directly and sync:

Direct modification Luau

    local state = InventoryService.GetState(player)

    -- Give VIP players more inventory space
    if player.GetRankInGroup(12345) > 254 then
        state.Settings.Limit = 50  -- VIP gets 50 slots
    end

    -- Disable dropping for new players
    state.Settings.Droppable = false

    -- Sync to client
    InventoryService.SyncSettings(player)

2

### Change UI Type

Switch a player's UI skin:

Reload client UI Luau

    -- Switch to admin UI
    InventoryService.ReloadClient(player, {
        configSettingName = "Admin"
    })

    -- The client will:
    -- 1. Destroy old UI
    -- 2. Load new GUI from DifferentUIs.Admin
    -- 3. Load new hooks from DifferentUIs.Admin.HookPresetName
    -- 4. Rebuild all slots

## Adding More Hotbar Slots

Give players extra hotbar slots (e.g., admins get 12 slots)

Stoway supports up to 12 hotbar slots. The recommended maximum is 12 because:

  * PC: Keys 1-0 plus `-` and `=` (= is Shift+)
  * Console: R1/R2 modifier combos for easy thumb access
  * Simple mental mapping: 1-10 on number row, 11-12 on the keys next to it

Binds.luau - Hotbar Slots 11-12 Luau

    -- src/shared/Binds.luau

    -- Slot 11 (Minus key)
    Binds.HotbarSlots[11] = {
        Display = "-",
        PC = { {Main = Enum.KeyCode.Minus} },
        Console = { {Main = Enum.KeyCode.ButtonX, Modifiers = {Enum.KeyCode.ButtonR2}} } -- R2 + X

    -- Slot 12 (Equals key)
    Binds.HotbarSlots[12] = {
        Display = "=",
        PC = { {Main = Enum.KeyCode.Equals} },
        Console = { {Main = Enum.KeyCode.ButtonY, Modifiers = {Enum.KeyCode.ButtonR2}} } -- R2 + Y

1

### Modify Per-Player Settings

In `OnPlayerAdded`, override `state.Settings.MaxHotbarSlots`:

Give admins 12 slots Luau

    -- src/server/StowayServerV1_2/init.luau

    local ADMIN_GROUP_ID = 12345
    local ADMIN_RANK = 255

    local OnPlayerAdded = function(player)
        local state = InventoryState.new(player)
        PlayerInventories[player] = state

        -- Give admins 12 slots
        local rank = player:GetRankInGroup(ADMIN_GROUP_ID)
        if rank >= ADMIN_RANK then
            state.Settings.MaxHotbarSlots = 12
        end

        InventoryService.SyncSettings(player)
    end

2

### Check Client Hotbar Binds

Ensure your client code reads the correct number of binds from `Binds.HotbarSlots` based on `state.Settings.MaxHotbarSlots`.

**⚠ Important:**

  * Maximum supported slots: 12
  * Slots 11-12 use `-` and `=` keys on keyboard
  * Console uses R2 modifier (left trigger) for slots 11-12
  * Always sync settings after modifying: `InventoryService.SyncSettings(player)`

🎯

### Global Settings For:

  * Game balance constants
  * Default stacking rules
  * Standard hotbar size
  * Base weight limits

👤

### Per-Player Settings For:

  * VIP/member bonuses
  * Permission-based limits
  * Game mode restrictions
  * Temporary effects

## Example: VIP Inventory Bonus

Complete example with global and per-player settings

VIP Configuration Luau

    -- src/server/StowayServerV1_2/init.luau

    local VIP_GROUP_ID = 12345
    local VIP_RANK = 255
    local VIP_LIMIT = 50     -- VIPs get 50 slots
    local NORMAL_LIMIT = 15  -- Normal players get 15

    local OnPlayerAdded = function(player)
        local state = InventoryState.new(player)
        PlayerInventories[player] = state

        -- Apply VIP bonus
        local rank = player:GetRankInGroup(VIP_GROUP_ID)
        if rank >= VIP_RANK then
            print("[Stoway] " .. player.Name .. " is a VIP!")
            state.Settings.Limit = VIP_LIMIT
            state.Settings.MaxStackSize = 10  -- VIPs stack higher
        else
            state.Settings.Limit = NORMAL_LIMIT
        end

        -- Sync settings to client
        InventoryService.SyncSettings(player)
    end

## Example: Restricted Slots (Disabled Backpack)

Block specific hotbar slots globally when backpack is disabled

This example shows how to prevent items from entering specific slots (e.g., 7 and 8) when the backpack is disabled. We implement this as a **Global Setting** that applies to all players.

1

### Configure Settings

Define which slots are limited in `src/shared/Settings.luau`.

Settings.luau Luau

    Settings.Hotbar = {
        MaxSlots = 10,
        -- Custom table of restricted slots (Global)
        LimitedSlots = {
            [7] = true,
            [8] = true,

2

### Modify AddOperation (Server)

In `src/server/StowayServerV1_2/Operations/AddOperation.luau`, require the global settings and check limits.

AddOperation.luau Luau

    local Settings = require(game:GetService("ReplicatedStorage").Shared.Settings)

    -- Inside the BackpackDisabled block
    if Settings.Storage.BackpackEnabled == false then
        local emptySlot = SlotManager.FindEmptyHotbarSlot(state)

        if emptySlot then
            -- CHECK: Is this slot limited?
            local limited = Settings.Hotbar.LimitedSlots
            if limited and limited[emptySlot] then
                return {
                    success = false,
                    reason = emptySlot .. " is limited"
            end
            -- ... Standard add logic ...
        end
    end

3

### Modify SwapOperation (Server)

In `src/server/StowayServerV1_2/Operations/SwapOperation.luau`, prevent swapping into limited slots.

SwapOperation.luau Luau

    local Settings = require(game:GetService("ReplicatedStorage").Shared.Settings)

    -- Inside ValidateSwap function
    if not Settings.Storage.BackpackEnabled then
        local limited = Settings.Hotbar.LimitedSlots
        if limited then
            if from.type == "Hotbar" and limited[from.slot] then
                return false, from.slot .. " is limited"
            end
            if to.type == "Hotbar" and limited[to.slot] then
                return false, to.slot .. " is limited"
            end
        end
    end

4

### Modify Client Controller

In `src/client/StowayClientv1_2/Operations/InventoryController.luau`, check global settings to block dragging.

InventoryController.luau Luau

    local GlobalSettings = require(ReplicatedStorage.Shared.Settings)

    -- Inside Swap function
    if not GlobalSettings.Storage.BackpackEnabled then
        local limited = GlobalSettings.Hotbar.LimitedSlots
        if limited then
            if fromType == "Hotbar" and limited[fromSlot] then
                warn(fromSlot .. " is limited")
                return "Slot is limited"
            end
            if toType == "Hotbar" and limited[toSlot] then
                warn(toSlot .. " is limited")
                return "Slot is limited"
            end
        end
    end

## Settings Reference

Quick reference for all settings

Setting | Layer | Type | Default | Syncs to Client
---|---|---|---|---
`Limit` | Both | number | 15 | ✔ Yes
`CanStack` | Both | boolean | true | ✔ Yes
`MaxStackSize` | Both | number | 5 | ✔ Yes
`MaxHotbarSlots` | Both | number | 10 | ✔ Yes
`Droppable` | Both | boolean | true | ✔ Yes
`UiType` | Player only | string | "Default" | ✔ Yes
`Hotbar.Type` | Global only | string | "Static" | ✔ Yes
`Storage.Sorting` | Global only | boolean | true | ✖ No

## Use Cases

When to use each settings layer

[ + #### Add Server Features Create operations that use settings ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ 🎨 #### Create GUI Skins Multiple UI configurations ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 💻 #### API Reference SyncSettings, ReloadClient ](https://zyn-ic.github.io/Stoway/server/inventory-service.html)


# 💻 Server Backend

Server-authoritative inventory management with modular operations, anti-exploit protection, and per-player settings.

[ 🛠 ### Architecture System design, data flow, and component overview. → ](https://zyn-ic.github.io/Stoway/server/architecture.html) [ 📜 ### API Reference Complete InventoryService method documentation. → ](https://zyn-ic.github.io/Stoway/server/inventory-service.html)

## Key Features

What the server backend provides

🔒

### Server-Authoritative

All operations validated server-side. Zero client trust.

⚡

### O(1) Lookups

Hash table lookups for instant item access by UUID.

🔒

### Anti-Exploit

Per-player operation lock prevents remote spam.

🎯

### Modular Operations

Add, Remove, Swap, Equip, Drop as separate modules.

📈

### Per-Player Settings

Runtime-configurable limits per player (VIP bonuses).

📦

### Smart Stacking

Metadata-aware grouping with configurable rules.

## Quick Links

Common tasks and references

[ + #### Add a Server Feature Create new operations and extend the system ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ ⚙ #### Configure Settings Global defaults vs per-player overrides ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 📝 #### Code Templates Copy-paste ready operation templates ](https://zyn-ic.github.io/Stoway/templates/index.html)


# 📚 Hooks System

Event-driven customization for the client. Hooks allow you to customize UI behavior, add sound effects, trigger analytics, and more - without modifying core code.

[ 🛠 ### Hook Manager Core API for registering and firing hooks. → ](https://zyn-ic.github.io/Stoway/client/hooks/hooks/hook-manager.html) [ 💬 ### Available Hooks Complete reference of all hook events. → ](https://zyn-ic.github.io/Stoway/client/hooks/hooks/available-hooks.html) [ 💡 ### Practical Examples Common use cases and code samples. → ](https://zyn-ic.github.io/Stoway/client/hooks/index.html)

## How Hooks Work

Understanding the hook system

🗣

#### 1. Register

Register callback functions for specific events

`Hooks:Register("OnSlotClick", callback)`

→

🔥

#### 2. Fire

When an event occurs, hooks fire with data

`Hooks:Fire("OnSlotClick", slotType, index)`

→

⚡

#### 3. Execute

All registered callbacks run with event data

`callback(slotType, index, uuid)`

## Why Use Hooks?

Benefits over direct modification

🔗

#### Non-Invasive

Don't modify core code. Safe from merge conflicts when updating.

🛑

#### Dynamic

Load different hooks based on UI skin or game mode.

📈

#### Modular

Organize hooks into presets. Mix and match as needed.

🚀

#### Performant

Lightweight. Only registered callbacks execute.

## Common Use Cases

What hooks are typically used for

#### 🔊 Sound Effects

Play click, hover, and drag sounds on inventory interactions.

#### 📷 Visual Feedback

Animate slots on hover, selection, and equip.

#### 📊 Analytics

Track item pickups, drops, and equips for gameplay metrics.

#### 🔮 Game Integration

Trigger achievements, quests, or other systems on events.

#### 🎯 Haptic Feedback

Controller vibration on gamepad interactions.

#### 🕑 Cooldowns

Debounce rapid clicks or prevent spam actions.

## Quick Start

Register your first hook

Basic Hook Registration Luau

    -- Get the HookManager from client
    local Client = require(path.to.StowayClient)

    -- Register a hook
    Client.Hooks:Register("OnSlotClick", function(slotType, index, uuid)
        -- Play sound
        SoundService.Click:Play()

        -- Log for analytics
        print("Clicked " .. slotType .. " #" .. index)
    end)

## Related Documentation

See also

[ 💬 #### All Hooks Complete reference ](https://zyn-ic.github.io/Stoway/client/hooks/hooks/available-hooks.html) [ 💡 #### Examples Practical code samples ](https://zyn-ic.github.io/Stoway/client/hooks/index.html) [ 📚 #### Use Hooks Guide Detailed tutorial ](https://zyn-ic.github.io/Stoway/guides/use-hooks.html) [ 🎨 #### GUI System UI customization ](https://zyn-ic.github.io/Stoway/client/index.html)


# 💻 Server Architecture

Understanding the server-authoritative backend structure, data flow, and how to extend it.

## System Overview

High-level architecture diagram

Client

StowayClientv1_2 →

InventoryAction (RemoteEvent) InventoryUpdate (RemoteEvent)

Server

InventoryService (init.luau)

#### Core

  * InventoryState
  * SlotManager
  * LimitChecker

#### Operations

  * AddOperation
  * RemoveOperation
  * SwapOperation
  * EquipOperation
  * DropOperation

#### Replication

  * Replicator
  * Actions

#### Utils

  * UUID
  * MetadataParser
  * StackChecker

## Directory Structure

File organization

src/server/StowayServerV1_2/ Tree

    src/server/StowayServerV1_2/
    - init.luau                    # Entry point & InventoryService API
    - -- Core Data & Logic
    - Core/
    -   InventoryState.luau       # Per-player inventory state container
    -   SlotManager.luau          # Hotbar/Storage slot management
    -   LimitChecker.luau         # Weight limit validation
    - -- Inventory Operations (Modular)
    - Operations/
    -   AddOperation.luau         # Add items (Limit → Stack → Hotbar → Storage)
    -   RemoveOperation.luau      # Remove items (Validate → Reduce/Destroy)
    -   SwapOperation.luau        # Swap any two slots
    -   EquipOperation.luau       # Equip/Unequip items
    -   DropOperation.luau        # Drop items to world
    - -- Client Communication
    - Replication/
    -   Replicator.luau           # Delta updates to clients
    -   Actions.luau              # Payload builders
    - -- Utilities
    - Utils/
    -   UUID.luau                # Unique ID generation
    -   MetadataParser.luau       # Parse metadata from tools
    -   StackChecker.luau         # Stacking validation
    - -- Debug (Remove in production!)
    - Debug/
    -   ChatCommands.luau         # /add, /swap, /equip, etc.

## Data Flow

How requests are processed

1

#### Client Request

Client fires `InventoryAction` RemoteEvent with operation type and arguments.

↓

2

#### Anti-Exploit Lock

Server checks `PlayerOperationLock[player]`. If locked, rejects request.

↓

3

#### Route to Handler

Operation type maps to handler in `OperationHandlers` table.

↓

4

#### Execute Operation

Handler calls operation module's `Execute()` function. Validates and modifies state.

↓

5

#### Replicate

If successful, Replicator sends delta update via `InventoryUpdate` RemoteEvent.

↓

6

#### Client Sync

Client's NetworkHandler processes update and syncs Fusion state.

## Key Components

What each module does

### InventoryState

Pure data container. No logic, just storage.

  * `state.Items` \- UUID → Item map
  * `state.Hotbar` \- Slot → UUID
  * `state.Storage` \- Array of UUIDs
  * `state.Settings` \- Per-player config

### SlotManager

Slot location management. Handles static vs dynamic containers.

  * `FindEmptyHotbarSlot()`
  * `SetHotbarSlot()`
  * `AppendStorage()`
  * `FindSlotByUUID()`

### LimitChecker

Weight tracking and validation.

  * `IsEnabled()`
  * `GetRemainingCapacity()`
  * `AddWeight()`
  * `RemoveWeight()`

### Operations

Modular operation handlers. Each handles one action.

  * Add → Remove → Swap
  * Equip → Unequip
  * Drop to world
  * Stack two stacks

## Anti-Exploit Features

Server-authoritative protections

🔒

#### Per-Player Operation Lock

Only one operation per player at a time. Prevents spam exploits.

🔔

#### Single Remote Entry Point

All requests go through `InventoryAction`. No direct state access.

🛡

#### Bounds Checking

All slot indices validated before access. Prevents out-of-bounds exploits.

💪

#### Pcall Wrapping

All operations wrapped in pcall. Server never crashes from bad input.

## Related Documentation

See also

[ 💻 #### API Reference Complete API documentation ](https://zyn-ic.github.io/Stoway/server/inventory-service.html) [ + #### Add Features How to extend the server ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ ⚙ #### Settings Global and per-player settings ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 🎨 #### Client Architecture Frontend structure ](https://zyn-ic.github.io/Stoway/client/architecture.html)


# 🎮 Input System

Cross-platform input handling with automatic switching between mouse/touch and gamepad. Features a context-based bind system that prevents conflicts between different modes.

## Walkthroughs

Use the beginner setup guide first, then continue with the advanced input walkthrough.

[ Beginner ### Quick Start Video Open the setup guide with the embedded beginner walkthrough and install flow. Open Quick Start → ](https://zyn-ic.github.io/Stoway/quickstart.html) [ Advanced ### Advanced Walkthrough Follow the longer Vimeo walkthrough for deeper console and input coverage. Watch on Vimeo → ](https://vimeo.com/1171249324)

[ 📍 ### Interaction Manager Automatic input type detection and mode switching. → ](https://zyn-ic.github.io/Stoway/client/input/interaction-manager.html) [ ⚔ ### Console Modes Combat vs Inventory mode switching and state management. → ](https://zyn-ic.github.io/Stoway/client/input/console-modes.html) [ ✓ ### Selection States Selection flow for swapping, dropping, and quick moving. → ](https://zyn-ic.github.io/Stoway/client/input/selection-states.html) [ 🔢 ### Binds Module Context-based input system for conflict-free bindings. → ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html)

## Architecture Overview

How the input system works together

Input System Flow Diagram

    UserInputService.PreferredInput
        +-- Mouse/Touch
        |   +-- InteractionManager
        |       +-- DragHandler.Enabled = true
        |       +-- ConsoleModeManager.Destroy()
        +-- Gamepad
            +-- InteractionManager
                +-- DragHandler.Enabled = false
                +-- ConsoleModeManager.Init()
                    +-- Mode Indicator (HUD badge)
                    +-- Combat Mode (L3 long press)
                    |   +-- Hotbar binds enabled
                    |   +-- Selection disabled
                    +-- Inventory Mode
                        +-- SelectionManager.Enabled = true
                        +-- Selection States: Idle → Source → Target/Drop

## Key Components

Each module's responsibility

Component | File | Purpose
---|---|---
InputManager | InputManager.luau | Creates IllusionIAS contexts and manages bind lifecycle
InteractionManager | InteractionManager.luau | Detects input type, switches between DragHandler and ConsoleMode
ConsoleModeManager | ConsoleModeManager.luau | Manages Combat/Inventory modes with visual indicator
SelectionManager | SelectionManager.luau | Handles slot selection flow for gamepad users
Binds | Binds.luau | Centralized bind configuration with PC/Console support

## Context System

How IllusionIAS contexts prevent bind conflicts

Stoway uses four contexts to organize binds. This allows the same physical key to do different things in different modes:

StowayGlobal

Always enabled. Contains backpack toggle and mode switch.

Tab B DPadLeft

StowayCombat

Enabled in Combat mode. Hotbar slot selection for equipping.

1-9 R1+X L1+X

StowayInventory

Enabled in Inventory mode. Selection and actions.

Y (Swap) X (Drop) A (QuickMove) B (Cancel)

StowayDrop

Enabled when dropping. Amount adjustment.

DPadUp DPadDown

See [Binds Module](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) for detailed configuration.

## Console Mode Flow

How gamepad users interact with inventory

⚔

#### Combat Mode

Character controls active

  * Hotbar slots work
  * Inventory closed
  * Selection disabled

Long Press L3 ↓

🎒

#### Inventory Mode

UI navigation active

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

From Inventory Mode

X pressed → Drop Mode

## Customization

Modifying the input system

[ Reference 🔢 ### Binds Module Configure all input bindings in one place. ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) [ Reference ⚔ ### Console Modes Customize mode switching behavior. ](https://zyn-ic.github.io/Stoway/client/input/console-modes.html) [ Reference ✓ ### Selection States Modify selection flow and actions. ](https://zyn-ic.github.io/Stoway/client/input/selection-states.html)


# 🔢 Binds Module

A centralized, context-based input configuration system that prevents bind conflicts by organizing bindings into logical contexts (Global, Combat, Inventory, Drop).

## Why Contexts?

The problem with traditional bind systems

#### ❌ The Problem

In traditional systems, if you bind "X" to "Drop" globally, it will trigger even when:

  * Opening the backpack
  * In a menu
  * During combat

This causes conflicts and confusing UX.

#### ✔ The Solution

Stoway's context system ensures "X" only triggers "Drop" when:

  * Gamepad input is active
  * Inventory mode is enabled
  * A slot is selected

Same key, different actions in different contexts.

## Context Structure

Four contexts for different interaction modes

Context Name

When Enabled

Contains

StowayGlobal

Always

Backpack toggle, Mode switch

StowayCombat

Combat Mode

Hotbar slot selection

StowayInventory

Inventory Mode

Swap, Drop, QuickMove, Cancel

StowayDrop

Dropping Item

Increase/Decrease amount

## File Structure

Location and organization

File Location Tree

    ReplicatedStorage/
    - Shared/
    -   Binds.luau       -- Main configuration file

Binds.luau Structure Luau

    local Binds = {}

    -- Helper function for applying binds
    function Binds.ApplyToAction(action, bindConfig, inputType?)

    -- Hotbar slot configuration (1-10)
    Binds.HotbarSlots = {
        [1] = { Display = "1", PC = {...}, Console = {...} },
        [2] = { ... },
        -- ... up to 10

    -- Global binds (always available)
    Binds.Global = {
        Backpack = { PC = {...}, Console = {...} },
        SwitchMode = { PC = {...}, Console = {...} },
        InventoryActions = { Swap = {...}, Drop = {...}, ... },
        DropActions = { Increase = {...}, Decrease = {...} }

    return Binds

## Bind Format

How to define input bindings

Bind Structure Luau

    -- Single bind entry
        Main = Enum.KeyCode."KeyName",        -- Required: primary key
        Modifiers = { Enum.KeyCode."Modifier" }  -- Optional: modifier keys

    -- Examples
    { Main = Enum.KeyCode.One }                                    -- Just "1"
    { Main = Enum.KeyCode.ButtonX, Modifiers = { Enum.KeyCode.ButtonR1 } }  -- R1 + X
    { Main = Enum.KeyCode.Tab }                                    -- Tab key

Field | Type | Required | Description
---|---|---|---
`Main` | Enum.KeyCode | Yes | The primary key to bind
`Modifiers` | {Enum.KeyCode} | No | Modifier keys that must be held

## Hotbar Slots Configuration

Slot binding for 1-10 keys

Hotbar Bind Examples Luau

    Binds.HotbarSlots = {
        [1] = {
            Display = "1",  -- Shown on slot
            PC = { {Main = Enum.KeyCode.One} },
            Console = { {Main = Enum.KeyCode.ButtonX, Modifiers = {Enum.KeyCode.ButtonR1}} }
        },
        [2] = {
            Display = "2",
            PC = { {Main = Enum.KeyCode.Two} },
            Console = { {Main = Enum.KeyCode.ButtonY, Modifiers = {Enum.KeyCode.ButtonR1}} }
        },
        [3] = {
            Display = "3",
            PC = { {Main = Enum.KeyCode.Three} },
            Console = { {Main = Enum.KeyCode.ButtonB, Modifiers = {Enum.KeyCode.ButtonR1}} }
        },
        [4] = {
            Display = "4",
            PC = { {Main = Enum.KeyCode.Four} },
            Console = { {Main = Enum.KeyCode.ButtonA, Modifiers = {Enum.KeyCode.ButtonR1}} }
        },
        [5] = {
            Display = "5",
            PC = { {Main = Enum.KeyCode.Five} },
            Console = { {Main = Enum.KeyCode.ButtonX, Modifiers = {Enum.KeyCode.ButtonL1}} }
        },
        [6] = {
            Display = "6",
            PC = { {Main = Enum.KeyCode.Six} },
            Console = { {Main = Enum.KeyCode.ButtonY, Modifiers = {Enum.KeyCode.ButtonL1}} }
        },
        [7] = {
            Display = "7",
            PC = { {Main = Enum.KeyCode.Seven} },
            Console = { {Main = Enum.KeyCode.ButtonB, Modifiers = {Enum.KeyCode.ButtonL1}} }
        },
        [8] = {
            Display = "8",
            PC = { {Main = Enum.KeyCode.Eight} },
            Console = { {Main = Enum.KeyCode.ButtonA, Modifiers = {Enum.KeyCode.ButtonL1}} }
        },
        [9] = {
            Display = "9",
            PC = { {Main = Enum.KeyCode.Nine} },
            Console = { {Main = Enum.KeyCode.DPadUp, Modifiers = {Enum.KeyCode.ButtonR1}} }
        },
        [10] = {
            Display = "0",
            PC = { {Main = Enum.KeyCode.Zero} },
            Console = { {Main = Enum.KeyCode.DPadDown, Modifiers = {Enum.KeyCode.ButtonR1}} }

**💡 Tip:** PC binds use number keys 1-0. Console binds use R1/L1 + face buttons for quick access.

## Global Binds

Bindings available in all contexts

Global Configuration Luau

    Binds.Global = {
        -- Toggle backpack visibility
        Backpack = {
            PC = {
                {Main = Enum.KeyCode.Tab},
                {Main = Enum.KeyCode.B}
            },
            Console = { {Main = Enum.KeyCode.DPadLeft} }
        },

        -- Switch between Combat/Inventory modes
        SwitchMode = {
            PC = { {Main = Enum.KeyCode.M} },  -- M key for testing
            Console = { {Main = Enum.KeyCode.ButtonL3} }  -- Long press L3
        },

        -- Inventory mode actions
        InventoryActions = {
            Swap = {
                Console = { {Main = Enum.KeyCode.ButtonY} }
            },
            Drop = {
                Console = { {Main = Enum.KeyCode.ButtonX} }
            },
            QuickMove = {
                Console = { {Main = Enum.KeyCode.ButtonL1, Modifiers = {Enum.KeyCode.ButtonR1}} }
            },
            Cancel = {
                Console = { {Main = Enum.KeyCode.ButtonB} }
        },

        -- Drop amount adjustment
        DropActions = {
            Increase = { Console = { {Main = Enum.KeyCode.DPadUp} } },
            Decrease = { Console = { {Main = Enum.KeyCode.DPadDown} } },

## Helper Function

ApplyToAction for easy binding

The `ApplyToAction` helper function simplifies applying binds to IllusionIAS actions:

ApplyToAction Usage Luau

    -- Signature
    function Binds.ApplyToAction(
        action,              -- IllusionIAS action object
        bindConfig,          -- Bind entry from Binds table
        inputType: ("PC" | "Console")?  -- Optional: filter by input type
    )

    -- Example 1: Apply all binds (PC + Console)
    local swapAction = IllusionIAS.new("ConsoleSwapAction")
    Binds.ApplyToAction(swapAction, Binds.Global.InventoryActions.Swap)

    -- Example 2: Apply only Console binds
    local dropAction = IllusionIAS.new("ConsoleDropAction")
    Binds.ApplyToAction(dropAction, Binds.Global.InventoryActions.Drop, "Console")

    -- Example 3: Apply PC binds only
    local backpackAction = IllusionIAS.new("BackpackAction")
    Binds.ApplyToAction(backpackAction, Binds.Global.Backpack, "PC")

## Customization Example

Adding a custom bind

1

### Add to Binds.luau

Custom bind entry Luau

    -- In Binds.Global section
    InventoryActions = {
        Swap = { Console = { {Main = Enum.KeyCode.ButtonY} } },
        Drop = { Console = { {Main = Enum.KeyCode.ButtonX} } },
        QuickMove = { Console = { {Main = Enum.KeyCode.ButtonL1, Modifiers = {Enum.KeyCode.ButtonR1}} } },
        Cancel = { Console = { {Main = Enum.KeyCode.ButtonB} } },
        -- Add your custom action
        EquipBest = {
            Console = { {Main = Enum.KeyCode.ButtonR3} }  -- R3 (right stick click)

2

### Create the Action

In SelectionManager.luau Luau

    -- Create action
    local equipBestAction = IllusionIAS.new("ConsoleEquipBestAction")
    Binds.ApplyToAction(equipBestAction, Binds.Global.InventoryActions.EquipBest, "Console")

    -- Connect handler
    equipBestAction.Activated:Connect(function(active, pressed)
        if not SelectionState.enabled or not pressed then return end
        SelectionManager.HandleEquipBest()
    end)

    -- Add to inventory context
    IllusionIAS.addContext(InputManager.ContextInventory, equipBestAction)

## Available Key Codes

Commonly used Enum.KeyCode values

Category | Key Codes
**Number Keys** | `One, Two, Three, Four, Five, Six, Seven, Eight, Nine, Zero`
**Letter Keys** | `A, B, C, ... Z`
**Gamepad - Face Buttons** | `ButtonA, ButtonB, ButtonX, ButtonY`
**Gamepad - Shoulder** | `ButtonL1, ButtonR1, ButtonL2, ButtonR2`
**Gamepad - Stick** | `ButtonL3, ButtonR3` (click)
**Gamepad - D-Pad** | `DPadUp, DPadDown, DPadLeft, DPadRight`
**Special** | `Tab, Escape, Space, Backspace, Enter`

## Related Documentation

See also

[ 🎮 #### Input System Complete input overview ](https://zyn-ic.github.io/Stoway/client/input/index.html) [ ⚔ #### Console Modes How modes enable/disable contexts ](https://zyn-ic.github.io/Stoway/client/input/console-modes.html) [ ✓ #### Selection States Actions triggered by selection ](https://zyn-ic.github.io/Stoway/client/input/selection-states.html) [ 📍 #### Interaction Manager Input type detection ](https://zyn-ic.github.io/Stoway/client/input/interaction-manager.html)


# 📝 Code Templates

Copy-paste ready code snippets for common tasks. Each template includes placeholders and comments to help you customize it for your needs.

## Server Templates

Backend code for extending inventory functionality

[ ⚙ ### Server Operation Template for creating new inventory operations Luau ](https://zyn-ic.github.io/Stoway/templates/server-operation-template.luau) 💬 ### Hook Preset Template for client-side hook configurations Luau 🎨 ### GUI Skin Config Template for Settings.DifferentUIs configuration Luau

## Usage

How to use these templates

1

### Download or Copy

Click the template link to download, or copy the code directly from the page.

2

### Customize Placeholders

Replace all `{{PLACEHOLDER}}` values with your specific code:

  * `{{OperationName}}` \- Your operation name (e.g., "Craft")
  * `{{param1}}` \- First parameter
  * `{{param2}}` \- Second parameter
  * `{{resultFields}}` \- Result fields

3

### Add to Your Project

Place the file in the appropriate location and wire it up per the guide.

See [Add Server Features](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) guide for complete instructions.

## Quick Reference

Click any card to see full documentation. Click outside to close.

#### + Add Item

    InventoryService.AddItem(player, "itemId", 1, nil, true)

##### AddItem

Adds an item to the player's inventory. Follows hotbar-first, then storage logic.

Signature Luau

    function InventoryService.AddItem(
        player: Player,
        itemId: string,
        amount: number,
        metadata: any?,
        REPLICATE: boolean
    ): AddResult

Parameters Table

Param| Type| Description
---|---|---
player| Player| The target player
itemId| string| Item ID (must exist in ServerStorage.Tools)
amount| number| Quantity to add
metadata| any?| Optional item metadata
REPLICATE| boolean| Sync to client if true

Returns Luau

        success = boolean,
        reason = string?,
        addedItems = { { uuid, slotType, slot } }?,
        addedAmount = number,
        overflow = number

#### ✖ Remove Item

    InventoryService.RemoveItem(player, uuid, amount, true)

##### RemoveItem

Removes an item from the player's inventory by UUID.

Signature Luau

    function InventoryService.RemoveItem(
        player: Player,
        uuid: string,
        amount: number?,
        REPLICATE: boolean
    ): RemoveResult

Parameters Table

Param| Type| Description
---|---|---
player| Player| The target player
uuid| string| Item UUID to remove
amount| number?| Amount to remove (nil = all)
REPLICATE| boolean| Sync to client if true

#### ⇆ Swap Slots

    InventoryService.SwapSlots(player, "Hotbar", 1, "Storage", 5, true)

##### SwapSlots

Swaps items between any two slots (Hotbar/Storage combinations).

Signature Luau

    function InventoryService.SwapSlots(
        player: Player,
        fromType: string,
        fromSlot: number,
        toType: string,
        toSlot: number,
        REPLICATE: boolean
    ): SwapResult

Parameters Table

Param| Type| Description
---|---|---
fromType| string| "Hotbar" or "Storage"
fromSlot| number| Source slot index (1-based)
toType| string| "Hotbar" or "Storage"
toSlot| number| Target slot index (1-based)
REPLICATE| boolean| Sync to client if true

#### ⛏ Equip Item

    InventoryService.EquipSlot(player, "Hotbar", 1, true)

##### EquipSlot

Equips an item from a slot, making it the active tool.

Signature Luau

    function InventoryService.EquipSlot(
        player: Player,
        slotType: string,
        slot: number,
        REPLICATE: boolean
    ): EquipResult

Parameters Table

Param| Type| Description
---|---|---
player| Player| The target player
slotType| string| "Hotbar" or "Storage"
slot| number| Slot index (1-based)
REPLICATE| boolean| Sync to client if true

#### 🗑 Drop Item

    InventoryService.DropItem(player, "Hotbar", 1, 1, true)

##### DropItem

Drops an item from a slot into the world as a pickup.

Signature Luau

    function InventoryService.DropItem(
        player: Player,
        slotType: string,
        slot: number,
        amount: number?,
        REPLICATE: boolean
    ): DropResult

Parameters Table

Param| Type| Description
---|---|---
player| Player| The target player
slotType| string| "Hotbar" or "Storage"
slot| number| Slot index
amount| number?| Amount to drop (nil = all)
REPLICATE| boolean| Sync to client if true

#### 🔍 Get State

    InventoryService.GetState(player)

##### GetState

Returns the player's inventory state for direct access.

Signature Luau

    function InventoryService.GetState(player: Player): InventoryStateType?

Returns Luau

    InventoryStateType {
        Items: { [uuid] = ItemInstance },
        ItemsByID: { [itemId] = { uuid } },
        Hotbar: { [slot] = uuid? },
        Storage: { uuid },
        EquippedItemUUID: string?,
        Weight: number,
        Settings: PlayerSettings

## Related

See also

[ + #### Add Server Features Step-by-step guide ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ 💻 #### API Reference Complete API docs ](https://zyn-ic.github.io/Stoway/server/inventory-service.html) [ ⚙ #### Settings Guide Configuration patterns ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html)


## Server API

All available methods for inventory management

Method | Parameters | Description
---|---|---
AddItem | player, itemId, amount, metadata? | Add to hotbar first, then storage. Auto-stacks.
AddToBackpack NEW | player, itemId, amount?, metadata? | Add directly to storage (bypasses hotbar).
RemoveItem | player, uuid, amount? | Remove item by UUID. Partial removal supported.
SwapSlots | player, fromType, fromSlot, toType, toSlot | Swap any two slots (Hotbar/Storage).
StackTwoSlots NEW | player, fromType, fromSlot, toType, toSlot | Merge two stacks (rarely needed - add auto-stacks).
EquipSlot | player, slotType, slot | Equip item from Hotbar or Storage.
UnequipSlot | player | Unequip current tool.
DropItem | player, slotType, slot | Drop item into world.
UpdateMetadata | player, uuid, updates | Update item metadata (delta sync).
ReloadClient NEW | player, config | Triggers a client-side UI/Hook reload. Config: `{ configSettingName, RefreshData }`
GetState | player | Get player's inventory state.

Server Usage Example Luau

    -- Initialize
    local InventoryService = require(game.ServerScriptService.StowayServerV1_2)
    InventoryService.Init()

    -- Add items
    InventoryService.AddItem(player, "Sword", 1)

    -- Add directly to backpack
    InventoryService.AddToBackpack(player, "Potion", 5)

## Network Protocol

Bidirectional communication standards

**🔒 Anti-Exploit Features:**

  * Per-player operation lock prevents remote spam
  * Spammed calls return `{ success = false, reason = "OPERATION_IN_PROGRESS" }`
  * Lock auto-releases on completion (success or error)
  * pcall wrapper prevents server crashes

### 1. Client Requests (Input)

Sent via `InventoryAction` RemoteEvent.

Operation | Arguments (Table) | Description
---|---|---
"Swap" | { fromType, fromSlot, toType, toSlot } | Swap item locations. Validates ownership and bounds.
"Stack" | { fromType, fromSlot, toType, toSlot } | Merge stack from source to target.
"Equip" | { slotType?, slot } | Equip item. `slotType` defaults to "Hotbar".
"Unequip" | {} | Unequip currently held item.
"Remove" | { uuid, amount? } | Delete item. Removes entire stack if amount is nil.
"Drop" | { slotType, slot } | Drop item into the world (spawns pickup).

### 2. Server Replication (Output)

Received via `InventoryUpdate` RemoteEvent. The server sends delta updates.

Action | Payload Structure | Description
---|---|---
"Init" | { Items, Hotbar, Storage, EquippedUUID } | Full state sync. Overwrites local state.
"Add" | { Slots: [{ UUID, SlotType, Slot, Item }] } | List of items added. `Item` contains full data.
"Remove" | { UUID, SlotType, Slot } | Single item removal from a specific slot.
"Swap" | { From: {Type, Slot}, To: {Type, Slot} } | Confirm swap (Client may predict this).
"Equip" | { UUID: string? } | Update equipped item UUID (or nil).
"UpdateMeta" | { UUID, Updates: {} } | Partial metadata update for an item.
"Reload" | { UiType, Data? } | Instructions to reload UI/Hooks and optional full state resync.

### 3. Initial Sync

Sent via `AskForStoway` RemoteFunction.

Handshake Luau

    -- Client Request
    local result = Remotes.AskForStoway:InvokeServer()

    -- Server Response
        success = true,
        data = {
            Items = {...},
            Hotbar = {...},
            Storage = {...},
            Settings = {...}

## Per-Player Settings

Runtime-configurable settings stored in state.Settings

Setting | Default | Description
---|---|---
Limit | 15 | Weight limit (0 = infinite)
CanStack | true | Enable/disable stacking
MaxStackSize | 5 | Max items per stack
MaxHotbarSlots | 9 | Hotbar slot count

## Debug Commands

Test commands (remove in production!)

**⚠ Remove `Debug/ChatCommands.luau` in production!** These commands bypass normal validation.

Command | Description
`/inv` | Display inventory state
`/add [id] [amt?]` | Add item (hotbar first)
`/addback [id] [amt?]` | Add directly to backpack
`/remove [uuid] [amt?]` | Remove item by UUID
`/swap [t1] [s1] [t2] [s2]` | Swap slots
`/stack [t1] [s1] [t2] [s2]` | Stack items together
`/equip [slot]` | Equip from hotbar
`/unequip` | Unequip current
`/drop [type] [slot]` | Drop item
`/clear` | Clear inventory
`/toggle_stack` | Toggle stacking
`/set_limit [n]` | Set weight limit


# ► Quick Start Guide

Get Stoway running in 5 minutes. This guide covers installation, basic configuration, and your first inventory operation.

🎥

## Video Walkthroughs

Start with the beginner setup video, then continue to the advanced walkthrough.

Beginner Walkthrough

Covers installation, first-time setup, and the core project structure.

Advanced Walkthrough

### Deep-dive follow-up

Use this once the beginner setup is working and you want the longer walkthrough.

[Watch Advanced Walkthrough](https://vimeo.com/1171249324)

## Step 1: Copy the Files

Add Stoway to your Roblox project

1

### Copy Server Code

Copy the entire `src/server/StowayServerV1_2` folder to your **ServerScriptService**

Expected Location Tree

    ServerScriptService/
    - StowayServerV1_2/
    -   init.luau
    -   Core/
    -   Operations/
    -   Replication/
    -   Utils/

2

### Copy Client Code

Copy the entire `src/client/StowayClientv1_2` folder to your **StarterPlayerScripts**

Expected Location Tree

    StarterPlayer/
    - StarterPlayerScripts/
    -   StowayClientv1_2/
    -     init.luau
    -   CoreState/
    -   Input/
    -   Hook/
    -   Operations/

3

### Copy Shared Code

Copy the shared modules to **ReplicatedStorage**

Required Files Tree

    ReplicatedStorage/
    - Shared/
    -   Settings.luau      # Configuration
    -   Binds.luau         # Input bindings
    -   RarityValues.luau  # Optional: item rarities
    - Packages/
    -   Fusion             # UI framework
    -   IllusionIAS        # Input system

## Step 2: Set Up GUI Templates

Create your inventory interface structure

4

### Create GUI Folder

In **ReplicatedStorage**, create a folder named `Gui` with your interface:

GUI Structure Tree

    ReplicatedStorage/
    - Gui/
    -   StowayGui/
    -     Stoway              # Main ScreenGui
    -       Backpack          # Storage container
    -         ScrollingFrame  # Dynamic slots
    -         SearchFilter    # Optional search
    -         Weight          # Optional weight label
    -       Hotbar            # Fixed slots (1-9)
    -     Slot/               # Slot template folder
    -       template          # Slot template Frame
    -         slotbutton      # ImageButton for item
    -         stackcount      # Amount label
    -         slotnumber      # Keybind display
    -         selecticon      # Equipped indicator
    -         rarityframe     # Rarity border

See [Create GUI Skins](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) for detailed template requirements.

## Step 3: Configure Settings

Edit src/shared/Settings.luau for your game

Essential Settings Luau

    -- ReplicatedStorage/Shared/Settings.luau

    -- Hotbar slot count
    Settings.Hotbar = {
        MaxSlots = 9,  -- Change to your preference

    -- Inventory capacity
    Settings.Storage = {
        Limit = 15,       -- 0 = unlimited
        CanStack = true,
        MaxStackSize = 5,

    -- UI Configuration (matches GUI folder names)
    Settings.DifferentUIs = {
        Default = {
            FolderName = "StowayGui",
            GuiName = "Stoway",
            HookPresetName = "DefaultHooks"

## Step 4: Initialize the System

Start Stoway in your game

5

### Server Initialization

Add this to a ServerScript (e.g., ModuleScript in ServerScriptService):

Server Setup Luau

    -- ServerScriptService/Main.luau

    local InventoryService = require(
        game.ServerScriptService.StowayServerV1_2
    )

    -- Initialize the system
    InventoryService.Init()

    print("[Stoway] Inventory system initialized!")

6

### Client Initialization

Client starts automatically via StarterPlayerScripts entry point

Client Entry Luau

    -- StarterPlayerScripts/StowayClientv1_2/init.luau

    local ClientInventoryService = require(script)

    -- Initialize client (called automatically)
    ClientInventoryService.Init()

## Step 5: Add Your First Item

Test the system with an item

Give Item Script Luau

    -- ServerScriptService/GiveItems.luau

    local InventoryService = require(
        game.ServerScriptService.StowayServerV1_2
    )

    -- Example: Give a tool to a player
    local player = game.Players.LocalPlayer  -- Use in a RemoteFunction context

    -- Add item to player's inventory
    local result = InventoryService.AddItem(
        player,
        "Sword",      -- Item ID (must exist in ServerStorage.Tools)
        1,             -- Amount
        nil,            -- Optional metadata
        true            -- Replicate to client
    )

    if result.success then
        print("Added item successfully!")
    else
        warn("Failed to add item: " .. result.reason)
    end

**💡 Tip:** Ensure your item exists in `ServerStorage.Tools` with the matching name before adding it to inventories.

## Next Steps

Where to go from here

[ + #### Add Server Features Learn to create custom operations ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ 🎨 #### Create GUI Skins Design custom 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) [ 🎮 #### Console Support Gamepad input and modes ](https://zyn-ic.github.io/Stoway/client/input/index.html)


# 📚 How-To Guides

Task-oriented tutorials for extending and customizing Stoway. Each guide includes step-by-step instructions and code examples.

## Getting Started

Essential guides for new users

[ Beginner ► ### 5-Min Quick Start Get Stoway running in your project with minimal configuration. 5 min read ](https://zyn-ic.github.io/Stoway/guides/quickstart.html) [ Beginner ⚙ ### Settings Configuration Understand global vs per-player settings and how to configure them. 8 min read ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html)

## Server Development

Extending the backend

[ Intermediate + ### Add Server Features Create new operations, add API methods, and wire them to the network protocol. 15 min read ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ Beginner ✖ ### Remove Features Disable sorting, rarity, death handling. Includes custom death patterns and disabling the client. 10 min read ](https://zyn-ic.github.io/Stoway/guides/remove-features.html) [ Beginner 💾 ### Add Persistence Save and load player inventories using ProfileStore or DataStore. 10 min read ](https://zyn-ic.github.io/Stoway/guides/persistence.html)

## Client Customization

UI, hooks, and input

[ Coming Soon 🎨 ### Create GUI Skins Design custom inventory interfaces using the template system. 12 min read ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ Beginner 📚 ### Use Hooks Customize client behavior with sound effects, animations, and analytics. 10 min read ](https://zyn-ic.github.io/Stoway/guides/use-hooks.html)

## Reference

Related documentation

[ #### 💻 Server API Complete reference for InventoryService methods ](https://zyn-ic.github.io/Stoway/server/inventory-service.html) [ #### 💬 Available Hooks All hooks and their arguments ](https://zyn-ic.github.io/Stoway/client/hooks/available-hooks.html) [ #### 🔢 Binds Module Context-based input configuration ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) [ #### 📝 Code Templates Copy-paste ready code snippets ](https://zyn-ic.github.io/Stoway/templates/index.html)


# 💾 Add Persistence

Save and load player inventories using ProfileStore or DataStore. Simple patterns for beginner to intermediate developers.

## Overview

The three steps of persistence

1

### Load

Fetch saved inventory when player joins

`ProfileStore:LoadProfile()`

→

2

### Replace

Apply loaded data to InventoryState

`state.Items = loadedData`

→

3

### Save

Store inventory when player leaves

`ProfileStore:Save()`

## Option 1: ProfileStore (Recommended)

Use the ProfileStore module for easy data management

Setup ProfileStore Luau

    -- ServerScriptService/Persistence.luau

    local ProfileService = require(game:GetService("ServerScriptService").ProfileStore)
    local InventoryService = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared"):WaitForChild("StowayServerV1_2"))

    -- Create ProfileStore key
    local PlayerData = ProfileService.GetProfileStore("PlayerData", {
        Template = {
            Items = {},           -- UUID -> Item data
            ItemsByID = {},       -- ItemId -> UUID array
            Hotbar = {},          -- Slot -> UUID
            Storage = {},         -- UUID array
            EquippedItemUUID = nil,
            Weight = 0,
            Settings = {
                Limit = 15,
                CanStack = true,
                MaxStackSize = 5,
                MaxHotbarSlots = 10,
                Droppable = true,
                UiType = "Default"
    })

    -- STEP 1: Load profile on player join
    local OnPlayerAdded = function(player)
        local profile = PlayerData.LoadProfileAsync("Player_" .. player.UserId)

        if profile then
            profile.AddUserId(player.UserId)  -- Prevent concurrent writes
            profile.Release()               -- Allow player to load into game

            -- Get or create inventory state
            local state = InventoryService.GetState(player)
            if state then
                -- STEP 2: Replace state with loaded data
                if profile.Data and profile.Data.Items then
                    state.Items = profile.Data.Items
                    state.ItemsByID = profile.Data.ItemsByID or {}
                    state.Hotbar = profile.Data.Hotbar or {}
                    state.Storage = profile.Data.Storage or {}
                    state.EquippedItemUUID = profile.Data.EquippedItemUUID
                    state.Weight = profile.Data.Weight or 0

                    -- Apply saved settings or use defaults
                    if profile.Data.Settings then
                        for k, v in pairs(profile.Data.Settings) do
                            state.Settings[k] = v
                        end
                    end

                    -- Sync to client
                    InventoryService.SyncSettings(player)
                end
            end
        else
            warn("Failed to load profile for", player.Name)
        end
    end

    -- STEP 3: Save on player leave
    local OnPlayerRemoving = function(player)
        local profile = PlayerData.FindProfileByUserId("Player_" .. player.UserId)
        if profile then
            -- Get latest state before saving
            local state = InventoryService.GetState(player)
            if state then
                profile.Data.Items = state.Items
                profile.Data.ItemsByID = state.ItemsByID
                profile.Data.Hotbar = state.Hotbar
                profile.Data.Storage = state.Storage
                profile.Data.EquippedItemUUID = state.EquippedItemUUID
                profile.Data.Weight = state.Weight
                profile.Data.Settings = state.Settings
            end

            -- Save and release
            profile:SaveAsync()
            profile:Release()
        end
    end

    game:GetService("Players").PlayerAdded:Connect(OnPlayerAdded)
    game:GetService("Players").PlayerRemoving:Connect(OnPlayerRemoving)

**💡 Why ProfileStore?**

  * Automatic profile locking prevents data corruption
  * Built-in session locking (players can't join full server)
  * Handles DataStore quotas and rate limits
  * Simple template system for data structure

## Option 2: DataStore API (No External Module)

Manual implementation using Roblox DataStoreService

Manual DataStore Implementation Luau

    -- ServerScriptService/Persistence.luau

    local DataStoreService = game:GetService("DataStoreService")
    local InventoryStore = DataStoreService:GetDataStore("InventoryStore")
    local InventoryService = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared"):WaitForChild("StowayServerV1_2"))

    local SavePlayerData = function(player)
        local state = InventoryService.GetState(player)
        if not state then return end

        local success, err = pcall(function()
            InventoryStore:SetAsync(
                "Player_" .. player.UserId,
                    Items = state.Items,
                    ItemsByID = state.ItemsByID,
                    Hotbar = state.Hotbar,
                    Storage = state.Storage,
                    EquippedItemUUID = state.EquippedItemUUID,
                    Weight = state.Weight,
                    Settings = state.Settings
            )
        end)

        if not success then
            warn("Failed to save inventory for", player.Name, err)
        end
    end

    local LoadPlayerData = function(player)
        local success, data = pcall(function()
            return InventoryStore:GetAsync("Player_" .. player.UserId)
        end)

        if success and data then
            local state = InventoryService.GetState(player)
            if state then
                state.Items = data.Items or {}
                state.ItemsByID = data.ItemsByID or {}
                state.Hotbar = data.Hotbar or {}
                state.Storage = data.Storage or {}
                state.EquippedItemUUID = data.EquippedItemUUID
                state.Weight = data.Weight or 0

                if data.Settings then
                    for k, v in pairs(data.Settings) do
                        state.Settings[k] = v
                    end
                end
            end
        end
    end

    -- Connect to player lifecycle
    game:GetService("Players").PlayerAdded:Connect(function(player)
        task.wait(2)  -- Wait for inventory to initialize
        LoadPlayerData(player)
    end)

    game:GetService("Players").PlayerRemoving:Connect(function(player)
        SavePlayerData(player)
    end)

    -- Auto-save every 60 seconds
    task.spawn(function()
        while true do
            task.wait(60)
            for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
                if player and player:IsA("Player") then
                    task.spawn(SavePlayerData, player)
                end
            end
        end
    end)

## What to Save

Only save these fields from InventoryState

When saving player inventory data, only persist the essential state fields. The inventory system manages runtime data like player references internally.

Field | Type | Description
---|---|---
`Items` | table | UUID -> ItemInstance mapping
`ItemsByID` | table | ItemId -> UUID array (for stacking)
`Hotbar` | table | Slot number -> UUID
`Storage` | table | Array of UUIDs
`EquippedItemUUID` | string? | Currently equipped item
`Weight` | number | Total item count
`Settings` | table | Player preferences (vip limits, etc)

**⚠ Don't Save:** Player reference, die state, or any runtime-only data.

## Best Practices

Tips for reliable data persistence

* **Load before player interacts with inventory**
Call LoadProfile in PlayerAdded before any inventory operations.
* **Save on both leave AND auto-save**
Players may crash or lose connection. Auto-save every 30-60 seconds.
* **Use ProfileStore for production**
Manual DataStore is fine for testing, but ProfileStore handles edge cases.
* **Validate loaded data**
Check that required fields exist before applying to state.
* **Handle errors gracefully**
If save fails, log it but don't crash the server.

## Next Steps

Related documentation

[ ⚙ #### Settings Configuration Per-player settings that persist ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ + #### Add Server Features Create operations that modify inventory ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ 💻 #### API Reference InventoryService methods ](https://zyn-ic.github.io/Stoway/server/inventory-service.html)


# ✖ Remove Features

Disable or remove inventory features you don't need. Also learn how to extend the player lifecycle for custom game mechanics like death handling and persistent items.

## Quick Reference

Common features to disable

📊

### Disable Sorting

Stop items from being auto-sorted

How to →

💎

### Disable Rarity

Remove rarity color display

How to →

☠

### Disable Death Re-equip

Stop auto-equipping on respawn

How to →

🚫

### Disable Client

Completely disable inventory UI

How to →

## Disable Sorting

Stop items from being auto-sorted in storage

Sorting is controlled by `Settings.Storage.Sorting` in `src/shared/Settings.luau`:

Settings.luau Luau

    Settings.Storage = {
        Limit = 15,
        CanStack = true,
        Sorting = false,    -- Set to false to disable sorting
        MaxStackSize = 5,
        SortOrder = "None"

**⚠ Note:** Setting `Sorting = false` also disables the SearchFilter functionality, as sorting and search use the same UI infrastructure.

## Disable Rarity

Remove rarity color frames from slots

Rarity display is controlled by two settings:

Settings.luau Luau

    -- Disable rarity colors on items
    Settings.Stacking = {
        RequiredFields = { "Type" },  -- Remove "Rarity" from required fields
        Blacklist = {},
        Rarity = false                 -- Set to false to hide rarity frames

Additionally, ensure your GUI templates don't have rarity frames, or hide them via SlotFactory:

SlotFactory.luau Luau

    -- In Create function, hide rarityframe if rarity is disabled
    if rarityframe then
        if Settings.Stacking.Rarity then
            -- Show with reactive colors
            scope:Hydrate(rarityframe) {
                BackgroundColor3 = scope:Computed(function(use)
                    local item = use(currentItem)
                    if item and item.Metadata and item.Metadata.Rarity then
                        return RarityValues.rarityColors[item.Metadata.Rarity]
                    end
                    return Color3.new(0.4, 0.4, 0.4)
                end),
                Visible = hasItem,
        else
            -- Always hide
            rarityframe.Visible = false
        end
    end

## Disable Death Re-equip

Stop items from being re-equipped when player respawns

The death handling code is in `src/server/StowayServerV1_2/init.luau`, lines 46-61:

Current Death Handling Luau

    player.CharacterAdded:Connect(function(char)

        char:FindFirstChildWhichIsA("Humanoid").Died:Once(function()
            state.died = true
        end)

        if state.died then
            InventoryService.ReloadClient(player, {configSettingName = state.Settings.UiType})

            if state.EquippedItemUUID then
                local slotType, index = SlotManager.FindSlotByUUID(state, state.EquippedItemUUID)
                InventoryService.UnequipSlot(player, true)
                InventoryService.EquipSlot(player, slotType, index, true)
            end

            state.died = false
        end
    end)

### Option 1: Disable Re-equip Only

Keep the reload but don't re-equip:

Disable Re-equip Luau

    player.CharacterAdded:Connect(function(char)

        char:FindFirstChildWhichIsA("Humanoid").Died:Once(function()
            state.died = true
        end)

        if state.died then
            -- Just reload UI, don't re-equip
            InventoryService.ReloadClient(player, {configSettingName = state.Settings.UiType})
            state.died = false
        end
    end)

### Option 2: Remove Death Handling Entirely

Comment out the entire CharacterAdded block:

Remove Death Handling Luau

    player.CharacterAdded:Connect(function(char)
        char:FindFirstChildWhichIsA("Humanoid").Died:Once(function() state.died = true end)

        if state.died then
            InventoryService.ReloadClient(player, {configSettingName = state.Settings.UiType})
            if state.EquippedItemUUID then
                local slotType, index = SlotManager.FindSlotByUUID(state, state.EquippedItemUUID)
                InventoryService.UnequipSlot(player, true)
                InventoryService.EquipSlot(player, slotType, index, true)
            end
            state.died = false
        end
    end)

## Customize Death Behavior

Extend the player lifecycle for custom game mechanics

The death handling pattern can be extended for various game mechanics:

#### 🗑 Clear Inventory on Death

Remove all items when player dies

See Example →

#### 🔧 Persistent Items

Always have essential items (flashlight, etc.)

See Example →

#### 🔓 Drop Items on Death

Drop inventory to world on death

See Example →

#### ⚠ Keep Selected Slot

Remember and restore equipped slot

See Example →

### Clear Inventory on Death

Remove all items when player dies

Clear on Death Luau

    player.CharacterAdded:Connect(function(char)

        char:FindFirstChildWhichIsA("Humanoid").Died:Once(function()
            state.died = true
        end)

        if state.died then
            -- Clear all items from inventory
            for uuid in pairs(state.Items) do
                RemoveOperation.Execute(state, uuid)
            end

            -- Reset hotbar and storage arrays
            state.Hotbar = {}
            state.Storage = {}
            state.Items = {}
            state.ItemsByID = {}
            state.EquippedItemUUID = nil
            state.Weight = 0

            -- Reload client
            InventoryService.ReloadClient(player, {configSettingName = state.Settings.UiType})

            state.died = false
        end
    end)

### Persistent Items

Always grant essential items on respawn (e.g., flashlight)

Persistent Items Luau

    -- Define persistent items (always granted on respawn)
    local PERSISTENT_ITEMS = {
        { Id = "flashlight", Amount = 1 },
        { Id = "compass", Amount = 1 },

    player.CharacterAdded:Connect(function(char)

        char:FindFirstChildWhichIsA("Humanoid").Died:Once(function()
            state.died = true
        end)

        if state.died then
            -- Reload UI first
            InventoryService.ReloadClient(player, {configSettingName = state.Settings.UiType})

            -- Grant persistent items to hotbar
            for _, item in ipairs(PERSISTENT_ITEMS) do
                InventoryService.AddItem(player, item.Id, item.Amount, nil, true)
            end

            -- Restore equipped item if exists
            if state.EquippedItemUUID then
                local slotType, index = SlotManager.FindSlotByUUID(state, state.EquippedItemUUID)
                if slotType and index then
                    InventoryService.EquipSlot(player, slotType, index, true)
                end
            end

            state.died = false
        end
    end)

**💡 Tip:** Persistent items are great for essential gameplay items like flashlights, maps, radios, or quest items that players should always have.

### Drop Items on Death

Drop inventory to world when player dies

Drop on Death Luau

    local ItemSpawner = require(script.World.ItemSpawner)

    player.CharacterAdded:Connect(function(char)

        local humanoid = char:FindFirstChildWhichIsA("Humanoid")

        humanoid.Died:Once(function()
            state.died = true

            -- Get spawn position
            local spawnPos = char:GetPrimaryPartCFrame().Position
            local dropPosition = spawnPos + Vector3.new(0, 3, 0)

            -- Drop all items to world
            for uuid, item in pairs(state.Items) do
                if item.Metadata and item.Metadata.Droppable ~= false then
                    -- Spawn item in world
                    ItemSpawner.Spawn({
                        Name = item.Id,
                        Amount = item.Amount,
                        Position = dropPosition + Vector3.new(
                            math.random(-5, 5),
                            0,
                            math.random(-5, 5)
                        ),
                        Metadata = item.Metadata,
                    })
                end
            end

            -- Clear inventory
            state.Hotbar = {}
            state.Storage = {}
            state.Items = {}
            state.ItemsByID = {}
            state.EquippedItemUUID = nil
            state.Weight = 0
        end)

        if state.died then
            InventoryService.ReloadClient(player, {configSettingName = state.Settings.UiType})
            state.died = false
        end
    end)

## Disable Client Completely

Completely disable the inventory UI for specific players

This feature is useful for spectators, certain game modes, or when you want to completely hide the inventory from specific players. It requires both server and client coordination.

### Server Side

Server - Disable Client Luau

    -- Add to init.luau

    function InventoryService.DisableClient(player: Player)
        local state = PlayerInventories[player]
        if not state then return { success = false, reason = "NO_INVENTORY" } end

        -- Send disable signal to client
        local remotesFolder = Replicator.GetRemotesFolder()
        local disableRemote = remotesFolder:FindFirstChild("DisableStoway")
        if disableRemote then
            disableRemote:FireClient(player)
        end

        return { success = true }
    end

### Client Side - Add to init.luau

Client - Handle Disable Luau

    -- Add this to SetupRemotes() or init section

    -- Get disable remote (create if needed)
    local remotesFolder = ReplicatedStorage:WaitForChild("Remotes")
    local disableRemote = remotesFolder:FindFirstChild("DisableStoway")
    if not disableRemote then
        disableRemote = Instance.new("RemoteEvent")
        disableRemote.Name = "DisableStoway"
        disableRemote.Parent = remotesFolder
    end

    -- Handle disable signal
    disableRemote.OnClientEvent:Connect(function()
        print("[Stoway] Disabling client...")

        -- 1. Destroy UI
        if InventoryGui then
            InventoryGui:Destroy()
            InventoryGui = nil
        end

        -- 2. Clean up scopes
        if HotbarScope then
            HotbarScope:doCleanup()
            HotbarScope = nil
        end
        if StorageScope then
            StorageScope:doCleanup()
            StorageScope = nil
        end

        -- 3. Destroy input
        InputManager.Destroy()
        InteractionManager.Destroy()
        SelectionManager.Destroy()
        DragHandler.Destroy()

        -- 4. Disconnect network
        NetworkHandler.Destroy()

        -- 5. Clean up connections
        for _, conn in ipairs(InputConnections) do
            conn:Disconnect()
        end
        table.clear(InputConnections)

        -- 6. Clear references
        BackpackFrame = nil
        HotbarFrame = nil
        ScrollingBackpack = nil
        DraggingGui = nil

        print("[Stoway] Client disabled successfully")
    end)

### Usage

Usage Example Luau

    -- Disable inventory for spectator players
    local Players = game:GetService("Players")

    Players.PlayerAdded:Connect(function(player)
        if player.GetRankInGroup(player, 123456) == 0 then  -- Not in group
            -- Wait for inventory to initialize
            task.wait(1)
            InventoryService.DisableClient(player)
        end
    end)

## Related Documentation

See also

[ + #### Add Features Create new operations ](https://zyn-ic.github.io/Stoway/guides/add-server-features.html) [ ⚙ #### Settings Guide Configuration reference ](https://zyn-ic.github.io/Stoway/guides/settings-configuration.html) [ 🛠 #### Architecture Server structure ](https://zyn-ic.github.io/Stoway/server/architecture.html) [ 📝 #### Templates Code templates ](https://zyn-ic.github.io/Stoway/templates/index.html)


# ✓ Selection States

Gamepad users select slots and perform actions through a state machine. Each state represents a step in the selection flow, from idle to completing actions like swap, drop, or quick move.

## State Overview

The selection state machine

🖼

#### Step 0: Idle

Ready to select a slot

🎒 SELECTING

D-Pad selects slot ↓

🔄

#### Step 1: Source Selected

First slot chosen, waiting for target

🎒 SWAP - SELECT TARGET

From Source State

Same slot pressed → Cancel

Y pressed → Perform Swap

X pressed → Enter Drop Mode

## State Details

What happens in each state

State | Step | Description | Enabled Actions
---|---|---|---
`Idle` | 0 | Waiting for user to select a slot with D-Pad | D-Pad navigation, B to cancel
`Source` | 1 | Source slot selected, waiting for target or action | Y: Swap, X: Drop, A: QuickMove, B: Cancel
`Dropping` | 2 | Adjusting drop amount with D-Pad Up/Down | DPadUp/Down: Amount, A: Drop, B: Cancel

## Actions

What each action does

Y

Swap

Swap the source item with the currently selected slot.

Select source → Y → Select target → Items swapped

X

Drop

Drop the selected item with configurable amount.

Select item → X → Adjust amount → A to confirm

A + R1

Quick Move

Move item to first empty slot in the other container.

Hotbar item → A+R1 → Moves to Storage

Storage item → A+R1 → Moves to Hotbar

B

Cancel

Cancel current selection and return to Idle state.

Any state → B → Clear selection

## Code Structure

SelectionManager implementation

Selection State Luau

    local SelectionState = {
        enabled = false,
        step = 0,           -- 0: idle, 1: source, 2: dropping
        sourceType = nil,     -- "Hotbar" | "Storage"
        sourceSlot = nil,      -- Slot index
        sourceFrame = nil,     -- Frame reference

Get Slot Info Luau

    function SelectionManager.GetSelectedSlotInfo()
        local selected = GuiService.SelectedObject
        if not selected then return nil end

        local slotFrame = selected.Parent
        local slotType

        if slotFrame.Parent.Name == "Hotbar" then
            slotType = "Hotbar"
        elseif slotFrame.Parent.Name == "ScrollingFrame" then
            slotType = "Storage"
        end

        local slotName = slotFrame.Name:gsub("_", "")
        local slotIndex = tonumber(slotName)

        return {
            Type = slotType,
            Index = slotIndex,
            Frame = slotFrame
    end

## Swap Handler

Step-by-step swap logic

HandleSwap Logic Luau

    function SelectionManager.HandleSwap()
        local selected = SelectionManager.GetSelectedSlotInfo()
        if not selected then return end

        -- Step 0: Select source
        if SelectionState.step == 0 then
            SelectionState.step = 1
            SelectionState.sourceType = selected.Type
            SelectionState.sourceSlot = selected.Index
            SelectionState.sourceFrame = selected.Frame

            SelectionManager.ApplySelectionHighlight(selected.Frame)
            SelectionManager.NotifyStateChanged()

        -- Step 1: Select target and swap
        elseif SelectionState.step == 1 then
            -- Same slot = cancel
            if SelectionState.sourceType == selected.Type
               and SelectionState.sourceSlot == selected.Index then
                SelectionManager.CancelSelection()
                return
            end

            -- Perform swap
            InventoryController.Swap(
                SelectionState.sourceType,
                SelectionState.sourceSlot,
                selected.Type,
                selected.Index,
                true
            )

            SelectionManager.CancelSelection()
        end
    end

## Drop Handler

Drop with amount selection

HandleDrop Logic Luau

    function SelectionManager.HandleDrop()
        local selected = SelectionManager.GetSelectedSlotInfo()
        if not selected then return end

        -- Get item data
        local store = require(script.Parent.Parent.CoreState.InventoryStore)
        local item = store:GetItemFromSlot(selected.Type, selected.Index)
        if not item then return end

        -- Enter dropping state
        SelectionState.step = 2
        SelectionManager.NotifyStateChanged()

        -- Disable inventory, enable drop context
        InputManager.SetInventoryEnabled(false)
        InputManager.SetDropEnabled(true)
        SelectionManager.LoadDropBinds()

        -- Show drop UI
        ConsoleDropUI.Show(item, 1)

        -- Set up callbacks
        ConsoleDropUI.OnDrop = function(amount)
            InventoryController.Drop(selected.Type, selected.Index, amount, true)
            SelectionManager.CloseDropUI()
        end

        ConsoleDropUI.OnDropAll = function()
            InventoryController.Drop(selected.Type, selected.Index, item.Amount, true)
            SelectionManager.CloseDropUI()
        end

        ConsoleDropUI.OnCancel = function()
            SelectionManager.CloseDropUI()
        end
    end

## Hooks Integration

Firing hooks on selection changes

SelectionManager fires hooks when slots are selected/deselected:

Selection Hooks Luau

    -- Apply highlight when slot is selected
    function SelectionManager.ApplySelectionHighlight(frame)
        if HookManager then
            HookManager:Fire("OnSelect", frame)
        end
    end

    -- Clear highlight when slot is deselected
    function SelectionManager.ClearSelectionHighlight(frame)
        if HookManager then
            HookManager:Fire("OnDeselect", frame)
        end
    end

Hook | Arguments | Fired When
---|---|---
`OnSelect` | `frame` | Slot is selected (step 0 → 1)
`OnDeselect` | `frame` | Selection is cleared

## Related Documentation

See also

[ 🎮 #### Input System Complete overview ](https://zyn-ic.github.io/Stoway/client/input/index.html) [ ⚔ #### Console Modes Mode switching ](https://zyn-ic.github.io/Stoway/client/input/console-modes.html) [ 🔢 #### Binds Module Action key bindings ](https://zyn-ic.github.io/Stoway/client/input/binds-module.html) [ 💬 #### Available Hooks All hooks reference ](https://zyn-ic.github.io/Stoway/client/hooks/available-hooks.html)


## InventoryUtils

Stateless helper functions for inventory operations

Function | Returns | Description
---|---|---
GenerateUUID() | string | Generates unique identifier for item instances using HttpService
CanStack(item1, item2, checkRarity) | boolean | Checks if two items can stack based on ID and metadata equality
FindEmptySlot(slots, maxSlots) | number? | Returns first empty slot index (1-based) or nil if full
ParseToolMetadata(tool) NEW | table? | Extracts all Attributes from a Tool for persistence

📊

### CanStack Deep Dive

How stacking eligibility is determined

Stack Check Logic Pseudocode

    1. Check ID match: item1.Id == item2.Id
    2. **Strict Check:** Name, Rarity, and Type MUST match exactly
    3. **Blacklist:** Check Settings.RarityBlacklist (e.g. Legendary)
       - If blacklisted, return false
    4. Return true if all checks pass

## ItemSpawner

Physical tool instantiation and world interaction

Function | Returns | Description
---|---|---
SpawnTool(player, itemId, metadata?) | Tool? | Clones tool from ServerStorage, applies metadata as Attributes Character Check
DespawnTool(player, tool) | void | Destroys a tool if it belongs to the player's character
DropTool(player, itemId, amount, metadata?) | void | Creates a pickup in the world at DropDistance from player

⚔

### SpawnTool Flow

How tools are instantiated on equip

Spawn Flow Pseudocode

    1. Check player.Character exists (safety)
    2. Find tool template in ServerStorage.Tools[itemId]
    3. Clone template
    4. Apply metadata as Attributes via SetAttribute()
    5. Parent clone to player.Character
    6. Return tool instance

**⚠ Safety Note:** SpawnTool includes a character existence check to prevent errors when players die during equip.


## InventoryState

Pure data container for player inventory state (V1.2)

Instantiated per-player. Holds all inventory state including items, slots, equipped status, and **per-player settings**. Uses `ItemsByID` for O(1) stack lookups.

Type Definitions Luau

    export type ItemInstance = {
        UUID: string,
        Id: string,
        Amount: number,
        Metadata: { [string]: any }?

    export type PlayerSettings = {
        Limit: number,           - Weight limit (0 = infinite)
        CanStack: boolean,       - Enable/disable stacking
        MaxStackSize: number,    - Max items per stack
        MaxHotbarSlots: number,  - Hotbar slot count
        UiType: string,          - Interface version to load

    export type InventoryStateType = {
        Player: Player,
        Items: { [string]: ItemInstance },      -- UUID -> Item
        ItemsByID: { [string]: {string} },    -- ItemId -> UUIDs (O(1) lookup)
        Hotbar: { [number]: string? },          -- Static: nil holes allowed
        Storage: { string },                    -- Dynamic: packed array
        EquippedItemUUID: string?,             -- UUID of equipped item
        Weight: number,                        -- Total item amounts
        Settings: PlayerSettings,              -- Per-player settings!

## V1.2 Changes

Key improvements from V1

Feature | V1 | V1.2
---|---|---
Equipped Tracking | Slot-based | UUID-based
Storage | Static array | Dynamic (packed)
Settings | Global only | Per-player
Weight Tracking | Not tracked | state.Weight

## Hotbar vs Storage

Static vs Dynamic slot management

📌

### Hotbar (Static)

Fixed slots with nil holes preserved

Hotbar maintains fixed 1-9 slots. When an item is removed, the slot becomes `nil` (hole preserved). Slots don't shift.

📦

### Storage (Dynamic)

Packed array that shifts on removal

Storage is a packed array. When an item is removed via `table.remove`, items shift up to fill the gap. No holes.

## Getters

Data access methods (no logic)

Method | Returns | Description
---|---|---
GetItem(uuid) | ItemInstance? | Get item by UUID
GetHotbarUUID(slot) | string? | Get UUID in hotbar slot
GetStorageUUID(index) | string? | Get UUID in storage index
IsEquipped(uuid) | boolean | Check if item is equipped


## Debug Chat Commands

In-game testing commands for StowayServerV1.2

**⚠ Production Warning**

Remove `Debug/ChatCommands.luau` before shipping! These commands bypass normal validation and are intended for testing only.

## Inventory Display

📋

### /inv

Display current inventory state

Shows weight, settings, equipped UUID, hotbar slots, and storage contents with full UUIDs.

Example Output

    ═══════════════════════════════════════════════════
    INVENTORY for PlayerName
    ───────────────────────────────────────────────────
    Weight: 7 / 15 (0 = Infinite)
    CanStack: true | MaxStack: 5
    Equipped UUID: abc-123-def
    ───────────────────────────────────────────────────
    HOTBAR (max 9):
      [1] Sword x1 (UUID:abc-123-def)
      [2] (empty)
      [3] Potion x5 (UUID:xyz-456-ghi)
    STORAGE: #1
      [1] Wood x5 (UUID:jkl-789-mno)
    ═══════════════════════════════════════════════════

❓

### /help

Show all available commands

## Item Operations

Command | Usage | Description
---|---|---
`/add` | `/add Sword 3` | Add item to hotbar first, then storage. Amount defaults to 1.
`/addback` | `/addback Potion 5` | Add directly to backpack (bypasses hotbar).
`/remove` | `/remove abc-123 2` | Remove amount from item by UUID. Omit amount to remove all.
`/clear` | `/clear` | Remove ALL items from inventory.

Examples

    /add Sword         - Adds 1 Sword to first empty hotbar slot
    /add Potion 10     - Adds 10 Potions (stacks across slots if needed)
    /addback Wood 5    - Adds 5 Wood directly to storage
    /remove abc-123-def- Removes entire stack with that UUID
    /remove abc-123-def 2 -- Removes 2 from that stack

## Slot Operations

Command | Usage | Description
---|---|---
`/swap` | `/swap Hotbar 1 Storage 2` | Swap items between any two slots.
`/stack` | `/stack Hotbar 1 Hotbar 2` | Merge two stacks (rarely needed - add auto-stacks).
`/equip` | `/equip 3` | Equip item from hotbar slot. Also accepts `/equip Storage 1`.
`/unequip` | `/unequip` | Unequip currently equipped item.
`/drop` | `/drop Hotbar 1` | Drop item from slot into world.

Slot Types

    Hotbar- Slots 1-9 (configurable via MaxHotbarSlots)
    Storage -- Dynamic array, indices start at 1

## Settings (Per-Player)

Modify player-specific settings at runtime

Command | Usage | Description
---|---|---
`/toggle_stack` | `/toggle_stack` | Toggle CanStack on/off for this player.
`/toggle_limit` | `/toggle_limit` | Toggle Limit between 0 (infinite) and 15.
`/set_limit` | `/set_limit 50` | Set weight limit. 0 = infinite.
`/set_max_stack` | `/set_max_stack 10` | Set max items per stack.
`/set_hotbar_slots` | `/set_hotbar_slots 5` | Set number of hotbar slots.

Testing Scenarios

    -- Test stacking disabled
    /toggle_stack
    /add Sword 3      - Creates 3 separate stacks

    -- Test infinite inventory
    /set_limit 0
    /add Wood 100     - No weight rejection

    -- Test small hotbar
    /set_hotbar_slots 3
    /add Sword 5      - 3 go to hotbar, 2 to storage

## UI Reload

Dynamically switch UI types and trigger full client reload

🔄

### /setui

Change UI type and reload client interface

Changes the player's UI type setting and triggers a full client reload with fresh inventory data. This allows you to swap between different UI implementations (e.g., Default → Mod) on the fly.

Usage

    /setui [uitype]

    Example:
    /setui Default   - Reload with Default UI
    /setui Mod       - Reload with Mod UI (if configured)

**📝 Configuration Required**

UI types must be defined in `Settings.DifferentUIs`:

Settings.luau

    Settings.DifferentUIs = {
        Default = {
            FolderName = "StowayGui",
            GuiName = "Stoway",
            HookPresetName = "DefaultHooks"
        },
        Mod = {
            FolderName = "ModStowayGui",
            GuiName = "ModStoway",
            HookPresetName = "ModHooks"

How It Works

    1. Sets player's Settings.UiType to specified type
    2. Calls InventoryService.ReloadClient() with fresh data
    3. Client destroys current UI and scopes
    4. Client loads new GUI folder from configuration
    5. Client rebuilds hotbar/storage with new hooks
    6. Client rebinds all keybindings

    ✅ Use Case: Testing different UI themes or mod interfaces
    ✅ Maintains inventory state during reload
    ✅ Applies new hooks and visual styles


# Quebec - a modular Roblox game framework

Thank you for checking out this framework! You can find the documentation of the latest release [here](https://baxoplenty.gitbook.io/quebec-docs/)

## 🎧 Discord Server

Seeking quick support or just want to connect? You can join our Discord server here: https://discord.gg/edfQBBz9ch

## 👾 Rojo Version

This project was built using the [crates.io](https://crates.io/) version of [Rojo 7](https://rojo.space/).

## 📚 Documentation

The documentation of the latest release can be found here: https://baxoplenty.gitbook.io/quebec-docs

It contains important information as well as usage, setup and other guides.

## 🧪 Example Setup

You can find an example setup using Rojo here: https://github.com/BaxoPlenty/quebec-rojo-setup

It contains 2 branches:

-   `multiplace` - for games having multiple places and shared code
-   `singleplace` - for games that only have one place

## ⛰ Roadmap

Quebec is still being actively developed. You can track the development status here: https://github.com/BaxoPlenty/quebec/issues

## 🤝 Contributing

Thank you for your interest in contributing! There are many ways to contribute to this project. Get started here: [Contributing Guide](/CONTRIBUTING.md)


First of all, **thank you** for checking out my framework. I really appreciate that 😄. I put a lot of work into this framework by exploring all sorts of different design patterns, possibilities of utility functions and performance improvements. Enough talking, let's get started!

What is Quebec?

Quebec is a **modular Roblox game framework** developed for **vanilla Luau**. It's entirely written in pure Luau and therefore 100% compatible with Roblox. The goal of Quebec is to create a **seamless developer experience**, because developing Roblox games can quickly become a mess, especially for bigger games.

To achieve this, Quebec uses **services and controllers**. These are called **"singletons"** in Quebec, because they are only instantiated once, and never multiple times. A singleton is essentially a place where your game logic lives.

Normally, a singleton has one specific field or task to avoid clutter and improve organization. The best example for this is: `DataService, MonetizationService & JoinService` , where each service has it's own purpose.

What are the benefits?

Quebec provides the developer with a **lot of benefits**. Some notable ones are:

  * Organized and well-structured codebase

  * Built-in utility functions and modules

  * Little to no performance loss

  * Lifecycle events to act upon game events seamlessly

  * Opt-in architecture: **You only use, what you need**

  * Load order: A singleton will only start, once it's dependencies have already started

  * ... and more

I am still not convinced

If you are uncertain if you need Quebec, **just try it out**. You can always revert back! And also, we accept new contributors on our GitHub 😉

[NextChangelog](https://baxoplenty.gitbook.io/quebec-docs/changelog)

Last updated 8 months ago


You can "mod" Quebec by creating custom lifecycle events. It is recommended to create a `lifecycles` folder in either a shared, client or server folder for **easy access**.

Creating A Custom Lifecycle

Lifecycles automatically have the following methods:

  * `getSingletons` \- to retrieve a list of all connected singletons

  * `invokeAll` \- to invoke the lifecycle on all connected singletons

  * `invokeAllSameThread` \- to invoke the lifecycle on all connected singletons, but in the same thread

We provide a `Quebec.lifecycle` function to easily create a lifecycle:

Copy

    local Quebec = require(PATH_TO_QUEBEC)

    local Lifecycle = Quebec.lifecycle({
        defaultMethod = "onPlayerAdded"
    })

    type Lifecycle = Quebec.CustomLifecycle<typeof(Lifecycle)> -- not mandatory, but useful

    function Lifecycle.run(self: Lifecycle)
        -- this code will run after all singletons have loaded which attach to your lifecycle

        for _, player in Players:GetPlayers() do
            -- this invokes the lifecycle on all singletons which use it
            self:invokeAll(player)
        end

        Players.PlayerAdded:Connect(function(player)
            -- the same as "invokeAll", but it runs the methods in the same thread
            self:invokeAllSameThread(player)
        end)
    end

    return Lifecycle

Using Custom Lifecycles

It's basically the same as registering normal lifecycle events, but instead of the lifecycle name, you require it:

Of course, you can also remap the lifecycle:

[PreviousMultiple Instances](https://baxoplenty.gitbook.io/quebec-docs/guides/multiple-instances)[NextModules](https://baxoplenty.gitbook.io/quebec-docs/core/modules)

Last updated 7 months ago


I have trouble loading dependencies

Make sure there are no cyclic dependencies, as those may cause problems. If you encounter one service starting before one of it's dependencies, then make sure you call `Quebec.singleton` **after** your requires:

Copy

    local Dependency = require(script.Parent.OtherService) -- correct!

    local Service = Quebec.singleton({})

    -- incorrect! this registers DependencyTwo AFTER your service
    local DependencyTwo = require(script.Parent.OtherServiceTwo)

If there is any undocumented issue here, you can get support on [Quebec's GitHub](https://github.com/BaxoPlenty/quebec/issues) on the **Issues** tab.

[PreviousInstallation](https://baxoplenty.gitbook.io/quebec-docs/installation)[NextGetting Started](https://baxoplenty.gitbook.io/quebec-docs/guides/getting-started)

Last updated 8 months ago


Adding dependencies to your singleton is a piece-of-cake with the new version, as you can use Roblox's vanilla `require` function:

Copy

    local OtherService = require(script.Parent.OtherService)

    local Service = Quebec.singleton({})

    -- ...

**The only rule is:** You must require all dependencies BEFORE you call `Quebec.singleton` because that function registers your singleton in a table. By requiring other services before that, it makes sure that they have a higher index in the list than the service that depends on them.

After requiring another service, you can use all of their functions like in a regular ModuleScript.

[PreviousLifecycle Events](https://baxoplenty.gitbook.io/quebec-docs/guides/lifecycle-events)[NextMultiple Instances](https://baxoplenty.gitbook.io/quebec-docs/guides/multiple-instances)

Last updated 8 months ago


Modules are meant to make certain processes more simple. The best example for this is the networking module, which creates RemoteEvents and RemoteFunctions for you.

[Networking](https://baxoplenty.gitbook.io/quebec-docs/core/modules/networking)

[PreviousModding](https://baxoplenty.gitbook.io/quebec-docs/guides/modding)[NextNetworking](https://baxoplenty.gitbook.io/quebec-docs/core/modules/networking)

Last updated 8 months ago


We are not planning to support the Roblox marketplace for our framework, as it can easily break existing systems due to auto-update functionality.

Rojo Template (recommended)

Roblox Model File

Source-Code

You can use [Rojo](https://rojo.space/) to sync local files with Roblox Studio. Quebec uses this in combination with Git Submodules to update Quebec reliably. We provide a [GitHub Repository](https://github.com/BaxoPlenty/quebec-rojo-setup) for the Rojo template. You'll find setup instructions in the repository README file.

Go to the [Releases](https://github.com/BaxoPlenty/quebec/releases) tab of the Quebec GitHub repository, and download the `Quebec.rbxm` of the latest release. You can then import this file into Roblox Studio. We recommend putting the ModuleScript into ReplicatedStorage, to avoid duplicates.

You can also just download the source code of the latest release under the [Releases](https://github.com/BaxoPlenty/quebec/releases) tab, however you will have to use a tool like [Rojo](https://rojo.space/) to sync those files with your game. We just recommend using the [Rojo Template](https://baxoplenty.gitbook.io/quebec-docs/installation#rojo-template-recommended) in that case.

[Getting Started](https://baxoplenty.gitbook.io/quebec-docs/guides/getting-started)

[PreviousMigration](https://baxoplenty.gitbook.io/quebec-docs/migration)[NextTroubleshooting](https://baxoplenty.gitbook.io/quebec-docs/troubleshooting)

Last updated 8 months ago


Quebec uses **singletons** as a place where your **game logic lives**. Singletons are essentially classes that only get **instantiated once**.

Your singletons can define **lifecycle events**, which get invoked by the Quebec main module. The best example for a lifecycle event is the `start` lifecycle, which gets invoked as soon as the singleton starts.

Defining a singleton

Singletons have to be **parented to the folder** you defined when you ran the Quebec main module. They must also be a **ModuleScript** and **can't be nested**. Here's example code of a singleton:

Copy

    local Quebec = require(PathToQuebecModule)

    -- Quebec.singleton(table) registers the singleton in the Registry
    local Singleton = Quebec.singleton({
        lifecycles = { "start" }
    })

    -- We recommend using the dot pattern for functions and then adding a first
    --   parameter called "self" instead of using a colon definition.
    -- This normally improves autocompletions.
    function Singleton.init(self: typeof(Singleton))
        -- optional initialization function
        -- note: this function is ran before any singleton runs. yielding here
        --       will halt the main Quebec thread
    end

    -- the lifecycle method being invoked on the start
    function Singleton.start(self: typeof(Singleton))
        print("started")
    end

    return Singleton

[PreviousGetting Started](https://baxoplenty.gitbook.io/quebec-docs/guides/getting-started)[NextLifecycle Events](https://baxoplenty.gitbook.io/quebec-docs/guides/lifecycle-events)

Last updated 8 months ago


Due to the new additions to how dependencies work, you now have to strictly define how many instances you have in your game. You do this by passing the `instances` property to the config parameter when calling `Quebec.run` :

The instance names aren't checked. Only the table length is. That means that you can put any strings inside of the table. It is good practice to name your instances, so you don't get lost during development.

Copy

    require(game.ReplicatedStorage.Quebec).run(script.services, {
        instances = { "main", "common" }
    })

Now, Quebec would wait for two instances to register before running all singletons. You have to define the current instance as well. If you have two instances, then the table must have a length of 2.

[PreviousDependencies](https://baxoplenty.gitbook.io/quebec-docs/guides/dependencies)[NextModding](https://baxoplenty.gitbook.io/quebec-docs/guides/modding)

Last updated 8 months ago


Sometimes, you might notice that the type solver is having problems to infer the type of `self` when you define a lifecycle event. There's a workaround to have autocompletions without compromising functionality:

Copy

    function Service.start(self: typeof(Service))
        -- self now has a type!
    end

With this method, you give self an explicit type using the `typeof` type function.

[PreviousClass](https://baxoplenty.gitbook.io/quebec-docs/core/utility-functions/class)

Last updated 8 months ago


Lifecycle events are a crucial component for most singletons, because they allow your singleton to react to certain events. The most common one is the `start` lifecycle event which gets fired as the singleton starts.

Available Lifecycle Events

Identifier

Description

Platform

`start`

Fires once when the singleton starts

Client + Server

`stop`

Fires once before the server shuts down. **Yielding here will halt the thread**

Server

`onTick(dt: number)`

Is directly hooked to `RunService.Heartbeat` runs after physics are done

Client + Server

`onPhysics(t: number, dt: number)`

Is directly hooked to `RunService.Stepped` and runs prior to physics

Client + Server

`onRender(dt: number)`

Is directly hooked to `RunService.RenderStepped`

Client

Using Lifecycle Events

You can use lifecycle events by explicitly defining them in your singleton:

Copy

    local Service = Quebec.singleton({
        lifecycles = {
          "start", -- regular lifecycle
          ["onPhysics"] = "doPhysicsStuff", -- remap the lifecycle to a different method
    })

    function Service:start()
      print("started")
    end

    -- remember, normally it's "onPhysics", but we remapped it
    function Service:doPhysicsStuff(t: number, dt: number)
      print("physics")
    end

    return Service

[PreviousCreating Singletons](https://baxoplenty.gitbook.io/quebec-docs/guides/creating-singletons)[NextDependencies](https://baxoplenty.gitbook.io/quebec-docs/guides/dependencies)

Last updated 8 months ago


Changes in v4.0.0

Quebec v4.0.0 features a completely new **way of dependency injection** via Roblox's vanilla `require`. In combination to that, a lot of **organizing **has been done. It's expected that this version will be a lot easier to update and built on.

Migrating from v3.1.0

Migration should be **simple**, as the **layout barely changed**. However, the way singletons and dependencies are defined are changed as well as the way you run the Quebec module.

You **can't migrate **the lifecycle events that **handled player joins and leaves**. See [Modding](https://baxoplenty.gitbook.io/quebec-docs/guides/modding) for more information.

Migrating the "Bootstrapper"

For this section, I'd like to link you to the [Getting Started](https://baxoplenty.gitbook.io/quebec-docs/guides/getting-started) page, as it pretty much contains all the information needed. You just have to replace the old content of your bootstrapper with the new content.

Migrating Singletons

This should also be fairly easy, but might cause a lot of work especially if you have a lot of distributed services.

As you might have read already, we switched over to using Roblox's vanilla `require` function for dependency injection, instead of the old resolver method. So:

Copy

    -- instead of:

    function Service:init(resolver)
        self.otherService = resolver("@services/otherService")
    end

    -- you now just do:

    local OtherService = require(script.Parent.OtherService)

    -- note: all requires must be done before creating your service.

Now, to create a service, you just do this:

Yeah, it is a little bit more boilerplate code, but it's worth it. You get better autocomplete and overall this is a better and more flexible approach than we had before.

[Previousv4.2.0](https://baxoplenty.gitbook.io/quebec-docs/changelog/v4.2.0)[NextInstallation](https://baxoplenty.gitbook.io/quebec-docs/installation)

Last updated 8 months ago