Skip to content

Commit

Permalink
Merge pull request #24 from eaglerforge/main
Browse files Browse the repository at this point in the history
massive update
  • Loading branch information
ZXMushroom63 authored Oct 4, 2024
2 parents 85ad018 + ce8ee39 commit e94c747
Show file tree
Hide file tree
Showing 16 changed files with 458 additions and 161 deletions.
27 changes: 26 additions & 1 deletion docs/apidoc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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());
```
```

## 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 `<object>.isCorrective()`;

Accessing children of a corrective object will also make them corrective. `correctedPlayer.fishEntity.isCorrective(); //true`
26 changes: 26 additions & 0 deletions docs/apidoc/promisify.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion docs/quirks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/tutorials/index.md
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 41 additions & 0 deletions docs/tutorials/spider.md
Original file line number Diff line number Diff line change
@@ -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);
```
33 changes: 33 additions & 0 deletions docs/tutorials/step.md
Original file line number Diff line number Diff line change
@@ -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!
106 changes: 82 additions & 24 deletions examplemods/astar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,44 +66,44 @@
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 = [
[1, 0, 0], [-1, 0, 0],
[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) {
Expand All @@ -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);
Expand All @@ -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);
})();
})();
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 })
});
Loading

0 comments on commit e94c747

Please sign in to comment.