diff --git a/gui/OverlayUtils.js b/gui/OverlayUtils.js index cae157b..f9e5c0f 100644 --- a/gui/OverlayUtils.js +++ b/gui/OverlayUtils.js @@ -307,6 +307,26 @@ class OverlayUtils { return this.setTrackedValue(idName, key, current + amount); } + getSessionSnapshot(idName) { + const overlay = this.ids.find((id) => id.name === idName); + const sections = (overlay?.sections || []).map((section) => { + const data = {}; + Object.entries(section.data || {}).forEach(([key, value]) => { + data[key] = typeof value === 'function' ? value() : value; + }); + + return { + title: section.title, + data, + }; + }); + + return { + trackedValues: { ...this.resolveTrackedValues(idName) }, + sections, + }; + } + getSessionElapsedMs(idName) { const startedAt = this.startTimes[idName]; if (startedAt !== undefined) { diff --git a/utils/Config.js b/utils/Config.js index 91cbb07..c27f369 100644 --- a/utils/Config.js +++ b/utils/Config.js @@ -103,6 +103,7 @@ const manifest = { 'OverlayPositions/music_overlay.json': {}, 'webhook.json': {}, 'miningstats.json': {}, + 'module_history.json': { version: 1, sessions: [] }, 'GemstoneRoutes/empty.json': {}, 'RoutewalkerRoutes/empty.json': {}, 'TunnelMinerRoutes/empty.json': {}, diff --git a/utils/ModuleBase.js b/utils/ModuleBase.js index b39842b..5a5d662 100644 --- a/utils/ModuleBase.js +++ b/utils/ModuleBase.js @@ -4,6 +4,7 @@ import { Categories } from '../gui/categories/CategorySystem'; import { Chat } from './Chat'; import { KeyBindUtils } from './Constants'; import { MacroState } from './MacroState'; +import { ModuleHistory } from './ModuleHistory'; import { Mixin } from './MixinManager'; import { ScheduleTask } from './ScheduleTask'; import { manager } from './SkyblockEvents'; @@ -160,6 +161,17 @@ export class ModuleBase { OverlayManager.startTime(this.oid, this.isMacro); } + if (this.isMacro) { + ModuleHistory.startSession(this.name, { + overlayId: this.oid, + category: this.subcategory, + isMacro: this.isMacro, + parentManaged: this.isParentManaged, + toggleContext, + getOverlayData: () => (this.oid ? OverlayManager.getSessionSnapshot(this.oid) : null), + }); + } + try { this.onEnable(); } catch (e) { @@ -173,6 +185,10 @@ export class ModuleBase { Mixin.set('macroEnabled', MacroState.isMacroRunning()); } + if (this.isMacro) { + ModuleHistory.endSession(this.name, toggleContext); + } + if (this.oid) { if (this.isMacro) { OverlayManager.pauseTime(this.oid); diff --git a/utils/ModuleHistory.js b/utils/ModuleHistory.js new file mode 100644 index 0000000..f4db6c4 --- /dev/null +++ b/utils/ModuleHistory.js @@ -0,0 +1,153 @@ +import { Utils } from './Utils'; + +const HISTORY_FILE = 'module_history.json'; +const SAMPLE_INTERVAL_MS = 60 * 1000; + +class ModuleHistoryTracker { + constructor() { + this.history = this.loadHistory(); + this.activeSessions = {}; + this.recoverStaleSessions(); + + register('step', () => this.sampleDueSessions()).setFps(1); + register('gameUnload', () => this.endAllSessions('game_unload')); + } + + loadHistory() { + const data = Utils.getConfigFile(HISTORY_FILE); + return data.sessions ? data : { version: 1, sessions: [] }; + } + + recoverStaleSessions() { + let changed = false; + + this.history.sessions.forEach((session) => { + if (!session || !session.active) return; + + session.minuteData = session.minuteData || []; + const lastPoint = session.minuteData[session.minuteData.length - 1]; + const endedAtMs = lastPoint?.timestampMs || session.enabledAtMs; + + session.active = false; + session.disabledAtMs = endedAtMs; + session.disabledAt = this.toIso(endedAtMs); + session.durationMs = Math.max(0, endedAtMs - (session.enabledAtMs || endedAtMs)); + session.endReason = 'startup_recovery'; + session.minuteData.push({ + type: 'end', + minute: Math.floor(endedAtMs / SAMPLE_INTERVAL_MS), + timestampMs: endedAtMs, + timestamp: session.disabledAt, + elapsedMs: session.durationMs, + overlayData: session.overlayData, + }); + changed = true; + }); + + if (changed) this.saveHistory(); + } + + startSession(moduleName, options = {}) { + if (!moduleName || !options.isMacro) return; + + if (this.activeSessions[moduleName]) { + this.endSession(moduleName, 'restart'); + } + + const now = Date.now(); + const session = { + id: `${moduleName}-${now}`, + module: moduleName, + overlayId: options.overlayId || null, + category: options.category || null, + isMacro: options.isMacro === true, + parentManaged: options.parentManaged === true, + toggleContext: options.toggleContext || 'user', + enabledAtMs: now, + enabledAt: this.toIso(now), + disabledAtMs: null, + disabledAt: null, + durationMs: 0, + active: true, + overlayData: null, + minuteData: [], + }; + + this.history.sessions.push(session); + this.activeSessions[moduleName] = { + session, + getOverlayData: options.getOverlayData, + nextSampleAt: now, + }; + + this.recordSample(moduleName, now, 'start'); + this.activeSessions[moduleName].nextSampleAt = now + SAMPLE_INTERVAL_MS; + this.saveHistory(); + } + + sampleDueSessions() { + const now = Date.now(); + let changed = false; + + Object.keys(this.activeSessions).forEach((moduleName) => { + const active = this.activeSessions[moduleName]; + if (!active || now < active.nextSampleAt) return; + + this.recordSample(moduleName, now, 'minute'); + active.nextSampleAt = Math.floor(now / SAMPLE_INTERVAL_MS) * SAMPLE_INTERVAL_MS + SAMPLE_INTERVAL_MS; + changed = true; + }); + + if (changed) this.saveHistory(); + } + + endSession(moduleName, reason = 'disabled') { + const active = this.activeSessions[moduleName]; + if (!active) return; + + const now = Date.now(); + this.recordSample(moduleName, now, 'end'); + + const session = active.session; + session.active = false; + session.disabledAtMs = now; + session.disabledAt = this.toIso(now); + session.durationMs = Math.max(0, now - session.enabledAtMs); + session.endReason = reason; + + delete this.activeSessions[moduleName]; + this.saveHistory(); + } + + endAllSessions(reason = 'disabled') { + Object.keys(this.activeSessions).forEach((moduleName) => this.endSession(moduleName, reason)); + } + + recordSample(moduleName, timestampMs = Date.now(), type = 'minute') { + const active = this.activeSessions[moduleName]; + if (!active) return; + + const session = active.session; + const overlayData = active.getOverlayData ? active.getOverlayData() : null; + session.overlayData = overlayData; + session.durationMs = Math.max(0, timestampMs - session.enabledAtMs); + session.minuteData.push({ + type, + minute: Math.floor(timestampMs / SAMPLE_INTERVAL_MS), + timestampMs, + timestamp: this.toIso(timestampMs), + elapsedMs: session.durationMs, + overlayData, + }); + } + + saveHistory() { + Utils.writeConfigFile(HISTORY_FILE, this.history); + } + + toIso(timestampMs) { + return new Date(timestampMs).toISOString(); + } +} + +export const ModuleHistory = new ModuleHistoryTracker();