> For the complete documentation index, see [llms.txt](https://tk-scripts.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tk-scripts.gitbook.io/docs/tk_housing/config.md).

# Config

```lua
Config = {}

Config.DebugMode = false -- false | true, prints extra debug info while you set the script up
Config.Framework = 'esx' -- 'esx' | 'qb'
Config.NotificationType = 'esx' -- 'ox' | 'esx' | 'qb' | 'mythic'
Config.Locale = 'en' -- 'en' | 'fi'

Config.UseOxLib = false -- false | true
Config.Target = 'none' -- 'none' | 'ox' | 'qb'

Config.Inventory = 'default' -- 'default' | 'qb_old' | 'qb_new' | 'ox' | 'qs'
Config.Clothing = 'illenium' -- 'illenium' | 'fivem-appearance' | 'qs-appearance' | 'bl_appearance' | 'crm-appearance' | 'tgiann-clothing' | 'op-clothing' | 'rcore_clothing' | 'qb-clothing' | 'ak47_qb_clothing' | 'esx_skin' | '17mov'
Config.Weather = 'cd_easytime' -- 'none' | 'cd_easytime' | 'qb-weathersync' | 'renewed' | 'av_weather' | 'weathersync' | 'esx_weather' | 'custom'
Config.Dispatch = 'default' -- 'default' | 'tk' | 'cd'

Config.HelpNotification = false -- false | true, use help notification instead of 3d text
Config.MenuAlign = 'top-left' -- 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center-left' | 'center-right' | 'center', where esx menus open

Config.InsideWeather = { -- weather and clock forced while a player is inside a property
    weather = 'SMOG', -- https://docs.fivem.net/natives/?_0xED712CA327900C8A
    hour = 19,
    minute = 0,
    timecycle = 'MPApartHigh', -- timecycle modifier forced while inside, empty or nil to not force one
    timecycleStrength = 1.0, -- 0.0-1.0
    timecycleVars = { -- timecycle variables added on top of the modifier above, shell interiors only
        postfx_exposure = -0.5,
        postfx_exposure_min = -0.5,
        postfx_desaturation = 1.3,
        reflection_hdr_mult = 0.0,
    },
}

Config.UISettings = {
    color = 'blue', -- 'default' | 'red' | 'rose' | 'pink' | 'fuchsia' | 'purple' | 'violet' | 'indigo' | 'blue' | 'sky' | 'cyan' | 'teal' | 'emerald' | 'green' | 'lime' | 'yellow' | 'amber' | 'orange'
    shade = 6, -- 1-9,
    theme = 'dark', -- 'dark' | 'light'
    animations = true, -- false | true, UI animations like the music player's analyser strip
}

Config.Commands = { -- false disables a command
    addProperty = 'addproperty', -- command that places a new property, needs real estate or admin permission
    adminPanel = 'adminhousing', -- lists every property with teleport / edit / transfer / delete options
    shellCreator = 'shellcreator', -- command that opens the shell creator to build a new interior
    shellEditor = 'shelleditor', -- command that opens the shell editor to edit an existing interior
    removeUnusedProperties = 'removeunusedproperties', -- server admin command, permanently deletes properties nobody has entered in the days you give
    removePropertyOwner = 'removepropertyowner', -- server admin command, clears a property's owner by its id
    dropBackups = 'housing_dropbackups', -- server console command, permanently drops old migration backup data when you add confirm
    clearThumbnails = 'housing_clearthumbnails', -- admin command, wipes every furniture thumbnail and interior photo for all players
    bakeThumbnails = 'housing_bakethumbnails', -- server console command, has one online player render every furniture thumbnail
    stopBakeThumbnails = 'housing_stopbakethumbnails', -- server console command, stops a furniture thumbnail bake in progress
}

Config.Controls = { -- https://docs.fivem.net/docs/game-references/controls/
    interact = 38, -- E, key for a spot's first option
    interactExtra = {47, 74, 29}, -- G, H, B, keys for a spot's 2nd, 3rd and 4th option
    takePhoto = 38, -- E
    cancelPhoto = 194, -- BACKSPACE
    getUp = 22, -- SPACE, get up from furniture, update the seat_get_up_hint locale if you change this
    stopBrowsing = 194, -- BACKSPACE, stop browsing a TV screen
    furnish = {
        cancel = 194, -- BACKSPACE
        place = 38, -- E, place a piece, or pick one up without it jumping to your cursor
        toggleFocus = 19, -- ALT
        toggleMode = 37, -- TAB
        remove = 178, -- DELETE
        toggleTracking = 137, -- CAPS LOCK, toggle whether the held piece follows your cursor
        surfaceLock = 20, -- Z, cycle which surface the held piece sticks to
        snap = 44, -- Q, hold to snap the held piece to the one next to it
        rotateUp = 15, -- Scroll up
        rotateDown = 14, -- Scroll down
        rotatePrimary = 36, -- LCTRL
        rotateSecondary = 21, -- LSHIFT
        gizmo = 47, -- G
        freeCam = 23, -- F
        nudgeUp = 172, -- ARROW UP
        nudgeDown = 173, -- ARROW DOWN
        nudgeLeft = 174, -- ARROW LEFT
        nudgeRight = 175, -- ARROW RIGHT
        nudgeRaise = 10, -- PAGE UP
        nudgeLower = 11, -- PAGE DOWN
        copy = 348, -- MIDDLE MOUSE, copy the piece you're aiming at and start placing a new one
    },
    areaPicker = { -- area tool in the add-property wizard
        add = 38, -- E
        undo = 20, -- Z, removes the last point
        finish = 191, -- ENTER
        cancel = 194, -- BACKSPACE, press twice when points are placed
        fly = 23, -- F, freecam on / off
        snap = 47, -- G, cycle grid size
        mode = 37, -- TAB, draw / rectangle
        level = 182, -- L, level all points to the lowest one
        grab = 44, -- Q, pick up / drop the highlighted point
        remove = 178, -- DELETE, delete the highlighted point
        heightUp = 172, -- ARROW UP
        heightDown = 173, -- ARROW DOWN
    },
}

Config.AreaPicker = { -- area, entrance and door tools in the add-property wizard, area keys in Config.Controls.areaPicker
    reach = 30.0, -- how far the aim looks for a surface (units)
    radius = 0.002, -- how wide the cursor looks for a surface (units)
    defaultHeight = 10.0, -- wall height a new area starts with (units)
    minHeight = 1.0, -- lowest wall height you can set (units)
    maxHeight = 60.0, -- highest wall height you can set (units)
    gridSizes = {0.5, 1.0, 2.0}, -- grid steps the snap key cycles through (units), then off
    grabDistance = 0.5, -- how close the aim has to be to a point to highlight it (units)
    level = true, -- false | true, lower every point to the lowest one so the walls match
    freecam = true, -- false | true, allow flying the camera in the wizard's area, entrance and door tools
    camera = { -- freecam feel while flying, higher smoothing = snappier, lower = floatier
        fov = 50.0, -- field of view the freecam starts at (degrees)
        moveSpeed = 12.0, -- units per second
        fastMult = 3.0, -- hold LEFT SHIFT to fly fast
        vertSpeed = 8.0, -- SPACE up / LEFT CTRL down, units per second
        lookSensitivity = 8.0, -- mouse look, lower = slower
        invertY = false, -- false | true
        moveSmoothing = 10.0,
        lookSmoothing = 22.0,
        fovStep = 2.0, -- how much the scroll wheel zooms per notch (degrees)
        fovMin = 20.0, -- most zoomed in the scroll wheel can go
        fovMax = 80.0, -- most zoomed out the scroll wheel can go
    },
}

Config.Dist = { -- how close the player has to be to interact
    house = 2,
    garage = 3.5,
    door = 2,
    exit = 2,
    garageExit = 3.5, -- the exit door inside a garage, house exits use exit
    storage = 2, -- storage spots only, placed furniture uses Config.FurnitureUses
    wardrobe = 2, -- wardrobe spots only, placed furniture uses Config.FurnitureUses
    yard = 100, -- how far away yard furniture is visible
}

Config.Jobs = {
    realEstate = { -- jobs that should be able to create properties, DONT CHANGE THIS LINE
        real_estate = 0, -- CHANGE JOB NAME AND GRADE HERE
        realestate = 0, -- you can also add more jobs here
    },
    police = { -- jobs that should be able to enter all properties, DONT CHANGE THIS LINE
        police = 0, -- CHANGE JOB NAME AND GRADE HERE
        leo = 0,
        sheriff = 0,
    },
}

Config.JobOwnedProperties = { -- a job can own a property (e.g. a police HQ), set from the admin panel
    enabled = true, -- false | true
    defaults = {
        access = 0, -- min grade to enter
        manage = 'boss', -- 'boss' or a grade number
        furnish = true, -- can members furnish
        storage = true, -- can members open storages
    },
    jobs = { -- per-job overrides, missing fields use defaults, label is shown on the door
        -- police = {label = 'LSPD', access = 0, manage = 'boss'},
    },

    selling = {
        enabled = true, -- false | true, can a job-owned property be sold
        proceeds = 'seller', -- 'seller' | 'burn', seller = money goes to the selling manager's bank
    },

    transferring = {
        enabled = true, -- false | true, can a job-owned property be transferred to a player
    },

    billing = true, -- false | true, job-owned properties get utility bills, a manager pays from their own bank
    mortgage = true, -- false | true, job-owned properties can have a mortgage, false also blocks assigning a mortgaged one
}

Config.ShowPropertyName = true -- false | true, show the property name after the door label, e.g. "House (Pink Cage)"
Config.EnableRemoving = true -- false | true, allow removing properties that don't have an owner
Config.EnableEditing = true -- false | true, allow editing properties that don't have an owner
Config.EnableCustomSpots = true -- false | true, allow adding custom spots in the property creator, an interior's own spots always stay
Config.SpotKinds = { -- spot kinds offered in the property creator and shell editor, each needs a Config.FurnitureUses row below
    'storage',
    'wardrobe',
    'mirror',
    'sink',
    'shower',
    'bathtub',
    'oven',
    'microwave',
    'switch',
    'music',
    'generator',
    --'battery', -- needs Config.Battery.enable = true
    'assistant',
}

Config.AllowAdmins = {
    createProperty = true,
    removeProperty = true,
}

Config.AdminGroups = { -- group names that count as admins on your server
    admin = true,
    --superadmin = true,
}

Config.MaxProperties = 99 -- how many properties one player can own
Config.SellBackPercentage = 50 -- % of the price you get back when selling
Config.MaxPermissions = 10 -- how many players can be given access to one property
Config.DisablePreviewing = false -- false | true
Config.DisableSelling = false -- false | true, player-owned only, job-owned use Config.JobOwnedProperties.selling
Config.DisableTransferring = false -- false | true, player-owned only, job-owned use Config.JobOwnedProperties.transferring
Config.NonOwnedMLODoorsUnlocked = true -- false | true, doors of unowned MLO properties stay unlocked

Config.PropertyList = {
    command = 'propertylist', -- command to open the property list from anywhere, false to disable
    item = '', -- usable item that opens the property list, '' = disabled
    showAllUnownedProperties = true, -- false | true, show all unowned properties in the property list
    buyFrom = 'anywhere', -- 'anywhere' | 'list', where players can buy, 'list' = from the property list only
    -- pair 'list' with showAllUnownedProperties = false so agents have to list a property first
    agents = 'both', -- 'job' (Config.Jobs.realEstate) | 'business' (Config.RealEstateBusiness) | 'both' | 'none', who can sell as an agent through the list
    listing = 'everyone', -- 'everyone' | 'agents', who can put a property up for sale in the list
}

Config.PropertyManager = {
    command = 'propertymanager', -- command to manage your properties from anywhere, false to disable
    item = '', -- usable item that opens the property manager menu, '' = disabled
    buttons = {
        enter = { -- buttons shown when outside
            lock = false,
            changeCode = true,
            managePermissions = true,
            furnish = false,
            security = true,
            bills = true,
            mortgage = true,
            transfer = true,
            sell = false,
        },
        exit = { -- buttons shown when inside
            lock = true,
            changeCode = true,
            managePermissions = true,
            furnish = true,
            security = true,
            bills = true,
            mortgage = true,
            transfer = true,
            sell = true,
        },
    },
}

Config.Furnishing = { -- storage / buyAt / fulfilment / sellBack only apply when acquisition = 'inventory'
    enable = true,
    command = 'furnish', -- command that opens the furnish menu, false to disable
    key = 'F7', -- default key for the command, false for none, players can rebind it in Settings > Key Bindings
    acquisition = 'instant', -- 'instant' | 'inventory', instant = buy and place in one step, inventory = buy first and place from storage
    storage = 'property', -- 'player' | 'property', where bought furniture is kept, player = an inventory item on any Config.Inventory except 'default' (the database otherwise), property = shared by everyone with furnish access
    buyAt = {
        menu = true, -- from the furnish menu inside the house
        shop = false, -- from the furniture shop
    },
    fulfilment = 'instant', -- 'instant' | 'delivery', delivery = arrives in a box at the front door after delivery.time
    sellPercentage = 50, -- % cash back when removing a placed piece
    sellBack = false, -- false | true, inventory mode: sell removed pieces for cash instead of returning them
    maxStorages = false, -- max storages per property, false = unlimited, a property or interior can override
    maxFurniture = false, -- max other furniture per property, false = unlimited, a property or interior can override
    item = 'furniture', -- inventory item for storage = 'player'
    removeOnSell = true, -- false | true, remove furniture when the property is sold
    freecam = true, -- false | true, allow flying the camera while furnishing

    styling = { -- the Style section of the furnish menu
        variants = true, -- false | true, swap a placed piece for a look-alike model (prop_chair_01 -> prop_chair_02) in place
        stacking = true, -- false | true, a piece placed on top of another attaches to it and moves with it
    },

    placement = { -- how the furnish cursor picks the surface a piece is put on
        reach = 10.0, -- how far in front of the player the cursor reaches (units)
        radius = 0.03, -- how wide the cursor looks for a surface (units)
        axisTolerance = 20.0, -- how much a surface can lean and still count as flat (degrees)
        ledgeReach = 0.35, -- how far below a wall hit to look for a shelf instead (units), 0 = off
        lift = 0.005, -- how far a piece lifts off its surface so the two don't flicker (units)
    },

    snapping = { -- lines a held piece up with placed pieces so walls and floors meet with no gap
        enable = true, -- false | true
        mode = 'hold', -- 'hold' | 'toggle', hold = on while the key is held, toggle = the key turns it on and off
        reach = 3.0, -- how far from the cursor to look for a piece to snap to (units)
        angleStep = 90.0, -- how much the scroll wheel turns a snapping piece (degrees)
        overlap = 0.01, -- how far a snapped piece sinks into the one next to it (units), raise if you still see a gap
    },

    buildParts = { -- shell creator pieces (walls, floors, stairs, railings) sold as furniture, so an owner can build inside their property
        enable = true, -- false | true
        free = true, -- false | true, when true build parts skip the shop and place straight away
        price = 250, -- what one build part costs when free = false
        categories = { -- which groups are listed, each is one category in the furnish menu
            floors = true,
            walls = true,
            halfwalls = true,
            lowwalls = true,
            doorways = true,
            windows = true,
            columns = true,
            platforms = true,
            railings = true,
            stairs = true,
            doors = false, -- a door placed as furniture doesn't open, decoration only
        },
    },

    shop = { -- furniture shop, used when buyAt.shop = true
        coords = vec4(70.70, -1567.44, 29.60, 54.48),
        ped = `a_m_y_business_01`, -- shop ped model
        scenario = nil, -- ped scenario, e.g. 'WORLD_HUMAN_CLIPBOARD', nil for none
        blip = {sprite = 478, color = 4, scale = 0.8},
        label = 'furniture_shop_blip', -- locale key for the blip name and interaction label
        item = '', -- usable item that opens the shop menu from anywhere, '' = disabled
    },

    delivery = { -- used when fulfilment = 'delivery'
        time = 1000 * 60 * 15, -- how long before an order arrives at the front door (ms)
        boxModel = 'prop_boxpile_07d', -- delivery box prop
        collectTimeout = 1000 * 60 * 60 * 1, -- how long an order waits at the door before it is given to the buyer (ms)
    },

    thumbnails = { -- furniture menu thumbnails are taken in game the first time the menu opens, then saved
        resolution = 256, -- thumbnail size in px (square)
        keying = 'difference', -- 'difference' | 'magenta' | 'green', how the background is cut out
        cameraAngle = 35.0, -- camera heading around the prop (degrees)
        cameraPitch = 20.0, -- camera pitch looking down at the prop (degrees)
        fitPadding = 1.1, -- 1.0 = prop touches the frame edge, higher leaves more border
        settleTimeout = 300, -- how long to wait for the prop to load before taking the shot (ms), raise if thumbnails look blurry
        captureFormat = 'jpeg', -- only used if direct capture fails (screenshot-basic), 'jpeg' is fastest
        captureQuality = 0.92, -- 0-1, only used if direct capture fails
        cacheFormat = 'webp', -- 'webp' | 'png', webp is about 6x smaller than png
        cacheQuality = 0.9, -- 0-1, lower to make the saved thumbnails smaller, ignored for png
        minUploadResolution = {w = 1280, h = 720}, -- players below this screen size do not upload thumbnails
        syncBandwidth = 512000, -- how fast thumbnails are sent to players (bytes per second)
    },

    gizmo = {
        enable = true, -- false | true, G opens move / rotate handles on the selected piece
        modes = { -- turning one off makes G always open the other
            move = true, -- false | true
            rotate = true, -- false | true
        },
        allowLocalSpace = true, -- false | true, TAB switches handles between world axes and the piece's own axes
        nudgeStep = 0.05, -- how far one arrow key / page key press nudges a picked-up piece (units)
        maxDist = 50, -- how far the player can be from the property
        camera = { -- freecam (F) and gizmo orbit camera feel, higher smoothing = snappier, lower = floatier
            fov = 50.0,
            moveSpeed = 8.0, -- units per second
            fastMult = 3.0, -- hold LEFT SHIFT to fly fast
            vertSpeed = 6.0, -- SPACE up / LEFT CTRL down, units per second
            lookSensitivity = 8.0, -- mouse look, lower = slower
            invertY = false, -- false | true
            moveSmoothing = 10.0,
            lookSmoothing = 22.0,
            fovStep = 2.0, -- how much the scroll wheel zooms per notch (degrees), only while no piece is held
            fovMin = 20.0, -- most zoomed in the scroll wheel can go
            fovMax = 80.0, -- most zoomed out the scroll wheel can go
            orbitSensitivity = 8.0, -- middle-mouse drag orbit, orbit only works while the gizmo is open
            panSpeed = 4.0, -- SHIFT + middle-mouse pan, scaled by distance
            minDistance = 1.0, -- closest the orbit camera can zoom to the piece
            maxDistance = 20.0, -- farthest the orbit camera can zoom out
            orbitSmoothing = 18.0,
            zoomDragSpeed = 3.0, -- CTRL + middle-mouse drag zoom
            bounds = { -- how far the furnish camera can go past the edges of the area (units)
                pad = 1.0, -- past the walls
                above = 1.5, -- above the ceiling
                below = 1.0, -- below the floor
            },
        },
    },
}

Config.EnableMotels = true -- false | true, motel rooms are rented for a fixed time, can't be bought or sold and free up when the time ends
Config.Motel = {
    duration = 60 * 24, -- rental period in minutes (24 hours)
    allowExtend = true, -- false | true, guests can check in again to extend their stay
    refundOnCheckout = false, -- false | true, refund unused time when checking out early
    oneRoomPerPlayer = true, -- false | true, a player can only hold one motel room at a time
    sweepInterval = 60 * 1000, -- how often expired rooms are checked (1 minute)
}

Config.Doorbell = {
    notifyAlways = true, -- false | true, notify the owner even when they aren't inside the property
    cooldown = 2500, -- how long between rings (ms)
    maxDist = 10, -- how far from the door you can be and still get let in
}

Config.PoliceLockdown = {
    enable = true, -- false | true, allow police to seal properties from the door menu
    jobs = {
        police = 0, -- job name, minimum grade allowed to seal a property, every member can enter a sealed one
    },
    maxDistance = 2.5, -- how far from the door you can be when changing the seal (units)
    barrier = {
        model = `prop_barrier_work05`, -- false | model hash or name, '' also disables the barrier
        streamDistance = 100.0, -- how far away the barrier is spawned (units)
    },
    placement = {
        duration = 1000, -- placement animation duration (ms)
        anim = {
            dict = 'weapons@first_person@aim_rng@generic@projectile@sticky_bomb@',
            name = 'plant_floor',
            flag = 17,
            advanced = true,
            hideWeapon = true,
        },
    },
}

Config.Lockpicking = { -- built-in lockpicking minigame, swap it for your own in client/main_editable.lua
    door = {
        enable = true, -- false | true, allow lockpicking other players' property doors
        item = 'lockpick', -- item needed to lockpick
        playersWithAccessNeeded = 0, -- how many players with access to the property need to be online
        pins = 4, -- how many pins the lock has
        timeLimit = 20, -- seconds before the pick breaks
    },
    storage = {
        enable = true, -- false | true, allow lockpicking other players' property storages
        item = 'lockpick',
        playersWithAccessNeeded = 0,
        pins = 3,
        timeLimit = 15,
    },
}

Config.Breaching = { -- police forcing open a door they hold no key to
    enable = true, -- false | true, allow police to breach property doors
    item = '', -- item police must carry, e.g. 'door_ram', '' = no item needed, never consumed
    duration = 4500, -- how long breaching takes (ms)
    lockGradeDuration = 1000, -- extra time per reinforced lock grade on the property (ms)
    animTime = 1500, -- how long one pass of the crowbar anim runs before it restarts (ms), does not change the breach time
    anim = { -- crowbar anim, do not add a duration key here or it overrides the breach time above
        dict = 'missheist_jewel@first_person',
        name = 'smash_case_e',
        flag = 17,
        advanced = true, -- pins the ped in place for the duration
        hideWeapon = true,
        prop = { -- the crowbar, pos / rot are tuned for the clip above
            model = `w_me_crowbar`,
            bone = 28422, -- right hand
            pos = vec3(0.257, 0.030, 0.034),
            rot = vec3(-1.0, -91.7, -9.2),
        },
    },
}

Config.Keys = { -- physical house keys, needs Config.Inventory set to anything but 'default' ('default' drops the metadata)
    enable = false, -- false | true, owners can cut keys to their property
    item = 'house_key', -- key item
}

Config.StorageBlacklist = { -- items that can't be stored in property storages, not case sensitive
    --money = true,
    --weapon_pistol = true,
}

Config.Garage = { -- garage options for storing and retrieving vehicles
    menu = 'default', -- 'default' | 'jg-advancedgarages' | 'custom', which garage menu opens
    clearEntrances = false, -- false | true, clear NPCs from garage entrances
    cooldown = 2000, -- how long between garage enters / exits (ms)
    exitOffset = 3.0, -- offset for the vehicle spawn spot when leaving a garage
    entryOffset = 3.0, -- offset for the vehicle spawn spot when entering a garage
    canStore = {
        NPCVehicles = true, -- false | true, allow storing NPC vehicles in garages
        OtherPlayerVehicles = true, -- false | true, allow storing other players' vehicles in garages
    },
    default = 'Centre', -- garage vehicles are put in if they somehow get lost
    propertyName = 'Property #%s', -- name your garage script sees for cars parked in a property garage, %s = property id, drop it for one shared name
}

Config.Blips = {
    owned = {
        enable = true, -- false | true
        color = 2, -- https://docs.fivem.net/docs/game-references/blips/ (scroll to the bottom)
        scale = 0.8, -- needs to be a float (eg. 1.0, 1.2, 2.0)
        sprite = 40, -- https://docs.fivem.net/docs/game-references/blips/
    },
    on_sale = {
        enable = true,
        color = 11,
        scale = 0.8,
        sprite = 350,
    },
    access = {
        enable = false,
        color = 3,
        scale = 0.8,
        sprite = 40,
    },
    occupied = {
        enable = false,
        color = 1,
        scale = 0.8,
        sprite = 40,
    },
}

Config.PropertyTypes = { -- only sets the map blip and groups listings, first entry is the default, missing blip fields use Config.Blips
    {
        id = 'property',
        label = 'House',
        blip = {
            owned = {sprite = 40},
            on_sale = {sprite = 350}
        }
    },
    {
        id = 'garage',
        label = 'Garage',
        blip = {
            owned = {sprite = 50},
            on_sale = {sprite = 50}
        }
    },
    {
        id = 'apartment',
        label = 'Apartment',
        blip = {
            owned = {sprite = 40},
            on_sale = {sprite = 350},
            occupied = {sprite = 40}
        }
    },
    {
        id = 'warehouse',
        label = 'Warehouse',
        blip = {
            owned = {sprite = 473},
            on_sale = {sprite = 473}
        }
    },
    {
        id = 'office',
        label = 'Office',
        blip = {
            owned = {sprite = 475},
            on_sale = {sprite = 475}
        }
    },
    {
        id = 'motel',
        label = 'Motel',
        rental = true,
        blip = {
            owned = {sprite = 475},
            on_sale = {sprite = 475},
            occupied = {sprite = 475}
        }
    },
}

Config.Markers = { -- markers drawn at interaction points, only when Config.Target = 'none'
    house = {
        enable = false,
        dist = 6,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    garage = {
        enable = false,
        dist = 6,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    door = {
        enable = false,
        dist = 6,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    exit = {
        enable = false,
        dist = 6,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    storage = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    wardrobe = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    mirror = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    water = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    light = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    switch = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    cooking = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    seat = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    music = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    generator = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    battery = {
        enable = false,
        dist = 10,
        type = 1,
        scale = vec3(0.25, 0.25, 0.25),
        color = vec4(80, 190, 255, 150),
        bob = false,
        faceCamera = true,
    },
    television = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    frame = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    assistant = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
    pole = {
        enable = false,
        dist = 4,
        type = 20,
        scale = vec3(0.3, 0.3, 0.3),
        color = vec4(255, 255, 255, 150),
        bob = false,
        faceCamera = true,
    },
}

-- dist: how close the player has to be to interact
-- marker: a Config.Markers row drawn at the prop
-- label: locale key shown on the interaction option
-- action: which built-in action runs it, defaults to the kind name
-- icon: optional FontAwesome icon the furnish menu badges the piece with

Config.FurnitureUses = { -- one row per thing a placed prop can do, a config/furniture.lua entry opts in with uses = {'storage'}
    storage = {dist = 2, marker = Config.Markers.storage, label = 'open_storage_menu', icon = 'box'},
    wardrobe = {dist = 2, marker = Config.Markers.wardrobe, label = 'open_wardrobe_menu', icon = 'shirt'},
    mirror = {dist = 2, marker = Config.Markers.mirror, label = 'get_haircut', icon = 'scissors'}, -- hair, tattoos and clothes, turn mirrors off with Config.Mirrors
    sink = {dist = 2, marker = Config.Markers.water, label = 'use_sink', action = 'water'},
    shower = {dist = 2, marker = Config.Markers.water, label = 'use_shower', action = 'water'},
    bathtub = {dist = 2, marker = Config.Markers.water, label = 'use_bathtub', action = 'water'},
    oven = {dist = 2, marker = Config.Markers.cooking, label = 'open_cooking_menu', action = 'cook'},
    microwave = {dist = 2, marker = Config.Markers.cooking, label = 'open_cooking_menu', action = 'cook'},
    light = {dist = 2, marker = Config.Markers.light, label = 'toggle_light'},
    switch = {dist = 2, marker = Config.Markers.switch, label = 'flip_switch', icon = 'toggle-on'},
    sit = {dist = 2, marker = Config.Markers.seat, label = 'sit_down', action = 'seat'},
    lay = {dist = 2, marker = Config.Markers.seat, label = 'lay_down', action = 'seat'},
    yoga = {dist = 2, marker = Config.Markers.seat, label = 'do_yoga', action = 'seat', icon = 'spa'},
    bench = {dist = 2, marker = Config.Markers.seat, label = 'use_bench', action = 'seat', icon = 'dumbbell'},
    pullups = {dist = 2, marker = Config.Markers.seat, label = 'do_pullups', action = 'seat', icon = 'dumbbell'},
    weights = {dist = 2, marker = Config.Markers.seat, label = 'lift_weights', action = 'seat', icon = 'dumbbell'},
    drink = {dist = 2, marker = Config.Markers.seat, label = 'have_a_drink', action = 'seat', icon = 'champagne-glasses'},
    music = {dist = 2, marker = Config.Markers.music, label = 'open_music_menu'},
    generator = {dist = 2, marker = Config.Markers.generator, label = 'open_generator', icon = 'bolt'},
    battery = {dist = 2, marker = Config.Markers.battery, label = 'open_battery', icon = 'car-battery'},
    television = {dist = 2, marker = Config.Markers.television, label = 'open_television_menu'},
    frame = {dist = 2, marker = Config.Markers.frame, label = 'set_picture', icon = 'image'},
    assistant = {dist = 2, marker = Config.Markers.assistant, label = 'open_assistant', icon = 'robot'},
    pole = {dist = 2, marker = Config.Markers.pole, label = 'hire_dancer', icon = 'music'},
    poledance = {dist = 2, marker = Config.Markers.seat, label = 'pole_dance', action = 'seat'},
}

Config.Rent = { -- recurring charge for rented properties
    interval = 1000 * 60 * 60 * 12, -- how often rent is charged (12 hours)
    paymentMode = 'automatic', -- 'automatic' | 'manual', automatic = taken from the bank, manual = paid in the property menu, needs Config.Bills.enable = true
    onNonPayment = 'repossess', -- 'repossess' | 'lock', repossess = property is put back on sale, lock = can't enter until paid
    gracePeriod = 1000 * 60 * 60 * 24, -- how long rent can stay unpaid before onNonPayment happens (24 hours), only while Config.Bills.enable is true
}

Config.Bills = { -- utility bills (electricity / water / internet) for owned and rented properties
    enable = true, -- false | true
    paymentMode = 'manual', -- 'automatic' | 'manual', automatic = taken from the bank, manual = paid in the door menu
    onNonPayment = 'lock', -- 'lock' | 'repossess', lock = can't enter until paid, repossess = property is put back on sale
    gracePeriod = 1000 * 60 * 60 * 24, -- how long a bill can stay unpaid before onNonPayment happens (24 hours)
    allowToggle = true, -- false | true, owners can disconnect utilities in the door menu, stops billing and the service
    historyDays = 30, -- how many days paid bills are kept in the history, 0 = keep forever
    services = {
        electricity = {
            enable = true,
            label = 'Electricity',
            costModel = 'percent', -- 'percent' | 'flat', % of the property price or a fixed amount
            amount = 0.4,
            interval = 1000 * 60 * 60 * 24 -- how often the bill is added (24 hours)
        },
        internet = {
            enable = true,
            label = 'Internet',
            costModel = 'flat',
            amount = 500,
            interval = 1000 * 60 * 60 * 24
        },
        water = {
            enable = true,
            label = 'Water',
            costModel = 'flat',
            amount = 0,
            interval = 1000 * 60 * 60 * 24,
            uses = {shower = 60, sink = 10, bathtub = 90} -- how much each use adds to the water bill
        },
    },
}

Config.Electricity = { -- basic = flat billing, advanced = bills the power furniture actually draws and lets generators and batteries power the devices cabled to them
    mode = 'basic', -- 'basic' | 'advanced', basic is a flat bill, advanced meters what's actually drawn
    updateInterval = 1000 * 15, -- how often power use is updated (15 seconds)
    saveInterval = 1000 * 60, -- how often state is saved when nothing changed (1 minute)
    maxOfflineHours = 24 * 7, -- most real hours counted after the server was stopped (1 week)
    baseWatts = 250, -- watts every owned property uses while it has electricity
    pricePerKwh = 18, -- price per kWh drawn from the grid
    defaultPolicy = 'local_first', -- 'local_first' | 'grid_first' | 'off_grid'
    defaultReservePercent = 15, -- battery % kept in reserve until an owner lowers it
    furnitureWatts = { -- watts each kind draws while active, a furniture entry can override with electricityWatts
        light = 40,
        music = 90,
        television = 180,
        oven = 2200,
        microwave = 1300,
        assistant = 20,
        heat = 1800,
        cool = 1400,
        fridge = 150,
        computer = 60,
        appliance = 3,
    },
    standbyWatts = { -- watts each kind draws while placed but not in use
        light = 0,
        music = 2,
        television = 2,
        oven = 0,
        microwave = 2,
        assistant = 20,
        heat = 0,
        cool = 0,
        fridge = 150,
        computer = 60,
        appliance = 3,
    },
    wiring = {
        requireItem = false, -- false | true, the item below is needed to start wiring
        item = 'wire_tool',
        maxLength = 35.0, -- longest total cable (units)
        maxPoints = 12, -- most cable points between the two ends
        reach = 12.0, -- how far you can aim (units)
        interactControl = 38, -- E
        undoControl = 178, -- DELETE, undo the last point
        cancelControl = 194, -- BACKSPACE
        showExisting = true, -- false | true, draw saved cables while wiring
        lineColor = {r = 80, g = 190, b = 255, a = 220}, -- rgba, the wiring tool's aim/outline accent colour
        cableColor = {r = 22, g = 22, b = 26, a = 255}, -- rgba, the cable's colour
        cableHighlight = {r = 96, g = 100, b = 112, a = 90}, -- rgba, a subtle sheen so the cable reads as round, not flat
        cableWidth = 0.009, -- units, half the cable's thickness up close (0.009 is roughly a 2cm cable)
        cableSag = 0.12, -- how much the cable dips per metre of run, capped at 0.35m
        showAlways = true, -- false | true, draw wired cables around the property even when the wiring tool is closed
        drawDistance = 30.0, -- units, cables further than this from the player are not drawn
    },
}

Config.Temperature = { -- indoor temperature, furniture with temperature = 'heat' or 'cool' heats or cools the property
    enable = false, -- false | true
    controlPermission = 'owner', -- 'owner' | 'use', owner includes managers, use = anyone inside
    defaultTarget = 22.0, -- thermostat setting a property starts at (degrees C)
    minTarget = 16.0, -- lowest thermostat setting (degrees C)
    maxTarget = 28.0, -- highest thermostat setting (degrees C)
    coldThreshold = 14.0, -- below this cold effects can begin (degrees C)
    hotThreshold = 30.0, -- above this heat effects can begin (degrees C)
    updateInterval = 5000, -- how often the temperature is updated (ms)
    pushInterval = 5000, -- how often occupants get the new value (ms)
    usageFlushInterval = 1000 * 60 * 10, -- how often electricity charges are written (10 minutes)
    maxElapsedMinutes = 2.0, -- most simulated minutes per update, stops catch-up after a stall
    occupiedOnly = true, -- false | true, freeze temperature and charges when nobody is inside
    ambientRate = 0.2, -- degrees moved toward the outdoor temperature per real minute
    devices = {
        heat = {
            ratePerDevice = 1.5,
            costPerMinutePerDevice = 2,
            maxDevices = 4,
        },
        cool = {
            ratePerDevice = 1.2,
            costPerMinutePerDevice = 3,
            maxDevices = 4,
        },
    },
    outdoor = {
        source = 'native', -- 'native' | 'custom', native reads the weather and clock, custom asks your own GetOutdoorTemperature
        fallbackWeather = 'CLEAR',
        fallbackHour = 12,
        fallbackTemperature = 18.0,
        nightStart = 22,
        nightEnd = 6,
        nightOffset = -4.0,
        weather = {
            EXTRASUNNY = 34.0,
            CLEAR = 28.0,
            CLOUDS = 19.0,
            OVERCAST = 16.0,
            RAIN = 12.0,
            CLEARING = 17.0,
            THUNDER = 11.0,
            SMOG = 23.0,
            FOGGY = 15.0,
            XMAS = -3.0,
            SNOW = -5.0,
            SNOWLIGHT = 0.0,
            BLIZZARD = -8.0,
            HALLOWEEN = 10.0,
            NEUTRAL = 20.0,
        },
    },
    effects = {
        enable = true, -- false | true, faint full-screen colour overlay when too hot or cold
        exposureDelay = 1000 * 60 * 2, -- how long at an uncomfortable temperature before effects begin (2 minutes)
        fadeTime = 5000, -- how long the overlay fades in and out (ms)
        maxStrength = 0.2, -- opacity 0.0-1.0
        fullStrengthDifference = 10.0, -- degrees past a threshold that reaches max strength
        coldColor = {r = 105, g = 170, b = 255},
        hotColor = {r = 255, g = 135, b = 72},
        updateInterval = 500, -- how often the effect is updated (ms)
    },
}

Config.Generator = { -- fuel-powered backup electricity, fuel and running state are saved on the furniture
    enable = true, -- false | true
    requireGenerator = false, -- false | true, when true electricity always needs a fueled, running generator
    capacity = 60 * 60 * 24, -- most runtime a generator can hold in seconds (24 hours)
    offlineConsumption = true, -- false | true, keep burning fuel while the server is stopped
    ratedWatts = 5000, -- most watts a generator can put out in advanced mode
    idleFuelFraction = 0.12, -- fuel use at zero load compared with full load, 0.0-1.0
    fuelItems = { -- item = runtime added per item in seconds
        diesel = 60 * 60,
    },
}

Config.Battery = { -- stores electricity for a property, charged by the generators cabled to it
    enable = false, -- false | true, needs Config.Electricity.mode = 'advanced'
    capacityWh = 10000, -- how much energy it holds (watt-hours)
    maxChargeWatts = 2500, -- fastest it can charge (watts)
    maxDischargeWatts = 3500, -- most it can put out (watts)
    efficiency = 0.92, -- energy kept through charging and discharging, 0.01-1.0
}

Config.Lights = { -- furniture with a light table or light = true (see config/furniture.lua) gives off real light
    enable = true, -- false | true
    switches = true, -- false | true, when false lamps are always on and wall switches do nothing
    brightness = true, -- false | true, brightness slider in the light popup
    color = true, -- false | true, colour picker in the light popup
    killGlow = true, -- false | true, also turn off a prop's own baked glow when its lamp is off (some models ignore it)
    blackout = 'bills', -- 'bills' | 'always' | false, when the interior's own light (baked glow, screens, neon) goes off, bills = while unpaid or disconnected
    blackoutToggle = true, -- false | true, owner can black out their interior from the door panel, a bills blackout can't be lifted
    blindsFlags = 264, -- room flags while blinds are closed (IPL/MLO only), 256 (no exterior render) + 8 (no exterior light), +4 if still wrong
    control = 'furnish', -- 'anyone' | 'furnish' | 'owner', who can change blinds, blackout and all lights at once, the owner always can and single lamps and switches are never restricted
    maxLinks = 32, -- how many lamps one wall switch can link to
    maxDrawn = 8, -- how many lights are drawn per frame, nearest first
    fadeTime = 200, -- how long a light takes to fade in or out (ms)
    defaults = {color = {255, 214, 170}, range = 6.0, intensity = 2.5}, -- used by furniture that just says light = true
    presets = { -- colour presets offered in the popup
        warm = {label = 'Warm', color = {255, 214, 170}},
        neutral = {label = 'Neutral', color = {255, 244, 229}},
        cool = {label = 'Cool', color = {214, 232, 255}},
    },
}

Config.Frames = { -- furniture with a frame field (see config/furniture.lua) shows a picture from a link or photo mode
    enable = true, -- false | true
    maxPerProperty = 12, -- how many frames with a picture one property can have, each one uses a bit of client memory
    resolution = 512, -- picture size in px (square)
    range = 25.0, -- how far from the player pictures are drawn (units)
    hosts = {}, -- allowed picture hosts, {} = any https host, or use the line below
    -- hosts = {'i.imgur.com', 'imgur.com', 'cdn.discordapp.com', 'media.discordapp.net', 'fivemanage.com', 'r2.fivemanage.com', 'api.fivemanage.com'},
}

Config.Assistant = { -- wall tablet that greets you, announces guests, reads your bills and takes commands
    enable = true, -- false | true
    persona = 'haviland', -- 'angel' | 'haviland' | 'og', default persona, owner can change it on the tablet
    voice = true, -- false | true, plays recorded voice lines, needs game build 3717+
    requireTablet = true, -- false | true, when false the assistant talks even with no tablet placed
    lineCooldown = 8000, -- how long before the same kind of line can play again (ms)
    moments = { -- which moments the assistant comments on
        greet = true, -- an owner or keyholder walks in
        bills = true, -- unpaid bills, right after the greeting
        moneyHigh = true, -- bank balance over moneyHighFrom and nothing due
        billionaire = true, -- bank balance over billionaireFrom, instead of moneyHigh
        doorbell = true,
        wakeUp = true, -- getting up from a bed
        wardrobe = true,
        storage = true,
        bathtub = true, -- using a bathtub or shower
        garage = true, -- driving a vehicle out of the garage
        haircut = true, -- opening the barber from a mirror
        tattoo = true, -- opening the tattoo shop from a mirror
        panel = true, -- opening the tablet
        alarm = true,
    },
    moneyHighFrom = 1000000, -- bank balance from which the assistant comments on your money
    billionaireFrom = 1000000000, -- bank balance from which it calls you a billionaire instead
    commands = true, -- false | true, command buttons on the tablet (lights, blinds, doors, bills, persona)
    tablets = { -- tablet model per persona (build 3717+), placed like any furniture, tech category of config/furniture.lua
        angel = 'm25_2_prop_m52_aitablet_01a',
        haviland = 'm25_2_prop_m52_aitablet_02a',
        og = 'm25_2_prop_m52_aitablet_03a',
    },
}

Config.Pole = { -- hire a dancer at a Dance Pole (games category in config/furniture.lua), players can dance on it too
    enable = true, -- false | true
    hireCost = 500, -- how much hiring a dancer costs, cash first then bank
    models = {'s_f_y_stripper_01', 's_f_y_stripper_02'}, -- ped models a dancer is picked from at random
    dancer = {offset = vec3(0.0, 0.0, 0.0), heading = 0.0}, -- fine-tune on top of where the dance anchors her
}

Config.Music = { -- furniture with a speaker table (see config/furniture.lua) opens the music player, heard only inside the property
    enable = true, -- false | true
    allowCustomUrl = true, -- false | true, players can paste their own stream URL instead of picking a station
    -- outsideRange = 18.0, -- how far the muffled version carries outside, only if the outside feed is re-enabled
    maxSources = 4, -- most streams playing at once per player, nearest win, also caps active speakers per property
    defaultVolume = 0.5, -- 0.0-1.0, volume a speaker starts at
    maxQueue = 50, -- most tracks one speaker's queue can hold
    sources = { -- which pasted link kinds players can use, needs allowCustomUrl
        stream = true,
        youtube = true,
        soundcloud = true,
        spotify = true,
    },
    search = { -- song search and radio station directory in the music player, needs allowCustomUrl, no API key needed
        enable = true, -- false | true
        limit = 20, -- most results one search returns
        stationCountry = '', -- country code the station directory is filtered to, '' for worldwide
    },
    stations = { -- stations grouped by genre, url must be a direct https audio stream (mp3/ogg/opus/flac/wav, no aac or m3u8)
        {id = 'groove_salad', label = 'SomaFM Groove Salad', genre = 'Chill', url = 'https://ice1.somafm.com/groovesalad-256-mp3'},
        {id = 'drone_zone', label = 'SomaFM Drone Zone', genre = 'Chill', url = 'https://ice1.somafm.com/dronezone-256-mp3'},
        {id = 'rp_mellow', label = 'Radio Paradise Mellow Mix', genre = 'Chill', url = 'https://stream.radioparadise.com/mellow-192'},
        {id = 'beat_blender', label = 'SomaFM Beat Blender', genre = 'Electronic', url = 'https://ice1.somafm.com/beatblender-128-mp3'},
        {id = 'defcon', label = 'SomaFM DEF CON Radio', genre = 'Electronic', url = 'https://ice1.somafm.com/defcon-128-mp3'},
        {id = 'indie_pop', label = 'SomaFM Indie Pop Rocks', genre = 'Pop & Rock', url = 'https://ice1.somafm.com/indiepop-128-mp3'},
        {id = 'u80s', label = 'SomaFM Underground 80s', genre = 'Pop & Rock', url = 'https://ice1.somafm.com/u80s-256-mp3'},
        {id = 'metal', label = 'SomaFM Metal Detector', genre = 'Pop & Rock', url = 'https://ice1.somafm.com/metal-128-mp3'},
        {id = 'rp_main', label = 'Radio Paradise Main Mix', genre = 'Eclectic', url = 'https://stream.radioparadise.com/mp3-192'},
        {id = 'boot_liquor', label = 'SomaFM Boot Liquor', genre = 'Country', url = 'https://ice1.somafm.com/bootliquor-128-mp3'},
        {id = 'secret_agent', label = 'SomaFM Secret Agent', genre = 'Lounge', url = 'https://ice1.somafm.com/secretagent-128-mp3'},
    },
}

Config.Cooking = { -- furniture with an oven or microwave kind (see config/furniture.lua) opens the recipe menu for that type
    enable = true, -- false | true
    itemImagePath = 'nui://ox_inventory/web/images/', -- false | image path prefix, '<name>.png' is appended
    recipes = { -- a recipe can also have a prop = {model, bone, pos = vec3, rot = vec3} held while cooking
        {
            label = 'Pizza',
            type = 'oven', -- 'oven' | 'microwave'
            ingredients = {
                {name = 'pizza_dough', amount = 1},
                {name = 'tomato', amount = 2},
                {name = 'cheese', amount = 1}
            },
            results = {
                {name = 'pizza', amount = 1}
            },
            duration = 12000, -- how long cooking takes (ms)
            anim = {
                dict = 'amb@prop_human_bbq@male@base',
                clip = 'base'
            },
        },
        {
            label = 'Lasagna',
            type = 'oven',
            ingredients = {
                {name = 'pasta', amount = 1},
                {name = 'tomato', amount = 2},
                {name = 'cheese', amount = 1}
            },
            results = {
                {name = 'lasagna', amount = 1}
            },
            duration = 15000,
            anim = {
                dict = 'amb@prop_human_bbq@male@base',
                clip = 'base'
            },
        },
        {
            label = 'Baked Salmon',
            type = 'oven',
            ingredients = {
                {name = 'raw_salmon', amount = 1},
                {name = 'lemon', amount = 1},
                {name = 'butter', amount = 1}
            },
            results = {
                {name = 'baked_salmon', amount = 1}
            },
            duration = 14000,
            anim = {
                dict = 'amb@prop_human_bbq@male@base',
                clip = 'base'
            },
        },
        {
            label = 'Chicken Pie',
            type = 'oven',
            ingredients = {
                {name = 'raw_chicken', amount = 1},
                {name = 'pie_crust', amount = 1},
                {name = 'mixed_vegetables', amount = 1}
            },
            results = {
                {name = 'chicken_pie', amount = 1}
            },
            duration = 15000,
            anim = {
                dict = 'amb@world_human_stand_impatient@male@no_sign@base',
                clip = 'base'
            },
        },
        {
            label = 'Mac and Cheese',
            type = 'microwave',
            ingredients = {
                {name = 'pasta', amount = 1},
                {name = 'cheese', amount = 1},
                {name = 'milk', amount = 1}
            },
            results = {
                {name = 'mac_and_cheese', amount = 1}
            },
            duration = 8000,
            anim = {
                dict = 'amb@world_human_stand_impatient@male@no_sign@base',
                clip = 'base'
            },
        },
        {
            label = 'Tomato Soup',
            type = 'microwave',
            ingredients = {
                {name = 'tomato', amount = 2},
                {name = 'salt', amount = 1},
                {name = 'milk', amount = 1}
            },
            results = {
                {name = 'tomato_soup', amount = 1}
            },
            duration = 7000,
            anim = {
                dict = 'amb@world_human_stand_impatient@male@no_sign@base',
                clip = 'base'
            },
        },
        {
            label = 'Stuffed Potato',
            type = 'microwave',
            ingredients = {
                {name = 'potato', amount = 1},
                {name = 'cheese', amount = 1},
                {name = 'butter', amount = 1}
            },
            results = {
                {name = 'stuffed_potato', amount = 1}
            },
            duration = 9000,
            anim = {
                dict = 'amb@world_human_stand_impatient@male@no_sign@base',
                clip = 'base'
            },
        },
    },
}

Config.Television = { -- furniture with a tv table (see config/furniture.lua) plays real video on its screen, every screen has its own channel
    enable = true, -- false | true
    requireUtilities = true, -- false | true, screens need power and internet, ignored when Config.Bills.enable is off
    renderDistance = 12.0, -- how close a player must be for the screen to draw (units)
    maxPlaying = 2, -- how many screens can play at once in one property, more than a couple of videos at a time slows the game down
    resolution = {w = 1280, h = 720}, -- browser size drawn onto the screen
    fullPageResolution = {w = 1280, h = 720}, -- browser size for a directly loaded web page, bigger is sharper but can crash the game on heavy live streams
    interactive = false, -- false | true, adds a "Browse screen" option that opens a browser window you can type and click in, the page follows on the TV
    defaultVolume = 50, -- 0-100
    audio = {
        spatial = true, -- false | true, the TV gets quieter the further away you stand, off = same volume anywhere inside renderDistance
        distance = 12.0, -- how far away the TV can still be heard (units)
    },
    allowCustomUrl = true, -- false | true, players can open any URL instead of picking a channel below, Twitch links are always refused (they crash the game)
    allowFullPage = true, -- false | true, loads web pages directly so any site works
    -- turn it off if untrusted players can reach a TV, many sites stay dark without it
    browser = {
        enable = true, -- false | true
        homepage = 'https://www.bing.com/search?q=news', -- Bing search pages open in the browser window, Google and Bing's own front page do not
        search = 'https://www.bing.com/search?q=%s', -- %s is what the player typed, add this host to allowedHosts if you set it
        zoom = 0.8, -- how zoomed the page on the TV is, 0.5-1.0, lower fits more of the page on the screen
        view = 'fullscreen', -- 'fullscreen' | 'tv', fullscreen covers the screen, tv puts the pointer on the TV prop itself
        screenInset = 0.08, -- how much of the prop's outline is bezel rather than picture, tv view only
    },
    allowedHosts = {}, -- restrict player-typed URLs to these hosts, e.g. {'youtube.com'}, empty = any host
    channels = { -- preset channels shown in the TV menu, a direct video file, HLS stream (.m3u8), YouTube link or any web page
        --[[{label = 'Lofi Radio', url = 'https://www.youtube.com/watch?v=rFZHOHl-L8A'},]]
    },
}

Config.WaterUse = { -- anim and water effect per use type, furniture can override it with waterUse
    shower = {
        anim = {
            dict = 'anim@mp_yacht@shower@male@',
            name = 'male_shower_idle_a',
        },
        ptfx = { -- offset / rot / scale are in the prop's local space
            asset = 'scr_carwash',
            name = 'ent_amb_car_wash_jet',
            offset = vec3(0.250, -0.300, 0.250),
            rot = vec3(-180.0, 0.0, -353.5),
            scale = 1.00,
        },
        duration = 10000, -- how long the use takes (ms)
        anchor = false, -- false | {offset = vec3, heading = n}, moves the ped onto the prop for the duration, for a tub
    },
    sink = {
        anim = {
            dict = 'missheist_agency3aig_23',
            name = 'urinal_sink_loop',
        },
        duration = 5000,
        ptfx = {
            asset = 'scr_carwash',
            name = 'ent_amb_car_wash_jet',
            offset = vec3(0.050, -0.140, 0.150),
            rot = vec3(-540.0, 0.0, -206.0),
            scale = 0.5,
        },
        anchor = false,
    },
    bathtub = {
        anim = {
            dict = 'timetable@jimmy@mics3_ig_15@',
            name = 'idle_a_jimmy',
            flag = 1,
        },
        duration = 10000,
        ptfx = {
            asset = 'scr_carwash',
            name = 'ent_amb_car_wash_jet',
            offset = vec3(0.010, 0.670, 0.820),
            rot = vec3(-140.0, 0.0, -216.5),
            scale = 0.7,
        },
        anchor = {
            offset = vec3(0.0, 0.0, 0.1),
            heading = 180.0,
        },
    },
}

-- scenario: game scenario the ped plays, brings its own equipment
-- anim / anims: clip played instead of a scenario, anims picks one at random
-- exitAnim: get-up clip, a kind without one just stands up
-- scene: plays anims as a synced scene on the prop, for clips that move around
-- freeze: pins the ped down, needed on a raised surface like a bed
-- hold: stay until the get-up key is pressed, false = can just walk away
-- attach: fix the ped to the piece and play the anim there
-- prop: item held while using, {model, bone, offset, rot}
-- stayPut: ped stays where they stand and only turns to face the piece
-- onFloor: ped stands on the floor beside the piece, not on it
-- slotRadius: how close another ped must be for a seat to count as taken
-- anchor: offset / heading / zOffset of the ped from the prop's box centre
-- seatWidth / seatSpacing / maxSeats: split a wide piece into several seats
-- on a sofa or bed the seats and facing come from the prop's own shape

Config.UsableFurniture = { -- one Config.FurnitureUses row per sit, lay or exercise kind
    enable = true,
    slotRadius = 0.45,
    sit = {
        anim = {
            dict = 'timetable@ron@ig_5_p3',
            name = 'ig_5_p3_base',
        },
        attach = {
            offset = vec3(0.0, 0.0, 0.0),
            rot = vec3(0.0, 0.0, 0.0),
        },
        anchor = {
            offset = vec3(0.0, -0.650, 0.0),
            heading = 180.0,
            zOffset = 0.550,
        },
        seatWidth = 0.62,
        seatSpacing = 0.62, -- units between two seats, centre to centre
        maxSeats = 3,
    },
    lay = {
        anim = {
            dict = 'anim@gangops@morgue@table@',
            name = 'ko_front',
        },
        exitAnim = {
            dict = 'get_up@sat_on_floor@to_stand',
            name = 'getup_0',
            duration = 2000,
        },
        freeze = true,
        anchor = {
            offset = vec3(0.0, 0.350, 0.0),
            heading = 180.0,
            zOffset = 0.0,
        },
        seatWidth = 0.75,
        seatSpacing = 0.6,
        maxSeats = 2,
    },
    yoga = {
        scenario = 'WORLD_HUMAN_YOGA',
        hold = false,
        anchor = {
            offset = vec3(0.0, 0.0, 0.0),
            heading = 90.0,
            zOffset = 0.0,
        },
        seatWidth = 0.6,
        seatSpacing = 0.6,
        maxSeats = 1,
    },
    bench = {
        anim = {
            dict = 'amb@prop_human_seat_muscle_bench_press@idle_a',
            name = 'idle_a',
        },
        attach = {
            offset = vec3(0.0, 0.0, 0.0),
            rot = vec3(0.0, 0.0, 0.0),
        },
        prop = {
            model = `prop_barbell_20kg`,
            bone = 28422,
            offset = vec3(0.0, 0.0, 0.0),
            rot = vec3(0.0, 0.0, 0.0),
        },
        hold = false,
        anchor = {
            offset = vec3(-0.014, -0.200, 0.000),
            heading = 180.0,
            zOffset = -0.24,
        },
        seatWidth = 0.6,
        seatSpacing = 0.6,
        maxSeats = 1,
    },
    pullups = {
        scenario = 'PROP_HUMAN_MUSCLE_CHIN_UPS',
        hold = false,
        onFloor = true,
        anchor = {
            offset = vec3(0.016, -0.148, 0.000),
            heading = 0.0,
            zOffset = 1.1,
        },
        seatWidth = 0.6,
        seatSpacing = 0.6,
        maxSeats = 1,
    },
    weights = {
        scenario = 'WORLD_HUMAN_MUSCLE_FREE_WEIGHTS',
        stayPut = true,
        hold = false,
        anchor = {
            offset = vec3(0.0, 0.0, 0.0),
            heading = 0.0,
            zOffset = 0.0,
        },
        seatWidth = 0.6,
        seatSpacing = 0.6,
        maxSeats = 1,
    },
    drink = {
        scenario = 'WORLD_HUMAN_DRINKING',
        stayPut = true,
        hold = false,
        anchor = {
            offset = vec3(0.0, 0.0, 0.0),
            heading = 0.0,
            zOffset = 0.0,
        },
        seatWidth = 0.6,
        seatSpacing = 0.6,
        maxSeats = 1,
    },
    poledance = { -- dance on the pole yourself, one of three club loops at random, a hired dancer (Config.Pole) uses this same anchor
        anims = {
            {
                dict = 'mini@strip_club@pole_dance@pole_dance1',
                name = 'pd_dance_01',
            },
            {
                dict = 'mini@strip_club@pole_dance@pole_dance2',
                name = 'pd_dance_02',
            },
            {
                dict = 'mini@strip_club@pole_dance@pole_dance3',
                name = 'pd_dance_03',
            },
        },
        scene = {
            offset = vec3(0.07, 0.3, 1.15),
            rate = 1.3,
        },
        anchor = {
            offset = vec3(-0.036, 0.001, 0.000),
            heading = 0.0,
            zOffset = 0.000,
        },
        seatWidth = 0.6,
        seatSpacing = 0.6,
        maxSeats = 1,
    },
}

Config.Mortgage = { -- pay part of the price now and the rest over time, a listing can set its own terms
    enabled = true, -- false | true
    paymentMode = 'manual', -- 'automatic' | 'manual', automatic = taken from the bank, manual = paid in the property menu
    onNonPayment = 'lock', -- 'repossess' | 'lock', repossess = property is put back on sale, lock = can't enter until paid up
    downPaymentPercent = 20, -- % of the price paid right away
    term = 24, -- how many payments the price is split into
    interval = 1000 * 60 * 60 * 24*7, -- how often a payment is due (1 week)
    interestRate = 5, -- % interest added on top of the loan, 0 = none
    gracePeriod = 3, -- how many missed payments before onNonPayment happens
    allowManualPayment = true, -- false | true, allow paying manually even when paymentMode = 'automatic'
    earlyPayoffInterestWaiver = 50, -- % of remaining interest skipped when paying off early
}

Config.Contracts = { -- a paper the player signs before a deal goes through, a realtor sale always has one
    enable = false, -- false | true, master switch for everything below
    purchase = true, -- false | true, buying a property outright
    rent = true, -- false | true, renting a property
    finance = true, -- false | true, buying with a mortgage
    motel = false, -- false | true, checking into a motel room
    sellBack = true, -- false | true, selling a property back
    transfer = true, -- false | true, handing a property to another player, both sign
    business = true, -- false | true, a realtor buying a property for their business
    timeout = 120, -- seconds the player has to sign before the paper expires
    sellerName = nil, -- seller name printed when nobody owns the property, nil = locale default
}

Config.StarterHousing = { -- new characters get an apartment in one shared building, units are made when needed and reused when freed
    enabled = false, -- false | true, set real building values below before enabling
    grantMode = 'free', -- 'free' | 'price' | 'rent' | 'mortgage', price = building.price, rent = building.rent, mortgage = Config.Mortgage with mortgageTerms
    chargePlayer = false, -- false | true, price mode: take building.price from the player, free if they can't afford it
    mortgageTerms = {downPaymentPercent = 0, term = 24, interestRate = 0}, -- mortgage mode terms, anything unset uses Config.Mortgage
    eligibility = {
        onlyNewCharacters = true, -- false | true, only brand-new characters qualify (ESX only)
        onlyIfNoProperty = false, -- false | true, skip players who already own a property
        oneTimePerIdentifier = true, -- false | true, each character only ever gets one, selling it won't grant another
        jobs = nil, -- list of allowed job names, e.g. {'unemployed'}, nil = any
        groups = nil, -- list of allowed permission groups, e.g. {'user'}, nil = any
    },
    ensureBuildingOnStart = false, -- false | true, create the building at resource start instead of on the first grant
    building = {
        entranceCoords = vec4(-270.99, -957.80, 30.22, 300.40), -- front door of the building
        houseInterior = 'k4_v16low1_shell', -- Config.HouseInteriors key used for every unit
        garageInterior = nil, -- Config.GarageInteriors key, nil = no garage
        name = 'Starter Apartments',
        address = 'Alta Street 305',
        image = nil, -- listing image url, nil = none
        price = 25000, -- used in price / mortgage modes, also the buy price of a freed unit
        rent = 500, -- used in rent mode, charged per Config.Rent.interval
    },
}

Config.Security = { -- one alarm per property bought up a ladder, plus a front-door camera and a reinforced lock
    enable = true, -- false | true
    requireUtilities = true, -- false | true, alarms and camera need power and internet, ignored when Config.Bills.enable is off
    alarms = {
        enable = true, -- false | true
        waypoint = true, -- false | true, keypress prompt on the alert to set a GPS waypoint
        alertStyle = 'banner', -- 'banner' | 'notify' | 'both', banner = on-screen, notify = notification
        audibleRange = 25.0, -- how far from the door the siren can be heard (units)
        sirenDuration = 20000, -- how long the siren keeps sounding (ms)
        sirenInterval = 900, -- how often the siren sound replays (ms)
        sounds = {
            siren = {name = 'Bell_02', set = 'ALARMS_SOUNDSET', bank = 'ALARM_BELL_02'}, -- looped at the door for sirenDuration, bank is optional and can be empty
        },
        tiers = { -- a grade only sets what the alarm notices, who gets alerted is per property, a type in no covers list is turned off
            {price = 2500,  label = 'Door alarm', covers = {'lockpick'}},
            {price = 6000,  label = 'Home alarm', covers = {'lockpick', 'entry'}},
            {price = 12000, label = 'Full alarm', covers = {'lockpick', 'entry', 'raid'}},
            {price = 18000, label = 'Vault alarm', covers = {'lockpick', 'entry', 'raid', 'storage'}},
        },
        dispatch = { -- add-on that also reports break-ins to your dispatch / phone resource, see ReportBreakIn
            enable = false, -- false | true
            price = 9000, -- one-time price, needs an alarm installed first
        },
    },
    camera = { -- front-door camera
        enable = true, -- false | true
        price = 8000, -- one-time price
        viewAccess = 'all', -- 'menu' | 'item' | 'command' | 'all', menu = from the door menu
        item = 'security_tablet', -- usable item that opens the feed
        command = 'frontdoorcam', -- command that opens the feed
        remote = true, -- false | true, true lists every property you can view a camera at, false = only the one you're in
        fov = 60.0,
        height = 2.0, -- camera mount height above the door (units)
    },
    lock = { -- reinforced front-door lock, each grade adds a pin to the lockpicking minigame
        enable = true, -- false | true
        tiers = {
            {price = 3000,  label = 'Reinforced lock'},
            {price = 7500,  label = 'Deadbolt lock'},
            {price = 15000, label = 'High-security lock'},
        },
    },
}

Config.IPLCustomization = { -- owners restyle an IPL interior from the door menu, only interiors with a customization list in config/interiors.lua
    enable = true, -- false | true
    charge = true, -- false | true, style changes cost money
    freeForOwner = false, -- false | true, the owner pays nothing, managers and keyholders still pay
    defaultPrice = 1000, -- price per changed group when the group sets no price
    account = 'bank', -- account the price is charged from
}

Config.RealEstateBusiness = { -- optional realtor business with its own grades, employees and money, separate from the framework's jobs
    enabled = true, -- false | true
    menuCommand = 'realestate', -- command that opens the business menu, false to disable
    menuItem = '', -- usable item that opens the business menu, '' = disabled
    requireDuty = false, -- false | true, creating and selling properties and using the office stash and wardrobe need being on duty
    adminCreator = true, -- false | true, admins can create and edit businesses from the admin panel

    selfPurchase = { -- players founding their own business
        enabled = false, -- false | true, when false businesses come from the admin panel or the list below
        price = 250000,
    },

    salary = { -- grade salaries paid from the business balance, skipped if it can't cover a cycle
        enabled = true, -- false | true
        interval = 1000 * 60 * 30, -- how often salaries are paid (30 minutes)
        onlineOnly = true, -- false | true, only pay employees who are online
    },

    caps = { -- limits a business boss can never go over
        maxGrades = 10,
        maxSalary = 5000, -- per cycle
        maxEmployees = 20,
        maxCommissionPercent = 30,
    },

    saleChannels = 'contract', -- 'instant' | 'contract' | 'both', instant = buy at the door, contract = a realtor writes a sale contract

    acquisition = { -- how businesses get the properties they sell
        mode = 'purchase', -- 'create' | 'purchase' | 'both', create = build stock with the property creator, purchase = buy unowned properties and resell them

        purchase = { -- purchase / both modes
            pricePercent = 100, -- % of the property price businesses pay, 80 = 20% discount
            sink = 'burn', -- 'burn' = purchase money is removed, or a business name below to credit it there
            playersCanBuyUnowned = true, -- false | true, false = only businesses can buy unowned properties
            holdingBills = false, -- false | true, business-owned properties get utility bills, paid from business funds
        },

        creation = { -- create / both modes, what a business pays to create properties
            cost = false, -- price per created unit, 0 or false = free
            perShell = { -- price per Config.HouseInteriors key, missing keys use cost
                -- ['k4_trailer1_shell'] = 75000,
            },
            owned = false, -- false | true, the business owns a property it creates instead of leaving it unowned stock
            -- motel rooms are always left unowned so guests can check in
        },
    },

    ownedProperties = { -- what members with the sellProperty flag can do in business-owned properties, the owner and editBusiness holders always can
        agentFurnish = true, -- false | true, furnish them
        agentStorage = true, -- false | true, open storages in them
        transfer = 'owner', -- 'owner' | 'manager' | false, who can transfer one to a player, manager = editBusiness holders too
    },

    office = { -- office points (blip / stash / wardrobe / boss menu), set in game from the business menu or the admin panel
        blip = {
            sprite = 475,
            color = 0,
            scale = 0.8,
            visibleToAll = false, -- false | true, false = only members see their own office blip
        },
        stash = {
            slots = 50,
            maxWeight = 100000,
        },
    },

    revenue = { -- every property a business created stays linked to it, its income is split like this
        initialSale = {businessPercent = 70, realtorPercent = 5, sink = 'burn'}, -- first sale of created stock, the realtor gets their cut on a contract sale, the rest goes to sink
        resale = {commissionPercent = 10, commissionFlat = 0, businessCutPercent = 70}, -- resale through a contract, the commission is taken from the sale price and split business / realtor
        sellBackCommission = false, -- false | true, also take the commission when selling back to the server
        allowBusinessOverride = false, -- false | true, businesses can change these splits from their menu, caps still apply
    },

    defaultGrades = { -- grades every new business starts with, new hires start at grade 0, flags: createProperty, buyProperty, sellProperty, manageEmployees, manageGrades, deposit, withdraw, editBusiness
        [0] = {
            label = 'Trainee',
            salary = 250,
            permissions = {},
        },
        [1] = {
            label = 'Realtor',
            salary = 500,
            permissions = {
                createProperty = true,
                sellProperty = true,
            },
        },
        [2] = {
            label = 'Manager',
            salary = 750,
            permissions = {
                createProperty = true,
                buyProperty = true,
                sellProperty = true,
                manageEmployees = true,
                deposit = true,
                withdraw = true,
            },
        },
        [3] = {
            label = 'Boss',
            salary = 1000,
            permissions = {
                createProperty = true,
                buyProperty = true,
                sellProperty = true,
                manageEmployees = true,
                manageGrades = true,
                deposit = true,
                withdraw = true,
                editBusiness = true,
            },
        },
    },

    businesses = { -- default businesses added once on startup by name, optional fields: owner, blip, stash, wardrobe, bossMenu, allowedTypes, maxProperties
        -- {
        --     name = 'dynasty8',
        --     label = 'Dynasty 8',
        --     owner = nil,
        --     blip = {coords = vec3(-712.62, 261.99, 84.14), sprite = 475, color = 2, scale = 0.8},
        --     stash = vec3(-716.68, 261.05, 84.14),
        --     wardrobe = vec3(-718.31, 257.34, 84.14),
        -- },
    },
}

```
