rock-mod
Version:
Rock-Mod is a powerful framework designed for creating and managing mods for Grand Theft Auto (GTA) games.
154 lines (153 loc) • 5.98 kB
JavaScript
/// <reference types="@classic-mp/types/client" />
/**
* Имя приватного rock-mod события для зеркалирования клиентских `console.*`
* вызовов в серверный stdout.
*
* Серверная сторона (`src/server/net/ccmp/CCMPNetManager.ts`) ловит это
* событие и печатает payload через свой `console.log`/`error`/`warn`. Это
* критически важно для отладки CCMP: на клиенте `console.log` идёт в игровую
* консоль (F8), и разработчик в серверном терминале не видит ничего.
*/
const CLIENT_LOG_EVENT = "rm::clientLog";
const FORWARDED_LEVELS = ["log", "info", "warn", "error", "debug"];
function safeJsonStringify(value) {
try {
return JSON.stringify(value, Object.getOwnPropertyNames(value));
}
catch (_a) {
return "[unjsonable]";
}
}
function safeToString(value) {
try {
const s = String(value);
return s === "[object Object]" ? safeJsonStringify(value) : s;
}
catch (_a) {
return "[unstringifiable]";
}
}
function safeReadString(value, key) {
try {
const v = value[key];
return typeof v === "string" ? v : undefined;
}
catch (_a) {
return undefined;
}
}
function looksLikeError(value) {
if (value instanceof Error) {
return true;
}
if (typeof value !== "object" || value === null) {
return false;
}
const v = value;
// Структурные признаки. Хотя бы один из: stack (V8 ставит на ошибки),
// или name+message (классический duck-type).
return typeof v["stack"] === "string" || (typeof v["name"] === "string" && typeof v["message"] === "string");
}
/**
* Превращает аргумент в JSON-сериализуемое значение.
*
* Проблема: `JSON.stringify(new Error("..."))` возвращает `"{}"` — у Error
* свойства `message`/`stack` не enumerable. Плюс под deno_core встречаются
* "ошибки" не наследующие `Error.prototype` (cross-realm, ops_throw,
* NotImplementedProxy через Proxy traps), для которых `instanceof Error`
* возвращает false, но визуально они ошибки.
*
* Стратегия:
* 1. Скаляры — как есть.
* 2. Если есть `.message`/`.stack` / похожее на Error — пакуем в
* `__error`-обёртку с явно перечисленными полями.
* 3. Объекты — пробуем `JSON.stringify`. Если пусто (`{}`/`[]`) или падает
* — добавляем `__toString` с `String(value)`.
* 4. Худший случай — голая строка `String(value)`.
*/
function serializeArg(value) {
var _a, _b;
if (value === null || value === undefined) {
return value;
}
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") {
return value;
}
if (looksLikeError(value)) {
return {
__error: true,
name: (_a = safeReadString(value, "name")) !== null && _a !== void 0 ? _a : "Error",
message: (_b = safeReadString(value, "message")) !== null && _b !== void 0 ? _b : safeToString(value),
stack: safeReadString(value, "stack"),
toString: safeToString(value),
};
}
try {
const json = JSON.stringify(value);
if (json && json !== "{}" && json !== "[]") {
return value;
}
// Пустой JSON для непустого объекта — non-enumerable props. Дублируем
// через String(...) чтобы на сервере хоть что-то прочиталось.
return {
__toString: safeToString(value),
__raw: value,
};
}
catch (_c) {
return {
__toString: safeToString(value),
};
}
}
/**
* Создаёт обёртку для конкретного level. Извлечено в именованную функцию,
* чтобы избежать `no-loop-func` (захват `ccmp` в замыкании внутри `for`).
*/
function createForwardingMethod(level, original) {
return (...args) => {
try {
original(...args);
}
catch (_a) {
// Локальный консоль-вывод не должен ломать форвард.
}
try {
const payload = {
level,
args: args.map(serializeArg),
};
ccmp.emitServer(CLIENT_LOG_EVENT, payload);
}
catch (_b) {
// Если сервер недоступен (раннее в boot до handshake) — молча.
}
};
}
/**
* Оборачивает `globalThis.console` так, чтобы каждый вызов
* `console.log/info/warn/error/debug` дополнительно отправлялся на сервер.
* Оригинальное поведение (вывод в локальный игровой лог) сохраняется.
*
* Идемпотентно: повторный `install()` ничего не делает.
*/
export class CCMPConsoleForwarder {
constructor() {
this._installed = false;
}
install() {
var _a;
if (this._installed) {
return;
}
this._installed = true;
for (const level of FORWARDED_LEVELS) {
const original = (_a = console[level]) === null || _a === void 0 ? void 0 : _a.bind(console);
if (!original) {
continue;
}
console[level] = createForwardingMethod(level, original);
}
}
}