297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
import { PlayerDropEvent } from "@modules/FxPlayerlist";
|
|
import { PDL_CRASH_REASON_CHAR_LIMIT, PDL_UNKNOWN_REASON_CHAR_LIMIT } from "./config";
|
|
|
|
const playerInitiatedRules = [
|
|
`exiting`,//basically only see this one
|
|
`disconnected.`, //need to keep the dot
|
|
`connecting to another server`,
|
|
`could not find requested level`,
|
|
`entering rockstar editor`,
|
|
`quit:`,
|
|
`reconnecting`,
|
|
`reloading game`,
|
|
];
|
|
|
|
//FIXME: remover essa categoria, checar com XXXX se os security podem vir como security ao invés de server-initiated
|
|
const serverInitiatedRules = [
|
|
//NOTE: this is not a real drop reason prefix, but something that the client sees only
|
|
`disconnected by server:`,
|
|
|
|
//NOTE: Happens only when doing "quit xxxxxx" in live console
|
|
`server shutting down:`,
|
|
|
|
//NOTE: Happens when txAdmin players - but soon specific player kicks (instead of kick all)
|
|
// will not fall under this category anymore
|
|
`[txadmin]`,
|
|
];
|
|
const timeoutRules = [
|
|
`server->client connection timed out`, //basically only see this one - FIXME: falta espaço?
|
|
`connection timed out`,
|
|
`timed out after 60 seconds`, //onesync timeout
|
|
];
|
|
const securityRules = [
|
|
`reliable network event overflow`,
|
|
`reliable network event size overflow:`,
|
|
`reliable server command overflow`,
|
|
`reliable state bag packet overflow`,
|
|
`unreliable network event overflow`,
|
|
`connection to cnl timed out`,
|
|
`server command overflow`,
|
|
`invalid client configuration. restart your game and reconnect`,
|
|
];
|
|
|
|
//The crash message prefix is translated, so we need to lookup the translations.
|
|
//This list covers the top 20 languages used in FiveM, with exceptions languages with no translations.
|
|
const crashRulesIntl = [
|
|
// (en) English - 18.46%
|
|
`game crashed: `,
|
|
// (pt) Portuguese - 15.67%
|
|
`o jogo crashou: `,
|
|
// (fr) French - 9.58%
|
|
`le jeu a cessé de fonctionner : `,
|
|
// (de) German - 9.15%
|
|
`spielabsturz: `,
|
|
// (es) Spanish - 6.08%
|
|
`el juego crasheó: `,
|
|
// (ar) Arabic - 6.04%
|
|
`تعطلت العبة: `,
|
|
// (nl) Dutch - 2.46%
|
|
`spel werkt niet meer: `,
|
|
// (tr) Turkish - 1.37%
|
|
`oyun çöktü: `,
|
|
// (hu) Hungarian - 1.31%
|
|
`a játék összeomlott: `,
|
|
// (it) Italian - 1.20%
|
|
`il gioco ha smesso di funzionare: `,
|
|
// (zh) Chinese - 1.02%
|
|
`游戏发生崩溃:`,
|
|
`遊戲已崩潰: `,
|
|
// (cs) Czech - 0.92%
|
|
`pád hry: `,
|
|
// (sv) Swedish - 0.69%
|
|
`spelet kraschade: `,
|
|
];
|
|
const exceptionPrefixesIntl = [
|
|
// (en) English - 18.46%
|
|
`unhandled exception: `,
|
|
// (pt) Portuguese - 15.67%
|
|
`exceção não tratada: `,
|
|
// (fr) French - 9.58%
|
|
`exception non-gérée : `,
|
|
// (de) German - 9.15%
|
|
`unbehandelte ausnahme: `,
|
|
// (es) Spanish - 6.08%
|
|
`excepción no manejada: `,
|
|
// (ar) Arabic - 6.04%
|
|
`استثناء غير معالج: `,
|
|
// (nl) Dutch - 2.46%
|
|
// NOTE: Dutch doesn't have a translation for "unhandled exception"
|
|
// (tr) Turkish - 1.37%
|
|
`i̇şlenemeyen özel durum: `,
|
|
// (hu) Hungarian - 1.31%
|
|
`nem kezelt kivétel: `,
|
|
// (it) Italian - 1.20%
|
|
`eccezione non gestita: `,
|
|
// (zh) Chinese - 1.02%
|
|
`未处理的异常:`,
|
|
`未處理的異常: `,
|
|
// (cs) Czech - 0.92%
|
|
`neošetřená výjimka: `,
|
|
// (sv) Swedish - 0.69%
|
|
`okänt fel: `,
|
|
];
|
|
|
|
const truncateReason = (reason: string, maxLength: number, prefix?: string) => {
|
|
prefix = prefix ? `${prefix} ` : '';
|
|
if (prefix) {
|
|
maxLength -= prefix.length;
|
|
}
|
|
const truncationSuffix = '[truncated]';
|
|
if (!reason.length) {
|
|
return prefix + '[tx:empty-reason]';
|
|
}else if (reason.length > maxLength) {
|
|
return prefix + reason.slice(0, maxLength - truncationSuffix.length) + truncationSuffix;
|
|
} else {
|
|
return prefix + reason;
|
|
}
|
|
}
|
|
|
|
const cleanCrashReason = (reason: string) => {
|
|
const cutoffIdx = reason.indexOf(': ') + 2;
|
|
const msg = reason.slice(cutoffIdx);
|
|
const exceptionPrefix = exceptionPrefixesIntl.find((prefix) => msg.toLocaleLowerCase().startsWith(prefix));
|
|
const saveMsg = exceptionPrefix
|
|
? 'Unhandled exception: ' + msg.slice(exceptionPrefix.length)
|
|
: msg;
|
|
return truncateReason(saveMsg, PDL_CRASH_REASON_CHAR_LIMIT);
|
|
}
|
|
|
|
/**
|
|
* Classifies a drop reason into a category, and returns a cleaned up version of it.
|
|
* The cleaned up version is truncated to a certain length.
|
|
* The cleaned up version of crash reasons have the prefix translated back into English.
|
|
*/
|
|
const guessDropReasonCategory = (reason: string): ClassifyDropReasonResponse => {
|
|
if (typeof reason !== 'string') {
|
|
return {
|
|
category: 'unknown' as const,
|
|
cleanReason: '[tx:invalid-reason]',
|
|
};
|
|
}
|
|
const reasonToMatch = reason.trim().toLocaleLowerCase();
|
|
|
|
if (!reasonToMatch.length) {
|
|
return {
|
|
category: 'unknown' as const,
|
|
cleanReason: '[tx:empty-reason]',
|
|
};
|
|
} else if (playerInitiatedRules.some((rule) => reasonToMatch.startsWith(rule))) {
|
|
return { category: 'player' };
|
|
} else if (serverInitiatedRules.some((rule) => reasonToMatch.startsWith(rule))) {
|
|
return { category: false };
|
|
} else if (timeoutRules.some((rule) => reasonToMatch.includes(rule))) {
|
|
return { category: 'timeout' };
|
|
} else if (securityRules.some((rule) => reasonToMatch.includes(rule))) {
|
|
return { category: 'security' };
|
|
} else if (crashRulesIntl.some((rule) => reasonToMatch.includes(rule))) {
|
|
return {
|
|
category: 'crash' as const,
|
|
cleanReason: cleanCrashReason(reason),
|
|
};
|
|
} else {
|
|
return {
|
|
category: 'unknown' as const,
|
|
cleanReason: truncateReason(reason, PDL_UNKNOWN_REASON_CHAR_LIMIT),
|
|
};
|
|
}
|
|
}
|
|
|
|
//NOTE: From fivem/code/components/citizen-server-impl/include/ClientDropReasons.h
|
|
export enum FxsDropReasonGroups {
|
|
//1 resource dropped the client
|
|
RESOURCE = 1,
|
|
//2 client initiated a disconnect
|
|
CLIENT,
|
|
//3 server initiated a disconnect
|
|
SERVER,
|
|
//4 client with same guid connected and kicks old client
|
|
CLIENT_REPLACED,
|
|
//5 server -> client connection timed out
|
|
CLIENT_CONNECTION_TIMED_OUT,
|
|
//6 server -> client connection timed out with pending commands
|
|
CLIENT_CONNECTION_TIMED_OUT_WITH_PENDING_COMMANDS,
|
|
//7 server shutdown triggered the client drop
|
|
SERVER_SHUTDOWN,
|
|
//8 state bag rate limit exceeded
|
|
STATE_BAG_RATE_LIMIT,
|
|
//9 net event rate limit exceeded
|
|
NET_EVENT_RATE_LIMIT,
|
|
//10 latent net event rate limit exceeded
|
|
LATENT_NET_EVENT_RATE_LIMIT,
|
|
//11 command rate limit exceeded
|
|
COMMAND_RATE_LIMIT,
|
|
//12 too many missed frames in OneSync
|
|
ONE_SYNC_TOO_MANY_MISSED_FRAMES,
|
|
};
|
|
|
|
const timeoutCategory = [
|
|
FxsDropReasonGroups.CLIENT_CONNECTION_TIMED_OUT,
|
|
FxsDropReasonGroups.CLIENT_CONNECTION_TIMED_OUT_WITH_PENDING_COMMANDS,
|
|
FxsDropReasonGroups.ONE_SYNC_TOO_MANY_MISSED_FRAMES,
|
|
];
|
|
const securityCategory = [
|
|
FxsDropReasonGroups.SERVER,
|
|
FxsDropReasonGroups.CLIENT_REPLACED,
|
|
FxsDropReasonGroups.STATE_BAG_RATE_LIMIT,
|
|
FxsDropReasonGroups.NET_EVENT_RATE_LIMIT,
|
|
FxsDropReasonGroups.LATENT_NET_EVENT_RATE_LIMIT,
|
|
FxsDropReasonGroups.COMMAND_RATE_LIMIT,
|
|
];
|
|
|
|
|
|
/**
|
|
* Classifies a drop reason into a category, and returns a cleaned up version of it.
|
|
* The cleaned up version is truncated to a certain length.
|
|
* The cleaned up version of crash reasons have the prefix translated back into English.
|
|
*/
|
|
export const classifyDrop = (payload: PlayerDropEvent): ClassifyDropReasonResponse => {
|
|
if (typeof payload.reason !== 'string') {
|
|
return {
|
|
category: 'unknown',
|
|
cleanReason: '[tx:invalid-reason]',
|
|
};
|
|
} else if (payload.category === undefined || payload.resource === undefined) {
|
|
return guessDropReasonCategory(payload.reason);
|
|
}
|
|
|
|
if (typeof payload.category !== 'number' || payload.category <= 0) {
|
|
return {
|
|
category: 'unknown',
|
|
cleanReason: truncateReason(
|
|
payload.reason,
|
|
PDL_UNKNOWN_REASON_CHAR_LIMIT,
|
|
'[tx:invalid-category]'
|
|
),
|
|
};
|
|
} else if (payload.category === FxsDropReasonGroups.RESOURCE) {
|
|
if (payload.resource === 'monitor') {
|
|
//if server shutting down, return ignore, otherwise return server-initiated
|
|
if (payload.reason === 'server_shutting_down') {
|
|
return { category: false };
|
|
} else {
|
|
return {
|
|
category: 'resource',
|
|
resource: 'txAdmin'
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
category: 'resource',
|
|
resource: payload.resource ? payload.resource : 'unknown',
|
|
}
|
|
}
|
|
} else if (payload.category === FxsDropReasonGroups.CLIENT) {
|
|
//check if it's crash
|
|
const reasonToMatch = payload.reason.trim().toLocaleLowerCase();
|
|
if (crashRulesIntl.some((rule) => reasonToMatch.includes(rule))) {
|
|
return {
|
|
category: 'crash',
|
|
cleanReason: cleanCrashReason(payload.reason),
|
|
}
|
|
} else {
|
|
return { category: 'player' };
|
|
}
|
|
} else if (timeoutCategory.includes(payload.category)) {
|
|
return { category: 'timeout' };
|
|
} else if (securityCategory.includes(payload.category)) {
|
|
return { category: 'security' };
|
|
} else if (payload.category === FxsDropReasonGroups.SERVER_SHUTDOWN) {
|
|
return { category: false };
|
|
} else {
|
|
return {
|
|
category: 'unknown',
|
|
cleanReason: truncateReason(
|
|
payload.reason,
|
|
PDL_UNKNOWN_REASON_CHAR_LIMIT,
|
|
'[tx:unknown-category]'
|
|
),
|
|
};
|
|
}
|
|
}
|
|
type SimpleDropCategory = 'player' | 'timeout' | 'security';
|
|
// type DetailedDropCategory = 'resource' | 'crash' | 'unknown';
|
|
// type DropCategories = SimpleDropCategory | DetailedDropCategory;
|
|
|
|
|
|
type ClassifyDropReasonResponse = {
|
|
category: SimpleDropCategory;
|
|
} | {
|
|
category: 'resource';
|
|
resource: string;
|
|
} | {
|
|
category: 'crash' | 'unknown';
|
|
cleanReason: string;
|
|
} | {
|
|
category: false; //server shutting down, ignore
|
|
}
|