Files

923 lines
22 KiB
JavaScript
Raw Permalink Normal View History

2026-03-29 21:41:17 +03:00
// CFX JS runtime
/// <reference path="./natives_server.d.ts" />
const EXT_FUNCREF = 10;
const EXT_LOCALFUNCREF = 11;
(function (global) {
let boundaryIdx = 1;
let lastBoundaryStart = null;
const isDuplicityVersion = IsDuplicityVersion();
const currentResourceName = GetCurrentResourceName();
// temp
global.FormatStackTrace = function (args, argLength) {
return Citizen.invokeNativeByHash(0, 0xd70c3bca, args, argLength, Citizen.resultAsString());
}
function getBoundaryFunc(pushFunc, id) {
return (func, ...args) => {
const boundary = id || (boundaryIdx++);
pushFunc(boundary);
function wrap(...args) {
return func(...args);
}
Object.defineProperty(wrap, 'name', { writable: true });
wrap.name = `__cfx_wrap_${boundary}`;
return wrap.call(boundary, ...args);
};
}
global.runWithBoundaryStart = getBoundaryFunc(boundary => {
Citizen.submitBoundaryStart(boundary);
lastBoundaryStart = boundary;
});
const runWithBoundaryEnd = getBoundaryFunc(Citizen.submitBoundaryEnd);
let refIndex = 0;
const nextRefIdx = () => refIndex++;
const refFunctionsMap = new Map();
const codec = msgpack.createCodec({
uint8array: true,
preset: false,
binarraybuffer: true
});
const pack = data => msgpack.encode(data, { codec });
const unpack = data => msgpack.decode(data, { codec });
// store for use by natives.js
global.msgpack_pack = pack;
global.msgpack_unpack = unpack;
/**
* @param {Function} refFunction
* @returns {string}
*/
Citizen.makeRefFunction = (refFunction) => {
const ref = nextRefIdx();
refFunctionsMap.set(ref, {
callback: refFunction,
refCount: 0
});
return Citizen.canonicalizeRef(ref);
};
/**
* @param {Function} refFunction
* @returns {string|null} the function reference, or null if the refFunction that was passed wasn't a function
*/
Citizen.getRefFunction = (refFunction) => {
if (typeof refFunction !== "function") {
return null;
}
return Citizen.makeRefFunction(refFunction);
}
function refFunctionPacker(refFunction) {
const ref = Citizen.makeRefFunction(refFunction);
return ref;
}
function refFunctionUnpacker(refSerialized) {
const fnRef = Citizen.makeFunctionReference(refSerialized);
const invoker = GetInvokingResource();
return function (...args) {
return runWithBoundaryEnd(() => {
let retvals = null;
try {
retvals = unpack(fnRef(pack(args)));
} catch (e) {
}
if (retvals === null) {
let errorMessage = `Error in nested ref call for ${currentResourceName}. `
// invoker can be null, we don't want to give an even worse
// error by erroring here :P
if (invoker) {
errorMessage += `${currentResourceName} tried to call a function reference in ${invoker} but the reference wasn't valid. `
if (GetResourceState(invoker) !== "started") {
errorMessage += `And ${invoker} isn't started, was the resource restarted mid call?`
} else {
errorMessage += `(did ${invoker} restart recently?)`
}
}
throw new Error(errorMessage);
}
switch (retvals.length) {
case 0:
return undefined;
case 1:
const rv = retvals[0];
if (rv && rv['__cfx_async_retval']) {
return new Promise((res, rej) => {
rv['__cfx_async_retval']((v, e) => {
if (e != null)
rej(e);
else {
switch(v.length) {
case 0:
res(undefined);
case 1:
res(v[0]);
default:
res(v);
}
}
});
});
}
return rv;
default:
return retvals;
}
});
};
}
const AsyncFunction = (async () => {}).constructor;
codec.addExtPacker(EXT_FUNCREF, AsyncFunction, refFunctionPacker);
codec.addExtPacker(EXT_FUNCREF, Function, refFunctionPacker);
codec.addExtUnpacker(EXT_FUNCREF, refFunctionUnpacker);
codec.addExtUnpacker(EXT_LOCALFUNCREF, refFunctionUnpacker);
/**
* Deletes ref function
*
* @param {int} ref
*/
Citizen.setDeleteRefFunction(function (ref) {
if (refFunctionsMap.has(ref)) {
const data = refFunctionsMap.get(ref);
if (--data.refCount <= 0) {
refFunctionsMap.delete(ref);
}
}
});
/**
* Invokes ref function
*
* @param {int} ref
* @param {UInt8Array} args
*/
Citizen.setCallRefFunction(function (ref, argsSerialized) {
if (!refFunctionsMap.has(ref)) {
console.error('Invalid ref call attempt:', ref);
return pack([]);
}
try {
return runWithBoundaryStart(() => {
const rv = refFunctionsMap.get(ref).callback(...unpack(argsSerialized));
if (rv instanceof Promise) {
return pack([{'__cfx_async_retval': (cb) => {
rv.then(v => {
if (cb != null)
cb([v], null);
}).catch(err => {
if (cb != null) {
let msg = '';
if (err) {
if (err.message) {
msg = err.message.toString();
} else {
msg = err.toString();
}
}
cb(null, msg);
}
});
}}]);
}
return pack([rv]);
});
} catch (e) {
global.printError('call ref', e);
return pack(null);
}
});
/**
* Duplicates ref function
*
* @param {int} ref
*/
Citizen.setDuplicateRefFunction(function (ref) {
if (refFunctionsMap.has(ref)) {
const refFunction = refFunctionsMap.get(ref);
++refFunction.refCount;
return ref;
}
return -1;
});
// Events
const emitter = new EventEmitter2();
const rawEmitter = new EventEmitter2();
const netSafeEventNames = new Set();
// Raw events
global.addRawEventListener = rawEmitter.on.bind(rawEmitter);
global.addRawEventHandler = global.addRawEventListener;
// Raw events configuration
global.setMaxRawEventListeners = rawEmitter.setMaxListeners.bind(rawEmitter);
// Client events
global.addEventListener = (name, callback, netSafe = false) => {
if (netSafe) {
netSafeEventNames.add(name);
}
RegisterResourceAsEventHandler(name);
emitter.on(name, callback);
};
global.on = global.addEventListener;
// Event Emitter configuration
global.setMaxEventListeners = emitter.setMaxListeners.bind(emitter);
// Net events
global.addNetEventListener = (name, callback) => global.addEventListener(name, callback, true);
global.onNet = global.addNetEventListener;
global.removeEventListener = emitter.off.bind(emitter);
// Convenience aliases for Lua similarity
global.AddEventHandler = global.addEventListener;
global.RegisterNetEvent = (name) => void netSafeEventNames.add(name);
global.RegisterServerEvent = global.RegisterNetEvent;
global.RemoveEventHandler = global.removeEventListener;
// Event triggering
global.emit = (name, ...args) => {
const dataSerialized = pack(args);
runWithBoundaryEnd(() => {
TriggerEventInternal(name, dataSerialized, dataSerialized.length);
});
};
global.TriggerEvent = global.emit;
if (isDuplicityVersion) {
global.emitNet = (name, source, ...args) => {
const dataSerialized = pack(args);
TriggerClientEventInternal(name, source, dataSerialized, dataSerialized.length);
};
global.TriggerClientEvent = global.emitNet;
global.TriggerLatentClientEvent = (name, source, bps, ...args) => {
const dataSerialized = pack(args);
TriggerLatentClientEventInternal(name, source, dataSerialized, dataSerialized.length, bps);
};
global.getPlayerIdentifiers = (player) => {
const numIds = GetNumPlayerIdentifiers(player);
let t = [];
for (let i = 0; i < numIds; i++) {
t[i] = GetPlayerIdentifier(player, i);
}
return t;
};
global.getPlayerTokens = (player) => {
const numIds = GetNumPlayerTokens(player);
let t = [];
for (let i = 0; i < numIds; i++) {
t[i] = GetPlayerToken(player, i);
}
return t;
};
global.getPlayers = () => {
const num = GetNumPlayerIndices();
let t = [];
for (let i = 0; i < num; i++) {
t[i] = GetPlayerFromIndex(i);
}
return t;
};
} else {
global.SendNUIMessage = (data) => {
const dataJson = JSON.stringify(data)
SendNuiMessage(dataJson)
}
global.emitNet = (name, ...args) => {
const dataSerialized = pack(args);
TriggerServerEventInternal(name, dataSerialized, dataSerialized.length);
};
global.TriggerServerEvent = global.emitNet;
global.TriggerLatentServerEvent = (name, bps, ...args) => {
const dataSerialized = pack(args);
TriggerLatentServerEventInternal(name, dataSerialized, dataSerialized.length, bps);
};
}
let currentStackDumpError = null;
function prepareStackTrace(error, trace) {
const frames = [];
let skip = false;
if (error.bs) {
skip = true;
}
if (!error.be) {
error.be = lastBoundaryStart;
}
for (const frame of trace) {
const functionName = frame.methodName;
if (functionName && functionName.startsWith('__cfx_wrap_')) {
const boundary = functionName.substring('__cfx_wrap_'.length) | 0;
if (boundary == error.bs) {
skip = false;
}
if (boundary == error.be) {
break;
}
}
if (skip) {
continue;
}
const fn = frame.file;
if (fn && !fn.startsWith('citizen:/')) {
const isConstruct = false;
const isEval = false;
const isNative = false;
const methodName = functionName;
const type = frame.typeName;
let frameName = '';
if (isNative) {
frameName = 'native';
} else if (isEval) {
frameName = `eval at ${frame.getEvalOrigin()}`;
} else if (isConstruct) {
frameName = `new ${functionName}`;
} else if (methodName && functionName && methodName !== functionName) {
frameName = `${type}${functionName} [as ${methodName}]`;
} else if (methodName || functionName) {
frameName = `${type}${functionName ? functionName : methodName}`;
}
frames.push({
file: fn,
line: frame.lineNumber | 0,
name: frameName
});
}
}
return frames;
}
class StackDumpError {
constructor(bs, be) {
this.bs = bs;
this.be = be;
Error.captureStackTrace(this);
}
}
function getError(where, e) {
const stackBlob = global.msgpack_pack(prepareStackTrace(e, parseStack(e.stack)));
const fst = global.FormatStackTrace(stackBlob, stackBlob.length);
if (fst !== null && fst !== undefined) {
return '^1SCRIPT ERROR in ' + where + ': ' + e.toString() + "^7\n" + fst;
}
return '';
}
global.printError = function (where, e) {
console.log(getError(where, e));
}
Citizen.setStackTraceFunction(function (bs, be) {
const sde = new StackDumpError(bs, be);
const rv = pack(prepareStackTrace(sde, parseStack(sde.stack)));
return rv;
});
let errorQueue = [];
function processErrorQueue() {
for (const error of errorQueue) {
console.log(getError('promise (unhandled rejection)', error.error));
}
errorQueue = [];
}
Citizen.setUnhandledPromiseRejectionFunction(function (event, promise, value) {
// unhandled
// we might get a `1` in which case it actually was handled and we should.. un-print
if (event === 0) {
let error = '';
if (value instanceof Error) {
error = value;
} else {
error = new Error((value || '').toString());
}
// grab the stack early so it'll remain valid
const stack = error.stack;
errorQueue.push({
error,
stack,
promise
});
global.setImmediate(processErrorQueue);
} else if (event === 1) {
errorQueue = errorQueue.filter(a => a.promise !== promise);
}
});
/**
* @param {string} name
* @param {UInt8Array} payloadSerialized
* @param {string} source
*/
Citizen.setEventFunction(function (name, payloadSerialized, source) {
runWithBoundaryStart(() => {
global.source = source;
if (source.startsWith('net')) {
if (emitter.listeners(name).length > 0 && !netSafeEventNames.has(name)) {
console.error(`Event ${name} was not safe for net`);
global.source = null;
return;
}
global.source = parseInt(source.substr(4));
} else if (isDuplicityVersion && source.startsWith('internal-net')) {
global.source = parseInt(source.substr(13));
}
// Running raw event listeners
try {
rawEmitter.emit(name, payloadSerialized, source);
} catch (e) {
console.error('Unhandled error during running raw event listeners', e);
}
const listeners = emitter.listeners(name);
if (listeners.length == 0) {
global.source = null;
return;
}
const payload = unpack(payloadSerialized) || [];
if(!Array.isArray(payload)) {
global.source = null;
return;
}
// Running normal event listeners
for (const listener of listeners) {
try {
const retval = listener.apply(null, payload);
if (retval instanceof Promise) {
(async () => {
try {
await retval;
} catch (e) {
console.error('Unhandled promise failure:', e);
}
})();
}
} catch (e) {
global.printError('event `' + name + '\'', e);
}
}
global.source = null;
});
});
// Compatibility layer for legacy exports
const exportsCallbackCache = {};
const exportKey = isDuplicityVersion ? 'server_export' : 'export';
const eventType = isDuplicityVersion ? 'Server' : 'Client';
const getExportEventName = (resource, name) => {
if(resource == "txAdmin") {
resource = "monitor";
}
return `__cfx_export_${resource}_${name}`;
}
on(`on${eventType}ResourceStart`, (resource) => {
if (resource === currentResourceName) {
const numMetaData = GetNumResourceMetadata(resource, exportKey) || 0;
for (let i = 0; i < numMetaData; i++) {
const exportName = GetResourceMetadata(resource, exportKey, i);
on(getExportEventName(resource, exportName), (setCB) => {
if (global[exportName]) {
setCB(global[exportName]);
}
});
}
}
});
on(`on${eventType}ResourceStop`, (resource) => {
exportsCallbackCache[resource] = {};
});
// export invocation
const createExports = () => {
return new Proxy(() => { }, {
get(t, k) {
const resource = k;
return new Proxy({}, {
get(t, k) {
if (!exportsCallbackCache[resource]) {
exportsCallbackCache[resource] = {};
}
if (!exportsCallbackCache[resource][k]) {
emit(getExportEventName(resource, k), (exportData) => {
exportsCallbackCache[resource][k] = exportData;
});
if (!exportsCallbackCache[resource][k]) {
throw new Error(`No such export ${k} in resource ${resource}`);
}
}
return (...args) => {
try {
return exportsCallbackCache[resource][k](...args);
} catch (e) {
throw new Error(`An error occurred while calling export ${k} of resource ${resource} - see above for details`);
}
};
},
set() {
throw new Error('cannot set values on an export resource');
}
});
},
apply(t, self, args) {
if (args.length !== 2) {
throw new Error('this needs 2 arguments');
}
const [exportName, func] = args;
on(getExportEventName(currentResourceName, exportName), (setCB) => {
setCB(func);
});
},
set() {
throw new Error('cannot set values on exports');
}
});
};
global.exports = createExports();
const EXT_ENTITY = 41;
const EXT_PLAYER = 42;
global.NewStateBag = (es) => {
return new Proxy({}, {
get(_, k) {
if (k === 'set') {
return (s, v, r) => {
const payload = msgpack_pack(v);
SetStateBagValue(es, s, payload, payload.length, r);
};
}
return GetStateBagValue(es, k);
},
set(_, k, v) {
const payload = msgpack_pack(v);
SetStateBagValue(es, k, payload, payload.length, isDuplicityVersion);
return true; // If the set() method returns false, and the assignment happened in strict-mode code, a TypeError will be thrown.
},
});
};
global.GlobalState = NewStateBag('global');
function getEntityStateBagId(entityGuid) {
if (isDuplicityVersion || NetworkGetEntityIsNetworked(entityGuid)) {
return `entity:${NetworkGetNetworkIdFromEntity(entityGuid)}`;
} else {
EnsureEntityStateBag(entityGuid);
return `localEntity:${entityGuid}`;
}
}
const entityTM = {
get(t, k) {
if (k === 'state') {
const es = getEntityStateBagId(t.__data);
if (isDuplicityVersion) {
EnsureEntityStateBag(t.__data);
}
return NewStateBag(es);
}
return null;
},
set() {
throw new Error('Not allowed at this time.');
},
__ext: EXT_ENTITY,
__pack: () => {
return String(NetworkGetNetworkIdFromEntity(this.__data));
},
__unpack: (data, t) => {
const ref = NetworkGetEntityFromNetworkId(Number(data));
return new Proxy({ __data: ref }, entityTM);
},
};
const playerTM = {
get(t, k) {
if (k === 'state') {
const pid = t.__data === -1 ? GetPlayerServerId(PlayerId()) : t.__data;
const es = `player:${pid}`;
return NewStateBag(es);
}
return null;
},
set() {
throw new Error('Not allowed at this time.');
},
__ext: EXT_PLAYER,
__pack: () => {
return String(this.__data);
},
__unpack: (data, t) => {
const ref = Number(data);
return new Proxy({ __data: ref }, playerTM);
},
};
global.Entity = (ent) => {
if (typeof ent === 'number') {
return new Proxy({ __data: ent }, entityTM);
}
return ent;
};
global.Player = (ent) => {
if (typeof ent === 'number' || typeof ent === 'string') {
return new Proxy({ __data: Number(ent) }, playerTM);
}
return ent;
};
if (!isDuplicityVersion) {
global.LocalPlayer = Player(-1);
}
/*
BEGIN
https://github.com/errwischt/stacktrace-parser/blob/0121cc6e7d57495437818676f6b69be7d34c2fa7/src/stack-trace-parser.js
MIT License
Copyright (c) 2014-2019 Georg Tavonius
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const UNKNOWN_FUNCTION = '<unknown>';
/**
* This parses the different stack traces and puts them into one format
* This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)
*/
function parseStack(stackString) {
const lines = stackString.split('\n');
return lines.reduce((stack, line) => {
const parseResult =
parseChrome(line) ||
parseWinjs(line) ||
parseGecko(line) ||
parseNode(line) ||
parseJSC(line);
if (parseResult) {
stack.push(parseResult);
}
return stack;
}, []);
}
const chromeRe = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
const chromeEvalRe = /\((\S*)(?::(\d+))(?::(\d+))\)/;
function parseChrome(line) {
const parts = chromeRe.exec(line);
if (!parts) {
return null;
}
const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
const submatch = chromeEvalRe.exec(parts[2]);
if (isEval && submatch != null) {
// throw out eval line/column and use top-most line/column number
parts[2] = submatch[1]; // url
parts[3] = submatch[2]; // line
parts[4] = submatch[3]; // column
}
const methodParts = (parts[1] || UNKNOWN_FUNCTION).split(/\./, 2);
const typeName = methodParts.length == 2 ? (methodParts[0] + '.') : '';
const methodName = methodParts[methodParts.length - 1];
return {
file: !isNative ? parts[2] : null,
methodName,
typeName,
arguments: isNative ? [parts[2]] : [],
lineNumber: parts[3] ? +parts[3] : null,
column: parts[4] ? +parts[4] : null,
};
}
const winjsRe = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
function parseWinjs(line) {
const parts = winjsRe.exec(line);
if (!parts) {
return null;
}
return {
file: parts[2],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: [],
lineNumber: +parts[3],
column: parts[4] ? +parts[4] : null,
};
}
const geckoRe = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i;
const geckoEvalRe = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
function parseGecko(line) {
const parts = geckoRe.exec(line);
if (!parts) {
return null;
}
const isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
const submatch = geckoEvalRe.exec(parts[3]);
if (isEval && submatch != null) {
// throw out eval line/column and use top-most line number
parts[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = null; // no column when eval
}
return {
file: parts[3],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: parts[2] ? parts[2].split(',') : [],
lineNumber: parts[4] ? +parts[4] : null,
column: parts[5] ? +parts[5] : null,
};
}
const javaScriptCoreRe = /^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;
function parseJSC(line) {
const parts = javaScriptCoreRe.exec(line);
if (!parts) {
return null;
}
return {
file: parts[3],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: [],
lineNumber: +parts[4],
column: parts[5] ? +parts[5] : null,
};
}
const nodeRe = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
function parseNode(line) {
const parts = nodeRe.exec(line);
if (!parts) {
return null;
}
const methodParts = (parts[1] || UNKNOWN_FUNCTION).split(/\./, 2);
const typeName = methodParts.length == 2 ? (methodParts[0] + '.') : '';
const methodName = methodParts[methodParts.length - 1];
return {
file: parts[2],
typeName,
methodName,
arguments: [],
lineNumber: +parts[3],
column: parts[4] ? +parts[4] : null,
};
}
// END
})(this || globalThis);