RX Scripts Logo
Housing

Configurables

All config & open sourced files included within this script.

Config Files

--[[
BY RX Scripts © rxscripts.xyz
--]]

Config = {}

Config.Locale = 'en'

Config.SaveInterval = 10              -- In Minutes
Config.DefaultRoutingBucket = 0
Config.UseMoney = 'bank'              -- Money account used for payments
Config.PostMessageCooldown = 300      -- In Seconds, cooldown on sending post messages to a house
Config.MaxHouses = 5                  -- Amount of houses a player can own
Config.SellReturn = 0.5               -- Percentage of the house price you get back when selling (0 - 1)
Config.LaptopProp = 'prop_laptop_01a' -- Prop used for the laptop
Config.UseStashUpgrade = true         -- Set to false if you want to disable the stash upgrades (for if ur using an inventory that doesnt offer this feature)
Config.LockUnsoldPropertyDoors = true -- Lock the doors of unsold properties

Config.Ringing = {
    refreshCooldown = 3000, -- In MS
    canOpenStash = true,    -- Can the player open the stash whilst being let in by ringing the doorbell?
    canOpenWardrobe = true, -- Can the player open the wardrobe whilst being let in by ringing the doorbell?
}

Config.Keyholders = {
    maxKeyholders = 5,      -- false to make unlimited
    canOpenStash = true,    -- Can the player open the stash as a keyholder?
    canOpenWardrobe = true, -- Can the player open the wardrobe as a keyholder?
}

Config.BreakIn = {
    enabled = true,
    canOpenStash = true,
    canOpenWardrobe = true,
    minimumCops = 0,
    copsJob = 'police',
    requiredItem = 'lockpick',
}

Config.Raid = {
    enabled = true,
    canOpenStash = true,
    canOpenWardrobe = true,
    requiredItem = 'phone',
    allowedJobs = { -- Minimum grade required
        { job = "police", grade = 0 },
    }
}

Config.Blips = {
    ownedPropertyOwner = { -- When you have the key to the property
        enabled = true,
        color = 26,
        sprite = 40,
        scale = 0.8,
        display = 4,
    },
    ownedPropertyStranger = { -- When you don't own the property
        enabled = true,
        color = 62,
        sprite = 40,
        scale = 0.8,
        display = 4,
    },
    unownedProperty = { -- When the property is unowned
        enabled = true,
        color = 0,
        sprite = 40,
        scale = 0.8,
        display = 4,
    },
}

Config.StashGrades = {
    { -- DEFAULT GRADE
        price = 0,
        weight = 150000,
        slots = 10,
    },
    {
        price = 25000,
        weight = 300000,
        slots = 20,
    },
    {
        price = 50000,
        weight = 500000,
        slots = 30,
    }
}

Config.ShellsZ = -50 -- Z axis for shells

Config.MLODoorAutoClose = {
    enabled = true, -- Enable automatic door closing for MLO properties
    closeTime = 5000 -- Time in milliseconds before door auto-closes (5 seconds)
}

Config.Commands = {
    enterfix = 'housing:enterfix', -- Used to tp back into the property if fallen through the map
    admin = 'housing:admin',       -- Opens the admin panel
}

--[[
    IMPORTANT: Changing these keybinds only affects NEW players!
    Once a player joins the server, these keybinds are saved to their FiveM settings.
    Existing players must either:
    - Clear their FiveM cache, or
    - Manually change the keybinds in their FiveM Settings > Key Bindings > FiveM
--]]
Config.PlacementKeys = {
    moveMode = '1',        -- Switch to position adjustment
    turnMode = '2',        -- Switch to rotation adjustment
    coordinateMode = '3',  -- Switch between global/local axis
    groundAlign = 'LMENU', -- Align object with ground (Alt key)
    confirm = 'RETURN',    -- Finish and save position (Enter)
    cancel = 'BACK',       -- Discard changes (Backspace)
}

-- Placement Camera Settings
Config.PlacementCamera = {
    enabled = true,    -- Enable orbital camera during placement
    hidePlayer = true, -- Hide player during placement for better visibility
}

--[[
    YOU CAN USE ACE PERMISSIONS TO ALLOW CERTAIN PLAYERS/GROUPS TO ACCESS THE ADMIN HOUSING PANEL
    EXAMPLE:
        add_ace group.admin housing allow
        add_ace identifier.fivem:1432744 housing allow #Rejox

    OR YOU CAN USE THE STAFF GROUPS/JOBS BELOW
--]]
Config.AllowedToCreate = {
    jobs = {
        -- 'real_estate_agent',
    },
    groups = {
        'admin',
        'superadmin',
    }
}

Config.GlobalPropertyNPC = {
    enabled = true,                                        -- Enable/disable the global property NPC
    withImages = true,                                     -- Set to false if you don't want to use images for the properties
    maxPrice = 1000000,                                    -- Maximum price for the price slider filter
    ped = {
        model = 'a_m_y_business_03',                       -- NPC model
        coords = vector4(-268.54, -957.31, 31.22, 205.89), -- NPC location (x, y, z, heading)
        scenario = 'WORLD_HUMAN_CLIPBOARD',                -- Animation scenario
    },
    blip = {
        enabled = true,
        sprite = 375,
        color = 2,
        scale = 0.8,
        label = 'Real Estate Agent',
    },
}

Config.UI = {
    color = {
        primary = { -- Different shades of primary color
            [50] = "#FEDDE9",
            [100] = "#FCBAD3",
            [200] = "#FA76A6",
            [300] = "#F7317A",
            [400] = "#D80955",
            [500] = "#95063B",
            [600] = "#76052E",
            [700] = "#580423",
            [800] = "#3B0217",
            [900] = "#1D010C",
            [950] = "#0F0106"
        },
    }
}

--[[
    ONLY CHANGE THIS PART IF YOU HAVE RENAMED SCRIPTS SUCH AS FRAMEWORK, TARGET, INVENTORY ETC
    RENAME THE SCRIPT NAME TO THE NEW NAME
--]]
---@type table Only change these if you have changed the name of a resource
Resources = {
    FM = { name = 'fmLib', export = 'new' },
    SCREENBASIC = { name = 'screenshot-basic', export = 'all' },
    OXTarget = { name = 'ox_target', export = 'all' },
    QBTarget = { name = 'qb-target', export = 'all' },
}
IgnoreScriptFoundLogs = false
ShowDebugPrints = false

--[[
    DON'T TOUCH ANYTHING BELOW HERE
    ADDING IPLS OR SHELLS SHOULD BE DONE IN config/ipls/ or config/shells/
--]]
Config.Shells = {}
Config.IPLs = {}

Opensource Files

All script-related open source code is contained within these files. Third-party components, including frameworks, inventory systems, and other external code, are separately maintained & open sourced in our fmLib repository.
--[[
BY RX Scripts © rxscripts.xyz
--]]

FM.callback.register('rxhousing:takeScreenshot', function(presignedUrl)
    local p = promise.new()

    if SCREENBASIC then
        SCREENBASIC:requestScreenshotUpload(presignedUrl, 'file', function(data)
            local resp = json.decode(data)
            if resp then
                p:resolve(resp)
            else
                p:resolve(false)
            end
        end)
    else
        p:resolve(false)
    end

    return Citizen.Await(p)
end)

function IsDoorModel(model)
    -- Common door model hashes
    local doorModels = {
        [`prop_com_ls_door_01`] = true,
        [`prop_com_ld_door_01`] = true,
        [`v_ilev_ra_door4l`] = true,
        [`v_ilev_ra_door4r`] = true,
        [`prop_ld_garaged_01`] = true,
        [`prop_cs6_03_door_l`] = true,
        [`prop_cs6_03_door_r`] = true,
        [`prop_magenta_door`] = true,
        [`v_ilev_roc_door4`] = true,
        [`v_ilev_epsstoredoor`] = true,
        [`prop_ss1_14_garage_door`] = true,
        [`prop_fnclink_03gate1`] = true,
        [`prop_facgate_08`] = true,
        [`v_ilev_mm_doorm_l`] = true,
        [`v_ilev_mm_doorm_r`] = true,
        [`prop_bh1_48_gate_1`] = true,
        [`prop_gate_prison_01`] = true,
        [`p_cut_door_01`] = true,
        [`prop_com_gar_door_01`] = true,
        [`prop_sec_gate_01c`] = true,
        [`prop_sec_gate_01d`] = true,
    }

    -- Check if model name contains 'door' or 'gate'
    local modelName = tostring(model):lower()
    if modelName:find('door') or modelName:find('gate') then
        return true
    end

    return doorModels[model] == true
end

function StartGizmoPlacing(obj, propertyId)
    -- Use our new placement system
    return PlaceFurniture(obj, propertyId)
end

function ShowMarker(type, coords)
    if type == 'laptop' or type == 'door' or type == 'stash' or type == 'clothing' then
        DrawMarker(2, coords, 0, 0, 0, 0, 180.0, 0, 0.3, 0.3, 0.3, 204, 0, 102, 100, false, false, 2, true, false, false, false)
    elseif type == 'entranceForSale' then
        DrawMarker(29, coords, 0, 0, 0, 0, 180.0, 0, 0.5, 0.5, 0.5, 255, 204, 0, 100, false, false, 2, true, false, false, false)
    elseif type == 'entranceHasKey' or type == 'entranceNoKey' then
        local rgb = type == 'entranceHasKey' and { 4, 107, 200 } or { 217, 217, 217 }
        DrawMarker(1, coords, 0, 0, 0, 0, 0, 0, 2.0, 2.0, 0.5, rgb[1], rgb[2], rgb[3], 100, false, false, 2, false, false, false, false)
    elseif type == 'storeVehicle' then
        DrawMarker(1, coords, 0, 0, 0, 0, 0, 0, 3.0, 3.0, 0.5, 4, 107, 200, 100, false, false, 2, false, false, false, false)
    elseif type == 'takeVehicle' then
        DrawMarker(36, coords, 0, 0, 0, 0, 0, 0, 1.0, 1.0, 1.0, 4, 107, 200, 100, false, false, 2, true, false, false, false)
    end
end

function BreakInMinigame()
    TaskStartScenarioInPlace(PlayerPedId(), "WORLD_HUMAN_STAND_MOBILE", 0, true)

    local result = lib.skillCheck({'easy', 'easy', 'easy'}, {'w', 'a', 's', 'd'})

    ClearPedTasks(PlayerPedId())
    return result
end

function RaidMinigame()
    TaskStartScenarioInPlace(PlayerPedId(), "WORLD_HUMAN_STAND_MOBILE", 0, true)

    local result = lib.skillCheck({'easy', 'easy', 'easy'}, {'w', 'a', 's', 'd'})

    ClearPedTasks(PlayerPedId())
    return result
end

function ShowHelpNotification(msg, thisFrame, beep, duration)
    AddTextEntry('helpNotification', msg)

    if thisFrame then
        DisplayHelpTextThisFrame('helpNotification', false)
    else
        if beep == nil then
            beep = true
        end
        BeginTextCommandDisplayHelp('helpNotification')
        EndTextCommandDisplayHelp(0, false, beep, duration or -1)
    end
end

function AddFurnitureTarget(object, furnitureId, propertyId, furnitureData)
    if not object or not DoesEntityExist(object) then return end

    local property = Houses[propertyId]
    if not property then return end

    local identifier = FM.player.getIdentifier()
    local isOwner = property.owner == identifier
    local status = GetPlayerPropertyStatus(propertyId)

    if OXTarget then
        local options = {}

        if furnitureData then
            if furnitureData.isStash and CanOpenStash(property, status.isBreakIn, status.rung, status.isRaid) then
                table.insert(options, {
                    label = 'Open Stash',
                    name = 'open_stash_' .. furnitureId,
                    icon = 'fas fa-box',
                    distance = 2.5,
                    onSelect = function(data)
                        FM.callback.async('rxhousing:getFurnitureStashId', function(stashData)
                            if stashData then
                                FM.inventory.openStash(stashData.stashId, property.owner, stashData.weight, stashData.slots)
                            end
                        end, furnitureId, propertyId)
                    end,
                })
            end

            if furnitureData.isWardrobe and CanOpenWardrobe(property, status.isBreakIn, status.rung, status.isRaid) then
                table.insert(options, {
                    label = 'Open Wardrobe',
                    name = 'open_wardrobe_' .. furnitureId,
                    icon = 'fas fa-tshirt',
                    distance = 2.5,
                    onSelect = function(data)
                        FM.player.openWardrobe(propertyId)
                    end,
                })
            end

            if furnitureData.isComputer and isOwner then
                table.insert(options, {
                    label = 'Manage Property',
                    name = 'open_computer_' .. furnitureId,
                    icon = 'fas fa-desktop',
                    distance = 2.5,
                    onSelect = function(data)
                        OpenPropertyMenu(property, true, true, nil)
                    end,
                })
            end
        end

        if isOwner then
            table.insert(options, {
                label = 'Reposition',
                name = 'reposition_furniture_' .. furnitureId,
                icon = 'fas fa-arrows-alt',
                distance = 2.5,
                onSelect = function(data)
                    FM.callback.async('rxhousing:getFurnitureById', function(furniture)
                        if furniture then
                            TriggerEvent('rxhousing:startFurnitureReposition', {
                                propertyId = propertyId,
                                furnitureId = furnitureId,
                                model = furniture.model
                            })
                        end
                    end, furnitureId)
                end,
            })

            table.insert(options, {
                label = 'Pick Up',
                name = 'pickup_furniture_' .. furnitureId,
                icon = 'fas fa-box',
                distance = 2.5,
                onSelect = function(data)
                    FM.callback.async('rxhousing:pickupFurniture', function(success, err)
                        if success then
                            FM.utils.notify(_L("furniture_picked_up"), "success")
                            RemoveFurnitureTarget(furnitureId)
                        else
                            FM.utils.notify(err or _L("unknown_error"), "error")
                        end
                    end, {
                        propertyId = propertyId,
                        furnitureId = furnitureId
                    })
                end,
            })
        end

        if #options > 0 then
            OXTarget:addLocalEntity(object, options)
        end

    elseif QBTarget then
        local options = {}

        if furnitureData then
            if furnitureData.isStash and CanOpenStash(property, status.isBreakIn, status.rung, status.isRaid) then
                table.insert(options, {
                    label = 'Open Stash',
                    icon = 'fas fa-box',
                    targeticon = 'fas fa-box',
                    action = function(entity)
                        FM.callback.async('rxhousing:getFurnitureStashId', function(stashData)
                            if stashData then
                                FM.inventory.openStash(stashData.stashId, property.owner, stashData.weight, stashData.slots)
                            end
                        end, furnitureId, propertyId)
                    end,
                })
            end

            if furnitureData.isWardrobe and CanOpenWardrobe(property, status.isBreakIn, status.rung, status.isRaid) then
                table.insert(options, {
                    label = 'Open Wardrobe',
                    icon = 'fas fa-tshirt',
                    targeticon = 'fas fa-tshirt',
                    action = function(entity)
                        FM.player.openWardrobe(propertyId)
                    end,
                })
            end

            if furnitureData.isComputer and isOwner then
                table.insert(options, {
                    label = 'Manage Property',
                    icon = 'fas fa-desktop',
                    targeticon = 'fas fa-desktop',
                    action = function(entity)
                        OpenPropertyMenu(property, true, true, nil)
                    end,
                })
            end
        end

        if isOwner then
            table.insert(options, {
                label = 'Reposition',
                icon = 'fas fa-arrows-alt',
                targeticon = 'fas fa-arrows-alt',
                action = function(entity)
                    FM.callback.async('rxhousing:getFurnitureById', function(furniture)
                        if furniture then
                            TriggerEvent('rxhousing:startFurnitureReposition', {
                                propertyId = propertyId,
                                furnitureId = furnitureId,
                                model = furniture.model
                            })
                        end
                    end, furnitureId)
                end,
            })

            table.insert(options, {
                label = 'Pick Up',
                icon = 'fas fa-box',
                targeticon = 'fas fa-box',
                action = function(entity)
                    FM.callback.async('rxhousing:pickupFurniture', function(success, err)
                        if success then
                            FM.utils.notify(_L("furniture_picked_up"), "success")
                            RemoveFurnitureTarget(furnitureId)
                        else
                            FM.utils.notify(err or _L("unknown_error"), "error")
                        end
                    end, {
                        propertyId = propertyId,
                        furnitureId = furnitureId
                    })
                end,
            })
        end

        if #options > 0 then
            QBTarget:AddTargetEntity(object, {
                options = options,
                distance = 2.5,
            })
        end
    end
end

function RemoveFurnitureTarget(furnitureId)
    local object = SpawnedFurniture[furnitureId]
    if not object or not DoesEntityExist(object) then return end

    if OXTarget then
        OXTarget:removeLocalEntity(object, {
            'reposition_furniture_' .. furnitureId,
            'pickup_furniture_' .. furnitureId,
            'open_stash_' .. furnitureId,
            'open_wardrobe_' .. furnitureId,
            'open_computer_' .. furnitureId
        })
    elseif QBTarget then
        QBTarget:RemoveTargetEntity(object)
    end
end

local MLODoorTargets = {}
local MLODoorEntityTargets = {} -- Track targets on actual door entities

-- Add target to a specific door entity (handles double doors)
function AddMLODoorTarget(doorEntity, propertyId, doorIndex, forceRefresh)
    if not OXTarget and not QBTarget then return end

    local property = Houses[propertyId]
    if not property then return end

    -- Handle double doors (array of entities)
    if type(doorEntity) == "table" then
        for _, entity in ipairs(doorEntity) do
            if entity and DoesEntityExist(entity) then
                local entityId = tostring(entity)
                -- Remove existing target first if refresh is forced or it already exists
                if forceRefresh or MLODoorEntityTargets[entityId] then
                    if OXTarget then
                        OXTarget:removeLocalEntity(entity)
                    elseif QBTarget then
                        QBTarget:RemoveTargetEntity(entity)
                    end
                    MLODoorEntityTargets[entityId] = nil
                end

                if not MLODoorEntityTargets[entityId] then
                    if OXTarget then
                        OXTarget:addLocalEntity(entity, {
                            {
                                label = "Open " .. property.label,
                                name = 'open_mlo_door_' .. propertyId .. '_' .. doorIndex,
                                icon = 'fas fa-door-open',
                                distance = 2.0,
                                onSelect = function()
                                    -- Re-fetch property data to ensure it's current
                                    local currentProperty = Houses[propertyId]
                                    if currentProperty then
                                        OpenPropertyMenu(currentProperty, false, false, doorIndex)
                                    end
                                end
                            }
                        })
                        MLODoorEntityTargets[entityId] = true
                    elseif QBTarget then
                        QBTarget:AddTargetEntity(entity, {
                            options = {
                                {
                                    label = "Open " .. property.label,
                                    icon = 'fas fa-door-open',
                                    action = function()
                                        -- Re-fetch property data to ensure it's current
                                        local currentProperty = Houses[propertyId]
                                        if currentProperty then
                                            OpenPropertyMenu(currentProperty, false, false, doorIndex)
                                        end
                                    end
                                }
                            },
                            distance = 2.0
                        })
                        MLODoorEntityTargets[entityId] = true
                    end
                end
            end
        end
    else
        -- Single door
        if not doorEntity or not DoesEntityExist(doorEntity) then return end

        local entityId = tostring(doorEntity)
        -- Remove existing target first if refresh is forced or it already exists
        if forceRefresh or MLODoorEntityTargets[entityId] then
            if OXTarget then
                OXTarget:removeLocalEntity(doorEntity)
            elseif QBTarget then
                QBTarget:RemoveTargetEntity(doorEntity)
            end
            MLODoorEntityTargets[entityId] = nil
        end

        if not MLODoorEntityTargets[entityId] then
            if OXTarget then
                OXTarget:addLocalEntity(doorEntity, {
                    {
                        label = "Open " .. property.label,
                        name = 'open_mlo_door_' .. propertyId .. '_' .. doorIndex,
                        icon = 'fas fa-door-open',
                        distance = 2.0,
                        onSelect = function()
                            -- Re-fetch property data to ensure it's current
                            local currentProperty = Houses[propertyId]
                            if currentProperty then
                                OpenPropertyMenu(currentProperty, false, false, doorIndex)
                            end
                        end
                    }
                })
                MLODoorEntityTargets[entityId] = true
            elseif QBTarget then
                QBTarget:AddTargetEntity(doorEntity, {
                    options = {
                        {
                            label = "Open " .. property.label,
                            icon = 'fas fa-door-open',
                            action = function()
                                -- Re-fetch property data to ensure it's current
                                local currentProperty = Houses[propertyId]
                                if currentProperty then
                                    OpenPropertyMenu(currentProperty, false, false, doorIndex)
                                end
                            end
                        }
                    },
                    distance = 2.0
                })
                MLODoorEntityTargets[entityId] = true
            end
        end
    end
end

-- Property NPC Target Functions
function AddPropertyNPCTarget(npcEntity)
    if not OXTarget and not QBTarget then return end
    if not npcEntity or not DoesEntityExist(npcEntity) then return end

    if OXTarget then
        OXTarget:addLocalEntity(npcEntity, {
            {
                label = 'Browse Properties',
                name = 'property_npc_browse',
                icon = 'fas fa-home',
                distance = 2.5,
                onSelect = function()
                    OpenPropertyNPCMenu()
                end
            }
        })
    elseif QBTarget then
        QBTarget:AddTargetEntity(npcEntity, {
            options = {
                {
                    action = function()
                        OpenPropertyNPCMenu()
                    end,
                    icon = "fas fa-home",
                    label = 'Browse Properties',
                }
            },
            distance = 2.5
        })
    end
end

function RemovePropertyNPCTarget(npcEntity)
    if not OXTarget and not QBTarget then return end
    if not npcEntity or not DoesEntityExist(npcEntity) then return end

    if OXTarget then
        OXTarget:removeLocalEntity(npcEntity, 'property_npc_browse')
    elseif QBTarget then
        QBTarget:RemoveTargetEntity(npcEntity)
    end
end

-- Remove target from a specific door entity (handles double doors)
function RemoveMLODoorTarget(doorEntity)
    if not doorEntity then return end

    -- Handle double doors (array of entities)
    if type(doorEntity) == "table" then
        for _, entity in ipairs(doorEntity) do
            if entity then
                local entityId = tostring(entity)
                if MLODoorEntityTargets[entityId] then
                    if OXTarget then
                        OXTarget:removeLocalEntity(entity)
                    elseif QBTarget then
                        QBTarget:RemoveTargetEntity(entity)
                    end
                    MLODoorEntityTargets[entityId] = nil
                end
            end
        end
    else
        -- Single door
        local entityId = tostring(doorEntity)
        if not MLODoorEntityTargets[entityId] then return end

        if OXTarget then
            OXTarget:removeLocalEntity(doorEntity)
        elseif QBTarget then
            QBTarget:RemoveTargetEntity(doorEntity)
        end

        MLODoorEntityTargets[entityId] = nil
    end
end


function RemoveMLODoorTargets()
    if OXTarget then
        for zoneName, zoneId in pairs(MLODoorTargets) do
            OXTarget:removeZone(zoneId)
        end
    elseif QBTarget then
        for _, zoneName in ipairs(MLODoorTargets) do
            QBTarget:RemoveZone(zoneName)
        end
    end

    MLODoorTargets = {}
end

-- Refresh MLO door targets for a specific property
function RefreshMLODoorTargets(propertyId)
    if not propertyId then return end

    local property = Houses[propertyId]
    if not property or property.propertyType ~= 'mlo' then return end

    -- Find all tracked door entities for this property
    if TrackedDoorEntities and TrackedDoorEntities[propertyId] then
        for doorIndex, doorEntity in pairs(TrackedDoorEntities[propertyId]) do
            -- Force refresh the target with updated property data
            AddMLODoorTarget(doorEntity, propertyId, doorIndex, true)
        end
    end
end