aboutsummaryrefslogtreecommitdiff
path: root/src/featureClass/featureManager.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/featureClass/featureManager.js')
-rw-r--r--src/featureClass/featureManager.js878
1 files changed, 878 insertions, 0 deletions
diff --git a/src/featureClass/featureManager.js b/src/featureClass/featureManager.js
new file mode 100644
index 0000000..5a4b240
--- /dev/null
+++ b/src/featureClass/featureManager.js
@@ -0,0 +1,878 @@
+/// <reference types="../../../CTAutocomplete" />
+/// <reference lib="es2015" />
+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 { fetch } from "../utils/networkUtils";
+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 = true
+
+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 = {} //{<moduleName>: {<event>: {time: 0, count: 0}}}
+ this.flameGraphData = []
+
+ this.longEventTime = 20
+
+ this.perfTrackingFeatures = false
+ this.stack = []
+
+
+ this.featureMetas = {}
+
+ this.featureSettingsData = {}
+
+ let fetchD = fetch("http://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() + 5000
+ }
+ 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", (args) => {
+ this.loadPerformanceData()
+ }, this)
+ this.registerCommand("soopylaginformationfast", (args) => {
+ 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("http://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: func,
+ context: context,
+ trigger: register(type, (...args) => {
+ try {
+ 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: id
+ }
+
+ 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