ModAPI.meta.title("AsyncSink"); ModAPI.meta.description("Library for patching and hooking into asynchronous filesystem requests for EaglercraftX."); const asyncSinkIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAL9JREFUOE9jZGBg+M9ABcAIMsgtPo3hzZ2zYONEVIxJZu9aOIsBbJCRtTHcEJAgLgBSh82ic0fPIgyCKQAJXrx4EcUsfX19sBiIRrYU5gu4Qchew2cQyHSQYehBgdNruFwEcybMZci+gIcRIa+hhxu6LzBiDZvX0A1BDyuivYbLIJK8pqevjze5GlsbMxAdayCT/PQwDRS2gaQror2m36KH4SqjZybwxEl0gsQWRkM01ogpVQh6jaJihBgXEFIDAAIQ9AFDJlrxAAAAAElFTkSuQmCC"; ModAPI.meta.icon(asyncSinkIcon); ModAPI.meta.credits("By ZXMushroom63"); (function AsyncSinkFn() { const ResourceLocation = ModAPI.reflect.getClassByName("ResourceLocation").constructors.find(x => x.length === 1); //AsyncSink is a plugin to debug and override asynchronous methods in EaglercraftX async function runtimeComponent() { let booleanResult; if (ModAPI.is_1_12) { const _boolRes = ModAPI.reflect.getClassById("net.lax1dude.eaglercraft.internal.teavm.BooleanResult").constructors[0]; booleanResult = (b) => _boolRes(b * 1); } else { booleanResult = (b) => ModAPI.hooks.methods.nlevit_BooleanResult__new(b * 1); } const wrap = ModAPI.hooks.methods.otji_JSWrapper_wrap; const unwrap = ModAPI.hooks.methods.otji_JSWrapper_unwrap; function getAsyncHandlerName(name) { var suffix = `$AsyncHandlers_${name}$_asyncCall_$`; return ModAPI.hooks._rippedMethodKeys.find(x => x.endsWith(suffix)); } var fs_debugging = false; const encoder = new TextEncoder('utf-8'); var filesystemPlatform = (ModAPI.hooks.methods.nlevit_IndexedDBFilesystem$AsyncHandlers_readWholeFile || ModAPI.hooks.methods.nleit_IndexedDBFilesystem$AsyncHandlers_readWholeFile) ? true : false; if (!filesystemPlatform) { console.warn("AsyncSink requires EaglercraftX u37 or greater to work! Attempting to run anyway..."); } const AsyncSink = {}; const originalSuspend = ModAPI.hooks.TeaVMThread.prototype.suspend; AsyncSink.startDebugging = function hookIntoSuspend() { ModAPI.hooks.TeaVMThread.prototype.suspend = function suspend(...args) { console.log("[AsyncSink] Context suspended! Callback: ", args[0]); return originalSuspend.apply(this, args); } } AsyncSink.stopDebugging = function unhookFromSuspend() { ModAPI.hooks.TeaVMThread.prototype.suspend = originalSuspend; } AsyncSink.startDebuggingFS = function hookIntoSuspend() { fs_debugging = true; } AsyncSink.stopDebuggingFS = function unhookFromSuspend() { fs_debugging = false; } // @type Map AsyncSink.FS = new Map(); AsyncSink.L10N = new Map(); AsyncSink.FSOverride = new Set(); AsyncSink.MIDDLEWARE = []; //takes in a ResourceLocation and removes cached data. Use to only reload a specific texture if you know where it is stored. AsyncSink.clearResourcePointer = function clearResourcePointer(resourceLocation) { if (!resourceLocation) { return; } var res = ModAPI.util.wrap((resourceLocation.isModProxy === true) ? resourceLocation.getRef() : resourceLocation); res.cachedPointer = null; res.cachedPointerType = 0; ModAPI.mc.getTextureManager().mapTextureObjects.remove(res.getRef()); } AsyncSink.setFile = function setFile(path, data) { if (typeof data === "string") { data = encoder.encode(data).buffer; } AsyncSink.FSOverride.add(path); AsyncSink.FS.set(path, data); return true; } AsyncSink.deleteFile = function deleteFile(path) { AsyncSink.FSOverride.delete(path); AsyncSink.FS.delete(path); return true; } AsyncSink.hideFile = function hideFile(path) { AsyncSink.FSOverride.add(path); AsyncSink.FS.delete(path); return true; } AsyncSink.getFile = function getFile(path) { return AsyncSink.FS.get(path) || new ArrayBuffer(0); } AsyncSink.fileExists = function fileExists(path) { return AsyncSink.FS.has(path); } var readWholeFileName = getAsyncHandlerName("readWholeFile"); var writeWholeFileName = getAsyncHandlerName("writeWholeFile"); var deleteFileName = getAsyncHandlerName("deleteFile"); var fileExistsName = getAsyncHandlerName("fileExists"); const originalReadWholeFile = ModAPI.hooks.methods[readWholeFileName]; ModAPI.hooks.methods[readWholeFileName] = function (...args) { if (fs_debugging) { console.log("[AsynkSinkFS] File read request sent: " + ModAPI.util.ustr(args[1])); } if (AsyncSink.FSOverride.has(ModAPI.util.ustr(args[1]))) { if (fs_debugging) { console.log("[AsynkSinkFS] Replied with copy from fake filesystem."); } return wrap(AsyncSink.getFile(ModAPI.util.ustr(args[1]))); } var ev = { method: "read", file: ModAPI.util.ustr(args[1]), shim: false, shimOutput: new ArrayBuffer() }; AsyncSink.MIDDLEWARE.forEach((fn) => { fn(ev) }); if (ev.shim) { return wrap(ev.shimOutput); } return originalReadWholeFile.apply(this, args); }; const originalWriteWholeFile = ModAPI.hooks.methods[writeWholeFileName]; ModAPI.hooks.methods[writeWholeFileName] = function (...args) { if (fs_debugging) { console.log("[AsynkSinkFS] File write request sent: " + ModAPI.util.ustr(args[1]), args[2]); } if (AsyncSink.FSOverride.has(ModAPI.util.ustr(args[1]))) { if (fs_debugging) { console.log("[AsynkSinkFS] Writing to fake filesystem."); } AsyncSink.setFile(ModAPI.util.ustr(args[1]), args[2]); return booleanResult(true); } var ev = { method: "write", file: ModAPI.util.ustr(args[1]), data: args[2], shim: false, shimOutput: true }; AsyncSink.MIDDLEWARE.forEach((fn) => { fn(ev) }); if (ev.shim) { return booleanResult(ev.shimOutput); } return originalWriteWholeFile.apply(this, args); }; const originalDeleteFile = ModAPI.hooks.methods[deleteFileName]; ModAPI.hooks.methods[deleteFileName] = function (...args) { if (fs_debugging) { console.log("[AsynkSinkFS] File delete request sent: " + ModAPI.util.ustr(args[1])); } if (AsyncSink.FSOverride.has(ModAPI.util.ustr(args[1]))) { if (fs_debugging) { console.log("[AsynkSinkFS] Deleting entry from fake filesystem."); } AsyncSink.deleteFile(ModAPI.util.ustr(args[1])); return booleanResult(true); } var ev = { method: "delete", file: ModAPI.util.ustr(args[1]), shim: false, shimOutput: true }; AsyncSink.MIDDLEWARE.forEach((fn) => { fn(ev) }); if (ev.shim) { return booleanResult(ev.shimOutput); } return originalDeleteFile.apply(this, args); }; const originalFileExists = ModAPI.hooks.methods[fileExistsName]; ModAPI.hooks.methods[fileExistsName] = function (...args) { if (fs_debugging) { console.log("[AsynkSinkFS] File exists request sent: " + ModAPI.util.ustr(args[1])); } if (AsyncSink.FSOverride.has(ModAPI.util.ustr(args[1]))) { if (fs_debugging) { console.log("[AsynkSinkFS] Replying with information from fake filesystem."); } var result = AsyncSink.fileExists(ModAPI.util.ustr(args[1])); return booleanResult(result); } var ev = { method: "exists", file: ModAPI.util.ustr(args[1]), shim: false, shimOutput: true }; AsyncSink.MIDDLEWARE.forEach((fn) => { fn(ev) }); if (ev.shim) { return booleanResult(ev.shimOutput); } return originalFileExists.apply(this, args); }; const L10NRead = ModAPI.util.getMethodFromPackage("net.minecraft.util.StatCollector", "translateToLocal"); const originalL10NRead = ModAPI.hooks.methods[L10NRead]; ModAPI.hooks.methods[L10NRead] = function (...args) { var key = ModAPI.util.ustr(args[0]); if (AsyncSink.L10N.has(key)) { return ModAPI.util.str(AsyncSink.L10N.get(key)); } return originalL10NRead.apply(this, args); }; const L18NFormat = ModAPI.util.getMethodFromPackage("net.minecraft.client.resources.I18n", "format"); const originalL18NFormat = ModAPI.hooks.methods[L18NFormat]; ModAPI.hooks.methods[L18NFormat] = function (...args) { var key = ModAPI.util.ustr(args[0]); if (AsyncSink.L10N.has(key)) { args[0] = ModAPI.util.str(AsyncSink.L10N.get(key)); } return originalL18NFormat.apply(this, args); }; const LanguageMapTranslate = ModAPI.util.getMethodFromPackage("net.minecraft.util.text.translation.LanguageMap", "tryTranslateKey"); const originalLanguageMapTranslate = ModAPI.hooks.methods[LanguageMapTranslate]; ModAPI.hooks.methods[LanguageMapTranslate] = function (...args) { var key = ModAPI.util.ustr(args[1]); if (AsyncSink.L10N.has(key)) { args[1] = ModAPI.util.str(AsyncSink.L10N.get(key)); } return originalLanguageMapTranslate.apply(this, args); }; const LanguageMapCheckTranslate = ModAPI.util.getMethodFromPackage("net.minecraft.util.text.translation.LanguageMap", "isKeyTranslated"); const originalLanguageMapCheckTranslate = ModAPI.hooks.methods[LanguageMapCheckTranslate]; ModAPI.hooks.methods[LanguageMapCheckTranslate] = function (...args) { var key = ModAPI.util.ustr(args[1]); if (AsyncSink.L10N.has(key)) { return 1; } return originalLanguageMapTranslate.apply(this, args); }; const L10NCheck = ModAPI.util.getMethodFromPackage("net.minecraft.util.StatCollector", "canTranslate"); const originalL10NCheck = ModAPI.hooks.methods[L10NCheck]; ModAPI.hooks.methods[L10NCheck] = function (...args) { if (AsyncSink.L10N.has(ModAPI.util.ustr(args[0]))) { return 1; } return originalL10NCheck.apply(this, args); }; globalThis.AsyncSink = AsyncSink; ModAPI.events.newEvent("lib:asyncsink"); ModAPI.events.callEvent("lib:asyncsink", {}); console.log("[AsyncSink] Loaded!"); } runtimeComponent(); ModAPI.dedicatedServer.appendCode(runtimeComponent); async function assureAsyncSinkResources() { const dec = new TextDecoder("utf-8"); const enc = new TextEncoder("utf-8"); var resourcePackKey = ModAPI.is_1_12 ? "_net_lax1dude_eaglercraft_v1_8_internal_PlatformFilesystem_1_12_2_" : (await indexedDB.databases()).find(x => x?.name?.endsWith("_resourcePacks")).name; const dbRequest = indexedDB.open(resourcePackKey); const db = await promisifyIDBRequest(dbRequest); const transaction = db.transaction(["filesystem"], "readonly"); const objectStore = transaction.objectStore("filesystem"); var object = (await promisifyIDBRequest(objectStore.get(["resourcepacks/manifest.json"])))?.data; var resourcePackList = object ? JSON.parse(dec.decode(object)) : { resourcePacks: [] }; var pack = { domains: ["minecraft", "eagler"], folder: "AsyncSinkLib", name: "AsyncSinkLib", timestamp: Date.now() }; if (!Array.isArray(resourcePackList.resourcePacks)) { resourcePackList.resourcePacks = []; } if (resourcePackList.resourcePacks.find(x => x.name === "AsyncSinkLib")) { var idx = resourcePackList.resourcePacks.indexOf(resourcePackList.resourcePacks.find(x => x.name === "AsyncSinkLib")); resourcePackList.resourcePacks[idx] = pack; } else { resourcePackList.resourcePacks.push(pack); } const writeableTransaction = db.transaction(["filesystem"], "readwrite"); const writeableObjectStore = writeableTransaction.objectStore("filesystem"); await promisifyIDBRequest(writeableObjectStore.put({ path: "resourcepacks/manifest.json", data: enc.encode(JSON.stringify(resourcePackList)).buffer })); await promisifyIDBRequest(writeableObjectStore.put({ path: "resourcepacks/AsyncSinkLib/pack.mcmeta", data: enc.encode(JSON.stringify({ "pack": { "pack_format": ModAPI.is_1_12 ? 3 : 1, "description": "AsyncSink Library Resources" } })).buffer })); var icon = { path: "resourcepacks/AsyncSinkLib/pack.png", data: await (await fetch(asyncSinkIcon)).arrayBuffer() }; const imageTransaction = db.transaction(["filesystem"], "readwrite"); const imageObjectStore = imageTransaction.objectStore("filesystem"); await promisifyIDBRequest(imageObjectStore.put(icon)); } // Client side reminders to enable the AsyncSink Resource Pack var asyncSinkInstallStatus = false; var installMessage = document.createElement("span"); installMessage.innerText = "Please enable the AsyncSink resource pack\nIn game, use the .reload_tex command to load textures for modded blocks and items."; installMessage.style = "background-color: rgba(0,0,0,0.7); color: red; position: fixed; top: 0; left: 0; font-family: sans-serif; pointer-events: none; user-select: none;"; document.body.appendChild(installMessage); assureAsyncSinkResources(); setInterval(() => { var resourcePackEntries = ModAPI.mc.mcResourcePackRepository.getRepositoryEntries().getCorrective(); var array = resourcePackEntries.array || [resourcePackEntries.element]; asyncSinkInstallStatus = array.find(x => ModAPI.util.ustr(x.reResourcePack.resourcePackFile.getRef()) === "AsyncSinkLib") ? true : false; //assureAsyncSinkResources(); if (asyncSinkInstallStatus) { installMessage.style.display = "none"; } else { installMessage.style.display = "initial"; } }, 8000); ModAPI.events.newEvent("custom:asyncsink_reloaded"); ModAPI.addEventListener("sendchatmessage", (e) => { if (e.message.toLowerCase().startsWith(".reload_tex")) { e.preventDefault = true; ModAPI.mc.renderItem.itemModelMesher.simpleShapesCache.clear(); ModAPI.promisify(ModAPI.mc.refreshResources)().then(() => { ModAPI.events.callEvent("custom:asyncsink_reloaded", {}); ModAPI.events.callEvent("lib:asyncsink:registeritems", ModAPI.mc.renderItem); }); } }); ModAPI.events.newEvent("lib:asyncsink:registeritems"); const regItemsName = ModAPI.util.getMethodFromPackage("net.minecraft.client.renderer.entity.RenderItem", "registerItems"); const oldRegisterItems = ModAPI.hooks.methods[regItemsName]; ModAPI.hooks.methods[regItemsName] = function (...args) { oldRegisterItems.apply(this, args); ModAPI.events.callEvent("lib:asyncsink:registeritems", ModAPI.util.wrap(args[0])); } AsyncSink.Audio = {}; AsyncSink.Audio.Category = ModAPI.reflect.getClassByName("SoundCategory").staticVariables; AsyncSink.Audio.Objects = []; const SoundHandler_onResourceManagerReload = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.audio.SoundHandler", "onResourceManagerReload")]; ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.audio.SoundHandler", "onResourceManagerReload")] = function (...args) { SoundHandler_onResourceManagerReload.apply(this, args); if (ModAPI.util.isCritical()) { return; } var snd = ModAPI.mc.mcSoundHandler; var registry = (snd.sndRegistry || snd.soundRegistry).getCorrective().soundRegistry; console.log("[AsyncSink] Populating sound registry hash map with " + AsyncSink.Audio.Objects.length + " sound effects."); AsyncSink.Audio.Objects.forEach(pair => { registry.put(pair[0], pair[1]); }); } // key = "mob.entity.say" // values = SoundEntry[] // category: AsyncSink.Audio.Category.* // SoundEntry = {path: String, pitch: 1, volume: 1, streaming: false} const EaglercraftRandom = ModAPI.reflect.getClassByName("EaglercraftRandom").constructors.find(x => x.length === 0); function makeSoundEventAccessor(soundpoolentry, weight) { const SoundEventAccessorClass = ModAPI.reflect.getClassByName("SoundEventAccessor").class; var object = new SoundEventAccessorClass; var wrapped = ModAPI.util.wrap(object).getCorrective(); wrapped.entry = soundpoolentry; wrapped.weight = weight; return object; } function makeSoundEventAccessorComposite(rKey, pitch, volume, category) { const SoundEventAccessorCompositeClass = ModAPI.reflect.getClassByName("SoundEventAccessorComposite").class; var object = new SoundEventAccessorCompositeClass; var wrapped = ModAPI.util.wrap(object).getCorrective(); wrapped.soundLocation = rKey; wrapped.eventPitch = pitch; wrapped.eventVolume = volume; wrapped.category = category; wrapped.soundPool = ModAPI.hooks.methods.cgcc_Lists_newArrayList1(); wrapped.rnd = EaglercraftRandom(); return object; } function makeSoundEventAccessor112(rKey, subttl) { const SoundEventAccessorClass = ModAPI.reflect.getClassByName("SoundEventAccessor").class; var object = new SoundEventAccessorClass; var wrapped = ModAPI.util.wrap(object).getCorrective(); wrapped.location = rKey; wrapped.subtitle = ModAPI.util.str(subttl); wrapped.accessorList = ModAPI.hooks.methods.cgcc_Lists_newArrayList0(); wrapped.rnd = EaglercraftRandom(); return object; } if (ModAPI.is_1_12) { const soundType = ModAPI.reflect.getClassById("net.minecraft.client.audio.Sound$Type").staticVariables.FILE; const mkSound = ModAPI.reflect.getClassById("net.minecraft.client.audio.Sound").constructors[0]; const SoundEvent = ModAPI.reflect.getClassById("net.minecraft.util.SoundEvent"); const SoundEvents = ModAPI.reflect.getClassById("net.minecraft.init.SoundEvents"); AsyncSink.Audio.register = function addSfx(key, unused, values, subtitle) { subtitle ||= "(AsyncSink Sound)"; if (unused) { console.log("Category is not a property of the sound in 1.12, category for " + key + " will be unused."); } const rKey = ResourceLocation(ModAPI.util.str(key)); const ev = new SoundEvent.class; ev.$soundName = rKey; ModAPI.util.wrap(SoundEvent.staticVariables.REGISTRY).register(ModAPI.keygen.sound(key), rKey, ev); const outObj = SoundEvents.staticMethods.getRegisteredSoundEvent.method(ModAPI.util.str(key)); var snd = ModAPI.mc.mcSoundHandler.getCorrective(); var registry = snd.soundRegistry.soundRegistry; var soundPool = values.map(se => { return mkSound(ModAPI.util.str(se.path.replace("sounds/", "").replace(".ogg", "")), se.volume, se.pitch, 1, soundType, 1 * se.streaming); }); var eventAccessor = makeSoundEventAccessor112(rKey, subtitle); var eventAccessorWrapped = ModAPI.util.wrap(eventAccessor); soundPool.forEach(sound => { eventAccessorWrapped.accessorList.add(sound); }); AsyncSink.Audio.Objects.push([rKey, eventAccessor]); registry.put(rKey, eventAccessor); values.map(x => "resourcepacks/AsyncSinkLib/assets/minecraft/" + x.path + ".mcmeta").forEach(x => AsyncSink.setFile(x, new ArrayBuffer(0))); return outObj; } } else { const SoundPoolEntry = ModAPI.reflect.getClassByName("SoundPoolEntry").constructors.find(x => x.length === 4); AsyncSink.Audio.register = function addSfx(key, category, values) { if (!category) { throw new Error("[AsyncSink] Invalid audio category provided: " + category); } var snd = ModAPI.mc.mcSoundHandler; var registry = snd.sndRegistry.soundRegistry; var rKey = ResourceLocation(ModAPI.util.str(key)); var soundPool = values.map(se => { var path = ResourceLocation(ModAPI.util.str(se.path)); return SoundPoolEntry(path, se.pitch, se.volume, 1 * se.streaming); }).map(spe => { return makeSoundEventAccessor(spe, 1); // 1 = weight }); var compositeSound = makeSoundEventAccessorComposite(rKey, 1, 1, category); var compositeSoundWrapped = ModAPI.util.wrap(compositeSound); soundPool.forEach(sound => { compositeSoundWrapped.soundPool.add(sound); }); AsyncSink.Audio.Objects.push([rKey, compositeSound]); registry.put(rKey, compositeSound); values.map(x => "resourcepacks/AsyncSinkLib/assets/minecraft/" + x.path + ".mcmeta").forEach(x => AsyncSink.setFile(x, new ArrayBuffer(0))); return soundPool; } } })();