Files
red-valley/resources/[framework]/[addons]/jg-dealerships/client/cl-locations.lua
2026-03-29 21:41:17 +03:00

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)