5180 fichiers 38.5 Mo 0 téléchargements 231 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / components / wired-tools / WiredCreatorTools.helpers.ts
WiredCreatorTools.helpers.ts 11.6 Ko Télécharger le fichier
import type { HotelDateTimeParts, MonitorErrorLog, MonitorLog, MonitorRoomStats, MonitorSnapshot } from './WiredCreatorTools.types';

const HOTEL_TIME_FORMATTERS: Map<string, Intl.DateTimeFormat> = new Map();

export const createEmptyMonitorSnapshot = (): MonitorSnapshot => ({
    usageCurrentWindow: 0,
    usageLimitPerWindow: 0,
    isHeavy: false,
    delayedEventsPending: 0,
    delayedEventsLimit: 0,
    averageExecutionMs: 0,
    peakExecutionMs: 0,
    recursionDepthCurrent: 0,
    recursionDepthLimit: 0,
    killedRemainingSeconds: 0,
    usageWindowMs: 0,
    overloadAverageThresholdMs: 0,
    overloadPeakThresholdMs: 0,
    heavyUsageThresholdPercent: 0,
    heavyConsecutiveWindowsThreshold: 0,
    overloadConsecutiveWindowsThreshold: 0,
    heavyDelayedThresholdPercent: 0,
    logs: [],
    history: [],
    roomStats: null,
    errors: []
});

/** AIR WiredMenuMonitorTab: statistic colours, and the share of the limit where each one starts. */
export const WIRED_MONITOR_COLOR = { GREEN: '008000', ORANGE: 'BD7800', RED: 'ff5733' } as const;
export const WIRED_MONITOR_THRESHOLDS = {
    usage: [0.3, 0.7],
    furni: [0.6, 0.85],
    variables: [0.5, 0.8]
} as const;

/** Official colorize(): green below the first share of the limit, orange below the second, red from there (and at 0/0). */
export const wiredMonitorColor = (amount: number, limit: number, thresholds: readonly [number, number]): string => {
    const share = amount / limit;

    if (share < thresholds[0]) return WIRED_MONITOR_COLOR.GREEN;
    if (share < thresholds[1]) return WIRED_MONITOR_COLOR.ORANGE;

    return WIRED_MONITOR_COLOR.RED;
};

/** The permanent furni variable cap of a Habbo room, used until the server sends its own. */
export const WIRED_MONITOR_DEFAULT_PERMANENT_FURNI_VARIABLES_LIMIT = 23;

/** AIR WiredMenuMonitorTab.CLEAR_LOGS_TIMEOUT: "Clear all" stays disabled this long after a clear. */
export const WIRED_MONITOR_CLEAR_LOGS_TIMEOUT_MS = 4000;

/**
 * The room counters the monitor shows: the server's room stats packet (AIR 1891) once one arrived (`hasVariableTotals` then also allows
 * the permanent user and global lines), otherwise what the room engine holds, against the room item limit.
 * Floor and wall furni are counted apart, as AIR does.
 */
export const resolveMonitorRoomCounters = (
    roomStats: MonitorRoomStats | null,
    local: { floorCount: number; wallCount: number; itemLimit: number; permanentFurniVariables: number }
): MonitorRoomStats & { hasVariableTotals: boolean } => {
    if (roomStats) return { ...roomStats, hasVariableTotals: true };

    const itemLimit = Math.max(0, local.itemLimit || 0);

    return {
        floorCount: local.floorCount,
        floorLimit: itemLimit,
        wallCount: local.wallCount,
        wallLimit: itemLimit,
        permanentFurniVariables: local.permanentFurniVariables,
        permanentFurniVariablesLimit: WIRED_MONITOR_DEFAULT_PERMANENT_FURNI_VARIABLES_LIMIT,
        permanentUserVariables: 0,
        permanentUserVariablesLimit: 0,
        permanentGlobalVariables: 0,
        permanentGlobalVariablesLimit: 0,
        hasVariableTotals: false
    };
};

/**
 * An official statistics line ("Floor furni: <font color=\"#008000\">12/2500</font>") split into its label and the
 * value the font tag colours, so the line renders without its HTML.
 */
export const splitMonitorStatLine = (line: string): { label: string; value: string } => {
    const text = line ?? '';
    const strip = (value: string) => value.replace(/<[^>]*>/g, '').trim();
    const index = text.search(/<font\b/i);

    if (index < 0) return { label: strip(text).replace(/:$/, ''), value: '' };

    return { label: strip(text.slice(0, index)).replace(/:$/, '').trim(), value: strip(text.slice(index)) };
};

export const getHotelTimeFormatter = (timeZone: string): Intl.DateTimeFormat => {
    const formatterTimeZone = timeZone || 'UTC';
    const existingFormatter = HOTEL_TIME_FORMATTERS.get(formatterTimeZone);

    if (existingFormatter) return existingFormatter;

    let formatter: Intl.DateTimeFormat = null;

    try {
        formatter = new Intl.DateTimeFormat('en-GB', {
            timeZone: formatterTimeZone,
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hourCycle: 'h23'
        });
    } catch {
        formatter = new Intl.DateTimeFormat('en-GB', {
            timeZone: 'UTC',
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hourCycle: 'h23'
        });
    }

    HOTEL_TIME_FORMATTERS.set(formatterTimeZone, formatter);

    return formatter;
};

export const getHotelDateTimeParts = (epochMs: number, timeZone: string): HotelDateTimeParts => {
    const normalizedEpochMs = Number.isFinite(epochMs) ? epochMs : Date.now();
    const date = new Date(normalizedEpochMs);
    const formatter = getHotelTimeFormatter(timeZone);
    const formattedParts = formatter.formatToParts(date);
    const partsMap = new Map<string, string>();

    for (const part of formattedParts) {
        if (part.type === 'literal') continue;

        partsMap.set(part.type, part.value);
    }

    return {
        year: Number(partsMap.get('year') ?? date.getUTCFullYear()),
        month: Number(partsMap.get('month') ?? date.getUTCMonth() + 1),
        day: Number(partsMap.get('day') ?? date.getUTCDate()),
        hour: Number(partsMap.get('hour') ?? date.getUTCHours()),
        minute: Number(partsMap.get('minute') ?? date.getUTCMinutes()),
        second: Number(partsMap.get('second') ?? date.getUTCSeconds()),
        millisecond: ((normalizedEpochMs % 1000) + 1000) % 1000
    };
};

export const formatMonitorLatestOccurrence = (latestOccurrenceSeconds: number, nowMs: number): string => {
    if (latestOccurrenceSeconds <= 0) return '/';

    const diffMs = Math.max(0, nowMs - latestOccurrenceSeconds * 1000);
    const diffSeconds = Math.floor(diffMs / 1000);

    if (diffSeconds < 5) return 'Just now';
    if (diffSeconds < 60) return `${diffSeconds}s ago`;

    const diffMinutes = Math.floor(diffSeconds / 60);

    if (diffMinutes < 60) return `${diffMinutes}m ago`;

    const diffHours = Math.floor(diffMinutes / 60);

    if (diffHours < 24) return `${diffHours}h ago`;

    const diffDays = Math.floor(diffHours / 24);

    return `${diffDays}d ago`;
};

export const formatMonitorHistoryOccurrence = (occurredAtSeconds: number): string => {
    if (occurredAtSeconds <= 0) return '/';

    return new Date(occurredAtSeconds * 1000).toLocaleString('en-GB');
};

export const formatVariableTimestamp = (timestamp: number): string => {
    if (!timestamp || timestamp <= 0) return '/';

    return new Date(timestamp * 1000).toLocaleString('en-GB');
};

export const formatMonitorSource = (sourceLabel: string, sourceId: number): string => {
    const normalizedLabel = (sourceLabel || '').trim();

    if (!normalizedLabel && !(sourceId > 0)) return 'Room monitor';
    if (sourceId > 0) return `${normalizedLabel || 'wired'} (#${sourceId})`;

    return normalizedLabel;
};

export const normalizeMonitorReason = (reason: string): string => {
    const normalizedReason = (reason || '').trim();

    return normalizedReason || 'No detailed reason was recorded for this entry.';
};

/** AIR calls a placement failure PLACEMENT_FAILURE; our diagnostics (the 5101 detail, the history) call it PLACEMENT_FAILED. */
const MONITOR_DIAGNOSTIC_TYPE_OF_ERROR: Record<string, string> = { PLACEMENT_FAILURE: 'PLACEMENT_FAILED' };

/** The diagnostic type behind a row of AIR's error list. */
export const monitorDiagnosticType = (errorName: string): string => MONITOR_DIAGNOSTIC_TYPE_OF_ERROR[errorName] ?? errorName;

/**
 * AIR's error list (15) as the store keeps it: the server sends the milliseconds since the latest occurrence, so the
 * time is pinned to the client clock when the packet arrives; -1 (never) stays -1.
 */
export const toMonitorErrorLogs = (
    errors: ReadonlyArray<{ errorId: number; errorName: string; category: string; amount: number; msSinceLatest: number }>,
    receivedAtMs: number
): MonitorErrorLog[] =>
    (errors ?? []).map((error) => ({
        errorId: Number(error.errorId ?? 0),
        name: String(error.errorName ?? ''),
        category: String(error.category ?? ''),
        amount: Number(error.amount ?? 0),
        latestOccurredAtMs: Number(error.msSinceLatest ?? -1) < 0 ? -1 : receivedAtMs - Number(error.msSinceLatest)
    }));

/**
 * The monitor log table: AIR's rows in the server's order (type, category, amount, latest), each with the detail of its
 * latest occurrence from our monitor packet (5101) for the info window.
 */
export const buildMonitorLogs = (errors: ReadonlyArray<MonitorErrorLog>, details: MonitorSnapshot['logs'], nowMs: number): MonitorLog[] =>
    (errors ?? []).map((error) => {
        const detail = (details ?? []).find((entry) => entry.type === monitorDiagnosticType(error.name));
        const amount = Math.max(0, error.amount);

        return {
            errorId: error.errorId,
            type: error.name,
            category: error.category,
            amount: String(amount),
            latest: amount > 0 && error.latestOccurredAtMs >= 0 ? formatMonitorLatestOccurrence(error.latestOccurredAtMs / 1000, nowMs) : '/',
            latestReason: normalizeMonitorReason(detail?.latestReason),
            latestSourceLabel: String(detail?.latestSourceLabel ?? ''),
            latestSourceId: Number(detail?.latestSourceId ?? 0)
        };
    });

/** AIR's info text of an error (`wiredmenu.error_info.<id>`, one paragraph per line break) or, without it, ours. */
export const resolveMonitorErrorDescription = (officialText: string, fallback: string[] = []): string[] => {
    const paragraphs = (officialText ?? '')
        .split(/\\n|\n/)
        .map((paragraph) => paragraph.trim())
        .filter((paragraph) => paragraph.length > 0);

    return paragraphs.length ? paragraphs : fallback;
};

/** A pointer (AIR requiresExecutionContext) only exists while a stack runs: nobody holds it, so it cannot be highlighted or managed. */
export const isStackOnlyVariable = (definition?: { requiresExecutionContext?: boolean } | null): boolean => !!definition?.requiresExecutionContext;

/** Official WiredMenuOverviewTab: at most 1000 holders highlighted, 400 when each shows a value. */
export const WIRED_VARIABLE_HIGHLIGHT_MAX = 1000;
export const WIRED_VARIABLE_HIGHLIGHT_MAX_WITH_VALUE = 400;

/** Whether a variable has too many holders to highlight (`wiredmenu.variable_overview.highlight.error.too_many`). */
export const exceedsVariableHighlightLimit = (holderCount: number, hasValue: boolean): boolean =>
    holderCount > (hasValue ? WIRED_VARIABLE_HIGHLIGHT_MAX_WITH_VALUE : WIRED_VARIABLE_HIGHLIGHT_MAX);

/** Official VariableTypePicker: the furni / users / global / context buttons are titled `wiredfurni.params.sourcetype.*`. */
export const wiredSourceTypeLabelKey = (type: string): string => `wiredfurni.params.sourcetype.${type === 'user' ? 'users' : type}`;

const WIRED_INT_MIN = -2147483648;
const WIRED_INT_MAX = 2147483647;

/** A variable value as the server stores it: a whole number held inside the int32 range, so what is shown is what is saved. */
export const clampWiredVariableValue = (value: number): number =>
    Number.isFinite(value) ? Math.min(WIRED_INT_MAX, Math.max(WIRED_INT_MIN, Math.trunc(value))) : 0;

/** A typed variable value, NaN when the text holds no number (the caller then cancels the edit). */
export const parseWiredVariableValue = (raw: string): number => {
    const parsed = parseInt(raw.trim(), 10);

    return Number.isNaN(parsed) ? Number.NaN : clampWiredVariableValue(parsed);
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser