diff --git a/docs/apidoc/index.md b/docs/apidoc/index.md index 5336311..73623df 100644 --- a/docs/apidoc/index.md +++ b/docs/apidoc/index.md @@ -94,6 +94,9 @@ The ModAPI object has the following methods: - Triggers a right click ingame. - `getFPS() : int` - Gets the frames per second of the game + - `promisify(asyncJavaMethod: Method | Constructor) : PromisifiedJavaRunner` + - Allows running java methods that are @Async/@Async dependent. + - More [PromisifyDocumentation](promisify.md) ## Handling strings, numbers and booleans to and from java. @@ -141,4 +144,26 @@ For example, take the method `setRenderViewEntity()` on `ModAPI.mcinstance`. Ins ```javascript var entityIndex = 1; //Index of the entity to look for. 0 means first, which is usually the player, so 1 is usually a natural entity. ModAPI.mc.setRenderViewEntity(ModAPI.world.loadedEntityList.get(entityIndex).getRef()); -``` \ No newline at end of file +``` + +## Corrective Proxies +By default, accessing a global like `ModAPI.player` will return a proxy to the original player that removes $ prefixes, as well as making instance methods callable. TeaVM has a quirk where it adds numerical suffixes to some properties. For example `ModAPI.player.inGround0` instead of `ModAPI.player.inGround`. As this is a large issue due to these suffixes changing for every eaglercraft update, you can now bypass this by obtaining a corrective version of `ModAPI.player`, using `ModAPI.player.getCorrective()`. + +For example: +```javascript +ModAPI.player.inGround //returns undefined +ModAPI.player.inGround0 //returns 1 or 0, the correct value + +ModAPI.player.isCorrective() //returns false + +var correctedPlayer = ModAPI.player.getCorrective(); + +correctedPlayer.inGround //returns 1 or 0, the correct value +correctedPlayer.inGround0 //returns 1 or 0, the correct value + +correctedPlayer.isCorrective() //returns true +``` + +You can check if an object is corrective using `.isCorrective()`; + +Accessing children of a corrective object will also make them corrective. `correctedPlayer.fishEntity.isCorrective(); //true` \ No newline at end of file diff --git a/docs/apidoc/promisify.md b/docs/apidoc/promisify.md new file mode 100644 index 0000000..e19fac7 --- /dev/null +++ b/docs/apidoc/promisify.md @@ -0,0 +1,26 @@ +## ModAPI.promisify() +Some methods in java are asynchronous, meaning that they don't return a value/modify state immediately. Calling them in an event or in a patch to a normal function will cause a stack implosion, characterised by the client/dedicated server hanging without any error messages. + +In order to call them properly from javascript, you need to use the `ModAPI.promisify()` function. + +For example, here we have a simple client-side command that will try to use the `PlatformRuntime` class to download data from a URI: +```javascript +ModAPI.addEventListener("sendchatmessage", function downloadSomething(e) { + if (e.message.toLowerCase().startsWith("/downloadtest")) { + var arraybuffer = ModAPI.hooks.methods.nlevi_PlatformRuntime_downloadRemoteURI(ModAPI.util.str("data:text/plain,hi")); + console.log(arraybuffer); + } +}); +``` +This will cause the client to hang. The correct way of calling this asynchronous method is like this: +```javascript +ModAPI.addEventListener("sendchatmessage", function downloadSomething(e) { + if (e.message.toLowerCase().startsWith("/downloadtest")) { + ModAPI.promisify(ModAPI.hooks.methods.nlevi_PlatformRuntime_downloadRemoteURI)(ModAPI.util.str("data:text/plain,hi")).then(arraybuffer => { + console.log(arraybuffer); + }); + } +}); +``` + +You can replace the argument with any other method or constructor, including non asynchronous ones. \ No newline at end of file diff --git a/docs/quirks.md b/docs/quirks.md index 92e3643..fb72a17 100644 --- a/docs/quirks.md +++ b/docs/quirks.md @@ -2,7 +2,7 @@ When TeaVM compiles code, it sometimes does strange things. #### Property Suffixes -TeaVM will add suffixes to some variables, seemingly randomly. An example is the property `inGround` of any entity. When accessing this on the `ModAPI.player.fishEntity` object, TeaVM has renamed it to `inGround2`. Can be mitigated with `ModAPI.util.getNearestProperty`. +TeaVM will add suffixes to some variables, seemingly randomly. An example is the property `inGround` of any entity. When accessing this on the `ModAPI.player.fishEntity` object, TeaVM has renamed it to `inGround2`. Can be mitigated with `ModAPI.util.getNearestProperty`, or, even better, using a corrective version of ModAPI.player. #### Collapsing Methods When I was trying to hook into the server-side processing of chat messages, I found that chat packets were handled by the method `processChatMessage` in `NetHandlerPlayServer`. However, in the compiled JavaScript, this method no longer exists. This is because it is only used once, in the `processPacket` method of `C01PacketChatMessage`. TeaVM automatically saw this, and collapsed one method into the other. @@ -19,6 +19,9 @@ Calling methods while the TeaVM thread is in a critical transition state (see `M Update 22/09/2024: See Asynchronous Code +Update 4/10/2024: +@Async issue solved, see [PromisifyDocumentation](apidoc/promisify.md) + #### TeaVM thread suspension/resumption TeaVM allows for writing asynchronous callbacks, which eaglercraft uses for file operations and downloading from URIs. However, when a method that makes use of an async callback gets run from ModAPI, it triggers a stack implosion due to mismatches in value types upon return (as well as a whole other myriad of symptoms). Currently this is not supported by ModAPI, and it will take some time until it will be. In the meanwhile, avoid using constructors or methods that access a file or use other asynchronous apis. Examples: - Constructing an EntityPlayerMP diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index e69de29..f172438 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -0,0 +1,10 @@ +## Mod tutorials with ModAPI + +Prerequisites: + - Basic knowledge of JavaScript + - A good code editor (recommended: https://vscode.dev) + +### Beginner +- [Step Hack](step.md) +- [Spider Hack](spider.md) +- [VClip Exploit](comingsoon) \ No newline at end of file diff --git a/docs/tutorials/spider.md b/docs/tutorials/spider.md new file mode 100644 index 0000000..cd99e35 --- /dev/null +++ b/docs/tutorials/spider.md @@ -0,0 +1,41 @@ +## Spider Hack with ModAPI +A spider hack allows players to climb up any wall like a spider. + +Start by creating a new file in your code editor. Save it to a folder on your device, ensuring that the file extension is `.js`. + +The spider hack has a lot of similarities to the step hack, so lets copy the step hack and rename the `stepHackUpdateCode` function to `spiderHackUpdateCode`: +```javascript +ModAPI.require("player"); + +function spiderHackUpdateCode() { + //We will add code here. +} +ModAPI.addEventListener("update", spiderHackUpdateCode); +``` + +Most spider hacks work by checking if the player is walking into a wall, and then setting their vertical velocity to a constant amount (usually `0.2`, for parity with ladders). +Let's start by checking if the player is walking into a wall, by adding an if statement inside the `spiderHackUpdateCode` function: +```javascript +if (ModAPI.player.isCollidedHorizontally) { + +} +``` + +Now, let's set the player's vertical velocity: +```javascript +if (ModAPI.player.isCollidedHorizontally) { + ModAPI.player.motionY = 0.2; //Feel free to change this value to something bigger, smaller or even negative. +} +``` + +Time to see the final code: +```javascript +ModAPI.require("player"); + +function spiderHackUpdateCode() { + if (ModAPI.player.isCollidedHorizontally) { + ModAPI.player.motionY = 0.2; //Feel free to change this value to something bigger, smaller or even negative. + } +} +ModAPI.addEventListener("update", spiderHackUpdateCode); +``` \ No newline at end of file diff --git a/docs/tutorials/step.md b/docs/tutorials/step.md new file mode 100644 index 0000000..eed3453 --- /dev/null +++ b/docs/tutorials/step.md @@ -0,0 +1,33 @@ +## Step Hack with ModAPI +A step hack allows a player to walk up full blocks (or more) as if they were slabs. + +Start by creating a new file in your code editor. Save it to a folder on your device, ensuring that the file extension is `.js`. + +Let's start by requiring the player. This will allow us to change attributes on the player: +```javascript +ModAPI.require("player"); +``` + +Now, we have to detect when the player in a world. This will be done with the `update` event, which runs every tick while the player is in a world. In EaglerForge's ModAPI, to run code on an event, you have to create a function. Then, you register the function to an event. +```javascript +ModAPI.require("player"); + +function stepHackUpdateCode() { + //We will add code here. +} +ModAPI.addEventListener("update", stepHackUpdateCode); +``` + +Inside this method, lets change the player's `stepHeight`, which controls how high they can step. By default this is `0.5`, to alow players to walk up slabs or stairs. I'll change it to `2` for demonstration purposes. You can also try any other number, like `0`, `6`, etc. +```javascript +ModAPI.require("player"); + +function stepHackUpdateCode() { + ModAPI.player.stepHeight = 2; +} +ModAPI.addEventListener("update", stepHackUpdateCode); +``` + +Now, to load this mod, open your EaglerForge build, and in the start screen select `Upload Mod (.js)`. Upload the mod you created, and you should now be able to walk up two block tall pillars, or more depending on what `stepHeight` you selected. + +Disclaimer: using this on servers may get you banned/kicked for cheating! \ No newline at end of file diff --git a/examplemods/astar.js b/examplemods/astar.js index dc90951..4cfd6e7 100644 --- a/examplemods/astar.js +++ b/examplemods/astar.js @@ -6,19 +6,56 @@ ModAPI.require("player"); ModAPI.require("world"); + var tessellator = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.renderer.Tessellator", "getInstance")](); + var worldRenderer = tessellator.$getWorldRenderer(); + var glStateManagerSetColor = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "color")]; + var glStateManagerEnableBlend = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "enableBlend")]; + var glStateManagerDisableBlend = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "disableBlend")]; + var glStateManagerdisableTex2d = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "disableTexture2D")]; + var glStateManagerenableTex2d = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "enableTexture2D")]; + var glStateManagerdisableDepth = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "disableDepth")]; + var glStateManagerenableDepth = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "enableDepth")]; + var glStateManagerSetBlend = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "blendFunc")]; + var glStateManagerDepthMask = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager", "depthMask")]; + var eaglercraftGPUSetLineWidth = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU", "glLineWidth")]; + var positionVertexFormat = ModAPI.reflect.getClassByName("DefaultVertexFormats").staticVariables.POSITION; + globalThis.drawLine = function drawLine(positions, color) { + glStateManagerSetBlend(770, 771); + glStateManagerEnableBlend(); + eaglercraftGPUSetLineWidth(2); + glStateManagerdisableTex2d(); + glStateManagerdisableDepth(); + glStateManagerDepthMask(0); + + var renderManager = ModAPI.mc.getRenderManager(); + + glStateManagerSetColor(color.r, color.b, color.g, color.a); + + worldRenderer.$begin(3, positionVertexFormat); + positions.forEach(pos => { + worldRenderer.$pos(pos.x - renderManager.renderPosX, pos.y - renderManager.renderPosY, pos.z - renderManager.renderPosZ).$endVertex(); + }); + tessellator.$draw(); + + glStateManagerenableTex2d(); + glStateManagerDepthMask(1); + glStateManagerenableDepth(); + glStateManagerDisableBlend(); + } + var blockBreakCostMultiplier = 2; const costMap = Object.fromEntries(Object.keys(ModAPI.blocks).flatMap(x => { ModAPI.blocks[x].readableId = x; - return [x, (Math.floor(ModAPI.blocks[x].blockHardness * 10 * blockBreakCostMultiplier) + 1) || 99999]; //Block hardness is in decimals, unbreakable blocks are negative one, and base movement cost is 1. -1 + 1 = 0, and 0 || 99999 is 99999 + return [[x, (Math.floor(ModAPI.blocks[x].blockHardness * 10 * blockBreakCostMultiplier) + 1) || 99999]]; //Block hardness is in decimals, unbreakable blocks are negative one, and base movement cost is 1. -1 + 1 = 0, and 0 || 99999 is 99999 })); - var makeBlockPos = ModAPI.reflect.getClassById("net.minecraft.util.BlockPos").constructors.find(x=>x.length === 3); + var makeBlockPos = ModAPI.reflect.getClassById("net.minecraft.util.BlockPos").constructors.find(x => x.length === 3); function shouldPause(x, y, z) { return !ModAPI.world.isAreaLoaded0(makeBlockPos(x, y, z), 2); } - class APNode { + globalThis.APNode = class APNode { constructor(x, y, z, g, h, parent = null) { this.x = x; this.y = y; @@ -29,11 +66,11 @@ this.parent = parent; } } - + function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.z - b.z); } - + function getNeighbors(node) { const neighbors = []; const directions = [ @@ -41,32 +78,32 @@ [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1] ]; - + for (const [dx, dy, dz] of directions) { const x = node.x + dx; const y = node.y + dy; const z = node.z + dz; - + if (ModAPI.world.isBlockLoaded(makeBlockPos(Math.round(x), Math.round(y), Math.round(z)))) { neighbors.push(new APNode(x, y, z, 0, 0)); } } return neighbors; } - + function lookupCost(x, y, z) { var block = ModAPI.world.getBlockState(makeBlockPos(Math.round(x), Math.round(y), Math.round(z))).block; return costMap[block.readableId]; } - - function* aStar(start, goal) { + + globalThis.aStar = function* aStar(start, goal) { const openSet = []; const closedSet = new Set(); openSet.push(start); - + while (openSet.length > 0) { let current = openSet.reduce((prev, curr) => (prev.f < curr.f ? prev : curr)); - + if (current.x === goal.x && current.y === goal.y && current.z === goal.z) { const path = []; while (current) { @@ -76,17 +113,17 @@ yield* path.reverse(); return; } - + openSet.splice(openSet.indexOf(current), 1); closedSet.add(`${current.x},${current.y},${current.z}`); - + for (const neighbor of getNeighbors(current)) { if (closedSet.has(`${neighbor.x},${neighbor.y},${neighbor.z}`)) { continue; } - + const tentativeG = current.g + lookupCost(neighbor.x, neighbor.y, neighbor.z); - + if (!openSet.some(node => node.x === neighbor.x && node.y === neighbor.y && node.z === neighbor.z)) { neighbor.g = tentativeG; neighbor.h = heuristic(neighbor, goal); @@ -102,15 +139,36 @@ } } } - + yield [current.x, current.y, current.z]; } - + return []; } - - const start = new APNode(0, 0, 0, 0, 0); - const goal = new APNode(2, 2, 2, 0, 0); - - const pathGenerator = aStar(start, goal); -})(); \ No newline at end of file +})(); +var start = new APNode(-24, 73, 1, 0, 0); +var goal = new APNode(-30, 73, 1, 0, 0); +var pathGenerator = aStar(start, goal); +var positions = []; +var rendererPositions = []; +var timer = 0; +ModAPI.addEventListener("update", ()=>{ + timer++; + if (timer > 20) { + timer = 0; + } else { + return; + } + if (positions.length > 0 && shouldPause(...positions[positions.length - 1])) { + return; + } + var nextPos = pathGenerator.next(); + if (nextPos.value && nextPos.value.length === 3) { + positions.push(nextPos.value); + rendererPositions.push({x: nextPos.value[0] + 0.5, y: nextPos.value[1] + 0.5, z: nextPos.value[2] + 0.5}); + } +}); + +ModAPI.addEventListener("render", () => { + drawLine(rendererPositions, { r: 1, g: 0, b: 0, a: 0.5 }) +}); \ No newline at end of file diff --git a/examplemods/grapplehook.js b/examplemods/grapplehook.js index 2595f04..fa842e4 100644 --- a/examplemods/grapplehook.js +++ b/examplemods/grapplehook.js @@ -1,41 +1,44 @@ -PluginAPI.require("player"); //Require the player -var GrappleHookPlugin = { - oldXYZ: [0, 0, 0], //The previous hook position. - prev: "NONE", //The previous state - scaleH: 0.25, //Used for X and Z velocity - scaleV: 0.15, //((Grapple Y) minus (Player Y)) times scaleV - lift: 0.4, //Base vertical motion - crouchToCancel: true //Whether or not crouching should disable the grappling hook. -}; -PluginAPI.addEventListener("update", () => { //Every client tick - if (!PluginAPI.player.fishEntity) { //If the fish hook does not exist. - if (GrappleHookPlugin.prev === "GROUND" && (!GrappleHookPlugin.crouchToCancel || !PluginAPI.player.isSneaking())) { //If the old state was ground - GrappleHookPlugin.prev = "NONE"; //Update the state - var mx = GrappleHookPlugin.oldXYZ[0] - PluginAPI.player.posX; //Get delta X - var my = GrappleHookPlugin.oldXYZ[1] - PluginAPI.player.posY; //Get delta Y - var mz = GrappleHookPlugin.oldXYZ[2] - PluginAPI.player.posZ; //Get delta Z - mx *= GrappleHookPlugin.scaleH; //Multiply by horizontal scale - my *= GrappleHookPlugin.scaleV; //Multiply by vertical scale - mz *= GrappleHookPlugin.scaleH; //Multiply by horizontal scale - PluginAPI.player.motionX += mx; //Add x motion - PluginAPI.player.motionY += my + GrappleHookPlugin.lift; //Add y motion, plus base lift. - PluginAPI.player.motionZ += mz; //Add z motion - } else { - GrappleHookPlugin.prev = "NONE"; +(function grapplehook() { + PluginAPI.require("player"); //Require the player + var GrappleHookPlugin = { + oldXYZ: [0, 0, 0], //The previous hook position. + prev: "NONE", //The previous state + scaleH: 0.25, //Used for X and Z velocity + scaleV: 0.15, //((Grapple Y) minus (Player Y)) times scaleV + lift: 0.4, //Base vertical motion + crouchToCancel: true //Whether or not crouching should disable the grappling hook. + }; + var player = ModAPI.player.getCorrective(); //Gets the corrective version of the player object. This removes broken proerty suffixes. You usually don't need this, but in my case, I do. + PluginAPI.addEventListener("update", () => { //Every client tick + if (!player.fishEntity) { //If the fish hook does not exist. + if (GrappleHookPlugin.prev === "GROUND" && (!GrappleHookPlugin.crouchToCancel || !player.isSneaking())) { //If the old state was ground + GrappleHookPlugin.prev = "NONE"; //Update the state + var mx = GrappleHookPlugin.oldXYZ[0] - player.posX; //Get delta X + var my = GrappleHookPlugin.oldXYZ[1] - player.posY; //Get delta Y + var mz = GrappleHookPlugin.oldXYZ[2] - player.posZ; //Get delta Z + mx *= GrappleHookPlugin.scaleH; //Multiply by horizontal scale + my *= GrappleHookPlugin.scaleV; //Multiply by vertical scale + mz *= GrappleHookPlugin.scaleH; //Multiply by horizontal scale + player.motionX += mx; //Add x motion + player.motionY += my + GrappleHookPlugin.lift; //Add y motion, plus base lift. + player.motionZ += mz; //Add z motion + } else { + GrappleHookPlugin.prev = "NONE"; + } + } else if (GrappleHookPlugin.prev === "NONE") { //If the hook exists, but the previous state was NONE, update the state. + GrappleHookPlugin.prev = "AIR"; } - } else if (GrappleHookPlugin.prev === "NONE") { //If the hook exists, but the previous state was NONE, update the state. - GrappleHookPlugin.prev = "AIR"; - } - if ( - PluginAPI.player.fishEntity !== undefined && //If the fish hook exists - GrappleHookPlugin.prev === "AIR" && //And the hook was previously in the air - PluginAPI.player.fishEntity[PluginAPI.util.getNearestProperty(ModAPI.player.fishEntity, "inGround")] //And the hook is in the ground (the inGround property is botched with random suffixes sometimes) - ) { - GrappleHookPlugin.oldXYZ = [ //Set old grapple hook position - PluginAPI.player.fishEntity.posX, - PluginAPI.player.fishEntity.posY, - PluginAPI.player.fishEntity.posZ, - ]; - GrappleHookPlugin.prev = "GROUND";//Update state - } + if ( + player.fishEntity !== undefined && //If the fish hook exists + GrappleHookPlugin.prev === "AIR" && //And the hook was previously in the air + player.fishEntity.inGround //And the hook is in the ground + ) { + GrappleHookPlugin.oldXYZ = [ //Set old grapple hook position + player.fishEntity.posX, + player.fishEntity.posY, + player.fishEntity.posZ, + ]; + GrappleHookPlugin.prev = "GROUND";//Update state + } + }); }); \ No newline at end of file diff --git a/examplemods/mobnpcspawner.js b/examplemods/mobnpcspawner.js index 7b274e8..292b965 100644 --- a/examplemods/mobnpcspawner.js +++ b/examplemods/mobnpcspawner.js @@ -4,8 +4,8 @@ // Check if the sender is a player if (!ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP").instanceOf(event.sender.getRef())) { return; } - // Check if the command is "/spawnnpc" - if (event.command.toLowerCase().startsWith("/spawnnpc")) { + // Check if the command is "/spawnsheep" + if (event.command.toLowerCase().startsWith("/spawnsheep")) { const world = event.sender.getServerForPlayer(); const senderPos = event.sender.getPosition(); @@ -17,21 +17,28 @@ sheep.$setLocationAndAngles(senderPos.getX(), senderPos.getY(), senderPos.getZ(), senderPos.rotationYaw, senderPos.rotationPitch); // Disable AI (no AI behavior) - sheep.$getDataWatcher().$updateObject(15, 1); // AI flag, 15 is the byte for AI, 1 means no AI + //sheep.$setNoAI(1) // Disable gravity - sheep.$noGravity = 1; + //sheep.$noGravity = 1; // Make sheep invincible - sheep.$setEntityInvulnerable(1); + //sheep.$invulnerable = 1 + + if (globalThis.AsyncSink) { //If we can, start the AsyncSink debugger to see filesystem requests + AsyncSink.startDebuggingFS(); + } // Add the sheep to the world - world.spawnEntityInWorld(sheep); - - // Notify the player that the sheep has been spawned - const ChatComponentTextClass = ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText"); - event.sender.addChatMessage(ChatComponentTextClass.constructors[0](ModAPI.util.str("A special sheep has been spawned!"))); + ModAPI.promisify(ModAPI.hooks.methods.nmw_World_spawnEntityInWorld)(world.getRef(), sheep).then(result => { + // Notify the player that the sheep has been spawned + const ChatComponentTextClass = ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText"); + event.sender.addChatMessage(ChatComponentTextClass.constructors[0](ModAPI.util.str("A special sheep has been spawned!"))); + if (globalThis.AsyncSink) { //Stop debugging when we are done, otherwise the console will get filled up. + AsyncSink.stopDebuggingFS(); + } + }); // Prevent the command from executing further event.preventDefault = true; diff --git a/examplemods/npcspawner.js b/examplemods/npcspawner.js index 1663ba2..b2b8a1e 100644 --- a/examplemods/npcspawner.js +++ b/examplemods/npcspawner.js @@ -1,28 +1,10 @@ (() => { PluginAPI.dedicatedServer.appendCode(function () { - var ready = false; - var killFS = false; - function setup_filesystem_middleware() { - if (!ready) { - AsyncSink.MIDDLEWARE.push((ev)=>{ - if (killFS) { - ev.shim = true; - if (typeof ev.shimOutput === "boolean") { - ev.shimOutput = true; - } - } - }); - ready = true; - } - } PluginAPI.addEventListener("processcommand", (event) => { if (!ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP").instanceOf(event.sender.getRef())) { return; } - if (event.command.toLowerCase().startsWith("/spawnnpc")) { - if (!globalThis.AsyncSink) { - return console.error("NPC Spawner relies on the AsyncSink library."); - } - setup_filesystem_middleware(); + if (event.command.toLowerCase().startsWith("/spawnnpc2")) { + AsyncSink.startDebuggingFS(); const world = event.sender.getServerForPlayer(); const senderPos = event.sender.getPosition(); @@ -31,7 +13,7 @@ var UUID = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.lax1dude.eaglercraft.v1_8.EaglercraftUUID", "randomUUID")](); //Not using UUID to make patching easier for now - + const fakeProfile = GameProfileClass.constructors[1](null, ModAPI.util.str("Steve")); // Get the PlayerInteractionManager class @@ -39,23 +21,23 @@ const playerInteractionManager = PlayerInteractionManagerClass.constructors[0](world.getRef()); // Get the EntityPlayerMP class to spawn the fake player - killFS = true; const EntityPlayerMPClass = ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP"); - const fakePlayer = ModAPI.util.wrap(EntityPlayerMPClass.constructors[0]( - ModAPI.server.getRef(), world.getRef(), fakeProfile, playerInteractionManager - )); - killFS = false; + ModAPI.promisify(EntityPlayerMPClass.constructors[0])(ModAPI.server.getRef(), world.getRef(), fakeProfile, playerInteractionManager).then(result => { + console.log(result); + var fakePlayer = ModAPI.util.wrap(result); - // Set the fake player position to be near the command sender - console.log(senderPos); - fakePlayer.setPosition(senderPos.getX(), senderPos.getY(), senderPos.getZ()); + // Set the fake player position to be near the command sender + console.log(senderPos); + fakePlayer.setPosition(senderPos.getX(), senderPos.getY(), senderPos.getZ()); - // Add the fake player to the world - world.spawnEntityInWorld(fakePlayer.getRef()); + // Add the fake player to the world + world.spawnEntityInWorld(fakePlayer.getRef()); + + // Notify the player that the fake player has been spawned + const ChatComponentTextClass = ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText"); + event.sender.addChatMessage(ChatComponentTextClass.constructors[0](ModAPI.util.str("Fake Steve has been spawned!"))); + }); - // Notify the player that the fake player has been spawned - const ChatComponentTextClass = ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText"); - event.sender.addChatMessage(ChatComponentTextClass.constructors[0](ModAPI.util.str("Fake Steve has been spawned!"))); // Prevent the command from executing further event.preventDefault = true; diff --git a/examplemods/threadtesting.js b/examplemods/threadtesting.js index 35006aa..2a2d811 100644 --- a/examplemods/threadtesting.js +++ b/examplemods/threadtesting.js @@ -3,6 +3,11 @@ ModAPI.hooks._teavm.$rt_startThread(() => { return ModAPI.hooks.methods.nlevi_PlatformRuntime_downloadRemoteURI(ModAPI.util.str("data:text/plain,hi")) }, function (...args) { console.log(this, args) }) +// SUCCESS - Runs anywhere, anytime. Might work with async/await, but for now stick to .then() +ModAPI.promisify(ModAPI.hooks.methods.nlevi_PlatformRuntime_downloadRemoteURI)(ModAPI.util.str("data:text/plain,hi")).then(result => { + console.log(result); //Log arraybuffer +}); + //WIP - Pausing and resuming client thread globalThis.suspendTest = function (...args) { diff --git a/examplemods/xpspawner.js b/examplemods/xpspawner.js new file mode 100644 index 0000000..0f81082 --- /dev/null +++ b/examplemods/xpspawner.js @@ -0,0 +1,36 @@ +(() => { + PluginAPI.dedicatedServer.appendCode(function () { + PluginAPI.addEventListener("processcommand", (event) => { + // Check if the sender is a player + if (!ModAPI.reflect.getClassById("net.minecraft.entity.player.EntityPlayerMP").instanceOf(event.sender.getRef())) { return; } + + // Check if the command is "/spawnsheep" + if (event.command.toLowerCase().startsWith("/spawnxp")) { + const world = event.sender.getServerForPlayer(); + const senderPos = event.sender.getPosition(); + + // Create a sheep entity + const EntityXP = ModAPI.reflect.getClassByName("EntityXPOrb"); + const xporb = EntityXP.constructors[0](world.getRef(), senderPos.getX(), senderPos.getY(), senderPos.getZ(), 10); + + if (globalThis.AsyncSink) { //If we can, start the AsyncSink debugger to see filesystem requests + AsyncSink.startDebuggingFS(); + } + + // Add the sheep to the world + ModAPI.promisify(ModAPI.hooks.methods.nmw_World_spawnEntityInWorld)(world.getRef(), xporb).then(result => { + // Notify the player that the sheep has been spawned + const ChatComponentTextClass = ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText"); + event.sender.addChatMessage(ChatComponentTextClass.constructors[0](ModAPI.util.str("An xp orb has been spawned!"))); + + if (globalThis.AsyncSink) { //Stop debugging when we are done, otherwise the console will get filled up. + AsyncSink.stopDebuggingFS(); + } + }); + + // Prevent the command from executing further + event.preventDefault = true; + } + }); + }); +})(); \ No newline at end of file diff --git a/index.html b/index.html index 5411a26..27d2158 100644 --- a/index.html +++ b/index.html @@ -175,6 +175,7 @@ `; var freezeCallstack = `if(ModAPI.hooks.freezeCallstack){return false};`; + diff --git a/injector.js b/injector.js index 3059bb3..88877ef 100644 --- a/injector.js +++ b/injector.js @@ -263,6 +263,10 @@ var main;(function(){` } ); + _status("Applying bonus patches from patch registry..."); + await wait(50); + patchedFile = PatchesRegistry.patchFile(patchedFile); + if (globalThis.doShronk) { _status("Shrinking file..."); await wait(50); @@ -276,6 +280,7 @@ var main;(function(){` patchedFile = patchedFile.replace( ` id="game_frame">`, ` id="game_frame"> + \