504 lines
18 KiB
Lua
504 lines
18 KiB
Lua
--[[
|
|
Description:
|
|
Locations & interactions main file
|
|
|
|
Global Namespace:
|
|
Locations.Client
|
|
|
|
Globals:
|
|
Locations.Client.GetLocations
|
|
Locations.Client.GetLocationById
|
|
Locations.Client.GetAllInteractionData
|
|
Locations.Client.RemoveAllInteractions
|
|
Locations.Client.RemoveLocationInteractions
|
|
Locations.Client.RemoveInteractionType
|
|
Locations.Client.RecreateInteractionType
|
|
Locations.Client.RecreateInteractionTypeFromData
|
|
Locations.Client.RecreatePermissionRestrictedInteractions
|
|
Locations.Client.RefreshEmployeeStatusCache
|
|
Locations.Client.IsEmployeeAtLocation
|
|
Locations.Client.HasShowroomAccess
|
|
Locations.Client.CreateAllInteractions
|
|
Locations.Client.CreateLocationInteractions
|
|
Locations.Client.AddLocation
|
|
Locations.Client.UpdateLocation
|
|
Locations.Client.DeleteLocation
|
|
|
|
Exports:
|
|
N/A
|
|
]]--
|
|
|
|
Locations = Locations or {}
|
|
Locations.Client = Locations.Client or {}
|
|
|
|
local cachedLocations = {}
|
|
local locationInteractions = {}
|
|
local employeeStatusCache = {} ---@type table<string, boolean> locationId -> isEmployee
|
|
local showroomPermissionCache = {} ---@type table<string, boolean> locationId -> canAccessShowroom
|
|
local isCreatingInteractions = false -- Mutex to prevent concurrent interaction creation
|
|
local hasInitiallyLoaded = false -- Flag to track if initial CreateAllInteractions has completed
|
|
|
|
---@class InteractionTypeConfig
|
|
---@field getKeybind fun(): integer
|
|
---@field getPrompt fun(): string
|
|
---@field condition fun(location: Location): boolean
|
|
---@field canInteract? fun(location: Location): boolean
|
|
---@field hasPermissionRestriction boolean
|
|
---@field callback fun(location: Location): nil
|
|
|
|
---@type table<string, InteractionTypeConfig>
|
|
local interactionTypeConfigs = {
|
|
showroom_coords = {
|
|
getKeybind = function() return Config.OpenShowroomKeyBind end,
|
|
getPrompt = function() return Config.OpenShowroomPrompt end,
|
|
condition = function() return true end,
|
|
canInteract = function(location)
|
|
return showroomPermissionCache[location.id] ~= false
|
|
end,
|
|
hasPermissionRestriction = true,
|
|
callback = function(location) Showroom.Client.Open(location.id) end
|
|
},
|
|
management_coords = {
|
|
getKeybind = function() return Config.OpenManagementKeyBind end,
|
|
getPrompt = function() return Config.OpenManagementPrompt end,
|
|
condition = function(location) return location.type == "owned" end,
|
|
canInteract = function(location)
|
|
return employeeStatusCache[location.id] == true
|
|
end,
|
|
hasPermissionRestriction = true,
|
|
callback = function(location) DealershipManagement.Client.Open(location.id) end
|
|
},
|
|
sell_vehicle_coords = {
|
|
getKeybind = function() return Config.SellVehicleKeyBind end,
|
|
getPrompt = function() return Config.SellVehiclePrompt end,
|
|
condition = function(location) return location.enable_sell_vehicle end,
|
|
canInteract = function(location)
|
|
return showroomPermissionCache[location.id] ~= false
|
|
end,
|
|
hasPermissionRestriction = true,
|
|
callback = function(location) SellVehicle.Client.Request(location.id) end
|
|
}
|
|
}
|
|
|
|
---Check if player is an employee at a location (from cache)
|
|
---@param locationId string
|
|
---@return boolean
|
|
function Locations.Client.IsEmployeeAtLocation(locationId)
|
|
return employeeStatusCache[locationId] == true
|
|
end
|
|
|
|
---Check if player has showroom access at a location (from cache)
|
|
---@param locationId string
|
|
---@return boolean
|
|
function Locations.Client.HasShowroomAccess(locationId)
|
|
return showroomPermissionCache[locationId] ~= false
|
|
end
|
|
|
|
---Refresh the employee status cache for all locations
|
|
---If forceServerCheck is true, makes server calls to refresh permissions (used when job changes)
|
|
---Otherwise uses the permissions already in the location data
|
|
---@param locations? Location[]
|
|
---@param forceServerCheck? boolean
|
|
function Locations.Client.RefreshEmployeeStatusCache(locations, forceServerCheck)
|
|
locations = locations or cachedLocations or {}
|
|
|
|
for _, location in pairs(locations) do
|
|
Locations.Client.RefreshSingleLocationCache(location, forceServerCheck)
|
|
end
|
|
end
|
|
|
|
---Refresh the employee status cache for a single location
|
|
---@param location Location
|
|
---@param forceServerCheck? boolean
|
|
function Locations.Client.RefreshSingleLocationCache(location, forceServerCheck)
|
|
if not location then return end
|
|
|
|
if forceServerCheck then
|
|
-- Make server calls to refresh permissions (job/gang changed)
|
|
local showroomAllowed = lib.callback.await("jg-dealerships:server:check-showroom-whitelist", false, location.id)
|
|
showroomPermissionCache[location.id] = showroomAllowed
|
|
|
|
if location.type == "owned" then
|
|
local result = lib.callback.await("jg-dealerships:server:is-employee", false, location.id, false)
|
|
employeeStatusCache[location.id] = result and result.isEmployee or false
|
|
end
|
|
else
|
|
-- Use permissions from location data (already calculated on server)
|
|
showroomPermissionCache[location.id] = location.playerCanAccessShowroom ~= false
|
|
employeeStatusCache[location.id] = location.playerIsEmployee == true
|
|
end
|
|
DebugPrint("[RefreshCache] Location: " .. location.id .. " (" .. location.name .. ") showroom: " .. tostring(showroomPermissionCache[location.id]) .. ", employee: " .. tostring(employeeStatusCache[location.id]), "debug")
|
|
end
|
|
|
|
---@param location Location
|
|
---@param interactionType string
|
|
---@param customCoords? table[] Optional custom coordinates to use instead of location data
|
|
---@return boolean success Whether the interaction was created
|
|
local function createInteraction(location, interactionType, customCoords)
|
|
if not location then return false end
|
|
local config = interactionTypeConfigs[interactionType]
|
|
if not config then return false end
|
|
if not config.condition(location) then return false end
|
|
|
|
local coords = customCoords or location[interactionType]
|
|
if not coords or (type(coords) == "table" and #coords == 0) then return false end
|
|
|
|
if not locationInteractions[location.id] then
|
|
locationInteractions[location.id] = {}
|
|
end
|
|
|
|
if locationInteractions[location.id][interactionType] then
|
|
Interactions.Client.Remove(locationInteractions[location.id][interactionType])
|
|
end
|
|
|
|
local canInteract = config.canInteract and function() return config.canInteract(location) end or nil
|
|
|
|
local blipName = location.name
|
|
if Config.BlipNameFormat then
|
|
blipName = Config.BlipNameFormat:format(location.name)
|
|
end
|
|
|
|
locationInteractions[location.id][interactionType] = Interactions.Client.Create(
|
|
coords,
|
|
config.getKeybind(),
|
|
config.getPrompt(),
|
|
function() config.callback(location) end,
|
|
blipName,
|
|
canInteract
|
|
)
|
|
|
|
return true
|
|
end
|
|
|
|
---@return table locationInteractions
|
|
function Locations.Client.GetAllInteractionData()
|
|
return locationInteractions
|
|
end
|
|
|
|
---@param ignoreCache? boolean
|
|
---@return Location[] locations
|
|
function Locations.Client.GetLocations(ignoreCache)
|
|
if cachedLocations and #cachedLocations > 0 and not ignoreCache then
|
|
return cachedLocations
|
|
end
|
|
|
|
cachedLocations = lib.callback.await("jg-dealerships:server:get-all-locations", false)
|
|
return cachedLocations
|
|
end
|
|
|
|
---@param id string dealership IdD
|
|
---@return Location|false location
|
|
function Locations.Client.GetLocationById(id)
|
|
local locations = Locations.Client.GetLocations()
|
|
for _, location in ipairs(locations) do
|
|
if location.id == id then
|
|
return location
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function Locations.Client.RemoveAllInteractions()
|
|
for _, interactionTypes in pairs(locationInteractions) do
|
|
for _, interactions in pairs(interactionTypes) do
|
|
Interactions.Client.Remove(interactions)
|
|
end
|
|
end
|
|
locationInteractions = {}
|
|
hasInitiallyLoaded = false
|
|
end
|
|
|
|
---Remove all interactions for a specific location
|
|
---@param locationId string
|
|
function Locations.Client.RemoveLocationInteractions(locationId)
|
|
if not locationId then return end
|
|
if not locationInteractions[locationId] then return end
|
|
|
|
for _, interactions in pairs(locationInteractions[locationId]) do
|
|
Interactions.Client.Remove(interactions)
|
|
end
|
|
locationInteractions[locationId] = nil
|
|
end
|
|
|
|
---Remove specific interaction type for a location
|
|
---@param locationId string
|
|
---@param interactionType string
|
|
function Locations.Client.RemoveInteractionType(locationId, interactionType)
|
|
if not locationId or not interactionType then return end
|
|
if not locationInteractions[locationId] then return end
|
|
if not locationInteractions[locationId][interactionType] then return end
|
|
|
|
Interactions.Client.Remove(locationInteractions[locationId][interactionType])
|
|
locationInteractions[locationId][interactionType] = nil
|
|
end
|
|
|
|
---Re-create specific interaction type for a location
|
|
---@param locationId string
|
|
---@param interactionType string
|
|
function Locations.Client.RecreateInteractionType(locationId, interactionType)
|
|
if not locationId or not interactionType then return end
|
|
|
|
local location = Locations.Client.GetLocationById(locationId)
|
|
if not location then return end
|
|
|
|
createInteraction(location, interactionType)
|
|
end
|
|
|
|
---Re-create specific interaction type for a location using provided interactions data
|
|
---@param locationId string
|
|
---@param interactionType string
|
|
---@param interactions table[] Array of interaction definitions from the UI
|
|
function Locations.Client.RecreateInteractionTypeFromData(locationId, interactionType, interactions)
|
|
if not locationId or not interactionType then return end
|
|
if not interactions or #interactions == 0 then return end
|
|
|
|
local location = Locations.Client.GetLocationById(locationId)
|
|
if not location then return end
|
|
|
|
createInteraction(location, interactionType, interactions)
|
|
end
|
|
|
|
---Recreate all interactions that have permission restrictions (e.g. job-based)
|
|
---Called when player job changes to update visibility
|
|
function Locations.Client.RecreatePermissionRestrictedInteractions()
|
|
-- Skip if initial load hasn't completed yet - CreateAllInteractions will handle everything
|
|
if not hasInitiallyLoaded then
|
|
DebugPrint("[RecreatePermissionRestrictedInteractions] Skipping - initial load not complete", "debug")
|
|
return
|
|
end
|
|
|
|
-- Prevent concurrent interaction creation
|
|
if isCreatingInteractions then
|
|
DebugPrint("[RecreatePermissionRestrictedInteractions] Skipping - already creating interactions", "debug")
|
|
return
|
|
end
|
|
isCreatingInteractions = true
|
|
|
|
local locations = Locations.Client.GetLocations()
|
|
|
|
-- Force server check since permissions may have changed (job/gang change)
|
|
Locations.Client.RefreshEmployeeStatusCache(locations, true)
|
|
|
|
for _, location in pairs(locations) do
|
|
for interactionType, config in pairs(interactionTypeConfigs) do
|
|
if config.hasPermissionRestriction then
|
|
createInteraction(location, interactionType)
|
|
end
|
|
end
|
|
end
|
|
|
|
isCreatingInteractions = false
|
|
end
|
|
|
|
---Recreate permission-restricted interactions for a single location
|
|
---Called when employee status changes at a specific dealership
|
|
---@param dealershipId string
|
|
function Locations.Client.RecreatePermissionRestrictedInteractionsForLocation(dealershipId)
|
|
if not hasInitiallyLoaded then
|
|
DebugPrint("[RecreatePermissionRestrictedInteractionsForLocation] Skipping - initial load not complete", "debug")
|
|
return
|
|
end
|
|
|
|
local location = Locations.Client.GetLocationById(dealershipId)
|
|
if not location then
|
|
DebugPrint("[RecreatePermissionRestrictedInteractionsForLocation] Location not found: " .. tostring(dealershipId), "debug")
|
|
return
|
|
end
|
|
|
|
-- Refresh cache for just this location
|
|
Locations.Client.RefreshSingleLocationCache(location, true)
|
|
|
|
-- Recreate permission-restricted interactions for this location
|
|
for interactionType, config in pairs(interactionTypeConfigs) do
|
|
if config.hasPermissionRestriction then
|
|
createInteraction(location, interactionType)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Create all zones, blips & markers
|
|
---@param locations? Location[]
|
|
---@param ignoreCache? boolean
|
|
function Locations.Client.CreateAllInteractions(locations, ignoreCache)
|
|
-- Prevent concurrent interaction creation
|
|
if isCreatingInteractions then
|
|
DebugPrint("[CreateAllInteractions] Skipping - already creating interactions", "debug")
|
|
return
|
|
end
|
|
isCreatingInteractions = true
|
|
|
|
-- Remove existing interactions first to handle deleted/disabled locations
|
|
Locations.Client.RemoveAllInteractions()
|
|
DealershipZones.Client.RemoveAllZones()
|
|
DisplayVehicles.Client.DeleteAll()
|
|
|
|
locations = locations or Locations.Client.GetLocations(ignoreCache) or {}
|
|
cachedLocations = locations
|
|
|
|
-- Use permissions from location data (no extra server calls needed)
|
|
Locations.Client.RefreshEmployeeStatusCache(locations, false)
|
|
|
|
for _, location in pairs(locations) do
|
|
for interactionType in pairs(interactionTypeConfigs) do
|
|
createInteraction(location, interactionType)
|
|
end
|
|
|
|
DisplayVehicles.Client.SpawnAll(location.id)
|
|
DealershipZones.Client.CreateZone(location)
|
|
end
|
|
|
|
isCreatingInteractions = false
|
|
hasInitiallyLoaded = true
|
|
end
|
|
|
|
---Create interactions for a single location
|
|
---@param location Location
|
|
local function createLocationInteractions(location)
|
|
if not location then return end
|
|
|
|
-- Use permissions from location data if available, otherwise fetch from server
|
|
if location.playerCanAccessShowroom ~= nil then
|
|
showroomPermissionCache[location.id] = location.playerCanAccessShowroom
|
|
employeeStatusCache[location.id] = location.playerIsEmployee == true
|
|
else
|
|
-- Fallback to server calls (for locations created/updated via admin panel)
|
|
local showroomAllowed = lib.callback.await("jg-dealerships:server:check-showroom-whitelist", false, location.id)
|
|
showroomPermissionCache[location.id] = showroomAllowed
|
|
|
|
if location.type == "owned" then
|
|
local result = lib.callback.await("jg-dealerships:server:is-employee", false, location.id, false)
|
|
employeeStatusCache[location.id] = result and result.isEmployee or false
|
|
end
|
|
end
|
|
|
|
-- Create all interaction types for this location
|
|
for interactionType in pairs(interactionTypeConfigs) do
|
|
createInteraction(location, interactionType)
|
|
end
|
|
|
|
DisplayVehicles.Client.SpawnAll(location.id)
|
|
DealershipZones.Client.CreateZone(location)
|
|
end
|
|
|
|
---Add a new location to cache and create its interactions
|
|
---@param location Location
|
|
function Locations.Client.AddLocation(location)
|
|
if not location or not location.id then return end
|
|
|
|
-- Add to cache
|
|
cachedLocations[#cachedLocations + 1] = location
|
|
|
|
-- Create interactions for this location
|
|
createLocationInteractions(location)
|
|
end
|
|
|
|
---Update an existing location in cache and recreate its interactions
|
|
---@param location Location
|
|
function Locations.Client.UpdateLocation(location)
|
|
if not location or not location.id then return end
|
|
|
|
-- Remove existing interactions for this location
|
|
Locations.Client.RemoveLocationInteractions(location.id)
|
|
DealershipZones.Client.RemoveZone(location.id)
|
|
DisplayVehicles.Client.DeleteLocation(location.id)
|
|
|
|
-- Update cache
|
|
for i, loc in ipairs(cachedLocations) do
|
|
if loc.id == location.id then
|
|
cachedLocations[i] = location
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Recreate interactions for this location
|
|
createLocationInteractions(location)
|
|
end
|
|
|
|
---Delete a location from cache and remove its interactions
|
|
---@param locationId string
|
|
function Locations.Client.DeleteLocation(locationId)
|
|
if not locationId then return end
|
|
|
|
-- Remove interactions
|
|
Locations.Client.RemoveLocationInteractions(locationId)
|
|
DealershipZones.Client.RemoveZone(locationId)
|
|
DisplayVehicles.Client.DeleteLocation(locationId)
|
|
|
|
-- Remove from cache
|
|
for i, loc in ipairs(cachedLocations) do
|
|
if loc.id == locationId then
|
|
table.remove(cachedLocations, i)
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Clear permission caches
|
|
employeeStatusCache[locationId] = nil
|
|
showroomPermissionCache[locationId] = nil
|
|
end
|
|
|
|
-- Event so we can re-create all dealership locations on all clients (use sparingly - prefer granular events)
|
|
RegisterNetEvent("jg-dealerships:client:create-dealership-locations", function(locations, ignoreCache)
|
|
-- Prevent concurrent interaction creation
|
|
if isCreatingInteractions then
|
|
DebugPrint("[create-dealership-locations] Skipping - already creating interactions", "debug")
|
|
return
|
|
end
|
|
isCreatingInteractions = true
|
|
|
|
-- Immediately remove all existing interactions to ensure clean state
|
|
Locations.Client.RemoveAllInteractions()
|
|
DealershipZones.Client.RemoveAllZones()
|
|
DisplayVehicles.Client.DeleteAll()
|
|
|
|
-- Small delay to allow cleanup to complete before recreating
|
|
Wait(500)
|
|
|
|
-- Release mutex before calling CreateAllInteractions (which has its own mutex check)
|
|
isCreatingInteractions = false
|
|
Locations.Client.CreateAllInteractions(locations, ignoreCache)
|
|
end)
|
|
|
|
-- Event to add a single new location
|
|
RegisterNetEvent("jg-dealerships:client:add-location", function(location)
|
|
Locations.Client.AddLocation(location)
|
|
end)
|
|
|
|
-- Event to update a single location
|
|
RegisterNetEvent("jg-dealerships:client:update-location", function(location)
|
|
Locations.Client.UpdateLocation(location)
|
|
end)
|
|
|
|
-- Event to delete a single location
|
|
RegisterNetEvent("jg-dealerships:client:delete-location", function(locationId)
|
|
Locations.Client.DeleteLocation(locationId)
|
|
end)
|
|
|
|
-- Event when employee status changes (hired/fired/role update)
|
|
-- If dealershipId is provided, only refresh that location (used for built-in employee system)
|
|
-- If dealershipId is nil, refresh all locations (used for framework job changes)
|
|
RegisterNetEvent("jg-dealerships:client:employee-status-changed", function(dealershipId)
|
|
if dealershipId then
|
|
Locations.Client.RecreatePermissionRestrictedInteractionsForLocation(dealershipId)
|
|
else
|
|
Locations.Client.RecreatePermissionRestrictedInteractions()
|
|
end
|
|
end)
|
|
|
|
-- On script start - handle case where player is already loaded (script restart)
|
|
CreateThread(function()
|
|
Wait(1000)
|
|
-- Only create if player is already logged in and interactions haven't been created yet
|
|
if LocalPlayer.state.isLoggedIn and not hasInitiallyLoaded then
|
|
Locations.Client.CreateAllInteractions()
|
|
end
|
|
end)
|
|
|
|
-- Cleanup on resource stop
|
|
AddEventHandler("onResourceStop", function(resourceName)
|
|
if GetCurrentResourceName() ~= resourceName then return end
|
|
|
|
Locations.Client.RemoveAllInteractions()
|
|
DealershipZones.Client.RemoveAllZones()
|
|
end) |