> ## Documentation Index
> Fetch the complete documentation index at: https://mythicframework.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Characters - Data Structure

> Complete character data schema and metadata structure

Understanding the character data structure is essential for working with character data in Mythic Framework.

## Character Object

The complete character object structure stored in MongoDB:

```lua theme={null}
{
    _id = ObjectId("..."),              -- MongoDB document ID
    SID = 1,                             -- State ID (Character ID)
    User = 12345,                        -- Account ID of player who owns this character
    First = "John",                      -- First name
    Last = "Doe",                        -- Last name
    DOB = "1990-01-15",                  -- Date of birth (YYYY-MM-DD)
    Gender = 0,                          -- Gender (0 = Male, 1 = Female)
    Phone = 5551234567,                  -- Phone number (unique)
    Bio = "Character biography",         -- Character bio/backstory
    Origin = 1,                          -- Character origin

    -- Job Information (Array of jobs)
    Jobs = {
        {
            Id = "police",               -- Job identifier
            Name = "Los Santos Police Department",
            Grade = {
                Id = "officer",
                Name = "Police Officer",
                Level = 1
            },
            Workplace = {
                Id = "lspd",
                Name = "LSPD Mission Row"
            }
        }
    },

    -- Money
    Cash = 5000,                         -- Cash on hand (note: capital C)
    -- Note: Bank balances are stored in a separate 'bank_accounts' collection, NOT on the character

    -- Licenses
    Licenses = {
        Drivers = {
            Active = true,
            Points = 0,
            Suspended = false
        },
        Weapons = {
            Active = false,
            Suspended = false
        },
        Hunting = {
            Active = false,
            Suspended = false
        },
        Fishing = {
            Active = false,
            Suspended = false
        },
        Pilot = {
            Active = false,
            Suspended = false
        }
    },

    -- Last played timestamp
    LastPlayed = 1705334400,

    -- Position (stored separately, synced to character)
    position = {
        x = 123.45,
        y = -678.90,
        z = 21.00,
        heading = 180.0
    },

    -- Metadata (flexible additional data)
    metadata = {
        -- Appearance
        appearance = {
            -- Freemode ped data
            model = "mp_m_freemode_01",
            headBlend = {...},
            faceFeatures = {...},
            headOverlays = {...},
            components = {...},
            props = {...}
        },

        -- Licenses
        licenses = {
            "drivers_license",
            "weapon_license"
        },

        -- Stats
        playtime = 36000,                -- Total playtime in seconds
        arrests = 5,                     -- Times arrested
        kills = 10,                      -- Player kills
        deaths = 2,                      -- Deaths

        -- Reputation
        reputation = {
            police = 100,
            gang = -50
        },

        -- Status effects
        isDead = false,
        inJail = false,
        jailTime = 0,

        -- Needs (hunger, thirst, etc.)
        hunger = 100,
        thirst = 100,
        stress = 0,

        -- Custom data
        lastLogin = 1705334400,
        tutorial_completed = true
    },

    -- Timestamps
    createdAt = 1700000000,              -- Character creation timestamp
    updatedAt = 1705334400               -- Last update timestamp
}
```

***

## Required Fields

These fields are required when creating a character:

<ParamField path="First" type="string" required>
  First name

  **Validation:**

  * 2-20 characters
  * Letters only (may include spaces, hyphens, apostrophes)

  **Example:** `"John"`, `"Mary-Jane"`, `"O'Brien"`
</ParamField>

<ParamField path="Last" type="string" required>
  Last name

  **Validation:**

  * 2-30 characters
  * Letters only (may include spaces, hyphens, apostrophes)

  **Example:** `"Doe"`, `"Van Der Linde"`, `"O'Sullivan"`
</ParamField>

<ParamField path="DOB" type="string" required>
  Date of birth in YYYY-MM-DD format

  **Validation:**

  * Must be at least 18 years old
  * Format: `YYYY-MM-DD`

  **Example:** `"1990-01-15"`, `"1985-12-31"`
</ParamField>

<ParamField path="Gender" type="number" required>
  Gender (numeric value)

  **Values:**

  * `0` = Male
  * `1` = Female
</ParamField>

***

## Auto-Generated Fields

These fields are automatically set by the system:

<ResponseField name="SID" type="number">
  State ID - unique character identifier

  **Auto-incremented** starting from 1
</ResponseField>

<ResponseField name="User" type="number">
  Account ID of the player who owns this character

  **Automatically set** from the player's AccountID
</ResponseField>

<ResponseField name="Phone" type="number">
  Phone number - unique 10-digit number

  **Auto-generated** when character is created
</ResponseField>

<ResponseField name="createdAt" type="number">
  Unix timestamp of character creation

  **Set automatically** to `os.time()` on creation
</ResponseField>

<ResponseField name="updatedAt" type="number">
  Unix timestamp of last update

  **Updated automatically** on every save
</ResponseField>

***

## Default Values

Fields with default values if not specified:

```lua theme={null}
{
    Jobs = {},                 -- Empty jobs array (unemployed)
    Cash = 5000,               -- Starting cash (5000 by default)
    position = {               -- Default spawn position
        x = -258.211,
        y = -293.738,
        z = 21.6114,
        heading = 206.0
    },
    metadata = {}              -- Empty metadata
}
```

***

## Metadata Structure

The `metadata` field is a flexible object for storing additional character data:

### Appearance

```lua theme={null}
metadata.appearance = {
    model = "mp_m_freemode_01",  -- Or "mp_f_freemode_01" for female

    -- Head blend (heritage)
    headBlend = {
        shapeFirst = 0,
        shapeSecond = 0,
        skinFirst = 0,
        skinSecond = 0,
        shapeMix = 0.5,
        skinMix = 0.5
    },

    -- Face features
    faceFeatures = {
        noseWidth = 0.0,
        nosePeakHeight = 0.0,
        nosePeakLength = 0.0,
        -- ... (20 total features)
    },

    -- Head overlays (tattoos, makeup, etc.)
    headOverlays = {
        {  -- Blemishes
            style = 0,
            opacity = 0.0,
            color = 0
        },
        -- ... (13 total overlays)
    },

    -- Clothing components
    components = {
        { drawable = 0, texture = 0, palette = 0 },  -- Face
        { drawable = 0, texture = 0, palette = 0 },  -- Mask
        -- ... (12 total components)
    },

    -- Props (hats, glasses, etc.)
    props = {
        { drawable = -1, texture = 0 },  -- Hats/helmets
        { drawable = -1, texture = 0 },  -- Glasses
        -- ... (7 total props)
    }
}
```

### Licenses

```lua theme={null}
metadata.licenses = {
    "drivers_license",
    "weapon_license",
    "hunting_license",
    "fishing_license",
    "pilots_license"
}
```

### Statistics

```lua theme={null}
metadata.stats = {
    playtime = 36000,          -- Total playtime (seconds)
    arrests = 5,               -- Times arrested
    kills = 10,                -- PVP kills
    deaths = 2,                -- Times died
    robberies = 3,             -- Robberies completed
    drugsDealt = 50,           -- Drugs sold
    vehiclesSold = 2,          -- Vehicles sold
}
```

### Reputation

```lua theme={null}
metadata.reputation = {
    police = 100,              -- Positive = good standing
    ems = 50,
    gang = -50,                -- Negative = bad reputation
    civilian = 75
}
```

### Needs/Status

```lua theme={null}
metadata.needs = {
    hunger = 100,              -- 0-100
    thirst = 100,              -- 0-100
    stress = 0,                -- 0-100
    energy = 100               -- 0-100
}

metadata.status = {
    isDead = false,
    inJail = false,
    jailTime = 0,              -- Remaining jail time (seconds)
    injuryLevel = 0,           -- 0-100
    bleeding = false,
    broken_bones = {}
}
```

### Achievements

```lua theme={null}
metadata.achievements = {
    "first_job",
    "first_vehicle",
    "millionaire",
    "police_veteran"
}
```

### Custom Data

You can store any custom data in metadata:

```lua theme={null}
metadata.customData = {
    tutorial_completed = true,
    favorite_vehicle = "adder",
    home_location = { x = 100, y = 200, z = 30 },
    friends = { 2, 5, 10 },    -- Character SIDs
    last_robbery = 1705334400,
    settings = {
        hud_visible = true,
        minimap_zoom = 2
    }
}
```

***

## Working with Character Data

### Accessing Fields

```lua theme={null}
-- Get character using Fetch component
local player = Fetch:Source(source)
if not player then return end

local char = player:GetData('Character')
if not char then return end

-- Access fields using GetData()
local firstName = char:GetData('First')
local lastName = char:GetData('Last')
print(firstName, lastName)             -- Name

local stateId = char:GetData('SID')    -- Character ID
local jobs = char:GetData('Jobs')      -- Jobs array (table of job objects)
local cash = char:GetData('Cash')      -- Cash on hand

-- Access metadata
local metadata = char:GetData('MetaData') or {}
print(metadata.playtime)               -- Playtime
print(metadata.hunger)                 -- Hunger
```

### Updating Fields

```lua theme={null}
-- Update basic fields using SetData()
local currentCash = char:GetData('Cash')
char:SetData('Cash', 5000)

-- Update nested metadata
local metadata = char:GetData('MetaData') or {}
metadata.hunger = 75
metadata.thirst = 80
char:SetData('MetaData', metadata)

-- Note: Jobs are managed through the Jobs component, not SetData
-- Use Jobs:GiveJob(stateId, jobId, workplaceId, gradeId) to assign jobs
```

### Adding to Metadata

```lua theme={null}
-- Add license
local player = Fetch:Source(source)
local char = player:GetData('Character')

local metadata = char:GetData('MetaData') or {}
local licenses = metadata.licenses or {}

table.insert(licenses, 'pilots_license')
metadata.licenses = licenses

char:SetData('MetaData', metadata)

-- Increment stat
local metadata = char:GetData('MetaData') or {}
local arrests = (metadata.arrests or 0) + 1
metadata.arrests = arrests

char:SetData('MetaData', metadata)
```

***

## Database Queries

### Find by Name

```lua theme={null}
Database.Game:find({
    collection = 'characters',
    query = { First = 'John', Last = 'Doe' }
}, function(success, characters)
    -- process characters
end)
```

### Find by User (Account ID)

```lua theme={null}
local player = Fetch:Source(source)
local accountId = player:GetData('AccountID')

Database.Game:find({
    collection = 'characters',
    query = {
        User = accountId
    }
}, function(success, results)
    if success and results then
        -- Process characters
    end
end)
```

### Find by Job

```lua theme={null}
-- Note: Jobs is an array, so you need to query using array operators
Database.Game:find({
    collection = 'characters',
    query = {
        ['Jobs.Id'] = 'police'  -- Query for characters with police job
    }
}, function(success, results)
    if success and results then
        -- Process police characters
    end
end)
```

### Find Characters with Most Cash

```lua theme={null}
Database.Game:aggregate({
    collection = 'characters',
    aggregate = {
        {
            ['$project'] = {
                name = { ['$concat'] = { '$First', ' ', '$Last' } },
                Cash = 1
            }
        },
        {
            ['$match'] = {
                Cash = { ['$gte'] = 100000 }  -- $100k+ cash
            }
        },
        {
            ['$sort'] = { Cash = -1 }
        }
    }
}, function(success, results)
    if success and results then
        -- Process results
    end
end)
```

<Note>
  Bank balances are stored in the `bank_accounts` collection, not on the character document. Query that collection separately to find bank balance data.
</Note>

***

## Validation

### Name Validation

```lua theme={null}
function ValidateName(name)
    -- Length check
    if #name < 2 or #name > 30 then
        return false, 'Name must be 2-30 characters'
    end

    -- Character check (letters, spaces, hyphens, apostrophes only)
    if not string.match(name, "^[a-zA-Z%s%-']+$") then
        return false, 'Name contains invalid characters'
    end

    return true
end
```

### DOB Validation

```lua theme={null}
function ValidateDOB(dob)
    -- Format check
    local year, month, day = dob:match('(%d+)-(%d+)-(%d+)')

    if not year or not month or not day then
        return false, 'Invalid date format (use YYYY-MM-DD)'
    end

    -- Age check (must be 18+)
    local birthYear = tonumber(year)
    local currentYear = tonumber(os.date('%Y'))
    local age = currentYear - birthYear

    if age < 18 then
        return false, 'Character must be at least 18 years old'
    end

    return true
end
```

### Gender Validation

```lua theme={null}
function ValidateGender(gender)
    local genderNum = tonumber(gender)

    if not genderNum or (genderNum ~= 0 and genderNum ~= 1) then
        return false, 'Gender must be 0 (Male) or 1 (Female)'
    end

    return true
end
```

***

## Example: Complete Character Query

```lua theme={null}
-- Get character with all related data (for online character)
function GetCompleteCharacterData(source)
    local player = Fetch:Source(source)

    if not player then
        return nil
    end

    local char = player:GetData('Character')

    if not char then
        return nil
    end

    local stateId = char:GetData('SID')

    -- Get inventory item counts
    local itemCounts = Inventory.Items:GetCounts(stateId, 1)

    -- Get vehicles (database query)
    local vehicles = Database.Game:findOne({
        collection = 'vehicles',
        query = {owner = stateId}
    })

    -- Get properties (database query)
    local properties = Database.Game:findOne({
        collection = 'property_ownership',
        query = {owner = stateId}
    })

    -- Get bank accounts (database query)
    local bankAccounts = Database.Game:findOne({
        collection = 'bank_accounts',
        query = {owner = stateId}
    })

    return {
        character = {
            SID = char:GetData('SID'),
            First = char:GetData('First'),
            Last = char:GetData('Last'),
            Cash = char:GetData('Cash'),
            Jobs = char:GetData('Jobs'),  -- Array of job objects
            Phone = char:GetData('Phone'),
        },
        inventory = itemCounts,
        vehicles = vehicles,
        properties = properties,
        bankAccounts = bankAccounts
    }
end

-- For offline character (database query)
function GetCompleteOfflineCharacterData(stateId)
    -- Query database directly
    local char = Database.Game:findOne({
        collection = 'characters',
        query = {SID = stateId}
    })

    if not char then
        return nil
    end

    return {
        character = char,
    }
end
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use Metadata for Flexible Data" icon="brackets-curly">
    **✅ Good:**

    ```lua theme={null}
    -- Store flexible data in metadata
    metadata.customFeatures = {
        premium = true,
        vipLevel = 3,
        customSkin = 'skin_1'
    }
    ```

    **❌ Bad:**

    ```lua theme={null}
    -- Don't add new root-level fields
    premium = true,           -- This pollutes the schema
    vipLevel = 3,
    customSkin = 'skin_1'
    ```
  </Accordion>

  <Accordion title="Validate Before Updates" icon="shield-check">
    ```lua theme={null}
    function UpdateCharacterCash(source, amount)
        -- Validate character exists
        local player = Fetch:Source(source)
        if not player then
            return false, 'Player not found'
        end

        local char = player:GetData('Character')
        if not char then
            return false, 'Character not loaded'
        end

        -- Validate amount
        if type(amount) ~= 'number' then
            return false, 'Amount must be a number'
        end

        local currentCash = char:GetData('Cash')
        local newCash = currentCash + amount

        if newCash < 0 then
            return false, 'Insufficient funds'
        end

        -- Update cash using DataStore
        char:SetData('Cash', newCash)

        return true
    end
    -- Note: For bank operations, use the Banking component API
    ```
  </Accordion>

  <Accordion title="Index Commonly Queried Fields" icon="magnifying-glass">
    ```javascript theme={null}
    // MongoDB - Create indexes for performance
    db.characters.createIndex({ SID: 1 }, { unique: true })
    db.characters.createIndex({ Owner: 1 })
    db.characters.createIndex({ 'Jobs.Id': 1 })
    db.characters.createIndex({ Phone: 1 }, { unique: true })
    ```
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Characters - Exports" icon="cube" href="/api/characters/exports">
    Character management methods
  </Card>

  <Card title="Characters - Events" icon="bolt" href="/api/characters/events">
    Character lifecycle events
  </Card>

  <Card title="Database API" icon="database" href="/api/core/database">
    Database operations
  </Card>

  <Card title="Inventory Data Structure" icon="box" href="/api/inventory/item-definition">
    Inventory and item data
  </Card>
</CardGroup>

<Tip>
  **Metadata Flexibility:** The metadata field is perfect for storing custom data without modifying the core schema. Use it for resource-specific data, stats, settings, and any dynamic information.
</Tip>
