/// /// const Instant = Java.type("java.time.Instant"); import logger from "../logger"; const File = Java.type("java.io.File") import metadata from "../metadata.js" import soopyV2Server from "../socketConnection"; import NonPooledThread from "../utils/nonPooledThread"; import { setRendering } from "../utils/renderJavaUtils"; import { registerForge as registerForgeBase, unregisterForge as unregisterForgeBase } from "./forgeEvents.js" const JSLoader = Java.type("com.chattriggers.ctjs.engine.langs.js.JSLoader") const UrlModuleSourceProvider = Java.type("org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider") const UrlModuleSourceProviderInstance = new UrlModuleSourceProvider(null, null) const StrongCachingModuleScriptProviderClass = Java.type("org.mozilla.javascript.commonjs.module.provider.StrongCachingModuleScriptProvider") let StrongCachingModuleScriptProvider = new StrongCachingModuleScriptProviderClass(UrlModuleSourceProviderInstance) let CTRequire = new JSLoader.CTRequire(StrongCachingModuleScriptProvider) const System = Java.type("java.lang.System") let loadedModules = new Set() let shouldRequireForceNoCache = false function RequireNoCache(place) { if (!shouldRequireForceNoCache) { if (!logger.isDev) return require(place) if (!loadedModules.has(place)) { loadedModules.add(place) return require(place) //performance optimisation } } StrongCachingModuleScriptProvider = new StrongCachingModuleScriptProviderClass(UrlModuleSourceProviderInstance) CTRequire = new JSLoader.CTRequire(StrongCachingModuleScriptProvider) return CTRequire(place) } class FeatureManager { constructor() { this.isDev = logger.isDev this.messagePrefix = "&6[SOOPY V2]&7 " this.enabled = true //make triggers work with this context this.features = {}; this.events = {} this.eventObjects = {} this.soopyEventHandlers = {} this.parent = undefined this.finishedLoading = false this.commandFuncs = {} this.lastEventId = 0 this.customEvents = {} this.lastChatEventId = 0 this.forgeEvents = {} this.lastForgeEventId = 0 this.lastSoopyEventId = 0 this.featureSettingsDataLastUpdated = false //PERFORMANCE RECORDING this.recordingPerformanceUsage = false this.performanceUsage = {} //{: {: {time: 0, count: 0}}} this.flameGraphData = [] this.longEventTime = 20 this.perfTrackingFeatures = false this.stack = [] this.featureMetas = {} this.featureSettingsData = {} let fetchD = fetch("https://soopy.dev/api/soopyv2/ping") fetchD.load().then(() => { if (fetchD.responseCode() >= 400 || fetchD.responseCode() === -1) { ChatLib.chat(this.messagePrefix + "&cError: Could not connect to Soopy's server. This may cause issues with some features but will (hopefully) be back soon.") } new NonPooledThread(() => { this.loadSoopy() }).start() }) this.registerStep(false, 30, () => { if (this.featureSettingsDataLastUpdated) { new Thread(() => { this.saveFeatureSettings() }).start() } }, this) this.registerEvent("worldUnload", this.saveFeatureSettings, this) this.registerEvent("gameUnload", () => { this.saveFeatureSettings() this.unloadAllFeatures() this.enabled = false }, this) if (this.isDev) { this.registerStep(true, 2, () => { if (this.reloadModuleTime !== 0 && Date.now() - this.reloadModuleTime > 0) { new Thread(() => { this.reloadModuleTime = 0 this.reloadingModules.forEach(m => { this.unloadFeature(m) }) this.reloadingModules.forEach(m => { this.loadFeature(m) }) this.reloadingModules.clear() }).start() } }, this) this.watches = {} this.addedWatches = new Set() this.watchService = Java.type("java.nio.file.FileSystems").getDefault().newWatchService(); this.reloadingModules = new Set() this.reloadModuleTime = 0 new NonPooledThread(() => { while (this.enabled) { key = this.watchService.take(); let moduleToReload = this.watches[key] if (this.features[moduleToReload] && !this.reloadingModules.has(moduleToReload)) { //if enabled && not alr in queue this.reloadingModules.add(moduleToReload) this.reloadModuleTime = Date.now() + 100 } key.pollEvents()/*.forEach(event=>{ logger.logMessage(event.context().toString(), 1) })*/ key.reset(); } }).start() } this.registerCommand("soopyunloadfeature", (args) => { new Thread(() => { this.unloadFeature(args) }).start() }, this) this.registerCommand("soopyloadfeature", (args) => { new Thread(() => { this.loadFeature(args) }).start() }, this) this.registerCommand("soopyunload", () => { new Thread(() => { this.unloadSoopy() }).start() }, this) this.registerCommand("soopyload", () => { new NonPooledThread(() => { this.loadSoopy() }).start() }, this) this.registerCommand("soopyreload", () => { new NonPooledThread(() => { this.unloadSoopy() this.loadSoopy() }).start() }, this) this.registerCommand("soopyreloadfeature", (args) => { new Thread(() => { this.unloadFeature(args) this.loadFeature(args) }).start() }, this) this.registerCommand("soopysetlongeventtime", (args) => { this.longEventTime = parseInt(args) }, this) this.registerCommand("soopylaginformation", (full) => { if (full === "full") { this.loadPerformanceData() } else { this.loadPerformanceDataFast() } }, this) } getId() { return "FeatureManager" } loadPerformanceDataFast() { new Thread(() => { this.loadEventLag(true) }).start() } loadPerformanceData() { new NonPooledThread(() => { ChatLib.chat(this.messagePrefix + "Recording performance impact, this will take around 60 seconds to complete!") shouldRequireForceNoCache = true let eventLagData = this.loadEventLag() ChatLib.chat(this.messagePrefix + "ETA: 40s") this.perfTrackingFeatures = true this.unloadSoopy() this.loadSoopy() Thread.sleep(1000) let eventLagDataFull = this.loadEventLag() this.perfTrackingFeatures = false this.unloadSoopy() this.loadSoopy() Thread.sleep(1000) ChatLib.chat(this.messagePrefix + "ETA: 25s") let forgeLagData = this.loadForgeRenderLag() ChatLib.chat(this.messagePrefix + "ETA: 15s") let soopyLagData = this.loadSoopyLag() let lagData = { eventLagDataFull, eventLagData, forgeLagData, soopyLagData } shouldRequireForceNoCache = false let url = this.reportLagData(lagData) ChatLib.chat(this.messagePrefix + "Done!") new TextComponent(this.messagePrefix + "See the report at " + url).setClick("open_url", url).setHover("show_text", "Click to open the report.").chat() this.performanceUsage = {} this.flameGraphData = [] }).start() } reportLagData(data) { return fetch("https://soopy.dev/soopy/submitlag", { postData: data }).textSync() } loadSoopyLag() { // ChatLib.chat(this.messagePrefix + "Recording All Soopy Lag...") let framesWith = 0 let framesWithOut = 0 let event = this.registerEvent("renderWorld", () => { framesWith++ }, this) Thread.sleep(5000) this.unregisterEvent(event) this.unloadSoopy() Thread.sleep(1000) event = this.registerEvent("renderWorld", () => { framesWithOut++ }, this) Thread.sleep(5000) this.unregisterEvent(event) // ChatLib.chat(this.messagePrefix + "Soopy Lag:") // ChatLib.chat("&eFps without Soopy: &7" + (framesWithOut / 5)) // ChatLib.chat("&eFps with Soopy: &7" + (framesWith / 5)) this.loadSoopy() return { fpsWith: (framesWith / 5), fpsWithout: (framesWithOut / 5) } } loadForgeRenderLag() { // ChatLib.chat(this.messagePrefix + "Recording Forge-Rendering Lag...") let framesWith = 0 let framesWithOut = 0 let renderingForge = true let event = this.registerEvent("renderWorld", () => { if (renderingForge) { framesWith++ } else { framesWithOut++ } }, this) for (let i = 0; i < 10; i++) { Thread.sleep(1000) renderingForge = !renderingForge setRendering(renderingForge) } this.unregisterEvent(event) // ChatLib.chat(this.messagePrefix + "Forge Lag:") // ChatLib.chat("&eFps without forge: &7" + (framesWithOut / 5)) // ChatLib.chat("&eFps with forge: &7" + (framesWith / 5)) return { fpsWith: (framesWith / 5), fpsWithout: (framesWithOut / 5) } } loadEventLag(sendMessage = false) { this.recordingPerformanceUsage = true this.performanceUsage = {} this.flameGraphData = [] if (sendMessage) ChatLib.chat(this.messagePrefix + "Recording Event Lag...") Thread.sleep(10000) let totalMsGlobal = 0 this.recordingPerformanceUsage = false if (sendMessage) { ChatLib.chat(this.messagePrefix + "Event Lag:") Object.keys(this.performanceUsage).sort((a, b) => { let totalMsA = 0 Object.keys(this.performanceUsage[a]).forEach((event) => { totalMsA += this.performanceUsage[a][event].time }) let totalMsB = 0 Object.keys(this.performanceUsage[b]).forEach((event) => { totalMsB += this.performanceUsage[b][event].time }) return totalMsA - totalMsB }).forEach((moduleName) => { let totalMs = 0 let totalCalls = 0 Object.keys(this.performanceUsage[moduleName]).forEach((event) => { totalMs += this.performanceUsage[moduleName][event].time totalCalls += this.performanceUsage[moduleName][event].count }) totalMsGlobal += totalMs ChatLib.chat("&eModule: &7" + moduleName) ChatLib.chat("&eTotal: &7" + totalMs.toFixed(2) + "ms (" + totalCalls + " calls)") Object.keys(this.performanceUsage[moduleName]).sort((a, b) => { return this.performanceUsage[moduleName][a].time - this.performanceUsage[moduleName][b].time }).forEach((event) => { ChatLib.chat(" &eEvent:&7 " + event + " - " + this.performanceUsage[moduleName][event].time.toFixed(2) + "ms (" + this.performanceUsage[moduleName][event].count + " calls) [" + ((this.performanceUsage[moduleName][event].time / this.performanceUsage[moduleName][event].count).toFixed(2)) + "ms avg]") }) }) ChatLib.chat("&eTotal: &7" + totalMsGlobal.toFixed(2) + "ms") } return { performanceUsage: this.performanceUsage, flameGraphData: this.flameGraphData } } loadFeatureSettings() { logger.logMessage("Loading settings", 4) let data = FileLib.read("soopyAddonsData", "soopyaddonsbetafeaturesdata.json") if (!data) { this.loadDefaultFeatureSettings(); return; } try { data = JSON.parse(data) } catch (e) { ChatLib.chat(this.messagePrefix + "&cYour settings file corrupted and could not be read! Resetting to defaults.") data = {} } this.featureSettingsData = data this.ensureNewSettingsExist() } saveFeatureSettings() { if (!this.featureSettingsDataLastUpdated) return FileLib.write("soopyAddonsData", "soopyaddonsbetafeaturesdata.json", JSON.stringify(this.featureSettingsData)) this.featureSettingsDataLastUpdated = false logger.logMessage("Saved settings", 4) } loadDefaultFeatureSettings() { Object.keys(this.featureMetas).forEach((feature) => { this.featureSettingsData[feature] = { enabled: this.featureMetas[feature].defaultEnabled, subSettings: {} } }) this.featureSettingsDataLastUpdated = true logger.logMessage("Loaded default settings", 4) } ensureNewSettingsExist() { Object.keys(this.featureMetas).forEach((feature) => { if (!this.featureSettingsData[feature]) { this.featureSettingsData[feature] = { enabled: this.featureMetas[feature].defaultEnabled, subSettings: {} } this.featureSettingsDataLastUpdated = true logger.logMessage("Loaded default settings for " + feature, 4) } }) } startCatchingEvent(event) { if (this.eventObjects[event]) return //SBA compatability or something (removed) // if(event === "renderOverlay"){ // let lastPartialTick = undefined // this.eventObjects[event] = register(event, (...args)=>{ // let pTicks = Tessellator.getPartialTicks() // if(pTicks !== lastPartialTick){ // lastPartialTick = pTicks // this.triggerEvent(event, args) // } // }) // }else{ this.eventObjects[event] = register(event, (...args) => { // let start = Date.now() this.triggerEvent(event, args) // this.eventTimingData[event] = (this.eventTimingData[event] || 0)+(Date.now()-start) }) //} logger.logMessage("Registered " + event + " event", 4) } triggerEvent(event, args) { if (this.events[event]) { try { for (Event of Object.values(this.events[event])) { if (Event.context.enabled) { if (this.recordingPerformanceUsage) this.startRecordingPerformance(Event.context.getId(), event) let start = Date.now() Event.func?.call(Event.context, ...args) let time = Date.now() - start if (time > this.longEventTime) { logger.logMessage("Long event triggered [" + time + "ms] (" + Event.context.getId() + "/" + event + ")", 3) } if (this.recordingPerformanceUsage) this.stopRecordingPerformance(Event.context.getId(), event) } } } catch (e) { logger.logMessage("Error in " + event + " event: " + JSON.stringify(e, undefined, 2), 2) logger.logMessage(e.stack, 1) soopyV2Server.reportError(e, "Error in " + event + " event.") } } } triggerSoopy(event, args) { if (this.soopyEventHandlers[event]) { try { for (Event of Object.values(this.soopyEventHandlers[event])) { if (Event.context.enabled) { if (this.recordingPerformanceUsage) this.startRecordingPerformance(Event.context.getId(), event) let start = Date.now() Event.func?.call(Event.context, ...args) let time = Date.now() - start if (time > this.longEventTime) { logger.logMessage("Long event triggered [" + time + "ms] (" + Event.context.getId() + "/" + event + ")", 3) } if (this.recordingPerformanceUsage) this.stopRecordingPerformance(Event.context.getId(), event) } } } catch (e) { logger.logMessage("Error in soopy " + event + " event: " + JSON.stringify(e, undefined, 2), 2) logger.logMessage(e.stack, 1) soopyV2Server.reportError(e, "Error in soopy " + event + " event.") } } } stopCatchingEvent(event) { if (!this.eventObjects[event]) return this.eventObjects[event].unregister() delete this.eventObjects[event] delete this.events[event] logger.logMessage("Unregistered " + event + " event", 4) } registerEvent(event, func, context) { if (!this.events[event]) { this.events[event] = [] this.startCatchingEvent(event) } let theEvent = { func: func, context: context, id: this.lastEventId++, event: event } this.events[event].push(theEvent) return theEvent } registerSoopy(event, func, context) { if (!this.soopyEventHandlers[event]) { this.soopyEventHandlers[event] = [] } let theEvent = { func: func, context: context, id: this.lastSoopyEventId++, event: event } this.soopyEventHandlers[event].push(theEvent) return theEvent } registerChat(criteria, func, context) { let event = this.registerCustom("chat", func, context) event.trigger.setChatCriteria(criteria) return event } registerSoundPlay(criteria, func, context) { let event = this.registerCustom("soundPlay", func, context) event.trigger.setCriteria(criteria) return event } registerActionBar(criteria, func, context) { let event = this.registerCustom("actionBar", func, context) event.trigger.setChatCriteria(criteria) return event } registerCommand(commandName, func, context, completions) { let event = this.registerCustom("command", func, context) event.trigger.setName(commandName, true) if (completions) event.trigger.setTabCompletions(completions).setName(commandName, true) return event } registerStep(isFps, interval, func, context) { let event = this.registerCustom("step", func, context) event.trigger[isFps ? "setFps" : "setDelay"](interval) return event } registerCustom(type, func, context) { let id = this.lastChatEventId++ if (!func) throw new Error("Function must not be null") this.customEvents[id] = { func, context, trigger: register(type, (...args) => { try { if (this.customEvents[id]?.eventT && !this.customEvents[id].eventT.enabled) return if (context.enabled) { if (this.recordingPerformanceUsage) this.startRecordingPerformance(context.getId(), type) let start = Date.now() func.call(context, ...(args || [])) let time = Date.now() - start if (time > this.longEventTime) { logger.logMessage("Long event triggered [" + time + "ms] (" + context.getId() + "/" + type + ")", 3) } if (this.recordingPerformanceUsage) this.stopRecordingPerformance(context.getId(), type) } } catch (e) { logger.logMessage("Error in " + type + " event: " + JSON.stringify(e, undefined, 2), 2) logger.logMessage(e.stack, 1) soopyV2Server.reportError(e, "Error in " + type + " event.") } }), id, type, } return this.customEvents[id] } registerForge(event, func, priority, context) { let id = this.lastForgeEventId++ this.forgeEvents[id] = { func: func, context: context, trigger: registerForgeBase(event, priority, (...args) => { try { if (context.enabled) { if (this.recordingPerformanceUsage) this.startRecordingPerformance(context.getId(), event.class.name) let start = Date.now() func.call(context, ...(args || [])) let time = Date.now() - start if (time > this.longEventTime) { logger.logMessage("Long (forge) event triggered (" + context.getId() + "/" + event.class.toString() + ")", 3) } if (this.recordingPerformanceUsage) this.stopRecordingPerformance(context.getId(), event.class.name) } } catch (e) { logger.logMessage("Error in " + event.class.toString() + " (forge) event: " + JSON.stringify(e, undefined, 2), 2) logger.logMessage(e.stack, 1) soopyV2Server.reportError(e, "Error in " + event.class.toString() + " (forge) event.") } }), id: id } return this.forgeEvents[id] } unregisterForge(event) { if (!this.forgeEvents[event.id]) return unregisterForgeBase(this.forgeEvents[event.id].trigger) delete this.forgeEvents[event.id] } unregisterCustom(event) { event.trigger.unregister() delete this.customEvents[event.id] } unregisterEvent(event) { if (!this.events[event.event]) return this.events[event.event] = this.events[event.event].filter((e) => { return e.id !== event.id }) if (this.events[event.event].length === 0) { this.stopCatchingEvent(event.event) delete this.events[event.event] } } unregisterSoopy(event) { if (!this.soopyEventHandlers[event.event]) return this.soopyEventHandlers[event.event] = this.soopyEventHandlers[event.event].filter((e) => { return e.id !== event.id }) if (this.soopyEventHandlers[event.event].length === 0) { delete this.events[event.event] } } loadFeatureMetas() { let featuresDir = new File("./config/ChatTriggers/modules/" + metadata.name + "/features") featuresDir.list().forEach((pathName) => { if (pathName.includes(".")) return; try { let data = JSON.parse(FileLib.read(metadata.name + "/features/" + pathName, "metadata.json")) if (data === null) { return; } data.id = pathName this.featureMetas[pathName] = data } catch (e) { logger.logMessage("Error loading feature metadata for " + pathName, 1) logger.logMessage(JSON.stringify(e, undefined, 2), 1) } }) } addPerformanceTracking(feature) { let featureId = feature.getId() if (!this.perfTrackingFeatures) return Object.getOwnPropertyNames(Object.getPrototypeOf(feature)).forEach(key => { if (typeof (feature[key]) === "function") { let fun = feature[key].bind(feature) feature[key] = (...args) => { if (!this.recordingPerformanceUsage || Thread.currentThread().getId() !== 1 || !this.perfTrackingFeatures) { let err = undefined try { args ? fun(...args) : fun() } catch (e) { err = e } if (err) throw err return } let pushedId = false let start = this.getExactTime() if (this.stack.length === 0) { this.stack.push([featureId, 0]) // this.flameGraphData.push({ isEnter: true, thing: featureId, time: start }) pushedId = true } this.stack.push([featureId + "." + key, 0]) // this.flameGraphData.push({ isEnter: true, thing: featureId + "." + key, time: start }) let err = undefined try { args ? fun(...args) : fun() } catch (e) { err = e } let nowTime = this.getExactTime() let time = (nowTime - start) - this.stack[this.stack.length - 1][1] this.stack[this.stack.length - 2][1] += nowTime - start if (!this.performanceUsage[featureId]) this.performanceUsage[featureId] = {} if (!this.performanceUsage[featureId].functions) this.performanceUsage[featureId].functions = {} if (!this.performanceUsage[featureId].functions[key]) this.performanceUsage[featureId].functions[key] = { time: 0, count: 0 } this.performanceUsage[featureId].functions[key].count++ this.performanceUsage[featureId].functions[key].time += nowTime - start this.flameGraphData.push(this.stack.map(a => a[0]).join(";") + " " + (time)) this.stack.pop()[1] if (pushedId) { let time = (nowTime - start) - this.stack[this.stack.length - 1][1] this.flameGraphData.push(this.stack.map(a => a[0]).join(";") + " " + (time)) this.stack.pop() } if (err) throw err } } }) } unloadSoopy() { this.saveFeatureSettings() this.unloadAllFeatures() } loadSoopy() { ChatLib.chat(this.messagePrefix + "Loading SoopyV2...") let startLoading = Date.now() this.loadFeatureMetas() this.loadFeatureSettings() let loadedFeatures = new Map() Object.keys(this.featureMetas).forEach((feature) => { if (this.featureSettingsData[feature] && this.featureSettingsData[feature].enabled) { loadedFeatures.set(feature, false) new Thread(() => { this.loadFeature(feature) loadedFeatures.set(feature, true) }).start() } }) while ([...loadedFeatures.values()].some(a => !a)) { Thread.sleep(100) } this.finishedLoading = true ChatLib.chat(this.messagePrefix + "SoopyV2 Loaded!") logger.logMessage("SoopyV2 took " + ((Date.now() - startLoading) / 1000).toFixed(2) + "s to load", 3) } loadFeature(feature) { //run in seperate thread so onenable can do network requests if (this.features[feature]) return try { let LoadedFeature = RequireNoCache("../features/" + feature + "/index.js") // let LoadedFeature = RequireNoCache(new File("config/ChatTriggers/modules/" + metadata.name + "/features/" + feature + "/index.js")) this.features[feature] = LoadedFeature LoadedFeature.class.setId(feature) this.addPerformanceTracking(LoadedFeature.class) LoadedFeature.class._onEnable(this) logger.logMessage("Loaded feature " + feature, 3) if (this.isDev && !this.addedWatches.has(feature)) { this.addedWatches.add(feature) let path = Java.type("java.nio.file.Paths").get("./config/ChatTriggers/modules/SoopyV2/features/" + feature + "/"); this.watches[path.register(this.watchService, Java.type("java.nio.file.StandardWatchEventKinds").ENTRY_MODIFY)] = feature } } catch (e) { logger.logMessage("Error loading feature " + feature, 1) logger.logMessage(JSON.stringify(e, undefined, 2), 1) logger.logMessage(e.stack, 1) ChatLib.chat(this.messagePrefix + "Error loading feature " + feature) soopyV2Server.reportError(e, "Error loading feature " + feature) } return this } unloadFeature(feature) { if (!this.features[feature]) return this.features[feature].class._onDisable() delete this.features[feature] logger.logMessage("Unloaded feature " + feature, 3) return this } unloadAllFeatures() { Object.keys(this.features).forEach((feature) => { this.unloadFeature(feature) }) } isFeatureLoaded(feature) { return !!this.features[feature] } getLoadedFeatures() { return Object.keys(this.features) } createCustomEvent(eventId) { logger.logMessage("Registered custom " + eventId + " event", 4) return { trigger: (...args) => { this.triggerSoopy(eventId, args) } } } getExactTime() { return System.nanoTime() / 1000000 } startRecordingPerformance(feature, event) { if (!this.recordingPerformanceUsage) return if (!this.performanceUsage[feature]) this.performanceUsage[feature] = {} if (!this.performanceUsage[feature][event]) this.performanceUsage[feature][event] = { time: 0, count: 0 } let time = this.getExactTime() this.performanceUsage[feature][event].startTime = time } stopRecordingPerformance(feature, event) { if (!this.recordingPerformanceUsage) return let time = this.getExactTime() this.performanceUsage[feature][event].time += (time - this.performanceUsage[feature][event].startTime) this.performanceUsage[feature][event].count++ } } if (!global.soopyv2featuremanagerthing) { global.soopyv2featuremanagerthing = new FeatureManager() register("gameUnload", () => { global.soopyv2featuremanagerthing = undefined }) } export default global.soopyv2featuremanagerthing