local type = type local error = error local pairs = pairs local rawget = rawget local tonumber = tonumber local getmetatable = getmetatable local setmetatable = setmetatable local debug = debug local debug_getinfo = debug.getinfo local table_pack = table.pack local table_unpack = table.unpack local table_insert = table.insert local coroutine_yield = coroutine.yield local coroutine_running = coroutine.running --[[ Custom extensions --]] local msgpack = msgpack local msgpack_pack = msgpack.pack local msgpack_unpack = msgpack.unpack local msgpack_pack_args = msgpack.pack_args local Citizen = Citizen local Citizen_InvokeFunctionReference = Citizen.InvokeFunctionReference local isDuplicityVersion = IsDuplicityVersion() -- setup msgpack compat msgpack.set_string('string_compat') msgpack.set_integer('unsigned') msgpack.set_array('without_hole') msgpack.setoption('empty_table_as_array', true) -- setup json compat json.version = json._VERSION -- Version compatibility json.setoption("empty_table_as_array", true) json.setoption('with_hole', true) -- temp local _in = Citizen.InvokeNative local function FormatStackTrace() return _in(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) end local boundaryIdx = 1 local function dummyUseBoundary(idx) return nil end local function getBoundaryFunc(bfn, bid) return function(fn, ...) local boundary = bid if not boundary then boundary = boundaryIdx + 1 boundaryIdx = boundary end bfn(boundary, coroutine_running()) local wrap = function(...) dummyUseBoundary(boundary) local v = table_pack(fn(...)) return table_unpack(v) end local v = table_pack(wrap(...)) bfn(boundary, nil) return table_unpack(v) end end local runWithBoundaryStart = getBoundaryFunc(Citizen.SubmitBoundaryStart) local runWithBoundaryEnd = getBoundaryFunc(Citizen.SubmitBoundaryEnd) local AwaitSentinel = Citizen.AwaitSentinel() Citizen.AwaitSentinel = nil function Citizen.Await(promise) local coro = coroutine_running() assert(coro, "Current execution context is not in the scheduler, you should use CreateThread / SetTimeout or Event system (AddEventHandler) to be able to Await") if promise.state == 0 then local reattach = coroutine_yield(AwaitSentinel) promise:next(reattach, reattach) coroutine_yield() end if promise.state == 2 or promise.state == 4 then error(promise.value, 2) end return promise.value end Citizen.SetBoundaryRoutine(function(f) boundaryIdx = boundaryIdx + 1 local bid = boundaryIdx return bid, function() return runWithBoundaryStart(f, bid) end end) -- root-level alias (to prevent people from calling the game's function accidentally) Wait = Citizen.Wait CreateThread = Citizen.CreateThread SetTimeout = Citizen.SetTimeout ClearTimeout = Citizen.ClearTimeout --[[ Event handling ]] local eventHandlers = {} Citizen.SetEventRoutine(function(eventName, eventPayload, eventSource) -- set the event source local lastSource = _G.source _G.source = eventSource -- try finding an event handler for the event local eventHandlerEntry = eventHandlers[eventName] -- deserialize the event structure (so that we end up adding references to delete later on) local data = msgpack_unpack(eventPayload) if eventHandlerEntry and eventHandlerEntry.handlers then -- if this is a net event and we don't allow this event to be triggered from the network, return if eventSource:sub(1, 3) == 'net' then if not eventHandlerEntry.safeForNet then Citizen.Trace('event ' .. eventName .. " was not safe for net\n") _G.source = lastSource return end _G.source = tonumber(eventSource:sub(5)) elseif isDuplicityVersion and eventSource:sub(1, 12) == 'internal-net' then _G.source = tonumber(eventSource:sub(14)) end -- return an empty table if the data is nil if not data then data = {} end -- if this is a table... if type(data) == 'table' then -- loop through all the event handlers for _, handler in pairs(eventHandlerEntry.handlers) do local handlerFn = handler local handlerMT = getmetatable(handlerFn) if handlerMT and handlerMT.__call then handlerFn = handlerMT.__call end if type(handlerFn) == 'function' then local di = debug_getinfo(handlerFn) Citizen.CreateThreadNow(function() handler(table_unpack(data)) end, ('event %s [%s[%d..%d]]'):format(eventName, di.short_src, di.linedefined, di.lastlinedefined)) end end end end _G.source = lastSource end) local stackTraceBoundaryIdx Citizen.SetStackTraceRoutine(function(bs, ts, be, te) if not ts then ts = runningThread end local t local n = 0 local frames = {} local skip = false if bs then skip = true end repeat if ts then t = debug_getinfo(ts, n, 'nlfS') else t = debug_getinfo(n + 1, 'nlfS') end if t then if t.name == 'wrap' and t.source == '@citizen:/scripting/lua/scheduler.lua' then if not stackTraceBoundaryIdx then local b local u = 1 repeat b = debug.getupvalue(t.func, u) if b == 'boundary' then break end u = u + 1 until not b stackTraceBoundaryIdx = u end local _, boundary = debug.getupvalue(t.func, stackTraceBoundaryIdx) if boundary == bs then skip = false end if boundary == be then break end end if not skip then if t.source and t.source:sub(1, 1) ~= '=' and t.source:sub(1, 10) ~= '@citizen:/' then table_insert(frames, { file = t.source:sub(2), line = t.currentline, name = t.name or '[global chunk]' }) end end n = n + 1 end until not t return msgpack_pack(frames) end) local eventKey = 10 function AddEventHandler(eventName, eventRoutine) local tableEntry = eventHandlers[eventName] if not tableEntry then tableEntry = { } eventHandlers[eventName] = tableEntry end if not tableEntry.handlers then tableEntry.handlers = { } end eventKey = eventKey + 1 tableEntry.handlers[eventKey] = eventRoutine RegisterResourceAsEventHandler(eventName) return { key = eventKey, name = eventName } end function RemoveEventHandler(eventData) if not eventData or not eventData.key or not eventData.name then error('Invalid event data passed to RemoveEventHandler()', 2) end -- remove the entry eventHandlers[eventData.name].handlers[eventData.key] = nil end local ignoreNetEvent = { ['__cfx_internal:commandFallback'] = true, } function RegisterNetEvent(eventName, cb) if not ignoreNetEvent[eventName] then local tableEntry = eventHandlers[eventName] if not tableEntry then tableEntry = { } eventHandlers[eventName] = tableEntry end tableEntry.safeForNet = true end if cb then return AddEventHandler(eventName, cb) end end function TriggerEvent(eventName, ...) local payload = msgpack_pack_args(...) return runWithBoundaryEnd(function() return TriggerEventInternal(eventName, payload, payload:len()) end) end if isDuplicityVersion then function TriggerClientEvent(eventName, playerId, ...) local payload = msgpack_pack_args(...) return TriggerClientEventInternal(eventName, playerId, payload, payload:len()) end function TriggerLatentClientEvent(eventName, playerId, bps, ...) local payload = msgpack_pack_args(...) return TriggerLatentClientEventInternal(eventName, playerId, payload, payload:len(), tonumber(bps)) end RegisterServerEvent = RegisterNetEvent RconPrint = Citizen.Trace GetPlayerEP = GetPlayerEndpoint RconLog = function() end function GetPlayerIdentifiers(player) local numIds = GetNumPlayerIdentifiers(player) local t = {} for i = 0, numIds - 1 do table_insert(t, GetPlayerIdentifier(player, i)) end return t end function GetPlayerTokens(player) local numIds = GetNumPlayerTokens(player) local t = {} for i = 0, numIds - 1 do table_insert(t, GetPlayerToken(player, i)) end return t end function GetPlayers() local num = GetNumPlayerIndices() local t = {} for i = 0, num - 1 do table_insert(t, GetPlayerFromIndex(i)) end return t end local httpDispatch = {} AddEventHandler('__cfx_internal:httpResponse', function(token, status, body, headers, errorData) if httpDispatch[token] then local userCallback = httpDispatch[token] httpDispatch[token] = nil userCallback(status, body, headers, errorData) end end) function PerformHttpRequest(url, cb, method, data, headers, options) local followLocation = true if options and options.followLocation ~= nil then followLocation = options.followLocation end local t = { url = url, method = method or 'GET', data = data or '', headers = headers or {}, followLocation = followLocation } local id = PerformHttpRequestInternalEx(t) if id ~= -1 then httpDispatch[id] = cb else cb(0, nil, {}, 'Failure handling HTTP request') end end function PerformHttpRequestAwait(url, method, data, headers, options) local p = promise.new() PerformHttpRequest(url, function(...) p:resolve({...}) end, method, data, headers, options) Citizen.Await(p) return table.unpack(p.value) end else function TriggerServerEvent(eventName, ...) local payload = msgpack_pack_args(...) return TriggerServerEventInternal(eventName, payload, payload:len()) end function TriggerLatentServerEvent(eventName, bps, ...) local payload = msgpack_pack_args(...) return TriggerLatentServerEventInternal(eventName, payload, payload:len(), tonumber(bps)) end end local funcRefs = {} local funcRefIdx = 0 local function MakeFunctionReference(func) local thisIdx = funcRefIdx funcRefs[thisIdx] = { func = func, refs = 0 } funcRefIdx = funcRefIdx + 1 local refStr = Citizen.CanonicalizeRef(thisIdx) return refStr end function Citizen.GetFunctionReference(func) if type(func) == 'function' then return MakeFunctionReference(func) elseif type(func) == 'table' and rawget(func, '__cfx_functionReference') then return MakeFunctionReference(function(...) return func(...) end) end return nil end local function doStackFormat(err) local fst = FormatStackTrace() -- already recovering from an error if not fst then return nil end return string.format('^1SCRIPT ERROR: %s^7\n%s', err or '', fst) end Citizen.SetCallRefRoutine(function(refId, argsSerialized) local refPtr = funcRefs[refId] if not refPtr then Citizen.Trace('Invalid ref call attempt: ' .. refId .. "\n") return msgpack_pack(nil) end local ref = refPtr.func local err local retvals = false local cb = {} local di = debug_getinfo(ref) local waited = Citizen.CreateThreadNow(function() local status, result = xpcall(function() retvals = { ref(table_unpack(msgpack_unpack(argsSerialized))) } end, doStackFormat) if not status then err = result or '' end if cb.cb then cb.cb(retvals, err) elseif err then Citizen.Trace(err) end end, ('ref call [%s[%d..%d]]'):format(di.short_src, di.linedefined, di.lastlinedefined)) if not waited then if err then return msgpack_pack(nil) end return msgpack_pack(retvals) else return msgpack_pack({{ __cfx_async_retval = function(rvcb) cb.cb = rvcb end }}) end end) Citizen.SetDuplicateRefRoutine(function(refId) local ref = funcRefs[refId] if ref then ref.refs = ref.refs + 1 return refId end return -1 end) Citizen.SetDeleteRefRoutine(function(refId) local ref = funcRefs[refId] if ref then ref.refs = ref.refs - 1 if ref.refs <= 0 then funcRefs[refId] = nil end end end) local EXT_FUNCREF = 10 local EXT_LOCALFUNCREF = 11 msgpack.extend_clear(EXT_FUNCREF, EXT_LOCALFUNCREF) local funcref_mt = nil funcref_mt = msgpack.extend({ __gc = function(t) DeleteFunctionReference(rawget(t, '__cfx_functionReference')) end, __index = function(t, k) error('Cannot index a funcref', 2) end, __newindex = function(t, k, v) error('Cannot set indexes on a funcref', 2) end, __call = function(t, ...) local ref = rawget(t, '__cfx_functionReference') local args = msgpack_pack_args(...) -- as Lua doesn't allow directly getting lengths from a data buffer, and _s will zero-terminate, we have a wrapper in the game itself local rv = runWithBoundaryEnd(function() return Citizen_InvokeFunctionReference(ref, args) end) local rvs = msgpack_unpack(rv) -- handle async retvals from refs if rvs and type(rvs[1]) == 'table' and rawget(rvs[1], '__cfx_async_retval') and coroutine_running() then local p = promise.new() rvs[1].__cfx_async_retval(function(r, e) if r then p:resolve(r) elseif e then p:reject(e) end end) return table_unpack(Citizen.Await(p)) end if not rvs then error() end return table_unpack(rvs) end, __ext = EXT_FUNCREF, __pack = function(self, tag) local refstr = Citizen.GetFunctionReference(self) if refstr then return refstr else error(("Unknown funcref type: %d %s"):format(tag, type(self)), 2) end end, __unpack = function(data, tag) local ref = data -- add a reference DuplicateFunctionReference(ref) local tbl = { __cfx_functionReference = ref } tbl = setmetatable(tbl, funcref_mt) return tbl end, }) --[[ Also initialize unpackers for local function references --]] msgpack.extend({ __ext = EXT_LOCALFUNCREF, __pack = funcref_mt.__pack, __unpack = funcref_mt.__unpack, }) msgpack.settype("function", EXT_FUNCREF) -- exports compatibility local function getExportEventName(resource, name) if resource == "txAdmin" then resource = "monitor" end return string.format('__cfx_export_%s_%s', resource, name) end -- callback cache to avoid extra call to serialization / deserialization process at each time getting an export local exportsCallbackCache = {} local exportKey = (isDuplicityVersion and 'server_export' or 'export') do local resource = GetCurrentResourceName() local numMetaData = GetNumResourceMetadata(resource, exportKey) or 0 for i = 0, numMetaData-1 do local exportName = GetResourceMetadata(resource, exportKey, i) AddEventHandler(getExportEventName(resource, exportName), function(setCB) -- get the entry from *our* global table and invoke the set callback if _G[exportName] then setCB(_G[exportName]) end end) end end -- Remove cache when resource stop to avoid calling unexisting exports local function lazyEventHandler() -- lazy initializer so we don't add an event we don't need AddEventHandler(('on%sResourceStop'):format(isDuplicityVersion and 'Server' or 'Client'), function(resource) exportsCallbackCache[resource] = {} end) lazyEventHandler = function() end end -- Helper for newlines in nested error message local function prefixNewlines(str, prefix) str = tostring(str) if #str == 0 then return str end return prefix .. str:gsub("\n(.)", "\n" .. prefix .. "%1") end -- Handle an export with multiple return values. local function exportProcessResult(resource, exportName, status, ...) if not status then local result = tostring(select(1, ...)) if result:len() > 2048 then result = result:sub(1, 1024) .. '\n... [large output partially truncated] ...\n' .. result:sub(-1024) end error(('\n^5 An error occurred while calling export `%s` in resource `%s`:\n%s\n^5 ---'):format(exportName, resource, prefixNewlines(result, ' ')), 2) end return ... end -- invocation bit exports = {} setmetatable(exports, { __index = function(t, k) local resource = k return setmetatable({}, { __index = function(t2, k2) lazyEventHandler() if not k2 or type(k2) ~= 'string' then error('Invalid export name: ' .. tostring(k2), 2) end if not exportsCallbackCache[resource] then exportsCallbackCache[resource] = {} end if not exportsCallbackCache[resource][k2] then TriggerEvent(getExportEventName(resource, k2), function(exportData) exportsCallbackCache[resource][k2] = exportData end) if not exportsCallbackCache[resource][k2] then error('No such export ' .. k2 .. ' in resource ' .. resource, 2) end end return function(self, ...) -- TAILCALL return exportProcessResult(resource, k2, pcall(exportsCallbackCache[resource][k2], ...)) end end, __newindex = function(t, k, v) error('cannot set values on an export resource', 2) end }) end, __newindex = function(t, k, v) error('cannot set values on exports', 2) end, __call = function(t, exportName, func) AddEventHandler(getExportEventName(GetCurrentResourceName(), exportName), function(setCB) setCB(func) end) end }) -- NUI callbacks if not isDuplicityVersion then local origRegisterNuiCallback = RegisterNuiCallback local cbHandler --[==[ local cbHandler = load([[ -- Lua 5.4: Create a to-be-closed variable to monitor the NUI callback handle. local callback, body, resultCallback = ... local hasCallback = false local _ = defer(function() if not hasCallback then local di = debug.getinfo(callback, 'S') local name = ('function %s[%d..%d]'):format(di.short_src, di.linedefined, di.lastlinedefined) warn(("No NUI callback captured: %s"):format(name)) end end) local status, err = pcall(function() callback(body, function(...) hasCallback = true resultCallback(...) end) end) return status, err ]], '@citizen:/scripting/lua/scheduler.lua#nui')]==] if not cbHandler then cbHandler = load([[ local callback, body, resultCallback = ... local status, err = pcall(function() callback(body, resultCallback) end) return status, err ]], '@citizen:/scripting/lua/scheduler.lua#nui') end -- wrap RegisterNuiCallback to handle errors (and 'missed' callbacks) function RegisterNuiCallback(type, callback) origRegisterNuiCallback(type, function(body, resultCallback) local _, err = cbHandler(callback, body, resultCallback) if err then Citizen.Trace("error during NUI callback " .. type .. ": " .. tostring(err) .. "\n") end end) end -- 'old' function (uses events for compatibility, as people may have relied on this implementation detail) function RegisterNUICallback(type, callback) RegisterNuiCallbackType(type) AddEventHandler('__cfx_nui:' .. type, function(body, resultCallback) local _, err = cbHandler(callback, body, resultCallback) if err then Citizen.Trace("error during NUI callback " .. type .. ": " .. tostring(err) .. "\n") end end) end local _sendNuiMessage = SendNuiMessage function SendNUIMessage(message) _sendNuiMessage(json.encode(message)) end end -- entity helpers local EXT_ENTITY = 41 local EXT_PLAYER = 42 msgpack.extend_clear(EXT_ENTITY, EXT_PLAYER) local function NewStateBag(es) return setmetatable({}, { __index = function(_, s) if s == 'set' then return function(_, s2, v2, r2) local payload = msgpack_pack(v2) SetStateBagValue(es, s2, payload, payload:len(), r2) end end return GetStateBagValue(es, s) end, __newindex = function(_, s, v) local payload = msgpack_pack(v) SetStateBagValue(es, s, payload, payload:len(), isDuplicityVersion) end }) end GlobalState = NewStateBag('global') local function GetEntityStateBagId(entityGuid) if isDuplicityVersion or NetworkGetEntityIsNetworked(entityGuid) then return ('entity:%d'):format(NetworkGetNetworkIdFromEntity(entityGuid)) else EnsureEntityStateBag(entityGuid) return ('localEntity:%d'):format(entityGuid) end end local entityMT entityMT = { __index = function(t, s) if s == 'state' then local es = GetEntityStateBagId(t.__data) if isDuplicityVersion then EnsureEntityStateBag(t.__data) end return NewStateBag(es) end return nil end, __newindex = function() error('Setting values on Entity is not supported at this time.', 2) end, __ext = EXT_ENTITY, __pack = function(self, t) return tostring(NetworkGetNetworkIdFromEntity(self.__data)) end, __unpack = function(data, t) local ref = NetworkGetEntityFromNetworkId(tonumber(data)) return setmetatable({ __data = ref }, entityMT) end } msgpack.extend(entityMT) local playerMT playerMT = { __index = function(t, s) if s == 'state' then local pid = t.__data if pid == -1 then pid = GetPlayerServerId(PlayerId()) end local es = ('player:%d'):format(pid) return NewStateBag(es) end return nil end, __newindex = function() error('Setting values on Player is not supported at this time.', 2) end, __ext = EXT_PLAYER, __pack = function(self, t) return tostring(self.__data) end, __unpack = function(data, t) local ref = tonumber(data) return setmetatable({ __data = ref }, playerMT) end } msgpack.extend(playerMT) function Entity(ent) if type(ent) == 'number' then return setmetatable({ __data = ent }, entityMT) end return ent end function Player(ent) if type(ent) == 'number' or type(ent) == 'string' then return setmetatable({ __data = tonumber(ent) }, playerMT) end return ent end if not isDuplicityVersion then LocalPlayer = Player(-1) end