Advertisement
supinus

Police

Jun 25th, 2025 (edited)
339
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 49.26 KB | Gaming | 0 0
  1. ac.log('Script: Police')
  2. local sim = ac.getSim()
  3. local car = ac.getCar(0) or error()
  4. if not car then return end
  5. local wheels = car.wheels or error()
  6. local uiState = ac.getUI()
  7.  
  8. ui.setAsynchronousImagesLoading(true)
  9.  
  10. local localTesting = ac.dirname() == 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\assettocorsa\\extension\\lua\\online'
  11. local initialisation = true
  12.  
  13. -- Constants --
  14. local STEAMID = const(ac.getUserSteamID())
  15. local CSP_VERSION = const(ac.getPatchVersionCode())
  16. local CSP_MIN_VERSION = const(3116)
  17.  
  18. local SHARED_PLAYER_DATA = const('__ACP_SHARED_PLAYER_DATA')
  19. local SHARED_EVENT_KEY = const('__ACP_PLAYER_SHARED_UPDATE')
  20.  
  21. local CAR_ID = const(ac.getCarID(0))
  22. local CAR_NAME = const(ac.getCarName(0))
  23. local POLICE_CAR = const({ "ids_2022_ford_crown", "r34police_acp24" })
  24.  
  25. local DRIVER_NAME = const(ac.getDriverName(0))
  26. ---@param carID string
  27. local function isPoliceCar(carID)
  28.     for _, carName in ipairs(POLICE_CAR) do
  29.         if carID == carName then
  30.             return true
  31.         end
  32.     end
  33.     return false
  34. end
  35.  
  36. if CSP_VERSION < CSP_MIN_VERSION or not isPoliceCar(CAR_ID) then return end
  37.  
  38. local DRIVER_NATION_CODE = const(ac.getDriverNationCode(0))
  39. local UNIT = "km/h"
  40. local UNIT_MULT = 1
  41. if DRIVER_NATION_CODE == "USA" or DRIVER_NATION_CODE == "GBR" then
  42.     UNIT = "mph"
  43.     UNIT_MULT = 0.621371
  44. end
  45.  
  46. -- URL --
  47. local FIREBASE_URL = const('https://acp-server-97674-default-rtdb.firebaseio.com/')
  48.  
  49. -- UI --
  50. local WINDOW_WIDTH = const(sim.windowWidth / uiState.uiScale)
  51. local WIDTH_DIV = const({
  52.     _2 = WINDOW_WIDTH / 2,
  53.     _3 = WINDOW_WIDTH / 3,
  54.     _4 = WINDOW_WIDTH / 4,
  55.     _5 = WINDOW_WIDTH / 5,
  56.     _6 = WINDOW_WIDTH / 6,
  57.     _10 = WINDOW_WIDTH / 10,
  58.     _12 = WINDOW_WIDTH / 12,
  59.     _15 = WINDOW_WIDTH / 15,
  60.     _20 = WINDOW_WIDTH / 20,
  61.     _25 = WINDOW_WIDTH / 25,
  62.     _32 = WINDOW_WIDTH / 32,
  63. })
  64.  
  65. local WINDOW_HEIGHT = const(sim.windowHeight / uiState.uiScale)
  66. local HEIGHT_DIV = const({
  67.     _2 = WINDOW_HEIGHT / 2,
  68.     _3 = WINDOW_HEIGHT / 3,
  69.     _4 = WINDOW_HEIGHT / 4,
  70.     _12 = WINDOW_HEIGHT / 12,
  71.     _20 = WINDOW_HEIGHT / 20,
  72.     _40 = WINDOW_HEIGHT / 40,
  73.     _50 = WINDOW_HEIGHT / 50,
  74.     _60 = WINDOW_HEIGHT / 60,
  75.     _70 = WINDOW_HEIGHT / 70,
  76.     _80 = WINDOW_HEIGHT / 80,
  77. })
  78.  
  79. local FONT_MULT = const(WINDOW_HEIGHT / 1440)
  80.  
  81. local HUD_IMG = {}
  82.  
  83. local IMAGES = const({
  84.     police = {
  85.         url = "https://github.com/ele-sage/ACP-apps/raw/refs/heads/master/images/police.zip",
  86.         hud = {
  87.             "base.png",
  88.             "arrest.png",
  89.             "cams.png",
  90.             "logs.png",
  91.             "lost.png",
  92.             "menu.png",
  93.             "radar.png",
  94.         },
  95.     },
  96. })
  97.  
  98. ---@param key string
  99. local function loadImages(key)
  100.     web.loadRemoteAssets(IMAGES[key].url, function(err, data)
  101.         if err then
  102.             ac.error('Failed to load welcome images:', err)
  103.             return
  104.         end
  105.         local path = data .. '/' .. key .. '/'
  106.         local files = io.scanDir(path, "*")
  107.  
  108.         for i, file in ipairs(files) do
  109.             if table.contains(IMAGES.police.hud, file) then
  110.                 local k = file:match('(.+)%..+')
  111.                 HUD_IMG[k] = path .. file
  112.             end
  113.         end
  114.     end)
  115. end
  116.  
  117. loadImages("police")
  118.  
  119. local CAMERAS = const({
  120.     {
  121.         name = "BOBs SCRAPYARD",
  122.         pos = vec3(-3564, 31.5, -103),
  123.         dir = -8,
  124.         fov = 60,
  125.     },
  126.     {
  127.         name = "ARENA",
  128.         pos = vec3(-2283, 115.5, 3284),
  129.         dir = 128,
  130.         fov = 70,
  131.     },
  132.     {
  133.         name = "BANK",
  134.         pos = vec3(-716, 151, 3556.4),
  135.         dir = 12,
  136.         fov = 95,
  137.     },
  138.     {
  139.         name = "STREET RUNNERS",
  140.         pos = vec3(-57.3, 103.5, 2935.5),
  141.         dir = 16,
  142.         fov = 67,
  143.     },
  144.     {
  145.         name = "ROAD CRIMINALS",
  146.         pos = vec3(-2332, 101.1, 3119.2),
  147.         dir = 121,
  148.         fov = 60,
  149.     },
  150.     {
  151.         name = "RECKLESS RENEGADES",
  152.         pos = vec3(-2993.7, -24.4, -601.7),
  153.         dir = -64,
  154.         fov = 60,
  155.     },
  156.     {
  157.         name = "MOTION MASTERS",
  158.         pos = vec3(-2120.4, -11.8, -1911.5),
  159.         dir = 102,
  160.         fov = 60,
  161.     },
  162. })
  163.  
  164. local MSG_ARREST = const({
  165.     "`NAME` has been arrested for Speeding. The individual was driving a `CAR`.",
  166.     "We have apprehended `NAME` for Speeding. The suspect was behind the wheel of a `CAR`.",
  167.     "The driver of a `CAR`, identified as `NAME`, has been arrested for Speeding.",
  168.     "`NAME` has been taken into custody for Illegal Racing. The suspect was driving a `CAR`.",
  169.     "We have successfully apprehended `NAME` for Illegal Racing. The individual was operating a `CAR`.",
  170.     "The driver of a `CAR`, identified as `NAME`, has been arrested for Illegal Racing.",
  171.     "`NAME` has been apprehended for Speeding. The suspect was operating a `CAR` at the time of the arrest.",
  172.     "We have successfully detained `NAME` for Illegal Racing. The individual was driving a `CAR`.",
  173.     "`NAME` driving a `CAR` has been arrested for Speeding",
  174.     "`NAME` driving a `CAR` has been arrested for Illegal Racing."
  175. })
  176.  
  177. local MSG_LOST = const({
  178.     "We've lost sight of the suspect. The vehicle involved is described as a `CAR` driven by `NAME`.",
  179.     "Attention all units, we have lost visual contact with the suspect. The vehicle involved is a `CAR` driven by `NAME`.",
  180.     "We have temporarily lost track of the suspect. The vehicle description is a `CAR` with `NAME` as the driver.",
  181.     "Visual contact with the suspect has been lost. The suspect is driving a `CAR` and identified as `NAME`.",
  182.     "We have lost the suspect's visual trail. The vehicle in question is described as a `CAR` driven by `NAME`.",
  183.     "Suspect have been lost, Vehicle Description:`CAR` driven by `NAME`",
  184.     "Visual contact with the suspect has been lost. The suspect is driving a `CAR` and identified as `NAME`.",
  185.     "We have lost the suspect's visual trail. The vehicle in question is described as a `CAR` driven by `NAME`.",
  186. })
  187.  
  188. local MSG_ENGAGE = const({
  189.     "Control! I am engaging on a `CAR` traveling at `SPEED`",
  190.     "Pursuit in progress! I am chasing a `CAR` exceeding `SPEED`",
  191.     "Control, be advised! Pursuit is active on a `CAR` driving over `SPEED`",
  192.     "Attention! Pursuit initiated! Im following a `CAR` going above `SPEED`",
  193.     "Pursuit engaged! `CAR` driving at a high rate of speed over `SPEED`",
  194.     "Attention all units, we have a pursuit in progress! Suspect driving a `CAR` exceeding `SPEED`",
  195.     "Attention units! We have a suspect fleeing in a `CAR` at high speed, pursuing now at `SPEED`",
  196.     "Engaging on a high-speed chase! Suspect driving a `CAR` exceeding `SPEED`!",
  197.     "Attention all units! we have a pursuit in progress! Suspect driving a `CAR` exceeding `SPEED`",
  198.     "High-speed chase underway, suspect driving `CAR` over `SPEED`",
  199.     "Control, `CAR` exceeding `SPEED`, pursuit active.",
  200.     "Engaging on a `CAR` exceeding `SPEED`, pursuit initiated."
  201. })
  202.  
  203. local dataLoaded = {}
  204. dataLoaded['Settings'] = false
  205. dataLoaded['PlayerData'] = false
  206.  
  207. ---@param key string
  208. local function removeUtf8Char(key)
  209.     local newKey = ''
  210.     for i = 1, #key do
  211.         local c = key:sub(i, i)
  212.         if c:byte() < 128 then
  213.             newKey = newKey .. c
  214.         end
  215.     end
  216.     newKey = newKey:match('^%s*(.-)%s*$')
  217.     return newKey
  218. end
  219.  
  220. local CAR_NAME_NO_UTF8 = removeUtf8Char(CAR_NAME)
  221.  
  222. --------- Utils ------------
  223. ---@param keys string[]
  224. ---@param t table
  225. local function hasKeys(keys, t)
  226.     for i = 1, #keys do
  227.         if not t[keys[i]] then
  228.             ac.error('Missing key:', keys[i])
  229.             return false
  230.         end
  231.     end
  232.     return true
  233. end
  234.  
  235. ---@param number number
  236. ---@param decimal integer
  237. ---@return number
  238. local function truncate(number, decimal)
  239.     local power = 10 ^ decimal
  240.     return math.floor(number * power) / power
  241. end
  242.  
  243.  
  244. ---@param t table
  245. local function tableToVec3(t)
  246.     return vec3(t[1], t[2], t[3])
  247. end
  248.  
  249. ---@param t table
  250. local function tableToVec2(t)
  251.     return vec2(t[1], t[2])
  252. end
  253.  
  254. ---@param t table
  255. local function tableToRGBM(t)
  256.     return rgbm(t[1], t[2], t[3], t[4])
  257. end
  258.  
  259. ---@param err string
  260. ---@param response WebResponse
  261. ---@return boolean
  262. local function canProcessRequest(err, response)
  263.     if err then
  264.         ac.error('Failed to process request:', err)
  265.         return false
  266.     end
  267.     return response.status == 200 and response.body ~= ''
  268. end
  269.  
  270. ---@param response WebResponse
  271. ---@return boolean
  272. local function hasExistingData(response)
  273.     return response.status == 200 and response.body ~= 'null'
  274. end
  275.  
  276. ---@param v vec3
  277. ---@return vec3
  278. local function snapToTrack(v)
  279.     if physics.raycastTrack(v, vDown, 20, v) == -1 then
  280.         physics.raycastTrack(v, vUp, 20, v)
  281.     end
  282.     return v
  283. end
  284.  
  285. ---@param time number
  286. ---@return string
  287. local function formatTime(time)
  288.     local minutes = math.floor(time / 60)
  289.     local seconds = math.floor(time % 60)
  290.     local milliseconds = math.floor((time % 1) * 1000)
  291.     return ('%02d:%02d.%03d'):format(minutes, seconds, milliseconds)
  292. end
  293.  
  294. local DEFAULT_SETTINGS = const({
  295.     essentialSize = 20,
  296.     policeSize = 20,
  297.     hudOffset = vec2(0, 0),
  298.     fontSize = 20,
  299.     current = 1,
  300.     colorHud = rgbm(1, 0, 0, 1),
  301.     timeMsg = 10,
  302.     msgOffset = vec2(WIDTH_DIV._2, 10),
  303.     fontSizeMSG = 30,
  304.     menuPos = vec2(0, 0),
  305.     unit = UNIT,
  306.     unitMult = UNIT_MULT,
  307.     starsSize = 20,
  308.     starsPos = vec2(WINDOW_WIDTH, 0),
  309. })
  310.  
  311. ---@class Settings
  312. ---@field essentialSize number
  313. ---@field policeSize number
  314. ---@field hudOffset vec2
  315. ---@field fontSize number
  316. ---@field current number
  317. ---@field colorHud rgbm
  318. ---@field timeMsg number
  319. ---@field msgOffset vec2
  320. ---@field fontSizeMSG number
  321. ---@field menuPos vec2
  322. ---@field unit string
  323. ---@field unitMult number
  324. ---@field starsSize number
  325. ---@field starsPos vec2
  326. local Settings = class('Settings')
  327.  
  328. ---@return Settings
  329. function Settings.new()
  330.     local settings = table.clone(DEFAULT_SETTINGS, true)
  331.     setmetatable(settings, { __index = Settings })
  332.     return settings
  333. end
  334.  
  335. ---@param data table
  336. ---@return Settings
  337. function Settings.tryParse(data)
  338.     local hudOffset = data.hudOffset and tableToVec2(data.hudOffset) or vec2(0, 0)
  339.     local colorHud = data.colorHud and tableToRGBM(data.colorHud) or rgbm(1, 0, 0, 1)
  340.     local msgOffset = data.msgOffset and tableToVec2(data.msgOffset) or vec2(WIDTH_DIV._2, 10)
  341.     local menuPos = data.menuPos and tableToVec2(data.menuPos) or vec2(0, 0)
  342.     local starsPos = data.starsPos and tableToVec2(data.starsPos) or vec2(WINDOW_WIDTH, 0)
  343.     local settings = {
  344.         essentialSize = data.essentialSize or 20,
  345.         policeSize = data.policeSize or 20,
  346.         hudOffset = hudOffset,
  347.         fontSize = data.fontSize or 20,
  348.         current = data.current or 1,
  349.         colorHud = colorHud,
  350.         timeMsg = data.timeMsg or 10,
  351.         msgOffset = msgOffset,
  352.         fontSizeMSG = data.fontSizeMSG or 30,
  353.         menuPos = menuPos,
  354.         unit = data.unit or UNIT,
  355.         unitMult = data.unitMult or UNIT_MULT,
  356.         starsSize = data.starsSize or 20,
  357.         starsPos = starsPos,
  358.     }
  359.     setmetatable(settings, { __index = Settings })
  360.     return settings
  361. end
  362.  
  363. ---@param url string
  364. ---@param callback function
  365. function Settings.fetch(url, callback)
  366.     if localTesting then
  367.         local currentPath = ac.getFolder(ac.FolderID.ScriptOrigin)
  368.         local file = io.open(currentPath .. '/response/settingsResponse.json', 'r')
  369.         if not file then
  370.             ac.error('Failed to open response.json')
  371.             callback(Settings.new())
  372.             return
  373.         end
  374.         local data = JSON.parse(file:read('*a'))
  375.         file:close()
  376.         local settings = Settings.tryParse(data)
  377.         callback(settings)
  378.     else
  379.         web.get(url, function(err, response)
  380.             if canProcessRequest(err, response) then
  381.                 if hasExistingData(response) then
  382.                     local data = JSON.parse(response.body)
  383.                     if data then
  384.                         local settings = Settings.tryParse(data)
  385.                         callback(settings)
  386.                     else
  387.                         ac.error('Failed to parse settings data.')
  388.                         callback(Settings.new())
  389.                     end
  390.                 else
  391.                     callback(Settings.new())
  392.                 end
  393.             else
  394.                 ac.error('Failed to fetch settings:', err)
  395.                 callback(Settings.new())
  396.             end
  397.         end)
  398.     end
  399. end
  400.  
  401. ---@param callback function
  402. function Settings.allocate(callback)
  403.     local url = FIREBASE_URL .. 'Settings/' .. STEAMID .. '.json'
  404.     ac.log('Loading settings')
  405.     Settings.fetch(url, function(settings)
  406.         callback(settings)
  407.     end)
  408. end
  409.  
  410. ---@return table
  411. function Settings:export()
  412.     local data = {}
  413.     if self.essentialSize ~= DEFAULT_SETTINGS.essentialSize then
  414.         data.essentialSize = self.essentialSize
  415.     end
  416.     if self.policeSize ~= DEFAULT_SETTINGS.policeSize then
  417.         data.policeSize = self.policeSize
  418.     end
  419.     if self.hudOffset ~= DEFAULT_SETTINGS.hudOffset then
  420.         data.hudOffset = { self.hudOffset.x, self.hudOffset.y }
  421.     end
  422.     if self.fontSize ~= DEFAULT_SETTINGS.fontSize then
  423.         data.fontSize = self.fontSize
  424.     end
  425.     if self.current ~= DEFAULT_SETTINGS.current then
  426.         data.current = self.current
  427.     end
  428.     if self.colorHud ~= DEFAULT_SETTINGS.colorHud then
  429.         data.colorHud = { self.colorHud.r, self.colorHud.g, self.colorHud.b, self.colorHud.mult }
  430.     end
  431.     if self.timeMsg ~= DEFAULT_SETTINGS.timeMsg then
  432.         data.timeMsg = self.timeMsg
  433.     end
  434.     if self.msgOffset ~= DEFAULT_SETTINGS.msgOffset then
  435.         data.msgOffset = { self.msgOffset.x, self.msgOffset.y }
  436.     end
  437.     if self.fontSizeMSG ~= DEFAULT_SETTINGS.fontSizeMSG then
  438.         data.fontSizeMSG = self.fontSizeMSG
  439.     end
  440.     if self.menuPos ~= DEFAULT_SETTINGS.menuPos then
  441.         data.menuPos = { self.menuPos.x, self.menuPos.y }
  442.     end
  443.     if self.unit ~= DEFAULT_SETTINGS.unit then
  444.         data.unit = self.unit
  445.     end
  446.     if self.unitMult ~= DEFAULT_SETTINGS.unitMult then
  447.         data.unitMult = self.unitMult
  448.     end
  449.     if self.starsSize ~= DEFAULT_SETTINGS.starsSize then
  450.         data.starsSize = self.starsSize
  451.     end
  452.     if self.starsPos ~= DEFAULT_SETTINGS.starsPos then
  453.         data.starsPos = { self.starsPos.x, self.starsPos.y }
  454.     end
  455.     return data
  456. end
  457.  
  458. function Settings:save()
  459.     if localTesting then return end
  460.     local str = '{"' .. STEAMID .. '": ' .. JSON.stringify(self:export()) .. '}'
  461.     web.request('PATCH', FIREBASE_URL .. "Settings.json", str, function(err, response)
  462.         if err then
  463.             ac.error(err)
  464.             return
  465.         end
  466.     end)
  467. end
  468.  
  469. local patchCount = 0
  470.  
  471.  
  472. ---@class SectorStats
  473. ---@field name string
  474. ---@field records table<string, number>
  475. local SectorStats = class('SectorStats')
  476.  
  477. ---@param name string
  478. ---@param data table
  479. ---@return SectorStats
  480. function SectorStats.tryParse(name, data)
  481.     local records = {}
  482.     for carName, time in pairs(data) do
  483.         local nameWithoutUtf8 = removeUtf8Char(carName)
  484.         records[nameWithoutUtf8] = time
  485.     end
  486.     local sectorStats = {
  487.         name = name,
  488.         records = records,
  489.     }
  490.     setmetatable(sectorStats, { __index = SectorStats })
  491.     return sectorStats
  492. end
  493.  
  494. ---@param name string
  495. ---@param data table
  496. ---@return SectorStats|nil
  497. function SectorStats.allocate(name, data)
  498.     if type(data) == 'table' then
  499.         local sectorStats = SectorStats.tryParse(name, data)
  500.         if not sectorStats then
  501.             ac.error('Failed to allocate sector stat')
  502.             return nil
  503.         end
  504.         return sectorStats
  505.     end
  506.     if type(data) == 'number' then
  507.         local records = {}
  508.         records[CAR_NAME_NO_UTF8] = data
  509.         local sectorStats = {
  510.             name = name,
  511.             records = records,
  512.         }
  513.         setmetatable(sectorStats, { __index = SectorStats })
  514.         return sectorStats
  515.     end
  516.     ac.error('Failed to allocate sector stat')
  517.     return nil
  518. end
  519.  
  520. ---@param time number
  521. ---@return boolean
  522. function SectorStats:addRecord(time)
  523.     if not self.records[CAR_NAME_NO_UTF8] or self.records[CAR_NAME_NO_UTF8] > time then
  524.         self.records[CAR_NAME_NO_UTF8] = time
  525.         return true
  526.     end
  527.     return false
  528. end
  529.  
  530. ---@return table
  531. function SectorStats:export()
  532.     local records = {}
  533.     for carName, time in pairs(self.records) do
  534.         records[carName] = truncate(time, 3)
  535.     end
  536.     return {
  537.         [self.name] = records
  538.     }
  539. end
  540.  
  541. local lastRegister = {
  542.     kms = 0,
  543.     time = os.clock(),
  544. }
  545.  
  546. ---@class Player
  547. ---@field name string
  548. ---@field sectors SectorStats[]
  549. ---@field sectorsFormated table<string, table<string, string>>
  550. ---@field arrests integer
  551. ---@field getaways integer
  552. ---@field thefts integer
  553. ---@field heists integer
  554. ---@field deliveries integer
  555. ---@field overtake integer
  556. ---@field wins integer
  557. ---@field losses integer
  558. ---@field elo integer
  559. ---@field kms number
  560. ---@field time number
  561. local Player = class('Player')
  562.  
  563. ---@type Player | nil
  564. local player = nil
  565.  
  566. local sharedPlayerLayout = {
  567.     ac.StructItem.key(SHARED_PLAYER_DATA),
  568.     hudColor = ac.StructItem.rgbm(),
  569.     name = ac.StructItem.string(24),
  570.     sectorsFormated = ac.StructItem.array(ac.StructItem.struct({
  571.         name = ac.StructItem.string(16),
  572.         records = ac.StructItem.array(ac.StructItem.string(50), 20)
  573.     }), 5),
  574.     arrests = ac.StructItem.uint16(),
  575.     getaways = ac.StructItem.uint16(),
  576.     thefts = ac.StructItem.uint16(),
  577.     heists = ac.StructItem.uint16(),
  578.     deliveries = ac.StructItem.uint16(),
  579.     overtake = ac.StructItem.uint32(),
  580.     wins = ac.StructItem.uint16(),
  581.     losses = ac.StructItem.uint16(),
  582.     elo = ac.StructItem.uint16(),
  583.     kms = ac.StructItem.float(),
  584.     time = ac.StructItem.float(),
  585. }
  586.  
  587. ---@type Settings | nil
  588. local settings = nil
  589.  
  590. local sharedPlayerData = ac.connect(sharedPlayerLayout, true, ac.SharedNamespace.ServerScript)
  591.  
  592. local function updateSharedPlayerData()
  593.     if not player then return end
  594.     local hudC = rgbm.colors.red
  595.     if settings then
  596.         hudC = settings.colorHud
  597.     end
  598.     sharedPlayerData.hudColor = hudC
  599.     sharedPlayerData.name = player.name
  600.     sharedPlayerData.arrests = player.arrests
  601.     sharedPlayerData.getaways = player.getaways
  602.     sharedPlayerData.thefts = player.thefts
  603.     sharedPlayerData.heists = player.heists
  604.     sharedPlayerData.deliveries = player.deliveries
  605.     sharedPlayerData.overtake = player.overtake
  606.     sharedPlayerData.wins = player.wins
  607.     sharedPlayerData.losses = player.losses
  608.     sharedPlayerData.elo = player.elo
  609.     sharedPlayerData.kms = player.kms
  610.     sharedPlayerData.time = player.time
  611.     sharedPlayerData.sectorsFormated = {}
  612.     local i = 1
  613.     table.forEach(player.sectorsFormated, function(v, k)
  614.         sharedPlayerData.sectorsFormated[i].name = k .. '\0'
  615.         for j, entry in ipairs(v) do
  616.             local carName = string.sub(entry[1], 1, 45)
  617.             sharedPlayerData.sectorsFormated[i].records[j] = carName .. ' - ' .. entry[2] .. '\0'
  618.         end
  619.         i = i + 1
  620.     end)
  621. end
  622.  
  623. ---@return Player
  624. function Player.new()
  625.     local _player = {
  626.         name = DRIVER_NAME,
  627.         sectors = {},
  628.         sectorsFormated = {},
  629.         arrests = 0,
  630.         getaways = 0,
  631.         thefts = 0,
  632.         heists = 0,
  633.         deliveries = 0,
  634.         overtake = 0,
  635.         wins = 0,
  636.         losses = 0,
  637.         elo = 1200,
  638.         kms = 0,
  639.         time = 0,
  640.     }
  641.     setmetatable(_player, { __index = Player })
  642.     return _player
  643. end
  644.  
  645. ---@param data table
  646. ---@return Player
  647. function Player.tryParse(data)
  648.     if not data then
  649.         return Player.new()
  650.     end
  651.     local sectors = {}
  652.     if data.sectors then
  653.         for sectorName, sectorData in pairs(data.sectors) do
  654.             local sector = SectorStats.allocate(sectorName, sectorData)
  655.             if sector then
  656.                 table.insert(sectors, sector)
  657.             end
  658.         end
  659.     end
  660.     local _player = {
  661.         name = DRIVER_NAME,
  662.         sectors = sectors,
  663.         sectorsFormated = {},
  664.         arrests = data.arrests or 0,
  665.         getaways = data.getaways or 0,
  666.         thefts = data.thefts or 0,
  667.         heists = data.heists or 0,
  668.         deliveries = data.deliveries or 0,
  669.         overtake = data.overtake or 0,
  670.         wins = data.wins or 0,
  671.         losses = data.losses or 0,
  672.         elo = data.elo or 1200,
  673.         kms = data.kms or 0,
  674.         time = data.time or 0,
  675.     }
  676.     setmetatable(_player, { __index = Player })
  677.     return _player
  678. end
  679.  
  680. ---@param url string
  681. ---@param callback function
  682. function Player.fetch(url, callback)
  683.     if localTesting then
  684.         local currentPath = ac.getFolder(ac.FolderID.ScriptOrigin)
  685.         local file = io.open(currentPath .. '/response/playerResponse.json', 'r')
  686.         if not file then
  687.             ac.error('Failed to open playerResponse.json')
  688.             callback(Player.new())
  689.             return
  690.         end
  691.         local data = JSON.parse(file:read('*a'))
  692.         file:close()
  693.         local _player = Player.tryParse(data)
  694.         callback(_player)
  695.     else
  696.         web.get(url, function(err, response)
  697.             if canProcessRequest(err, response) then
  698.                 if hasExistingData(response) then
  699.                     local data = JSON.parse(response.body)
  700.                     if data then
  701.                         local _player = Player.tryParse(data)
  702.                         callback(_player)
  703.                     else
  704.                         ac.error('Failed to parse player data.')
  705.                         callback(Player.new())
  706.                     end
  707.                 else
  708.                     callback(Player.new())
  709.                 end
  710.             else
  711.                 ac.error('Failed to fetch player:', err)
  712.                 callback(Player.new())
  713.             end
  714.         end)
  715.     end
  716. end
  717.  
  718. ---@param callback function
  719. function Player.allocate(callback)
  720.     local url = FIREBASE_URL .. 'Players/' .. STEAMID .. '.json'
  721.     Player.fetch(url, function(_player)
  722.         callback(_player)
  723.     end)
  724. end
  725.  
  726. function Player:sortSectors()
  727.     for _, sector in ipairs(self.sectors) do
  728.         local entries = {}
  729.         for carName, time in pairs(sector.records) do
  730.             table.insert(entries, { carName, time })
  731.         end
  732.         table.sort(entries, function(a, b)
  733.             return a[2] < b[2]
  734.         end)
  735.         for i, entry in ipairs(entries) do
  736.             entries[i][2] = formatTime(entry[2])
  737.         end
  738.         self.sectorsFormated[sector.name] = entries
  739.     end
  740. end
  741.  
  742. ---@return table
  743. function Player:export()
  744.     local kms = truncate(car.distanceDrivenSessionKm - lastRegister.kms + self.kms, 3)
  745.     local time = math.round(os.clock() - lastRegister.time + self.time, 0)
  746.     local data = { name = self.name }
  747.  
  748.     if self.arrests > 0 then
  749.         data.arrests = self.arrests
  750.     end
  751.     if self.getaways > 0 then
  752.         data.getaways = self.getaways
  753.     end
  754.     if self.thefts > 0 then
  755.         data.thefts = self.thefts
  756.     end
  757.     if self.heists > 0 then
  758.         data.heists = self.heists
  759.     end
  760.     if self.deliveries > 0 then
  761.         data.deliveries = self.deliveries
  762.     end
  763.     if self.overtake > 0 then
  764.         data.overtake = self.overtake
  765.     end
  766.     if self.wins > 0 then
  767.         data.wins = self.wins
  768.     end
  769.     if self.losses > 0 then
  770.         data.losses = self.losses
  771.     end
  772.     if self.elo ~= 1200 then
  773.         data.elo = self.elo
  774.     end
  775.     if kms > 0 then
  776.         data.kms = kms
  777.     end
  778.     if time > 0 then
  779.         data.time = time
  780.     end
  781.  
  782.     lastRegister.kms = car.distanceDrivenSessionKm
  783.     lastRegister.time = os.clock()
  784.  
  785.     local sectors = {}
  786.     for _, sector in ipairs(self.sectors) do
  787.         if not sector then
  788.             break
  789.         end
  790.         local sectorData = sector:export()
  791.         for k, v in pairs(sectorData) do
  792.             sectors[k] = v
  793.         end
  794.     end
  795.     if next(sectors) then
  796.         data.sectors = sectors
  797.     end
  798.     self:sortSectors()
  799.     updateSharedPlayerData()
  800.     return data
  801. end
  802.  
  803. function Player:save()
  804.     local str = '{"' .. STEAMID .. '": ' .. JSON.stringify(self:export()) .. '}'
  805.     if localTesting or patchCount > 40 then return end
  806.     patchCount = patchCount + 1
  807.     web.request('PATCH', FIREBASE_URL .. "Players.json", str, function(err, response)
  808.         if err then
  809.             ac.error(err)
  810.             return
  811.         end
  812.     end)
  813. end
  814.  
  815. local canRun = false
  816. local function shouldRun()
  817.     if canRun then return true end
  818.     local isDataLoaded = dataLoaded['Settings'] and dataLoaded['PlayerData']
  819.     local hasNecessaryData = settings and player
  820.     local hasMinVersion = CSP_VERSION >= CSP_MIN_VERSION
  821.     if isDataLoaded and hasMinVersion and hasNecessaryData and isPoliceCar(CAR_ID) then
  822.         canRun = true
  823.     end
  824.     return canRun
  825. end
  826.  
  827. local settingsOpen = false
  828. local arrestLogsOpen = false
  829. local camerasOpen = false
  830.  
  831. local imageSize = vec2(0,0)
  832.  
  833. local pursuit = {
  834.     suspect = nil,
  835.     enable = false,
  836.     maxDistance = 250000,
  837.     minDistance = 40000,
  838.     nextMessage = 30,
  839.     level = 1,
  840.     id = -1,
  841.     timerArrest = 0,
  842.     hasArrested = false,
  843.     startedTime = 0,
  844.     timeLostSight = 0,
  845.     lostSight = false,
  846.     engage = false,
  847. }
  848.  
  849. local arrestations = {}
  850.  
  851. local playerInRangeUI = {
  852.     fontSize = {
  853.         div_2 = 20 / 2,
  854.         div_2_5 = 20 / 2.5,
  855.         div_3 = 20 / 3,
  856.         div_1_5 = 20 / 1.5,
  857.         div_1_2 = 20 / 1.2,
  858.     },
  859.     window = {
  860.         pos = vec2(0, 0),
  861.         size = vec2(0, 0),
  862.     },
  863.     box = {
  864.         pos1 = vec2(0, 0),
  865.         pos2 = vec2(0, 0),
  866.         offsetY = 0,
  867.     },
  868.     text = {
  869.         pos = vec2(0, 0),
  870.         size = vec2(0, 0),
  871.         offsetY = 0,
  872.     },
  873.     header = {
  874.         radar = {
  875.             pos = vec2(0, 0),
  876.         },
  877.         nearby = {
  878.             pos = vec2(0, 0),
  879.         },
  880.     },
  881.     color = rgbm(1, 1, 1, 0.5),
  882. }
  883.  
  884. local iconPos = {}
  885.  
  886. local function onSettingsChange()
  887.     settings:save()
  888.     ac.log('Settings updated')
  889. end
  890.  
  891. ---------------------------------------------------------------------------------------------- Firebase ----------------------------------------------------------------------------------------------
  892.  
  893. local acpPolice = ac.OnlineEvent({
  894.     message = ac.StructItem.string(110),
  895.     messageType = ac.StructItem.int16(),
  896.     yourIndex = ac.StructItem.int16(),
  897. }, function (sender, data)
  898.     if data.yourIndex == car.sessionID and data.messageType == 0 and pursuit.suspect ~= nil and sender == pursuit.suspect then
  899.         pursuit.hasArrested = true
  900.     end
  901. end)
  902.  
  903. local starsUI = {
  904.     starsPos = vec2(0, 0),
  905.     starsSize = vec2(0, 0),
  906.     startSpace = 0,
  907.     full = "https://acstuff.ru/images/icons_24/star_full.png",
  908.     empty = "https://acstuff.ru/images/icons_24/star_empty.png",
  909. }
  910.  
  911. local function updateStarsPos()
  912.     starsUI.starsPos = vec2(settings.starsPos.x - settings.starsSize / 2, settings.starsPos.y + settings.starsSize / 2)
  913.     starsUI.starsSize = vec2(settings.starsPos.x - settings.starsSize * 2, settings.starsPos.y + settings.starsSize * 2)
  914.     starsUI.startSpace = settings.starsSize / 1.5
  915. end
  916.  
  917. local function updateHudPos()
  918.     imageSize = vec2(WINDOW_HEIGHT/80 * settings.policeSize, WINDOW_HEIGHT/80 * settings.policeSize)
  919.     iconPos.arrest1 = vec2(imageSize.x - imageSize.x/12, imageSize.y/3.2)
  920.     iconPos.arrest2 = vec2(imageSize.x/1.215, imageSize.y/5)
  921.     iconPos.lost1 = vec2(imageSize.x - imageSize.x/12, imageSize.y/2.35)
  922.     iconPos.lost2 = vec2(imageSize.x/1.215, imageSize.y/3.2)
  923.     iconPos.logs1 = vec2(imageSize.x/1.215, imageSize.y/1.88)
  924.     iconPos.logs2 = vec2(imageSize.x/1.39, imageSize.y/2.35)
  925.     iconPos.menu1 = vec2(imageSize.x - imageSize.x/12, imageSize.y/1.88)
  926.     iconPos.menu2 = vec2(imageSize.x/1.215, imageSize.y/2.35)
  927.     iconPos.cams1 = vec2(imageSize.x/1.215, imageSize.y/2.35)
  928.     iconPos.cams2 = vec2(imageSize.x/1.39, imageSize.y/3.2)
  929.  
  930.     settings.fontSize = settings.policeSize * FONT_MULT
  931.     playerInRangeUI.fontSize.div_2 = settings.fontSize / 2
  932.     playerInRangeUI.fontSize.div_2_5 = settings.fontSize / 2.5
  933.     playerInRangeUI.fontSize.div_3 = settings.fontSize / 3
  934.     playerInRangeUI.fontSize.div_1_5 = settings.fontSize / 1.5
  935.     playerInRangeUI.fontSize.div_1_2 = settings.fontSize / 1.2
  936.  
  937.     playerInRangeUI.window.pos = vec2(settings.hudOffset.x + imageSize.x / 9.5, settings.hudOffset.y + imageSize.y / 5.3)
  938.     playerInRangeUI.window.size = vec2(imageSize.x * 3 / 5, imageSize.y / 2.8)
  939.     playerInRangeUI.box.pos1 = vec2(playerInRangeUI.window.size.x / 20, playerInRangeUI.window.size.y / 20)
  940.     playerInRangeUI.box.pos2 = vec2(playerInRangeUI.window.size.x - playerInRangeUI.window.size.x / 20, playerInRangeUI.window.size.y / 20)
  941.     playerInRangeUI.box.offsetY = playerInRangeUI.window.size.y / 10 + playerInRangeUI.box.pos2.y
  942.     ui.pushDWriteFont("Orbitron;Weight=Bold")
  943.     playerInRangeUI.header.radar.pos = vec2((playerInRangeUI.window.size.x - ui.measureDWriteText("RADAR ACTIVE", playerInRangeUI.fontSize.div_1_5).x) / 2, 0)
  944.     ui.popDWriteFont()
  945.     ui.pushDWriteFont("Orbitron;Weight=Regular")
  946.     playerInRangeUI.header.nearby.pos = vec2((playerInRangeUI.window.size.x - ui.measureDWriteText("NEARBY VEHICULE SPEED SCANNING", playerInRangeUI.fontSize.div_2_5).x) / 2, playerInRangeUI.fontSize.div_1_2)
  947.     ui.popDWriteFont()
  948.     playerInRangeUI.text.pos = vec2(playerInRangeUI.window.size.x / 20, 0)
  949.     playerInRangeUI.text.size = vec2(playerInRangeUI.window.size.x - playerInRangeUI.window.size.x / 10, playerInRangeUI.window.size.y / 5)
  950.     ui.pushDWriteFont("Orbitron;Weight=Regular")
  951.     playerInRangeUI.text.offsetY = (playerInRangeUI.window.size.y / 20 - ui.measureDWriteText("Player In Range", settings.fontSize / 20).y) / 2
  952.     ui.popDWriteFont()
  953. end
  954.  
  955. local function showStarsPursuit()
  956.     local starsColor = rgbm(1, 1, 1, os.clock()%2 + 0.3)
  957.     updateStarsPos()
  958.     for i = 1, 5 do
  959.         if i > pursuit.level/2 then
  960.             ui.drawIcon(ui.Icons.StarEmpty, starsUI.starsPos, starsUI.starsSize, rgbm(1, 1, 1, 0.2))
  961.         else
  962.             ui.drawIcon(ui.Icons.StarFull, starsUI.starsPos, starsUI.starsSize, starsColor)
  963.         end
  964.         starsUI.starsPos.x = starsUI.starsPos.x - settings.starsSize - starsUI.startSpace
  965.         starsUI.starsSize.x = starsUI.starsSize.x - settings.starsSize - starsUI.startSpace
  966.     end
  967. end
  968.  
  969. local showPreviewMsg = false
  970. local showPreviewStars = false
  971. COLORSMSGBG = rgbm(0.5,0.5,0.5,0.5)
  972.  
  973. local function initsettings()
  974.     imageSize = vec2(WINDOW_HEIGHT/80 * settings.policeSize, WINDOW_HEIGHT/80 * settings.policeSize)
  975.     updateHudPos()
  976.     updateStarsPos()
  977. end
  978.  
  979. local function previewMSG()
  980.     ui.beginTransparentWindow("previewMSG", vec2(0, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT))
  981.     ui.pushDWriteFont("Orbitron;Weight=800")
  982.     local tSize = ui.measureDWriteText("Messages from Police when being chased", settings.fontSizeMSG)
  983.     local uiOffsetX = settings.msgOffset.x - tSize.x/2
  984.     local uiOffsetY = settings.msgOffset.y
  985.     ui.drawRectFilled(vec2(uiOffsetX - 5, uiOffsetY-5), vec2(uiOffsetX + tSize.x + 5, uiOffsetY + tSize.y + 5), COLORSMSGBG)
  986.     ui.dwriteDrawText("Messages from Police when being chased", settings.fontSizeMSG, vec2(uiOffsetX, uiOffsetY), rgbm.colors.cyan)
  987.     ui.popDWriteFont()
  988.     ui.endTransparentWindow()
  989. end
  990.  
  991. local function previewStars()
  992.     ui.beginTransparentWindow("previewStars", vec2(0, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT))
  993.     showStarsPursuit()
  994.     ui.endTransparentWindow()
  995. end
  996.  
  997. local function uiTab()
  998.     ui.text('On Screen Message : ')
  999.     settings.timeMsg = ui.slider('##' .. 'Time Msg On Screen', settings.timeMsg, 1, 15, 'Time Msg On Screen' .. ': %.0fs')
  1000.     settings.fontSizeMSG = ui.slider('##' .. 'Font Size MSG', settings.fontSizeMSG, 10, 50, 'Font Size' .. ': %.0f')
  1001.     ui.text('Stars : ')
  1002.     settings.starsPos.x = ui.slider('##' .. 'Stars Offset X', settings.starsPos.x, 0, WINDOW_WIDTH, 'Stars Offset X' .. ': %.0f')
  1003.     settings.starsPos.y = ui.slider('##' .. 'Stars Offset Y', settings.starsPos.y, 0, WINDOW_HEIGHT, 'Stars Offset Y' .. ': %.0f')
  1004.     settings.starsSize = ui.slider('##' .. 'Stars Size', settings.starsSize, 10, 50, 'Stars Size' .. ': %.0f')
  1005.     ui.newLine()
  1006.     ui.text('Offset : ')
  1007.     settings.msgOffset.y = ui.slider('##' .. 'Msg On Screen Offset Y', settings.msgOffset.y, 0, WINDOW_HEIGHT, 'Msg On Screen Offset Y' .. ': %.0f')
  1008.     settings.msgOffset.x = ui.slider('##' .. 'Msg On Screen Offset X', settings.msgOffset.x, 0, WINDOW_WIDTH, 'Msg On Screen Offset X' .. ': %.0f')
  1009.     ui.newLine()
  1010.     ui.text('Preview : ')
  1011.     ui.sameLine()
  1012.     if ui.button('Message') then
  1013.         showPreviewMsg = not showPreviewMsg
  1014.         showPreviewStars = false
  1015.     end
  1016.     ui.sameLine()
  1017.     if ui.button('Stars') then
  1018.         showPreviewStars = not showPreviewStars
  1019.         showPreviewMsg = false
  1020.     end
  1021.     if showPreviewMsg then previewMSG()
  1022.     elseif showPreviewStars then previewStars() end
  1023.     if ui.button('Offset X to center') then settings.msgOffset.x = WINDOW_WIDTH/2 end
  1024.     ui.newLine()
  1025. end
  1026.  
  1027. local function settingsWindow()
  1028.     imageSize = vec2(WINDOW_HEIGHT/80 * settings.policeSize, WINDOW_HEIGHT/80 * settings.policeSize)
  1029.     ui.dwriteTextAligned("settings", 40, ui.Alignment.Center, ui.Alignment.Center, vec2(WINDOW_WIDTH/6.5,60), false, rgbm.colors.white)
  1030.     ui.drawLine(vec2(0,60), vec2(WINDOW_WIDTH/6.5,60), rgbm.colors.white, 1)
  1031.     ui.newLine(20)
  1032.     ui.sameLine(10)
  1033.     ui.beginGroup()
  1034.     ui.text('Unit : ')
  1035.     ui.sameLine(160)
  1036.     if ui.selectable('mph', settings.unit == 'mph',_, ui.measureText('km/h')) then
  1037.         settings.unit = 'mph'
  1038.         settings.unitMult = 0.621371
  1039.     end
  1040.     ui.sameLine(200)
  1041.     if ui.selectable('km/h', settings.unit == 'km/h',_, ui.measureText('km/h')) then
  1042.         settings.unit = 'km/h'
  1043.         settings.unitMult = 1
  1044.     end
  1045.     ui.sameLine(WINDOW_WIDTH/6.5 - 120)
  1046.     if ui.button('Close', vec2(100, WINDOW_HEIGHT/50)) then
  1047.         settingsOpen = false
  1048.         onSettingsChange()
  1049.     end
  1050.     ui.text('HUD : ')
  1051.     settings.hudOffset.x = ui.slider('##' .. 'HUD Offset X', settings.hudOffset.x, 0, WINDOW_WIDTH, 'HUD Offset X' .. ': %.0f')
  1052.     settings.hudOffset.y = ui.slider('##' .. 'HUD Offset Y', settings.hudOffset.y, 0, WINDOW_HEIGHT, 'HUD Offset Y' .. ': %.0f')
  1053.     settings.policeSize = ui.slider('##' .. 'HUD Size', settings.policeSize, 10, 50, 'HUD Size' .. ': %.0f')
  1054.     settings.fontSize = settings.policeSize * FONT_MULT
  1055.     ui.setNextItemWidth(300)
  1056.     ui.newLine()
  1057.     uiTab()
  1058.     updateHudPos()
  1059.     ui.endGroup()
  1060. end
  1061.  
  1062. ---------------------------------------------------------------------------------------------- Utils ----------------------------------------------------------------------------------------------
  1063.  
  1064. local function formatMessage(message)
  1065.     local msgToSend = message
  1066.     if pursuit.suspect == nil then
  1067.         msgToSend = string.gsub(msgToSend,"`CAR`", "No Car")
  1068.         msgToSend = string.gsub(msgToSend,"`NAME`", "No Name")
  1069.         msgToSend = string.gsub(msgToSend,"`SPEED`", "No Speed")
  1070.         return msgToSend
  1071.     end
  1072.     msgToSend = string.gsub(msgToSend,"`CAR`", string.gsub(string.gsub(ac.getCarName(pursuit.suspect.index), "%W", " "), "  ", ""))
  1073.     msgToSend = string.gsub(msgToSend,"`NAME`", "@" .. ac.getDriverName(pursuit.suspect.index))
  1074.     msgToSend = string.gsub(msgToSend,"`SPEED`", string.format("%d ", ac.getCarSpeedKmh(pursuit.suspect.index) * settings.unitMult) .. settings.unit)
  1075.     return msgToSend
  1076. end
  1077.  
  1078. ---------------------------------------------------------------------------------------------- HUD ----------------------------------------------------------------------------------------------
  1079.  
  1080. local policeLightsPos = {
  1081.     vec2(0,0),
  1082.     vec2(WINDOW_WIDTH/10,WINDOW_HEIGHT),
  1083.     vec2(WINDOW_WIDTH-WINDOW_WIDTH/10,0),
  1084.     vec2(WINDOW_WIDTH,WINDOW_HEIGHT)
  1085. }
  1086.  
  1087. local function showPoliceLights()
  1088.     local timing = math.floor(os.clock()*2 % 2)
  1089.     if timing == 0 then
  1090.         ui.drawRectFilledMultiColor(policeLightsPos[1], policeLightsPos[2], rgbm(1,0,0,0.5), rgbm(0,0,0,0), rgbm(0,0,0,0), rgbm(1,0,0,0.5))
  1091.         ui.drawRectFilledMultiColor(policeLightsPos[3], policeLightsPos[4], rgbm(0,0,0,0), rgbm(0,0,1,0.5), rgbm(0,0,1,0.5), rgbm(0,0,0,0))
  1092.     else
  1093.         ui.drawRectFilledMultiColor(policeLightsPos[1], policeLightsPos[2], rgbm(0,0,1,0.5), rgbm(0,0,0,0), rgbm(0,0,0,0), rgbm(0,0,1,0.5))
  1094.         ui.drawRectFilledMultiColor(policeLightsPos[3], policeLightsPos[4], rgbm(0,0,0,0), rgbm(1,0,0,0.5), rgbm(1,0,0,0.5), rgbm(0,0,0,0))
  1095.     end
  1096. end
  1097.  
  1098. local chaseLVL = {
  1099.     message = "",
  1100.     messageTimer = 0,
  1101.     color = rgbm.colors.white,
  1102. }
  1103.  
  1104. local function resetChase()
  1105.     pursuit.enable = false
  1106.     pursuit.nextMessage = 30
  1107.     pursuit.lostSight = false
  1108.     pursuit.timeLostSight = 2
  1109. end
  1110.  
  1111. local function lostSuspect()
  1112.     resetChase()
  1113.     pursuit.lostSight = false
  1114.     pursuit.timeLostSight = 0
  1115.     pursuit.level = 1
  1116.     ac.sendChatMessage(formatMessage(MSG_LOST[math.random(#MSG_LOST)]))
  1117.     pursuit.suspect = nil
  1118.     ac.setExtraSwitch(0, false)
  1119. end
  1120.  
  1121. local iconsColorOn = {
  1122.     [1] = rgbm.colors.red,
  1123.     [2] = rgbm.colors.white,
  1124.     [3] = rgbm.colors.white,
  1125.     [4] = rgbm.colors.white,
  1126.     [5] = rgbm.colors.white,
  1127.     [6] = rgbm.colors.white,
  1128. }
  1129.  
  1130. local playersInRange = {}
  1131.  
  1132. local function drawImage()
  1133.     iconsColorOn[2] = rgbm.colors.white
  1134.     iconsColorOn[3] = rgbm.colors.white
  1135.     iconsColorOn[4] = rgbm.colors.white
  1136.     iconsColorOn[5] = rgbm.colors.white
  1137.     iconsColorOn[6] = rgbm.colors.white
  1138.  
  1139.     if ui.rectHovered(iconPos.arrest2, iconPos.arrest1) then
  1140.         iconsColorOn[2] = rgbm.colors.red
  1141.         if pursuit.suspect and pursuit.suspect.speedKmh < 50 and car.speedKmh < 20 and uiState.isMouseLeftKeyClicked then
  1142.             pursuit.hasArrested = true
  1143.         end
  1144.     elseif ui.rectHovered(iconPos.cams2, iconPos.cams1) then
  1145.         iconsColorOn[3] = rgbm.colors.red
  1146.         if uiState.isMouseLeftKeyClicked then
  1147.             if camerasOpen then camerasOpen = false
  1148.             else
  1149.                 camerasOpen = true
  1150.                 arrestLogsOpen = false
  1151.                 if settingsOpen then
  1152.                     onSettingsChange()
  1153.                     settingsOpen = false
  1154.                 end
  1155.             end
  1156.         end
  1157.     elseif ui.rectHovered(iconPos.lost2, iconPos.lost1) then
  1158.         iconsColorOn[4] = rgbm.colors.red
  1159.         if pursuit.suspect and uiState.isMouseLeftKeyClicked then
  1160.             lostSuspect()
  1161.         end
  1162.     elseif ui.rectHovered(iconPos.logs2, iconPos.logs1) then
  1163.         iconsColorOn[5] = rgbm.colors.red
  1164.         if uiState.isMouseLeftKeyClicked then
  1165.             if arrestLogsOpen then arrestLogsOpen = false
  1166.             else
  1167.                 arrestLogsOpen = true
  1168.                 camerasOpen = false
  1169.                 if settingsOpen then
  1170.                     onSettingsChange()
  1171.                     settingsOpen = false
  1172.                 end
  1173.             end
  1174.         end
  1175.     elseif ui.rectHovered(iconPos.menu2, iconPos.menu1) then
  1176.         iconsColorOn[6] = rgbm.colors.red
  1177.         if uiState.isMouseLeftKeyClicked then
  1178.             if settingsOpen then
  1179.                 onSettingsChange()
  1180.                 settingsOpen = false
  1181.             else
  1182.                 settingsOpen = true
  1183.                 arrestLogsOpen = false
  1184.                 camerasOpen = false
  1185.             end
  1186.         end
  1187.     end
  1188.     ui.image(HUD_IMG.base, imageSize, rgbm.colors.white)
  1189.     ui.drawImage(HUD_IMG.radar, vec2(0,0), imageSize, iconsColorOn[1])
  1190.     ui.drawImage(HUD_IMG.arrest, vec2(0,0), imageSize, iconsColorOn[2])
  1191.     ui.drawImage(HUD_IMG.cams, vec2(0,0), imageSize, iconsColorOn[3])
  1192.     ui.drawImage(HUD_IMG.lost, vec2(0,0), imageSize, iconsColorOn[4])
  1193.     ui.drawImage(HUD_IMG.logs, vec2(0,0), imageSize, iconsColorOn[5])
  1194.     ui.drawImage(HUD_IMG.menu, vec2(0,0), imageSize, iconsColorOn[6])
  1195. end
  1196.  
  1197. local function playerSelected(suspect)
  1198.     if suspect.speedKmh > 50 then
  1199.         pursuit.suspect = suspect
  1200.         pursuit.nextMessage = 30
  1201.         pursuit.level = 1
  1202.         local msgToSend = "Officer " .. DRIVER_NAME .. " is chasing you. Run! "
  1203.         pursuit.startedTime = settings.timeMsg
  1204.         pursuit.engage = true
  1205.         acpPolice{message = msgToSend, messageType = 0, yourIndex = ac.getCar(pursuit.suspect.index).sessionID}
  1206.         ac.setExtraSwitch(0, true)
  1207.     end
  1208. end
  1209.  
  1210. local function hudInChase()
  1211.     ui.pushDWriteFont("Orbitron;Weight=Black")
  1212.     ui.sameLine(20)
  1213.     ui.beginGroup()
  1214.     ui.newLine(1)
  1215.     local textPursuit = "LVL : " .. math.floor(pursuit.level/2)
  1216.     ui.dwriteTextWrapped(ac.getDriverName(pursuit.suspect.index) .. '\n'
  1217.                         .. string.gsub(string.gsub(ac.getCarName(pursuit.suspect.index), "%W", " "), "  ", "")
  1218.                         .. '\n' .. string.format("Speed: %d ", pursuit.suspect.speedKmh * settings.unitMult) .. settings.unit
  1219.                         .. '\n' .. textPursuit, settings.fontSize/2, rgbm.colors.white)
  1220.     ui.dummy(vec2(imageSize.x/5,imageSize.y/20))
  1221.     ui.newLine(30)
  1222.     ui.sameLine()
  1223.     if ui.button('Cancel Chase', vec2(imageSize.x/5, imageSize.y/20)) then
  1224.         lostSuspect()
  1225.     end
  1226.     ui.endGroup()
  1227.     ui.popDWriteFont()
  1228. end
  1229.  
  1230. local function drawText()
  1231.     ui.pushDWriteFont("Orbitron;Weight=Bold")
  1232.     ui.dwriteDrawText("RADAR ACTIVE", playerInRangeUI.fontSize.div_1_5, playerInRangeUI.header.radar.pos, rgbm.colors.red)
  1233.     ui.popDWriteFont()
  1234.     ui.pushDWriteFont("Orbitron;Weight=Regular")
  1235.     ui.dwriteDrawText("NEARBY VEHICULE SPEED SCANNING", playerInRangeUI.fontSize.div_2_5, playerInRangeUI.header.nearby.pos, rgbm.colors.white)
  1236.  
  1237.     playerInRangeUI.box.pos1.y = playerInRangeUI.header.nearby.pos.y + playerInRangeUI.fontSize.div_1_2
  1238.     playerInRangeUI.box.pos2.y = playerInRangeUI.box.pos1.y + playerInRangeUI.box.offsetY
  1239.     for i = 1, #playersInRange do
  1240.         playerInRangeUI.color = rgbm(1,1,1,0.7)
  1241.         ui.drawRect(playerInRangeUI.box.pos1, playerInRangeUI.box.pos2, rgbm(1,1,1,0.1), 1)
  1242.         if ui.rectHovered(playerInRangeUI.box.pos1, playerInRangeUI.box.pos2) then
  1243.             playerInRangeUI.color = rgbm(1,0,0,1)
  1244.             if ui.mouseClicked(0) then
  1245.                 playerSelected(playersInRange[i].player)
  1246.             end
  1247.         end
  1248.         playerInRangeUI.text.pos.x = (playerInRangeUI.window.size.x - ui.measureDWriteText(playersInRange[i].text, playerInRangeUI.fontSize.div_2).x) / 2
  1249.         playerInRangeUI.text.pos.y = playerInRangeUI.box.pos1.y + playerInRangeUI.text.offsetY
  1250.         ui.dwriteDrawText(playersInRange[i].text, settings.fontSize/2, vec2(playerInRangeUI.text.pos), playerInRangeUI.color)
  1251.         playerInRangeUI.box.pos1.y = playerInRangeUI.box.pos1.y + playerInRangeUI.box.offsetY
  1252.         playerInRangeUI.box.pos2.y = playerInRangeUI.box.pos1.y + playerInRangeUI.box.offsetY
  1253.     end
  1254.     ui.dummy(playerInRangeUI.box.pos1)
  1255.     ui.popDWriteFont()
  1256. end
  1257.  
  1258. local function radarUI()
  1259.     ui.toolWindow('radarText', playerInRangeUI.window.pos, playerInRangeUI.window.size, true, true, function ()
  1260.         if pursuit.suspect then hudInChase()
  1261.         else drawText() end
  1262.     end)
  1263.     ui.transparentWindow('radar', vec2(settings.hudOffset.x, settings.hudOffset.y), imageSize, true, function ()
  1264.         drawImage()
  1265.     end)
  1266. end
  1267.  
  1268. local function hidePlayers()
  1269.     local hideRange = 500
  1270.     for i = ac.getSim().carsCount - 1, 0, -1 do
  1271.         local playerCar = ac.getCar(i)
  1272.         if playerCar and playerCar.isConnected and ac.getCarBrand(i) ~= "DZST traffic" then
  1273.             if not isPoliceCar(ac.getCarID(i)) then
  1274.                 if playerCar.position.x > car.position.x - hideRange and playerCar.position.z > car.position.z - hideRange and playerCar.position.x < car.position.x + hideRange and playerCar.position.z < car.position.z + hideRange then
  1275.                     ac.hideCarLabels(i, false)
  1276.                 else
  1277.                     ac.hideCarLabels(i, true)
  1278.                 end
  1279.             end
  1280.         end
  1281.     end
  1282. end
  1283.  
  1284. local RADAR_RANGE = 250
  1285.  
  1286. local function radarUpdate()
  1287.     local previousSize = #playersInRange
  1288.     local j = 1
  1289.     for i, c in ac.iterateCars.serverSlots() do
  1290.       if not c.isHidingLabels and c.isConnected and not isPoliceCar(c:id()) then
  1291.             if c.position.x > car.position.x - RADAR_RANGE and c.position.z > car.position.z - RADAR_RANGE and c.position.x < car.position.x + RADAR_RANGE and c.position.z < car.position.z + RADAR_RANGE then
  1292.                 playersInRange[j] = {}
  1293.                 playersInRange[j].player = c
  1294.                 playersInRange[j].text = string.sub(ac.getDriverName(c.index), 1, 20) .. string.format(" - %d ", c.speedKmh * settings.unitMult) .. settings.unit
  1295.                 j = j + 1
  1296.                 if j == 9 then break end
  1297.             end
  1298.         end
  1299.     end
  1300.     for i = j, previousSize do playersInRange[i] = nil end
  1301. end
  1302.  
  1303. ---------------------------------------------------------------------------------------------- Chase ----------------------------------------------------------------------------------------------
  1304.  
  1305. local function inRange()
  1306.     local distance_x = pursuit.suspect.position.x - car.position.x
  1307.     local distance_z = pursuit.suspect.position.z - car.position.z
  1308.     local distanceSquared = distance_x * distance_x + distance_z * distance_z
  1309.     if(distanceSquared < pursuit.minDistance) then
  1310.         pursuit.enable = true
  1311.         pursuit.lostSight = false
  1312.         pursuit.timeLostSight = 2
  1313.     elseif (distanceSquared < pursuit.maxDistance) then resetChase()
  1314.     else
  1315.         if not pursuit.lostSight then
  1316.             pursuit.lostSight = true
  1317.             pursuit.timeLostSight = 2
  1318.         else
  1319.             pursuit.timeLostSight = pursuit.timeLostSight - ui.deltaTime()
  1320.             if pursuit.timeLostSight < 0 then lostSuspect() end
  1321.         end
  1322.     end
  1323. end
  1324.  
  1325. local function sendChatToSuspect()
  1326.     if pursuit.enable then
  1327.         if 0 < pursuit.nextMessage then
  1328.             pursuit.nextMessage = pursuit.nextMessage - ui.deltaTime()
  1329.         elseif pursuit.nextMessage < 0 then
  1330.             local nb = tostring(pursuit.level)
  1331.             acpPolice{message = nb, messageType = 1, yourIndex = ac.getCar(pursuit.suspect.index).sessionID}
  1332.             if pursuit.level < 10 then
  1333.                 pursuit.level = pursuit.level + 1
  1334.                 chaseLVL.messageTimer = settings.timeMsg
  1335.                 chaseLVL.message = "CHASE LEVEL " .. math.floor(pursuit.level/2)
  1336.                 if pursuit.level > 8 then
  1337.                     chaseLVL.color = rgbm.colors.red
  1338.                 elseif pursuit.level > 6 then
  1339.                     chaseLVL.color = rgbm.colors.orange
  1340.                 elseif pursuit.level > 4 then
  1341.                     chaseLVL.color = rgbm.colors.yellow
  1342.                 else
  1343.                     chaseLVL.color = rgbm.colors.white
  1344.                 end
  1345.             end
  1346.             pursuit.nextMessage = 30
  1347.         end
  1348.     end
  1349. end
  1350.  
  1351. local function showPursuitMsg()
  1352.     local text = ""
  1353.     if chaseLVL.messageTimer > 0 then
  1354.         chaseLVL.messageTimer = chaseLVL.messageTimer - ui.deltaTime()
  1355.         text = chaseLVL.message
  1356.     end
  1357.     if pursuit.startedTime > 0 then
  1358.         if pursuit.suspect then
  1359.             text = "You are chasing " .. ac.getDriverName(pursuit.suspect.index) .. " driving a " .. string.gsub(string.gsub(ac.getCarName(pursuit.suspect.index), "%W", " "), "  ", "") .. " ! Get him! "
  1360.         end
  1361.         if pursuit.startedTime > 6 then showPoliceLights() end
  1362.         if pursuit.engage and pursuit.startedTime < 8 then
  1363.             ac.sendChatMessage(formatMessage(MSG_ENGAGE[math.random(#MSG_ENGAGE)]))
  1364.             pursuit.engage = false
  1365.         end
  1366.     end
  1367.     if text ~= "" then
  1368.         local textLenght = ui.measureDWriteText(text, settings.fontSizeMSG)
  1369.         local rectPos1 = vec2(settings.msgOffset.x - textLenght.x/2, settings.msgOffset.y)
  1370.         local rectPos2 = vec2(settings.msgOffset.x + textLenght.x/2, settings.msgOffset.y + settings.fontSizeMSG)
  1371.         local rectOffset = vec2(10, 10)
  1372.         if ui.time() % 1 < 0.5 then
  1373.             ui.drawRectFilled(rectPos1 - vec2(10,0), rectPos2 + rectOffset, COLORSMSGBG, 10)
  1374.         else
  1375.             ui.drawRectFilled(rectPos1 - vec2(10,0), rectPos2 + rectOffset, rgbm(0,0,0,0.5), 10)
  1376.         end
  1377.         ui.dwriteDrawText(text, settings.fontSizeMSG, rectPos1, chaseLVL.color)
  1378.     end
  1379. end
  1380.  
  1381. local function arrestSuspect()
  1382.     if pursuit.hasArrested and pursuit.suspect then
  1383.         local msgToSend = formatMessage(MSG_ARREST[math.random(#MSG_ARREST)])
  1384.         table.insert(arrestations, msgToSend .. os.date("\nDate of the Arrestation: %c"))
  1385.         ac.sendChatMessage(msgToSend .. "\nPlease Get Back Pit, GG!")
  1386.         pursuit.id = pursuit.suspect.sessionID
  1387.         player.arrests = player.arrests and player.arrests + 1 or 1
  1388.         pursuit.startedTime = 0
  1389.         pursuit.suspect = nil
  1390.         pursuit.timerArrest = 1
  1391.         player:save()
  1392.     elseif pursuit.hasArrested then
  1393.         if pursuit.timerArrest > 0 then
  1394.             pursuit.timerArrest = pursuit.timerArrest - ui.deltaTime()
  1395.         else
  1396.             acpPolice{message = "BUSTED!", messageType = 2, yourIndex = pursuit.id}
  1397.             pursuit.timerArrest = 0
  1398.             pursuit.suspect = nil
  1399.             pursuit.id = -1
  1400.             pursuit.hasArrested = false
  1401.             pursuit.startedTime = 0
  1402.             pursuit.enable = false
  1403.             pursuit.level = 1
  1404.             pursuit.nextMessage = 20
  1405.             pursuit.lostSight = false
  1406.             pursuit.timeLostSight = 0
  1407.         end
  1408.     end
  1409. end
  1410.  
  1411. local function chaseUpdate()
  1412.     if pursuit.startedTime > 0 then pursuit.startedTime = pursuit.startedTime - ui.deltaTime()
  1413.     else pursuit.startedTime = 0 end
  1414.     if pursuit.suspect then
  1415.         sendChatToSuspect()
  1416.         inRange()
  1417.     end
  1418.     arrestSuspect()
  1419. end
  1420.  
  1421. ---------------------------------------------------------------------------------------------- Menu ----------------------------------------------------------------------------------------------
  1422.  
  1423. local function arrestLogsUI()
  1424.     ui.dwriteTextAligned("Arrestation Logs", 40, ui.Alignment.Center, ui.Alignment.Center, vec2(WINDOW_WIDTH/4,60), false, rgbm.colors.white)
  1425.     ui.drawLine(vec2(0,60), vec2(WINDOW_WIDTH/4,60), rgbm.colors.white, 1)
  1426.     ui.newLine(15)
  1427.     ui.sameLine(10)
  1428.     ui.beginGroup()
  1429.     local allMsg = ""
  1430.     ui.dwriteText("Click on the button next to the message you want to copy.", 15, rgbm.colors.white)
  1431.     ui.sameLine(WINDOW_WIDTH/4 - 120)
  1432.     if ui.button('Close', vec2(100, WINDOW_HEIGHT/50)) then arrestLogsOpen = false end
  1433.     for i = 1, #arrestations do
  1434.         if ui.smallButton("#" .. i .. ": ") then
  1435.             ui.setClipboardText(arrestations[i])
  1436.         end
  1437.         ui.sameLine()
  1438.         ui.dwriteTextWrapped(arrestations[i], 15, rgbm.colors.white)
  1439.     end
  1440.     if #arrestations == 0 then
  1441.         ui.dwriteText("No arrestation logs yet.", 15, rgbm.colors.white)
  1442.     end
  1443.     ui.newLine()
  1444.     if ui.button("Set all messages to ClipBoard") then
  1445.         for i = 1, #arrestations do
  1446.             allMsg = allMsg .. arrestations[i] .. "\n\n"
  1447.         end
  1448.         ui.setClipboardText(allMsg)
  1449.     end
  1450.     ui.endGroup()
  1451. end
  1452.  
  1453. local buttonPos = WINDOW_WIDTH/65
  1454.  
  1455. local function camerasUI()
  1456.     ui.dwriteTextAligned("Surveillance Cameras", 40, ui.Alignment.Center, ui.Alignment.Center, vec2(WINDOW_WIDTH/6.5,60), false, rgbm.colors.white)
  1457.     ui.drawLine(vec2(0,60), vec2(WINDOW_WIDTH/6.5,60), rgbm.colors.white, 1)
  1458.     ui.newLine(20)
  1459.     ui.beginGroup()
  1460.     ui.sameLine(buttonPos)
  1461.     if ui.button('Close', vec2(WINDOW_WIDTH/6.5 - buttonPos*2,30)) then camerasOpen = false end
  1462.     ui.newLine()
  1463.     for i = 1, #CAMERAS do
  1464.         local h = math.rad(CAMERAS[i].dir + ac.getCompassAngle(vec3(0, 0, 1)))
  1465.         ui.newLine()
  1466.         ui.sameLine(buttonPos)
  1467.         if ui.button(CAMERAS[i].name, vec2(WINDOW_WIDTH/6.5 - buttonPos*2,30)) then
  1468.             ac.setCurrentCamera(ac.CameraMode.Free)
  1469.             ac.setCameraPosition(CAMERAS[i].pos)
  1470.             ac.setCameraDirection(vec3(math.sin(h), 0, math.cos(h)))
  1471.             ac.setCameraFOV(CAMERAS[i].fov)
  1472.         end
  1473.     end
  1474.     if ac.getSim().cameraMode == ac.CameraMode.Free then
  1475.         ui.newLine()
  1476.         ui.newLine()
  1477.         ui.sameLine(buttonPos)
  1478.         if ui.button('Police car camera', vec2(WINDOW_WIDTH/6.5 - buttonPos*2,30)) then ac.setCurrentCamera(ac.CameraMode.Cockpit) end
  1479.     end
  1480. end
  1481.  
  1482.  
  1483. local menuSize = {vec2(WINDOW_WIDTH/4, WINDOW_HEIGHT/3), vec2(WINDOW_WIDTH/6.4, WINDOW_HEIGHT/2.2)}
  1484. local buttonPressed = false
  1485.  
  1486. local function moveMenu()
  1487.     if ui.windowHovered() and ui.mouseDown() then buttonPressed = true end
  1488.     if ui.mouseReleased() then buttonPressed = false end
  1489.     if buttonPressed then settings.menuPos = settings.menuPos + ui.mouseDelta() end
  1490. end
  1491.  
  1492. ---------------------------------------------------------------------------------------------- updates ----------------------------------------------------------------------------------------------
  1493.  
  1494. local initUiSize = false
  1495.  
  1496. function script.drawUI()
  1497.     if not shouldRun() then return end
  1498.     if not initUiSize then
  1499.         initsettings()
  1500.         initUiSize = true
  1501.     end
  1502.     radarUI()
  1503.     if pursuit.suspect then showStarsPursuit() end
  1504.     showPursuitMsg()
  1505.     if settingsOpen then
  1506.         ui.toolWindow('settings', settings.menuPos, menuSize[2], true, function ()
  1507.             ui.childWindow('childsettings', menuSize[2], true, function () settingsWindow() moveMenu() end)
  1508.         end)
  1509.     elseif arrestLogsOpen then
  1510.         ui.toolWindow('ArrestLogs', settings.menuPos, menuSize[1], true, function ()
  1511.             ui.childWindow('childArrestLogs', menuSize[1], true, function () arrestLogsUI() moveMenu() end)
  1512.         end)
  1513.     elseif camerasOpen then
  1514.         ui.toolWindow('Cameras', settings.menuPos, menuSize[2], true, function ()
  1515.             ui.childWindow('childCameras', menuSize[2], true, function () camerasUI() moveMenu() end)
  1516.         end)
  1517.     end
  1518. end
  1519.  
  1520. local function loadSettings()
  1521.     Settings.allocate(function(allocatedSetting)
  1522.         ac.log("Settings Allocated")
  1523.         settings = allocatedSetting
  1524.         dataLoaded['Settings'] = true
  1525.     end)
  1526. end
  1527.  
  1528. local function loadPlayerData()
  1529.     Player.allocate(function(allocatedPlayer)
  1530.         if allocatedPlayer then
  1531.             player = allocatedPlayer
  1532.             player:sortSectors()
  1533.             dataLoaded['PlayerData'] = true
  1534.             updateSharedPlayerData()
  1535.             ac.log(player.arrests)
  1536.         end
  1537.     end)
  1538. end
  1539.  
  1540. local delay = 1
  1541.  
  1542. function script.update(dt)
  1543.     if initialisation then
  1544.         initialisation = false
  1545.         loadSettings()
  1546.         loadPlayerData()
  1547.     end
  1548.     if not shouldRun() then return end
  1549.     if delay > 0 then delay = delay - dt end
  1550.     if delay < 0 then
  1551.         delay = 0
  1552.         updateSharedPlayerData()
  1553.         ac.broadcastSharedEvent(SHARED_EVENT_KEY, 'update')
  1554.     end
  1555.     radarUpdate()
  1556.     chaseUpdate()
  1557. end
  1558.  
  1559. ac.onCarJumped(0, function (carIndex)
  1560.     if isPoliceCar(CAR_ID) then
  1561.         if pursuit.suspect then lostSuspect() end
  1562.     end
  1563. end)
  1564.  
  1565. ui.registerOnlineExtra(ui.Icons.Settings, "Settings", nil, settingsWindow, nil, ui.OnlineExtraFlags.Tool, 'ui.WindowFlags.AlwaysAutoResize')
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement