Merge pull request #41 from eaglerforge/main

Tutorial Drop 2
This commit is contained in:
ZXMushroom63 2024-12-09 17:01:26 +08:00 committed by GitHub
commit bc0bba9bde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 840 additions and 441 deletions

View File

@ -1,5 +1,10 @@
# EaglerForgeInjector
A modding API injector for vanilla eaglercraft builds.
An advanced modding API injector for vanilla eaglercraft builds.
Current features:
- Method hooking/monkey patching
- Reflection
- Custom classes
- Modify the dedicated server
### How to use:
#### Online

File diff suppressed because one or more lines are too long

View File

@ -23,7 +23,7 @@ Tutorials:
- [Disable All Particles](disable_all_particles.md)
- [Slippery Mod](slippery.md)
- [/hat mod](hat.md)
- [/spawnxp command](comingsoon)
- [/spawnxp command](spawnxp.md)
### Advanced
Prerequisites:
@ -33,6 +33,6 @@ Prerequisites:
- Your EaglerForgeInjector processed.html opened in an editor (optional)
Tutorials:
- [Custom Blocks](comingsoon)
- [Custom Blocks](custom_block.md)
- [Custom Items](comingsoon)
- [Timescale Command](comingsoon)

67
docs/tutorials/spawnxp.md Normal file
View File

@ -0,0 +1,67 @@
## /spawnxp command
This tutorial will cover spawning in entities using the [`ModAPI.promisify()`](../apidoc/promisify.md) API.
As usual, we'll start with the boilerplate:
```javascript
(function SpawnXPMod() {
ModAPI.meta.title("Spawn XP");
ModAPI.meta.description("Adds a /spawnxp command.");
ModAPI.meta.credits("By <author_name>");
//future code
})();
```
We'll move context to the server, and add a command listener, and ensure the sender is a player:
```javascript
PluginAPI.dedicatedServer.appendCode(function () { //Run this code on the server
PluginAPI.addEventListener("processcommand", (event) => { //Command processing event
if (
!ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP")
.instanceOf(event.sender.getRef())
) {
return;
}
});
});
```
Next, we'll check that the command is /spawnxp. If it is, we'll get the current dimension of the player, and spawn an XP orb at the sender's position. To construct the XP orb, we'll get the `EntityXPOrb` class and get the [first constructor](https://nurmarvin.github.io/Minecraft-1.8-JavaDocs/net/minecraft/entity/EntityXPOrb.html)
```javascript
PluginAPI.dedicatedServer.appendCode(function () { //Run this code on the server
PluginAPI.addEventListener("processcommand", (event) => { //Command processing event
if (
!ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP") //If it isn't a player, exit the method
.instanceOf(event.sender.getRef())
) {
return;
}
if (event.command.toLowerCase().startWith("/spawnxp")) {
const world = event.sender.getServerForPlayer(); //get the current dimension
const senderPos = event.sender.getPosition(); //get the player's current position
const EntityXP = ModAPI.reflect.getClassByName("EntityXPOrb"); // Get the entity xp orb class
const xporb = EntityXP.constructors[0](world.getRef(), senderPos.getX(), senderPos.getY(), senderPos.getZ(), 10); //Construct the first constructor, with arguments, World, x, y, z, xp value
//Get the spawn entity in world method
var spawnEntityInWorldMethod = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.world.World", "spawnEntityInWorld")];
//Because this method reads and writes data to chunks, it tries to save and load the eaglercraft state, which won't work if eaglercraft is also running its own code.
//To work around this issue, use ModAPI.promisify() to convert the method into one that returns a promise
spawnEntityInWorldMethod = ModAPI.promisify(spawnEntityInWorldMethod);
//we can now use the spawnEntityInWorld method
//wherever you see getRef(), it means to retrieve the raw, untouched data that TeaVM can use.
spawnEntityInWorldMethod(world.getRef(), xporb).then(result => {
// Get the chat component class
const ChatComponentTextClass = ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText");
// Construct the chat component and send it to the client.
event.sender.addChatMessage(ChatComponentTextClass.constructors[0](ModAPI.util.str("An xp orb has been spawned!")));
});
//Stop the unknown command error
event.preventDefault = true;
}
});
});

File diff suppressed because one or more lines are too long

View File

@ -19,22 +19,14 @@ function fixupBlockIds() {
}
function makeSteveBlock() {
var blockClass = ModAPI.reflect.getClassById("net.minecraft.block.Block");
var iproperty = ModAPI.reflect.getClassById("net.minecraft.block.properties.IProperty").class;
var makeBlockState = ModAPI.reflect.getClassById("net.minecraft.block.state.BlockState").constructors.find(x => x.length === 2);
var blockSuper = ModAPI.reflect.getSuper(blockClass, (x) => x.length === 2);
var creativeBlockTab = ModAPI.reflect.getClassById("net.minecraft.creativetab.CreativeTabs").staticVariables.tabBlock;
var nmb_BlockSteve = function nmb_BlockSteve() {
function nmb_BlockSteve() {
blockSuper(this, ModAPI.materials.rock.getRef());
this.$defaultBlockState = this.$blockState.$getBaseState();
this.$setCreativeTab(creativeBlockTab);
}
ModAPI.reflect.prototypeStack(blockClass, nmb_BlockSteve);
nmb_BlockSteve.prototype.$isOpaqueCube = function () {
return 1;
}
nmb_BlockSteve.prototype.$createBlockState = function (t) {
return makeBlockState(this, ModAPI.array.object(iproperty, 0));
}
globalThis.nmb_BlockSteve = nmb_BlockSteve;
}
function registerSteveClientSide() {

View File

@ -0,0 +1,27 @@
(() => {
ModAPI.meta.title("/ray_trace_test");
ModAPI.meta.description("Mod to test server-side raycasting.");
ModAPI.meta.credits("By ZXMushroom63");
PluginAPI.dedicatedServer.appendCode(function () {
PluginAPI.addEventListener("processcommand", (event) => {
if (event.command.toLowerCase().startsWith("/ray_trace_test")) {
if (
ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP").instanceOf(event.sender.getRef())
) {
//raytrace distance = 6
//the 0 on the end is for client side view bobbing (frame based). we are on the server so using 0 as default.
var movingObjectPosition = event.sender.rayTrace(6, 0).getCorrective();
console.log(movingObjectPosition);
var hitVec = movingObjectPosition.hitVec;
event.sender.addChatMessage(ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText").constructors[0](
movingObjectPosition.toString() //This is a java string, but that's ok since it goes into a java method.
));
}
event.preventDefault = true;
}
});
});
})();

121
examplemods/guns.js Normal file
View File

@ -0,0 +1,121 @@
(()=>{
const itemTexture = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADySURBVFhH7ZQxDoMwDEVNr4G6VmJC6mk4Itdh7VpxjsBPDaKVCLZxypInIccg5zs/CVQoKAj8uHLjeETouo6HvlQcU3yJ932PIKkTceRAFH8NA6dE3IzbVogdQBOPtuVXfk5IJ8jWhKY4SxPaQvcmLEWuTUj/A1sqiEEcTQBENDWjvh3mvZtxccLiwMqvE0DrhNUBCCy1KSdAUsPiwFYcpM5ENrYWh3tdI4cT4dk0MSKPXzMSRSC+PMi14mcO4e7esv2iJqyHEGDVPCR6jyMC5luERXOfuoY7QFi8MKsDe6tXk8OBv7Ge/E96DZeKFwoOEE1wUX7TFh5zsgAAAABJRU5ErkJggg==";
ModAPI.meta.title("guns");
ModAPI.meta.version("v1.0");
ModAPI.meta.icon(itemTexture);
ModAPI.meta.description("Requires AsyncSink.");
function PistolItem() {
var DamageSourceClass = ModAPI.reflect.getClassByName("DamageSource");
var creativeMiscTab = ModAPI.reflect.getClassById("net.minecraft.creativetab.CreativeTabs").staticVariables.tabMisc;
var itemClass = ModAPI.reflect.getClassById("net.minecraft.item.Item");
var itemSuper = ModAPI.reflect.getSuper(itemClass, (x) => x.length === 1);
var nmi_ItemPistol = function nmi_ItemPistol() {
itemSuper(this); //Use super function to get block properties on this class.
this.$setCreativeTab(creativeMiscTab);
}
function entityRayCast(player, world, range){
var eyePosition = player.getPositionEyes(0.0);
var lookVector = player.getLook(0.0);
var targetPosition = eyePosition.addVector(lookVector.xCoord * range, lookVector.yCoord * range, lookVector.zCoord * range);
var entities = world.getEntitiesWithinAABBExcludingEntity(
player.getRef(),
player.getEntityBoundingBox().expand(range, range, range).getRef()
).getCorrective().array;
var closestEntity = null;
var closestDistance = range;
// Iterate through all entities to find the one the player is looking at
for (var i = 0; i < entities.length; i++) {
if (!entities[i]) {
continue;
}
var entity = entities[i];
// Check if the entity's bounding box intersects with the player's ray
var entityBB = entity.getEntityBoundingBox().expand(0.3, 0.3, 0.3);
var intercept = entityBB.calculateIntercept(eyePosition.getRef(), targetPosition.getRef());
if (intercept != null) {
var distance = eyePosition.distanceTo(intercept.hitVec.getRef());
if (distance < closestDistance) {
closestDistance = distance;
closestEntity = entity;
}
}
}
var rayTraceResult = closestEntity;
if (rayTraceResult != null){
return rayTraceResult;
} else{
return null;
}
}
ModAPI.reflect.prototypeStack(itemClass, nmi_ItemPistol);
nmi_ItemPistol.prototype.$onItemRightClick = function ($itemstack, $world, $player) {
DamageSourceClass.staticMethods.$callClinit.method();
//Noticed that the gun only worked after an entity in the world takes damage XD
//TeaVM is very optimised. Using $callClinit tells it to hurry up pretty much lol
var cactus = DamageSourceClassstaticVariables.cactus;
var world = ModAPI.util.wrap($world);
var entityplayer = ModAPI.util.wrap($player);
var shotentity = entityRayCast(entityplayer, world, 12.0)
if (shotentity != null){
shotentity.attackEntityFrom(cactus, 10);
world.playSoundAtEntity(entityplayer.getRef(), ModAPI.util.str("tile.piston.out"), 1.0, 1.8);
}
return $itemstack;
}
function internal_reg() {
var pistol_item = (new nmi_ItemPistol()).$setUnlocalizedName(
ModAPI.util.str("pistol")
);
itemClass.staticMethods.registerItem.method(ModAPI.keygen.item("pistol"), ModAPI.util.str("pistol"), pistol_item);
ModAPI.items["pistol"] = pistol_item;
return pistol_item;
}
if (ModAPI.items) {
return internal_reg();
} else {
ModAPI.addEventListener("bootstrap", internal_reg);
}
}
ModAPI.dedicatedServer.appendCode(PistolItem);
var pistol_item = PistolItem();
ModAPI.addEventListener("lib:asyncsink", async () => {
ModAPI.addEventListener("custom:asyncsink_reloaded", ()=>{
ModAPI.mc.renderItem.registerItem(pistol_item, ModAPI.util.str("pistol"));
});
AsyncSink.L10N.set("item.pistol.name", "Pistol");
AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/models/item/pistol.json", JSON.stringify(
{
"parent": "builtin/generated",
"textures": {
"layer0": "items/pistol"
},
"display": {
"thirdperson": {
"rotation": [ 5, 80, -45 ],
"translation": [ 0, 1, -3 ],
"scale": [ 1.0, 1.0, 1.0 ]
},
"firstperson": {
"rotation": [ 0, -135, 25 ],
"translation": [ 0, 4, 2 ],
"scale": [ 1.8, 1.8, 1.8 ]
}
}
}
));
AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/textures/items/pistol.png", await (await fetch(
itemTexture
)).arrayBuffer());
});
})();

View File

@ -24,29 +24,21 @@
}
var itemClass = ModAPI.reflect.getClassById("net.minecraft.item.Item");
var blockClass = ModAPI.reflect.getClassById("net.minecraft.block.Block");
var iproperty = ModAPI.reflect.getClassById("net.minecraft.block.properties.IProperty").class;
var makeBlockState = ModAPI.reflect.getClassById("net.minecraft.block.state.BlockState").constructors.find(x => x.length === 2);
var blockSuper = ModAPI.reflect.getSuper(blockClass, (x) => x.length === 2); //Get super function from the block class with a target length of two. ($this (mandatory), material (optional))
var creativeBlockTab = ModAPI.reflect.getClassById("net.minecraft.creativetab.CreativeTabs").staticVariables.tabBlock;
var breakBlockMethod = blockClass.methods.breakBlock.method;
var nmb_BlockUnlucky = function nmb_BlockUnlucky() {
function nmb_BlockUnlucky() {
blockSuper(this, ModAPI.materials.rock.getRef()); //Use super function to get block properties on this class.
this.$defaultBlockState = this.$blockState.$getBaseState();
this.$setCreativeTab(creativeBlockTab);
}
ModAPI.reflect.prototypeStack(blockClass, nmb_BlockUnlucky);
nmb_BlockUnlucky.prototype.$isOpaqueCube = function () {
return 1;
}
nmb_BlockUnlucky.prototype.$createBlockState = function () {
return makeBlockState(this, ModAPI.array.object(iproperty, 0));
}
nmb_BlockUnlucky.prototype.$breakBlock = function ($world, $blockpos, $blockstate) {
var world = ModAPI.util.wrap($world);
var blockpos = ModAPI.util.wrap($blockpos);
if (Math.random() < 1) { //was gonna add random events but couldn't be bothered. Enjoy exploding!
world.newExplosion(null, blockpos.getX() + 0.5, blockpos.getY() + 0.5,
blockpos.getZ() + 0.5, 9, 1, 1);
blockpos.getZ() + 0.5, 9, 1, 0);
}
return breakBlockMethod(this, $world, $blockpos, $blockstate);
}

View File

@ -24,7 +24,7 @@
var example_item = (new nmi_ItemExample()).$setUnlocalizedName(
ModAPI.util.str("exampleitem")
);
itemClass.staticMethods.registerItem0.method(ModAPI.keygen.item("exampleitem"), ModAPI.util.str("exampleitem"), example_item);
itemClass.staticMethods.registerItem.method(ModAPI.keygen.item("exampleitem"), ModAPI.util.str("exampleitem"), example_item);
ModAPI.items["exampleitem"] = example_item;
return example_item;

View File

@ -117,7 +117,7 @@
<summary>
How do I compile my own unobfuscated unsigned Eaglercraft build?
</summary>
<a href="docs/compiling_client.md">tutorial here</a>
<a href="docs/compiling_client">tutorial here</a>
</details>
<details>
<summary>How does this tool work?</summary>
@ -169,7 +169,7 @@
<!-- Code assets -->
<script src="postinit.js"></script>
<script src="modloader.injector.js"></script>
<script src="modloader.js"></script>
<script src="modgui.js"></script>
<script src="efserver.js"></script>
</body>

View File

@ -18,24 +18,17 @@ function entriesToStaticVariableProxy(entries, prefix) {
var getComponents = "";
entries.forEach((entry) => {
getComponents += `
case \`${entry.name}\`:
return ${entry.variable};
break;`;
case \`${entry.name}\`: return ${entry.variable};`;
});
getComponents += `
default:
return Reflect.get(a,b,c);`
default: return Reflect.get(a,b,c);`
var setComponents = "";
entries.forEach((entry) => {
setComponents += `
case \`${entry.name}\`:
${entry.variable} = c;
break;`;
case \`${entry.name}\`: ${entry.variable} = c; break;`;
});
setComponents += `
default:
a[b]=c;`
setComponents += ` default: a[b]=c;`
/*/
ModAPI.hooks._rippedStaticIndexer[\`${prefix.replace(
@ -196,8 +189,7 @@ var main;(function(){`
(match) => {
if (
match.includes("__init_") ||
match.includes("__clinit_") ||
match.includes("_$callClinit")
match.includes("__clinit_")
) {
return match;
}
@ -212,13 +204,14 @@ var main;(function(){`
ModAPI.hooks.methods[\`${fullName}\`]=` +
match.replace(fullName + "(", "(")
);
return match;
}
);
var staticVariables = [
...patchedFile.matchAll(/var \S+?_\S+?_\S+? = null;/gm),
...patchedFile.matchAll(/var \S+?_\S+?_\S+? = /gm),
].flatMap((x) => {
return x[0];
}).filter(x => {
return (!x.includes("$_clinit_$")) && (!x.includes("$lambda$"))
});
patchedFile = patchedFile.replaceAll(
/var \S+?_\S+? = \$rt_classWithoutFields\(\S*?\);/gm,
@ -232,7 +225,7 @@ var main;(function(){`
if (entry.startsWith(prefix)) {
var variableName = entry
.replace("var ", "")
.replace(" = null;", "");
.replace(" = ", "");
var segments = variableName.split("_");
segments.splice(0, 2);
var name = segments.join("_");
@ -262,7 +255,7 @@ var main;(function(){`
if (entry.startsWith(prefix)) {
var variableName = entry
.replace("var ", "")
.replace(" = null;", "");
.replace(" = ", "");
var segments = variableName.split("_");
segments.splice(0, 2);
var name = segments.join("_");
@ -317,6 +310,7 @@ var main;(function(){`
\<script id="modapi_postinit_data"\>globalThis.modapi_postinit = \`${globalThis.modapi_postinit}\`;\<\/script\>
\<script id="libserverside"\>{"._|_libserverside_|_."}\<\/script\>`
);
patchedFile = patchedFile.replace(`<title>EaglercraftX 1.8</title>`, `<title>EFI ${globalThis.ModAPIVersion}</title>`);
patchedFile = patchedFile.replaceAll(/main\(\);\s*?}/gm, (match) => {
return match.replace("main();", "main();ModAPI.hooks._postInit();");
});

View File

@ -36,18 +36,19 @@ async function shronk(input) {
const parser = new DOMParser();
const doc = parser.parseFromString(inputHtml, 'text/html');
const scriptTags = doc.querySelectorAll('script');
await wait(100); //trying to get chrome to gc
for (let i = 0; i < scriptTags.length; i++) {
const scriptTag = scriptTags[i];
const code = scriptTag.textContent;
_status("[ASYNC_PLUGIN_1] Transpiling script #" + (i + 1) + " of length " + Math.round(code.length / 1000) + "k...");
await wait(50);
await wait(150);
const output = Babel.transform(code, {
plugins: []
});
scriptTag.textContent = output.code;
await wait(10);
}
_status("[ASYNC_PLUGIN_1] Job complete!");

View File

@ -24,7 +24,7 @@ globalThis.modapi_guikit = "(" + (() => {
</h5>
</header>
<table class="modTable">
<table id="modapi_gui_modTable">
<thead>
<tr>
<td>
@ -162,15 +162,18 @@ globalThis.modapi_guikit = "(" + (() => {
font-size: 1.25rem;
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1);
}
#modapi_gui_modTable {
min-width: 40vw;
}
</style>
</div>`;
async function fileToDataURI(file) {
async function fileToText(file) {
return new Promise((res, rej) => {
var fr = new FileReader();
fr.addEventListener("error", (e) => { rej(e); });
fr.addEventListener("load", (e) => { res(fr.result); });
fr.readAsDataURL(file);
fr.readAsText(file);
});
}
window.modapi_displayModGui = async function (cb) {
@ -191,7 +194,7 @@ globalThis.modapi_guikit = "(" + (() => {
document.querySelector("#modapi_gui_container")._cb = cb;
var modsList = await getMods();
var tbody = document.querySelector("#modapi_gui_container .modTable tbody");
var tbody = document.querySelector("#modapi_gui_container #modapi_gui_modTable tbody");
tbody.innerHTML = "";
modsList.forEach((modtxt, i) => {
if (!modtxt) { return }
@ -245,7 +248,6 @@ globalThis.modapi_guikit = "(" + (() => {
var button = document.createElement("button");
button.innerText = "Delete";
button.style.height = "3rem";
button.style.marginTop = "calc(50% - 1.5rem)";
button.addEventListener("click", async () => {
await removeMod(i);
window.modapi_displayModGui();
@ -254,7 +256,7 @@ globalThis.modapi_guikit = "(" + (() => {
controls.appendChild(button);
tr.appendChild(mod);
tr.appendChild(spacer);
tr.appendChild(button);
tr.appendChild(controls);
tbody.appendChild(tr);
});
var once = false;
@ -279,7 +281,7 @@ globalThis.modapi_guikit = "(" + (() => {
if (!mod || mod.length === 0) {
return;
}
await addMod("web@" + mod);
await addMod(mod);
window.modapi_displayModGui();
}
window.modapi_uploadmod = async () => {
@ -292,7 +294,7 @@ globalThis.modapi_guikit = "(" + (() => {
return;
}
for (let i = 0; i < f.files.length; i++) {
await addMod("web@" + (await fileToDataURI(f.files[i])).replaceAll(";base64", ";fs=" + f.files[i].name + ";base64"));
await addFileMod(f.files[i].name, (await fileToText(f.files[i])));
}
window.modapi_displayModGui();
});

View File

@ -1,212 +0,0 @@
globalThis.modapi_modloader = `function promisifyIDBRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function getDatabase() {
const dbRequest = indexedDB.open("EF_MODS");
const db = await promisifyIDBRequest(dbRequest);
if (!db.objectStoreNames.contains("filesystem")) {
db.close();
const version = db.version + 1;
const upgradeRequest = indexedDB.open("EF_MODS", version);
upgradeRequest.onupgradeneeded = (event) => {
const upgradedDb = event.target.result;
upgradedDb.createObjectStore("filesystem");
};
return promisifyIDBRequest(upgradeRequest);
}
return db;
}
async function getMods() {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readonly");
const objectStore = transaction.objectStore("filesystem");
const object = await promisifyIDBRequest(objectStore.get("mods.txt"));
return object ? (await object.text()).split("|") : [];
}
async function saveMods(mods) {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readwrite");
const objectStore = transaction.objectStore("filesystem");
const encoder = new TextEncoder();
const modsData = encoder.encode(mods.join("|"));
const modsBlob = new Blob([modsData], { type: "text/plain" });
await promisifyIDBRequest(objectStore.put(modsBlob, "mods.txt"));
}
async function addMod(mod) {
const mods = await getMods();
mods.push(mod);
await saveMods(mods);
}
async function removeMod(index) {
const mods = await getMods();
if (index >= 0 && index < mods.length) {
mods.splice(index, 1);
await saveMods(mods);
}
}
async function resetMods() {
await saveMods([]);
console.log("Mods reset");
}
window.modLoader = async function modLoader(modsArr = []) {
if (!window.eaglerMLoaderMainRun) {
var searchParams = new URLSearchParams(location.search);
searchParams.getAll("mod").forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from search params: " + modToAdd
);
modsArr.push(modToAdd);
});
searchParams.getAll("plugin").forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from search params: " + modToAdd
);
modsArr.push(modToAdd);
});
if (
!!eaglercraftXOpts &&
!!eaglercraftXOpts.Mods &&
Array.isArray(eaglercraftXOpts.Mods)
) {
eaglercraftXOpts.Mods.forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from eaglercraftXOpts: " +
modToAdd
);
modsArr.push(modToAdd);
});
}
console.log("[EaglerML] Searching in iDB");
try {
var idbMods = await getMods();
modsArr = modsArr.concat(idbMods
.filter(x => { return x && x.length > 0 })
.flatMap(x => { if (x.startsWith("web@")) { return x.replace("web@", "") } return x })
);
} catch (error) {
console.error(error);
}
window.eaglerMLoaderMainRun = true;
}
if (window.noLoadMods === true) {
modsArr.splice(0, modsArr.length);
}
function checkModsLoaded(totalLoaded, identifier) {
console.log(
"[EaglerML] Checking if mods are finished :: " +
totalLoaded +
"/" +
modsArr.length
);
if (totalLoaded >= modsArr.length) {
clearInterval(identifier);
window.ModGracePeriod = false;
if (
window.eaglerMLoaderMainRun &&
ModAPI &&
ModAPI.events &&
ModAPI.events.callEvent
) {
ModAPI.events.callEvent("load", {});
}
console.log(
"[EaglerML] Checking if mods are finished :: All mods loaded! Grace period off."
);
}
}
function methodB(currentMod) {
try {
console.log("[EaglerML] Loading " + currentMod + " via method B.");
var script = document.createElement("script");
script.setAttribute("data-hash", ModAPI.util.hashCode("web@"+currentMod));
script.src = currentMod;
script.setAttribute("data-isMod", "true");
script.onerror = () => {
console.log(
"[EaglerML] Failed to load " + currentMod + " via method B!"
);
script.remove();
totalLoaded++;
};
script.onload = () => {
console.log(
"[EaglerML] Successfully loaded " + currentMod + " via method B."
);
totalLoaded++;
};
document.body.appendChild(script);
} catch (error) {
console.log(
"[EaglerML] Oh no! The mod " + currentMod + " failed to load!"
);
totalLoaded++;
}
}
window.ModGracePeriod = true;
var totalLoaded = 0;
var loaderCheckInterval = null;
modsArr.forEach((c) => {
let currentMod = c;
console.log("[EaglerML] Starting " + currentMod);
try {
var req = new XMLHttpRequest();
req.open("GET", currentMod);
req.onload = function xhrLoadHandler() {
console.log("[EaglerML] Loading " + currentMod + " via method A.");
var script = document.createElement("script");
script.setAttribute("data-hash", ModAPI.util.hashCode("web@"+currentMod));
try {
script.src =
"data:text/javascript," + encodeURIComponent(req.responseText);
} catch (error) {
methodB(currentMod);
return;
}
script.setAttribute("data-isMod", "true");
script.onerror = () => {
console.log(
"[EaglerML] Failed to load " + currentMod + " via method A!"
);
script.remove();
totalLoaded++;
};
script.onload = () => {
console.log(
"[EaglerML] Successfully loaded " + currentMod + " via method A."
);
totalLoaded++;
};
document.body.appendChild(script);
};
req.onerror = function xhrErrorHandler() {
methodB(currentMod);
};
req.send();
} catch (error) {
methodB(currentMod);
}
});
loaderCheckInterval = setInterval(() => {
checkModsLoaded(totalLoaded, loaderCheckInterval);
}, 500);
console.log(
"[EaglerML] Starting to load " + modsArr.length + " mods..."
);
window.returnTotalLoadedMods = function returnTotalLoadedMods() {
return totalLoaded;
};
};`;

View File

@ -1,177 +1,220 @@
function promisifyIDBRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function getDatabase() {
const dbRequest = indexedDB.open("EF_MODS");
const db = await promisifyIDBRequest(dbRequest);
if (!db.objectStoreNames.contains("filesystem")) {
db.close();
const version = db.version + 1;
const upgradeRequest = indexedDB.open("EF_MODS", version);
upgradeRequest.onupgradeneeded = (event) => {
const upgradedDb = event.target.result;
upgradedDb.createObjectStore("filesystem");
};
return promisifyIDBRequest(upgradeRequest);
globalThis.modapi_modloader = "(" + (() => {
globalThis.promisifyIDBRequest = function promisifyIDBRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
return db;
}
globalThis.getDatabase = async function getDatabase() {
const dbRequest = indexedDB.open("EF_MODS");
const db = await promisifyIDBRequest(dbRequest);
async function getMods() {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readonly");
const objectStore = transaction.objectStore("filesystem");
const object = await promisifyIDBRequest(objectStore.get("mods.txt"));
return object ? (await object.text()).split("|") : [];
}
if (!db.objectStoreNames.contains("filesystem")) {
db.close();
const version = db.version + 1;
const upgradeRequest = indexedDB.open("EF_MODS", version);
upgradeRequest.onupgradeneeded = (event) => {
const upgradedDb = event.target.result;
upgradedDb.createObjectStore("filesystem");
};
return promisifyIDBRequest(upgradeRequest);
}
async function saveMods(mods) {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readwrite");
const objectStore = transaction.objectStore("filesystem");
const encoder = new TextEncoder();
const modsData = encoder.encode(mods.join("|"));
const modsBlob = new Blob([modsData], { type: "text/plain" });
await promisifyIDBRequest(objectStore.put(modsBlob, "mods.txt"));
}
return db;
}
async function addMod(mod) {
const mods = await getMods();
mods.push(mod);
await saveMods(mods);
}
globalThis.getMods = async function getMods() {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readonly");
const objectStore = transaction.objectStore("filesystem");
const object = await promisifyIDBRequest(objectStore.get("mods.txt"));
var out = object ? (await object.text()).split("|").toSorted() : [];
db.close();
return out;
}
async function removeMod(index) {
const mods = await getMods();
if (index >= 0 && index < mods.length) {
mods.splice(index, 1);
globalThis.getMod = async function getMod(mod) {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readonly");
const objectStore = transaction.objectStore("filesystem");
const object = await promisifyIDBRequest(objectStore.get("mods/" + mod));
var out = object ? (await object.text()) : "";
db.close();
return out;
}
globalThis.saveMods = async function saveMods(mods) {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readwrite");
const objectStore = transaction.objectStore("filesystem");
const encoder = new TextEncoder();
const modsData = encoder.encode(mods.toSorted().join("|"));
const modsBlob = new Blob([modsData], { type: "text/plain" });
await promisifyIDBRequest(objectStore.put(modsBlob, "mods.txt"));
db.close();
}
globalThis.addMod = async function addMod(mod) {
const mods = await getMods();
mods.push("web@" + mod);
await saveMods(mods);
}
}
async function resetMods() {
await saveMods([]);
console.log("Mods reset");
}
globalThis.addFileMod = async function addFileMod(mod, textContents) {
const mods = await getMods();
mods.push(mod);
await saveMods(mods);
window.modLoader = async function modLoader(modsArr = []) {
if (!window.eaglerMLoaderMainRun) {
var searchParams = new URLSearchParams(location.search);
searchParams.getAll("mod").forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from search params: " + modToAdd
);
modsArr.push(modToAdd);
});
searchParams.getAll("plugin").forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from search params: " + modToAdd
);
modsArr.push(modToAdd);
});
if (
!!eaglercraftXOpts &&
!!eaglercraftXOpts.Mods &&
Array.isArray(eaglercraftXOpts.Mods)
) {
eaglercraftXOpts.Mods.forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from eaglercraftXOpts: " +
modToAdd
);
modsArr.push(modToAdd);
});
}
console.log("[EaglerML] Searching in iDB");
try {
var idbMods = await getMods();
modsArr = modsArr.concat(idbMods
.filter(x => { return x && x.length > 0 })
.flatMap(x => { if (x.startsWith("web@")) { return x.replace("web@", "") } return x })
);
} catch (error) {
console.error(error);
}
window.eaglerMLoaderMainRun = true;
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readwrite");
const objectStore = transaction.objectStore("filesystem");
const encoder = new TextEncoder();
const modsData = encoder.encode(textContents);
const modsBlob = new Blob([modsData], { type: "text/plain" });
await promisifyIDBRequest(objectStore.put(modsBlob, "mods/" + mod));
db.close();
}
if (window.noLoadMods === true) {
modsArr.splice(0, modsArr.length);
}
function checkModsLoaded(totalLoaded, identifier) {
console.log(
"[EaglerML] Checking if mods are finished :: " +
totalLoaded +
"/" +
modsArr.length
);
if (totalLoaded >= modsArr.length) {
clearInterval(identifier);
window.ModGracePeriod = false;
if (
window.eaglerMLoaderMainRun &&
ModAPI &&
ModAPI.events &&
ModAPI.events.callEvent
) {
ModAPI.events.callEvent("load", {});
globalThis.removeMod = async function removeMod(index) {
const mods = await getMods();
if (index >= 0 && index < mods.length) {
var deleted = mods.splice(index, 1)[0];
await saveMods(mods);
if (!deleted.startsWith("web@")) {
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readwrite");
const objectStore = transaction.objectStore("filesystem");
await promisifyIDBRequest(objectStore.delete("mods/" + deleted));
db.close();
}
console.log(
"[EaglerML] Checking if mods are finished :: All mods loaded! Grace period off."
);
}
}
function methodB(currentMod) {
try {
console.log("[EaglerML] Loading " + currentMod + " via method B.");
var script = document.createElement("script");
script.setAttribute("data-hash", ModAPI.util.hashCode("web@"+currentMod));
script.src = currentMod;
script.setAttribute("data-isMod", "true");
script.onerror = () => {
console.log(
"[EaglerML] Failed to load " + currentMod + " via method B!"
);
script.remove();
totalLoaded++;
};
script.onload = () => {
console.log(
"[EaglerML] Successfully loaded " + currentMod + " via method B."
);
totalLoaded++;
};
document.body.appendChild(script);
} catch (error) {
console.log(
"[EaglerML] Oh no! The mod " + currentMod + " failed to load!"
);
totalLoaded++;
}
globalThis.resetMods = async function resetMods() {
console.log("Resetting mods...");
const db = await getDatabase();
const transaction = db.transaction(["filesystem"], "readwrite");
const objectStore = transaction.objectStore("filesystem");
await promisifyIDBRequest(objectStore.clear());
console.log("Mods reset");
db.close();
}
window.ModGracePeriod = true;
var totalLoaded = 0;
var loaderCheckInterval = null;
modsArr.forEach((c) => {
let currentMod = c;
console.log("[EaglerML] Starting " + currentMod);
try {
var req = new XMLHttpRequest();
req.open("GET", currentMod);
req.onload = function xhrLoadHandler() {
globalThis.modLoader = async function modLoader(modsArr = []) {
if (!window.eaglerMLoaderMainRun) {
var searchParams = new URLSearchParams(location.search);
searchParams.getAll("mod").forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from search params: " + modToAdd
);
modsArr.push("web@" + modToAdd);
});
searchParams.getAll("plugin").forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from search params: " + modToAdd
);
modsArr.push("web@" + modToAdd);
});
if (
!!eaglercraftXOpts &&
!!eaglercraftXOpts.Mods &&
Array.isArray(eaglercraftXOpts.Mods)
) {
eaglercraftXOpts.Mods.forEach((modToAdd) => {
console.log(
"[EaglerML] Adding mod to loadlist from eaglercraftXOpts: " +
modToAdd
);
modsArr.push(modToAdd);
});
}
console.log("[EaglerML] Searching in iDB");
try {
var idbMods = await getMods();
modsArr = modsArr.concat(idbMods
.filter(x => { return x && x.length > 0 })
);
} catch (error) {
console.error(error);
}
window.eaglerMLoaderMainRun = true;
}
if (window.noLoadMods === true) {
modsArr.splice(0, modsArr.length);
}
function checkModsLoaded(totalLoaded, identifier) {
console.log(
"[EaglerML] Checking if mods are finished :: " +
totalLoaded +
"/" +
modsArr.length
);
if (totalLoaded >= modsArr.length) {
clearInterval(identifier);
window.ModGracePeriod = false;
if (
window.eaglerMLoaderMainRun &&
ModAPI &&
ModAPI.events &&
ModAPI.events.callEvent
) {
ModAPI.events.callEvent("load", {});
}
console.log(
"[EaglerML] Checking if mods are finished :: All mods loaded! Grace period off."
);
}
}
function methodB(currentMod) {
try {
console.log("[EaglerML] Loading " + currentMod + " via method B.");
var script = document.createElement("script");
script.setAttribute("data-hash", ModAPI.util.hashCode("web@" + currentMod));
script.src = currentMod;
script.setAttribute("data-isMod", "true");
script.onerror = () => {
console.log(
"[EaglerML] Failed to load " + currentMod + " via method B!"
);
script.remove();
totalLoaded++;
};
script.onload = () => {
console.log(
"[EaglerML] Successfully loaded " + currentMod + " via method B."
);
totalLoaded++;
};
document.body.appendChild(script);
} catch (error) {
console.log(
"[EaglerML] Oh no! The mod " + currentMod + " failed to load!"
);
totalLoaded++;
}
}
window.ModGracePeriod = true;
var totalLoaded = 0;
var loaderCheckInterval = null;
modsArr.sort();
for (let i = 0; i < modsArr.length; i++) {
let currentMod = modsArr[i];
var isIDBMod = !currentMod.startsWith("web@");
if (!isIDBMod) {
currentMod = currentMod.replace("web@", "");
}
console.log("[EaglerML] Starting " + currentMod);
try {
var responseText = isIDBMod ? await getMod(currentMod) : await (await fetch(currentMod)).text();
console.log("[EaglerML] Loading " + currentMod + " via method A.");
var script = document.createElement("script");
script.setAttribute("data-hash", ModAPI.util.hashCode("web@"+currentMod));
script.setAttribute("data-hash", ModAPI.util.hashCode((isIDBMod ? "" : "web@") + currentMod));
try {
script.src =
"data:text/javascript," + encodeURIComponent(req.responseText);
"data:text/javascript," + encodeURIComponent(responseText);
} catch (error) {
methodB(currentMod);
return;
@ -191,22 +234,18 @@ window.modLoader = async function modLoader(modsArr = []) {
totalLoaded++;
};
document.body.appendChild(script);
};
req.onerror = function xhrErrorHandler() {
} catch (error) {
methodB(currentMod);
};
req.send();
} catch (error) {
methodB(currentMod);
}
}
});
loaderCheckInterval = setInterval(() => {
checkModsLoaded(totalLoaded, loaderCheckInterval);
}, 500);
console.log(
"[EaglerML] Starting to load " + modsArr.length + " mods..."
);
window.returnTotalLoadedMods = function returnTotalLoadedMods() {
return totalLoaded;
loaderCheckInterval = setInterval(() => {
checkModsLoaded(totalLoaded, loaderCheckInterval);
}, 500);
console.log(
"[EaglerML] Starting to load " + modsArr.length + " mods..."
);
window.returnTotalLoadedMods = function returnTotalLoadedMods() {
return totalLoaded;
};
};
};
}).toString() + ")();"

View File

@ -1,3 +1,4 @@
globalThis.ModAPIVersion = "v2.3.3";
globalThis.modapi_postinit = "(" + (() => {
//EaglerForge post initialization code.
//This script cannot contain backticks, escape characters, or backslashes in order to inject into the dedicated server code.
@ -22,7 +23,7 @@ globalThis.modapi_postinit = "(" + (() => {
ModAPI.meta._versionMap = {};
ModAPI.array = {};
ModAPI.version = "v2.3.1";
ModAPI.version = "__modapi_version_code__";
ModAPI.flavour = "injector";
ModAPI.GNU = "terry pratchett";
ModAPI.credits = ["ZXMushroom63", "radmanplays", "Murturtle", "OtterCodes101", "TheIdiotPlays", "OeildeLynx31", "Stpv22"];
@ -889,12 +890,16 @@ globalThis.modapi_postinit = "(" + (() => {
}
ModAPI.events.newEvent("bootstrap", "server");
const bootstrapClass = ModAPI.reflect.getClassById("net.minecraft.init.Bootstrap");
const originalBootstrap = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.init.Bootstrap", "register")];
ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.init.Bootstrap", "register")] = function (...args) {
if (bootstrapClass.staticVariables.alreadyRegistered) {
return;
}
var x = originalBootstrap.apply(this, args);
ModAPI.util.bootstrap();
ModAPI.events.callEvent("bootstrap", {});
console.log("[ModAPI] Hooked into bootstrap. .blocks, .items, .materials and .enchantments are now accessible.");
ModAPI.events.callEvent("bootstrap", {});
return x;
}
@ -954,8 +959,8 @@ globalThis.modapi_postinit = "(" + (() => {
}
function qhash(txt, arr) {
var interval = 4095 - arr.length;
if (interval < 1) {
var interval = 4095; //used to be 4095 - arr.length, but that increases incompatibility based on load order and otehr circumstances
if (arr.length >= 4095) {
console.error("[ModAPI.keygen] Ran out of IDs while generating for " + txt);
return -1;
}
@ -968,7 +973,7 @@ globalThis.modapi_postinit = "(" + (() => {
}
var hash = x;
while (arr.includes(hash)) {
hash = (hash + 1) % (interval + arr.length);
hash = (hash + 1) % interval;
}
return hash;
}
@ -989,4 +994,4 @@ globalThis.modapi_postinit = "(" + (() => {
var values = [...ModAPI.reflect.getClassById("net.minecraft.block.Block").staticVariables.blockRegistry.$modapi_specmap.values()];
return qhash(block, values);
}
}).toString() + ")();";
}).toString().replace("__modapi_version_code__", ModAPIVersion) + ")();";