From 8d75013fe78de84ad16a0a61d0bd8e0d569fcacc Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Tue, 10 Dec 2024 15:58:22 +0800 Subject: [PATCH 01/11] add crafting recipe for guns --- examplemods/diamondBlockCustomCraft.js | 2 +- examplemods/guns_craftable.js | 185 +++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 examplemods/guns_craftable.js diff --git a/examplemods/diamondBlockCustomCraft.js b/examplemods/diamondBlockCustomCraft.js index d6a9520..28e3b8d 100644 --- a/examplemods/diamondBlockCustomCraft.js +++ b/examplemods/diamondBlockCustomCraft.js @@ -34,7 +34,7 @@ var recipeInternal = []; Object.keys(recipeLegend).forEach((key) => { recipeInternal.push(ToChar(key)); - var ingredient = ModAPI.blocks[recipeLegend[key].id].getRef(); + var ingredient = (recipeLegend[key].type === "block" ? ModAPI.blocks : ModAPI.items)[recipeLegend[key].id].getRef(); recipeInternal.push(ingredient); }); diff --git a/examplemods/guns_craftable.js b/examplemods/guns_craftable.js new file mode 100644 index 0000000..3795cb5 --- /dev/null +++ b/examplemods/guns_craftable.js @@ -0,0 +1,185 @@ +(()=>{ + 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 recoilSpeed = 0; //recoil controller + 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); + } + + ModAPI.addEventListener("update", ()=>{ //recoil update loop (client) + ModAPI.player.rotationPitch -= recoilSpeed; + recoilSpeed *= 0.7; + }); + + function entityRayCast(player, world, range) { + const HEADSHOT_MAX_DISTANCE_FROM_HEAD = 0.72; + var eyePosition = player.getPositionEyes(0.0); + var targetPosition = player.rayTrace(range, 0).hitVec; + var entities = world.getEntitiesWithinAABBExcludingEntity( + player.getRef(), + player.getEntityBoundingBox().expand(range, range, range).getRef() + ).getCorrective().array; + var closestEntity = null; + var isHeadshot = false; + 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; + isHeadshot = entity.getPositionEyes(0.0).distanceTo(intercept.hitVec.getRef()) < HEADSHOT_MAX_DISTANCE_FROM_HEAD; + } + } + } + + var rayTraceResult = closestEntity; + if (rayTraceResult != null){ + return {entity: rayTraceResult, headshot: isHeadshot}; + } 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 = DamageSourceClass.staticVariables.cactus; + var world = ModAPI.util.wrap($world); + var entityplayer = ModAPI.util.wrap($player); + var shotentitydata = entityRayCast(entityplayer, world, 16.0); + if (shotentitydata != null){ + if (world.isRemote) { + recoilSpeed += 4; + } else { + shotentitydata.entity.attackEntityFrom(cactus, 10 + (16 * shotentitydata.headshot)); + if (shotentitydata.headshot) { + console.log("H E A D S H O T"); + } + world.playSoundAtEntity(entityplayer.getRef(), ModAPI.util.str("tile.piston.out"), 1.0, 1.8); + } + } else if (!world.isRemote) { + world.playSoundAtEntity(entityplayer.getRef(), ModAPI.util.str("random.click"), 1.0, 1.8); + } + return $itemstack; + } + + async function addGunRecipe(gunItem) { + var ObjectClass = ModAPI.reflect.getClassById("java.lang.Object").class; + function ToChar(char) { + return ModAPI.reflect.getClassById("java.lang.Character").staticMethods.valueOf.method(char[0].charCodeAt(0)); + } + + // Define the recipe legend to map characters to items + var recipeLegend = { + "I": { + type: "item", + id: "iron_ingot" // Using dirt blocks + }, + "C": { + type: "block", + id: "iron_block" // Using dirt blocks + }, + "Q": { + type: "item", + id: "gunpowder" // Using dirt blocks + }, + }; + + // Define the crafting grid pattern for the recipe + var recipePattern = [ + "IIC", + " QI" + ]; + + // Convert the recipe pattern and legend into the required format + var recipeInternal = []; + Object.keys(recipeLegend).forEach((key) => { + recipeInternal.push(ToChar(key)); + var ingredient = (recipeLegend[key].type === "block" ? ModAPI.blocks : ModAPI.items)[recipeLegend[key].id].getRef(); + recipeInternal.push(ingredient); + }); + + var recipeContents = recipePattern.flatMap(row => ModAPI.util.str(row)); + var recipe = ModAPI.util.makeArray(ObjectClass, recipeContents.concat(recipeInternal)); + + var resultItem = ModAPI.reflect.getClassById("net.minecraft.item.ItemStack").constructors[4](gunItem, 1); + + + // Register the recipe with CraftingManager + var craftingManager = ModAPI.reflect.getClassById("net.minecraft.item.crafting.CraftingManager").staticMethods.getInstance.method(); + ModAPI.hooks.methods.nmic_CraftingManager_addRecipe(craftingManager, resultItem, recipe); + } + + function internal_reg() { + var pistol_item = (new nmi_ItemPistol()).$setUnlocalizedName( + ModAPI.util.str("pistol") + ).$setMaxStackSize(1); + itemClass.staticMethods.registerItem.method(ModAPI.keygen.item("pistol"), ModAPI.util.str("pistol"), pistol_item); + ModAPI.items["pistol"] = pistol_item; + addGunRecipe(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()); + }); +})(); \ No newline at end of file From 26de5a69b1f3722713aff92be5a765c2675248a3 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Tue, 10 Dec 2024 17:09:34 +0800 Subject: [PATCH 02/11] add metadata to timescale command --- docs/tutorials/comingsoon.md | 2 +- docs/tutorials/index.md | 4 ++-- examplemods/timescale_command.js | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/comingsoon.md b/docs/tutorials/comingsoon.md index 8276d92..a19fb3c 100644 --- a/docs/tutorials/comingsoon.md +++ b/docs/tutorials/comingsoon.md @@ -1 +1 @@ -# Coming Soon \ No newline at end of file +## Coming Soon / It's not done yet. \ No newline at end of file diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 9706e3d..ced90a0 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -34,5 +34,5 @@ Prerequisites: Tutorials: - [Custom Blocks](custom_block.md) -- [Custom Items](comingsoon) -- [Timescale Command](comingsoon) \ No newline at end of file +- [Custom Items](comingsoon.md) +- [Timescale Command](comingsoon.md) \ No newline at end of file diff --git a/examplemods/timescale_command.js b/examplemods/timescale_command.js index fbf4176..5f90184 100644 --- a/examplemods/timescale_command.js +++ b/examplemods/timescale_command.js @@ -1,4 +1,7 @@ (() => { + ModAPI.title("Timescale Command"); + ModAPI.description("/timescale 0.5 to halve the speed of time"); + ModAPI.credits("By ZXMushroom63"); PluginAPI.addEventListener("sendchatmessage", (event) => { if (event.message.toLowerCase().startsWith("/timescale")) { var speed = parseFloat(event.message.split(" ")[1]); From b07b700173f9b10aa0445e3706c66ee86e911018 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 <116805577+ZXMushroom63@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:23:42 +0800 Subject: [PATCH 03/11] Add discord server to readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a2cb36..ae2bdd5 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,7 @@ Current features: Go to https://eaglerforge.github.io/EaglerForgeInjector/ and upload an unminified, unobfuscated, unsigned EaglercraftX offline download. #### Portable Offline -Download this repository as a .zip, and extract it. Open index.html with your preferred browser (use `ctrl` + `O` on a new tab) and upload an unminified, unobfuscated, unsigned EaglercraftX offline download. \ No newline at end of file +Download this repository as a .zip, and extract it. Open index.html with your preferred browser (use `ctrl` + `O` on a new tab) and upload an unminified, unobfuscated, unsigned EaglercraftX offline download. + +## Discord server +[https://discord.gg/rbxN7kby5W](https://discord.gg/rbxN7kby5W) From e866954a2729b05d5313a1e012dfa423c017b214 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Tue, 10 Dec 2024 20:13:54 +0800 Subject: [PATCH 04/11] fix bug with unlucky blocks --- examplemods/unlucky_blocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examplemods/unlucky_blocks.js b/examplemods/unlucky_blocks.js index 97dd51b..cc2f459 100644 --- a/examplemods/unlucky_blocks.js +++ b/examplemods/unlucky_blocks.js @@ -38,7 +38,7 @@ 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, 0); + blockpos.getZ() + 0.5, 9, 1, 1); } return breakBlockMethod(this, $world, $blockpos, $blockstate); } From 3a7dfa0818abefabb9a97e98351d94128e16e5e6 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Tue, 10 Dec 2024 20:21:04 +0800 Subject: [PATCH 05/11] fix bug in timescale.js --- examplemods/timescale_command.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examplemods/timescale_command.js b/examplemods/timescale_command.js index 5f90184..29630bf 100644 --- a/examplemods/timescale_command.js +++ b/examplemods/timescale_command.js @@ -1,7 +1,7 @@ (() => { - ModAPI.title("Timescale Command"); - ModAPI.description("/timescale 0.5 to halve the speed of time"); - ModAPI.credits("By ZXMushroom63"); + ModAPI.meta.title("Timescale Command"); + ModAPI.meta.description("/timescale 0.5 to halve the speed of time"); + ModAPI.meta.credits("By ZXMushroom63"); PluginAPI.addEventListener("sendchatmessage", (event) => { if (event.message.toLowerCase().startsWith("/timescale")) { var speed = parseFloat(event.message.split(" ")[1]); From 237f28fbdd9280fc3aff94c3998e77332213c213 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Wed, 11 Dec 2024 16:07:44 +0800 Subject: [PATCH 06/11] Fix issues in custom block tutorial --- docs/tutorials/custom_block.md | 10 +++++----- examplemods/Tutorial_Custom_Block.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/custom_block.md b/docs/tutorials/custom_block.md index 6048efa..46784f6 100644 --- a/docs/tutorials/custom_block.md +++ b/docs/tutorials/custom_block.md @@ -1,6 +1,6 @@ -## Custom Blocks Tutorial With ModAPI +## Custom Block Tutorial With ModAPI This tutorial will show you how to make custom blocks with ModAPI. It will use my AsyncSink library to load the resources for the block. -This tutorial will be making a block with the durability of dirt that explodes when broken. +We'll be making a block with the durability of dirt that explodes when broken. As always, we'll start with the default boilerplate starter code: ```javascript @@ -8,7 +8,7 @@ As always, we'll start with the default boilerplate starter code: ModAPI.meta.title("Custom Block Demo"); ModAPI.meta.version("v1.0"); ModAPI.meta.description("Adds a block that blows up when used."); - ModAPI.meta.credits("By ZXMushroom63"); + ModAPI.meta.credits("By "); })(); ``` Let's get our blocks texture done ahead of time. @@ -20,7 +20,7 @@ Store this at the beginning of the function using a constant. Also use that cons ModAPI.meta.title("Custom Block Demo"); ModAPI.meta.version("v1.0"); ModAPI.meta.description("Adds a block that blows up when used."); - ModAPI.meta.credits("By ZXMushroom63"); + ModAPI.meta.credits("By "); ModAPI.meta.icon(texture); })(); @@ -188,7 +188,7 @@ When it's loaded, we'll: }); AsyncSink.L10N.set("tile.custom_block.name", "My Custom Block"); //Set the name of the block - //Required boilerplate for block and item models. + //Make an in-memory resource pack for the block. This is standard between, EaglerForge, Forge, Fabric, and NeoForge (pretty much any modding API) AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/models/block/custom_block.json", JSON.stringify( { "parent": "block/cube_all", diff --git a/examplemods/Tutorial_Custom_Block.js b/examplemods/Tutorial_Custom_Block.js index ff62438..14b4164 100644 --- a/examplemods/Tutorial_Custom_Block.js +++ b/examplemods/Tutorial_Custom_Block.js @@ -3,7 +3,7 @@ ModAPI.meta.title("Custom Block Demo"); ModAPI.meta.version("v1.0"); ModAPI.meta.description("Adds a block that blows up when used."); - ModAPI.meta.credits("By ZXMushroom63"); + ModAPI.meta.credits("By "); ModAPI.meta.icon(texture); From eeb0143f85df4a52a6fbc9660f2e1b04915c33a6 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Wed, 11 Dec 2024 16:07:58 +0800 Subject: [PATCH 07/11] Custom item tutorial --- docs/tutorials/custom_item.md | 127 ++++++++++++++++++++++++++++ docs/tutorials/index.md | 2 +- examplemods/Tutorial_Custom_Item.js | 94 ++++++++++++++++++++ 3 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/custom_item.md create mode 100644 examplemods/Tutorial_Custom_Item.js diff --git a/docs/tutorials/custom_item.md b/docs/tutorials/custom_item.md new file mode 100644 index 0000000..fe14988 --- /dev/null +++ b/docs/tutorials/custom_item.md @@ -0,0 +1,127 @@ +## Custom Item Tutorial with ModAPI +This tutorial will cover making custom items with ModAPI. It is recommended that you follow the [custom block tutorial](custom_block.md) first, as this tutorial is more fast-paced. The custom item we'll be adding will set the player's velocity to a random value when used, to demonstrate how methods can be overridden. + +We'll begin with a constant item texture encoded into a data URI, as well as some metadata. +```javascript +(function CustomItemMod() { + const itemTexture = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAKZJREFUOE9j/P//PxMDBIBoEP6HREOl4PLIciA2AyPIgMcM//7KgvWSDJjBBpx9/+YvJzc3Sbq12DhB6sEGsJ19/+YnmQawYhigzc7FcPXnN4KugbqAHWQAy9n3b34T4wJkw6EGYLqAoNVQBWS5ANlwZBfAvUCs/0EGkW0AzBKqGoCSDgh5A80F2KMRpAgfAKUT6kcjsfEPUycmKMQgy8AETkgUZWcAS3CPIf4oSPsAAAAASUVORK5CYII="; + + ModAPI.meta.title("Custom Item Mod"); + ModAPI.meta.icon(itemTexture); + ModAPI.meta.description("it's a custom item. what more do you want"); + ModAPI.meta.credits("By "); +})(); +``` + +Add a function that will contain the code to register the item called `CustomItem`. Inside it we'll do something really similar to the custom blocks tutorial, where we'll define a custom item class, and then register it (or, if on the server, wait until the `bootstrap` event fires). The reason it's contained in a function is to make the job of running the same code on the server and client easier. + +```javascript +(function CustomItemMod() { + //... + + function CustomItem() { + var creativeMiscTab = ModAPI.reflect.getClassById("net.minecraft.creativetab.CreativeTabs").staticVariables.tabMisc; //chuck it in the miscellaneous category ig + var itemClass = ModAPI.reflect.getClassById("net.minecraft.item.Item"); //Get the item class + var itemSuper = ModAPI.reflect.getSuper(itemClass, (x) => x.length === 1); //Get the super() function of the item class that has a length of 1 + function CustomItem() { + itemSuper(this); //Use super function to get block properties on this class. + this.$setCreativeTab(creativeMiscTab); //Set the creative tab of the item to be the misc tab + } + ModAPI.reflect.prototypeStack(itemClass, CustomItem); // ModAPI equivalent of `extends` in java + CustomItem.prototype.$onItemRightClick = function ($itemstack, $world, $player) { //example of how to override a method + //use ModAPI.util.wrap to create a proxy of the player and the world without $ prefixes on the properties and methods + var player = ModAPI.util.wrap($player); + var world = ModAPI.util.wrap($world); + + if (!world.isRemote) { //If we are on the server + // Math.random() returns a number from 0.0 to 1.0, so we subtract 0.5 and then multiply by 2 to make it become -1.0 to 1.0 instead + player.motionX += (Math.random() - 0.5) * 3; + player.motionZ += (Math.random() - 0.5) * 3; + + player.motionY += Math.random() * 1.5; // gravity is a thing, so no negative numbers here otherwise it'll be boring + } + + return $itemstack; + } + + // Internal registration function. This will be used to actually register the item on both the client and the server. + function internal_reg() { + // Construct an instance of the CustomItem, and set it's unlocalized name (translation id) + var custom_item = (new CustomItem()).$setUnlocalizedName( + ModAPI.util.str("custom_item") + ); + //Register it using ModAPI.keygen() to get the item id. + itemClass.staticMethods.registerItem.method(ModAPI.keygen.item("custom_item"), ModAPI.util.str("custom_item"), custom_item); + + //Expose it to ModAPI + ModAPI.items["custom_item"] = custom_item; + + //return the instance. + return custom_item; + } + + //if the item global exists (and it will on the client), register the item and return the registered instance. + if (ModAPI.items) { + return internal_reg(); + } else { + //Otherwise attatch the registration method to the bootstrap method. + ModAPI.addEventListener("bootstrap", internal_reg); + } + } +})(); +``` + +Now let's run the `CustomItem` function on the server and the client, and then use [AsyncSink](../../examplemods/AsyncSink.js) to create an in-memory resource pack to load item textures and models! +```javascript +(function CustomItemMod() { + const itemTexture = "..."; + + ModAPI.meta.title("Custom Item Mod"); + ModAPI.meta.icon(itemTexture); + ModAPI.meta.description("it's a custom item. what more do you want"); + ModAPI.meta.credits("By "); + + function CustomItem() { + // ... + } + + // Run the function when the dedicated server loads. + ModAPI.dedicatedServer.appendCode(CustomItem); + + // Run the function on the client + var custom_item = CustomItem(); + + ModAPI.addEventListener("lib:asyncsink", async () => { + ModAPI.addEventListener("custom:asyncsink_reloaded", ()=>{ + ModAPI.mc.renderItem.registerItem(custom_item, ModAPI.util.str("custom_item")); + }); + AsyncSink.L10N.set("item.custom_item.name", "Cool Custom Item"); + AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/models/item/custom_item.json", JSON.stringify( + { + "parent": "builtin/generated", + "textures": { + "layer0": "items/custom_item" + }, + "display": { + "thirdperson": { + "rotation": [ -90, 0, 0 ], + "translation": [ 0, 1, -3 ], + "scale": [ 0.55, 0.55, 0.55 ] + }, + "firstperson": { + "rotation": [ 0, -135, 25 ], + "translation": [ 0, 4, 2 ], + "scale": [ 1.7, 1.7, 1.7 ] + } + } + } + )); + AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/textures/items/custom_item.png", await (await fetch( + itemTexture + )).arrayBuffer()); + }); +})(); +``` + +That's it! Upload your completed mod, run `.reload_tex` in chat in a singleplayer world and use your new item!\ +[completed mod]() \ No newline at end of file diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index ced90a0..55b24fb 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -34,5 +34,5 @@ Prerequisites: Tutorials: - [Custom Blocks](custom_block.md) -- [Custom Items](comingsoon.md) +- [Custom Items](custom_item.md) - [Timescale Command](comingsoon.md) \ No newline at end of file diff --git a/examplemods/Tutorial_Custom_Item.js b/examplemods/Tutorial_Custom_Item.js new file mode 100644 index 0000000..9c4106b --- /dev/null +++ b/examplemods/Tutorial_Custom_Item.js @@ -0,0 +1,94 @@ +(function CustomItemMod() { + const itemTexture = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAKZJREFUOE9j/P//PxMDBIBoEP6HREOl4PLIciA2AyPIgMcM//7KgvWSDJjBBpx9/+YvJzc3Sbq12DhB6sEGsJ19/+YnmQawYhigzc7FcPXnN4KugbqAHWQAy9n3b34T4wJkw6EGYLqAoNVQBWS5ANlwZBfAvUCs/0EGkW0AzBKqGoCSDgh5A80F2KMRpAgfAKUT6kcjsfEPUycmKMQgy8AETkgUZWcAS3CPIf4oSPsAAAAASUVORK5CYII="; + + ModAPI.meta.title("Custom Item Mod"); + ModAPI.meta.icon(itemTexture); + ModAPI.meta.description("it's a custom item. what more do you want"); + ModAPI.meta.credits("By "); + + function CustomItem() { + var creativeMiscTab = ModAPI.reflect.getClassById("net.minecraft.creativetab.CreativeTabs").staticVariables.tabMisc; //chuck it in the miscellaneous category ig + var itemClass = ModAPI.reflect.getClassById("net.minecraft.item.Item"); //Get the item class + var itemSuper = ModAPI.reflect.getSuper(itemClass, (x) => x.length === 1); //Get the super() function of the item class that has a length of 1 + function CustomItem() { + itemSuper(this); //Use super function to get block properties on this class. + this.$setCreativeTab(creativeMiscTab); //Set the creative tab of the item to be the misc tab + } + ModAPI.reflect.prototypeStack(itemClass, CustomItem); // ModAPI equivalent of `extends` in java + CustomItem.prototype.$onItemRightClick = function ($itemstack, $world, $player) { //example of how to override a method + //use ModAPI.util.wrap to create a proxy of the player and the world without $ prefixes on the properties and methods + var player = ModAPI.util.wrap($player); + var world = ModAPI.util.wrap($world); + + if (!world.isRemote) { //If we are on the server + // Math.random() returns a number from 0.0 to 1.0, so we subtract 0.5 and then multiply by 2 to make it become -1.0 to 1.0 instead + player.motionX += (Math.random() - 0.5) * 3; + player.motionZ += (Math.random() - 0.5) * 3; + + player.motionY += Math.random() * 1.5; // gravity is a thing, so no negative numbers here otherwise it'll be boring + } + + return $itemstack; + } + + // Internal registration function. This will be used to actually register the item on both the client and the server. + function internal_reg() { + // Construct an instance of the CustomItem, and set it's unlocalized name (translation id) + var custom_item = (new CustomItem()).$setUnlocalizedName( + ModAPI.util.str("custom_item") + ); + //Register it using ModAPI.keygen() to get the item id. + itemClass.staticMethods.registerItem.method(ModAPI.keygen.item("custom_item"), ModAPI.util.str("custom_item"), custom_item); + + //Expose it to ModAPI + ModAPI.items["custom_item"] = custom_item; + + //return the instance. + return custom_item; + } + + //if the item global exists (and it will on the client), register the item and return the registered instance. + if (ModAPI.items) { + return internal_reg(); + } else { + //Otherwise attatch the registration method to the bootstrap method. + ModAPI.addEventListener("bootstrap", internal_reg); + } + } + + // Run the function when the dedicated server loads. + ModAPI.dedicatedServer.appendCode(CustomItem); + + // Run the function on the client + var custom_item = CustomItem(); + + ModAPI.addEventListener("lib:asyncsink", async () => { + ModAPI.addEventListener("custom:asyncsink_reloaded", ()=>{ + ModAPI.mc.renderItem.registerItem(custom_item, ModAPI.util.str("custom_item")); + }); + AsyncSink.L10N.set("item.custom_item.name", "Cool Custom Item"); + AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/models/item/custom_item.json", JSON.stringify( + { + "parent": "builtin/generated", + "textures": { + "layer0": "items/custom_item" + }, + "display": { + "thirdperson": { + "rotation": [ -90, 0, 0 ], + "translation": [ 0, 1, -3 ], + "scale": [ 0.55, 0.55, 0.55 ] + }, + "firstperson": { + "rotation": [ 0, -135, 25 ], + "translation": [ 0, 4, 2 ], + "scale": [ 1.7, 1.7, 1.7 ] + } + } + } + )); + AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/textures/items/custom_item.png", await (await fetch( + itemTexture + )).arrayBuffer()); + }); +})(); \ No newline at end of file From 7a020f5c429c9710ead3d3c355c47703bd63f3ea Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Wed, 11 Dec 2024 16:08:12 +0800 Subject: [PATCH 08/11] Add description to f11fix --- examplemods/f11fix.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examplemods/f11fix.js b/examplemods/f11fix.js index 7b17bf1..e52eec0 100644 --- a/examplemods/f11fix.js +++ b/examplemods/f11fix.js @@ -1,6 +1,8 @@ (() => { ModAPI.meta.title("Fullscreen Fixer"); + ModAPI.meta.description("Makes HTML guis still appear in fullscreen."); ModAPI.meta.credits("By ZXMushroom63"); + var oldF11 = HTMLElement.prototype.requestFullscreen; HTMLElement.prototype.requestFullscreen = function () { if (this instanceof HTMLBodyElement) { From a8cd453a68d1692c1234d51474bbf491ff946369 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Wed, 11 Dec 2024 16:08:36 +0800 Subject: [PATCH 09/11] Fix typo and some useless code --- examplemods/useless_item_example_mod.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examplemods/useless_item_example_mod.js b/examplemods/useless_item_example_mod.js index 16530c0..8bc7f1f 100644 --- a/examplemods/useless_item_example_mod.js +++ b/examplemods/useless_item_example_mod.js @@ -1,7 +1,7 @@ // This is an example mod on how to register an item. (()=>{ const itemTexture = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAKZJREFUOE9j/P//PxMDBIBoEP6HREOl4PLIciA2AyPIgMcM//7KgvWSDJjBBpx9/+YvJzc3Sbq12DhB6sEGsJ19/+YnmQawYhigzc7FcPXnN4KugbqAHWQAy9n3b34T4wJkw6EGYLqAoNVQBWS5ANlwZBfAvUCs/0EGkW0AzBKqGoCSDgh5A80F2KMRpAgfAKUT6kcjsfEPUycmKMQgy8AETkgUZWcAS3CPIf4oSPsAAAAASUVORK5CYII="; - //this texture is really baad, so the item appears 2d in game. + //this texture is REALLY bad, so the item appears 2d in game. (it uses partially transparent pixels around the edges in some spots ;-;) ModAPI.meta.title("Adding items demo."); ModAPI.meta.version("v1.0"); ModAPI.meta.icon(itemTexture); @@ -11,7 +11,7 @@ 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_ItemExample = function nmi_ItemExample() { + function nmi_ItemExample() { itemSuper(this); //Use super function to get block properties on this class. this.$setCreativeTab(creativeMiscTab); } From 4c7fdf5f86fa0cfc613f8b4a9e1f606f3f7db840 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Wed, 11 Dec 2024 17:56:06 +0800 Subject: [PATCH 10/11] partially complete timescale tutorial --- docs/tutorials/index.md | 2 +- docs/tutorials/timescale.md | 25 +++++++++++++++++++++++++ examplemods/timescale_command.js | 10 +++++----- 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 docs/tutorials/timescale.md diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 55b24fb..58e0d8b 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -35,4 +35,4 @@ Prerequisites: Tutorials: - [Custom Blocks](custom_block.md) - [Custom Items](custom_item.md) -- [Timescale Command](comingsoon.md) \ No newline at end of file +- [Timescale Command](timescale.md) \ No newline at end of file diff --git a/docs/tutorials/timescale.md b/docs/tutorials/timescale.md new file mode 100644 index 0000000..8f07c37 --- /dev/null +++ b/docs/tutorials/timescale.md @@ -0,0 +1,25 @@ +## Timescale Mod with ModAPI +This mod will cover adding a new command that controls the speed that Eaglercraft runs at. This tutorial assumes that you have some knowledge on how to use [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) in JavaScript. + +Let's get our basic template code down: + +```javascript +(function TimescaleCommand() { + ModAPI.meta.title("Timescale Command"); + ModAPI.meta.description("use /timescale to control time"); + ModAPI.meta.credits("By "); +})() +``` + +Our mod is going to be split into 2 distinct parts: client-side and server-side. The client will modify `ModAPI.mc.timer.timerSpeed` when a `/timescale` command is sent, while the server will set the timescale to a global variable, and then modify `net.minecraft.server.MinecraftServer`'s `getCurrentTimeMillis()` to change the rate calculations happen at on the server.\ +\ +However, there is a slight logistical problem: the server gets the time in milliseconds as a JavaScript [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt), which means we can't just multiply it by a number, we have to either multiply or divide it by another BigInt. +```javascript +var x = 1n * 1.0; //TypeError + +var x = 1n * 1n; //Success +``` +To allow us to both speed up and slow down time, we'll need to check if the speed inputted is greater or equal to 1. If it is, we round it and then convert it to a `BigInt`, which we'll store on the `globalThis` object. If it's less we'll need to find the speed to the power of -1, or `1 / speed`. We can then round this value and convert that to a `BigInt` to store on `globalThis`. We'll also set `globalThis.timeScaleDividing` to `true`, to signal to replaced `getCurrentTimeMillis()` to divide by the `BigInt` speed factor instead of multiply.\ +\ +Finally, due to our `BigInt` rounding shenanigans on the server, we have to replicate the rounding inaccuracy on the client. + diff --git a/examplemods/timescale_command.js b/examplemods/timescale_command.js index 29630bf..0b44021 100644 --- a/examplemods/timescale_command.js +++ b/examplemods/timescale_command.js @@ -6,14 +6,14 @@ if (event.message.toLowerCase().startsWith("/timescale")) { var speed = parseFloat(event.message.split(" ")[1]); if (!speed) { - PluginAPI.javaClient.$timer.$timerSpeed = 1; + PluginAPI.mc.timer.timerSpeed = 1; } else { if (speed < 1) { speed = 1 / Math.round(1 / speed); } else { speed = Math.round(speed); } - PluginAPI.javaClient.$timer.$timerSpeed = speed; + PluginAPI.mc.timer.timerSpeed = speed; } PluginAPI.displayToChat("[Timescale] Set world timescale to " + speed.toFixed(2) + "."); } @@ -37,13 +37,13 @@ } } if (ModAPI.server) { - ModAPI.server.currentTime = PluginAPI.hooks.methods.nms_MinecraftServer_getCurrentTimeMillis(); + ModAPI.server.currentTime = PluginAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.server.MinecraftServer", "getCurrentTimeMillis")](); } event.preventDefault = true; } }); - const original_getCurrentTime = ModAPI.hooks.methods.nms_MinecraftServer_getCurrentTimeMillis; - PluginAPI.hooks.methods.nms_MinecraftServer_getCurrentTimeMillis = function () { + const original_getCurrentTime = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.server.MinecraftServer", "getCurrentTimeMillis")]; + PluginAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.server.MinecraftServer", "getCurrentTimeMillis")] = function () { if (globalThis.timeScaleDividing) { return original_getCurrentTime() / globalThis.timeScale; } else { From ee6307b532fa89e1664e1bd12ad120405452d008 Mon Sep 17 00:00:00 2001 From: ZXMushroom63 Date: Wed, 11 Dec 2024 22:26:31 +0800 Subject: [PATCH 11/11] time scale command --- docs/tutorials/timescale.md | 80 +++++++++++++++++++++++++++++++- examplemods/timescale_command.js | 1 + 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/timescale.md b/docs/tutorials/timescale.md index 8f07c37..84ddecf 100644 --- a/docs/tutorials/timescale.md +++ b/docs/tutorials/timescale.md @@ -21,5 +21,83 @@ var x = 1n * 1n; //Success ``` To allow us to both speed up and slow down time, we'll need to check if the speed inputted is greater or equal to 1. If it is, we round it and then convert it to a `BigInt`, which we'll store on the `globalThis` object. If it's less we'll need to find the speed to the power of -1, or `1 / speed`. We can then round this value and convert that to a `BigInt` to store on `globalThis`. We'll also set `globalThis.timeScaleDividing` to `true`, to signal to replaced `getCurrentTimeMillis()` to divide by the `BigInt` speed factor instead of multiply.\ \ -Finally, due to our `BigInt` rounding shenanigans on the server, we have to replicate the rounding inaccuracy on the client. +Finally, due to our `BigInt` rounding shenanigans on the server, we have to replicate the rounding inaccuracy on the client.\ +\ +Let's implement the client side part. +```javascript +(function TimescaleCommand() { + ModAPI.meta.title("Timescale Command"); + ModAPI.meta.description("use /timescale to control time"); + ModAPI.meta.credits("By "); + ModAPI.addEventListener("sendchatmessage", (event) => { // before a message gets sent to the server + if (event.message.toLowerCase().startsWith("/timescale")) { //if it is the timescale command + var speed = parseFloat(event.message.split(" ")[1]); //get the part of the message after the space + if (!speed) { //If it doesn't exist, set it to 1. + speed = 1; + } else { //If it does exist: + if (speed < 1) { //When the speed is less than 1, round the denominator (1 over x) + speed = 1 / Math.round(1 / speed); + } else { + // When the speed is greater or equal to 1, round the numerator (x over 1) + speed = Math.round(speed); + } + // Set the speed + ModAPI.mc.timer.timerSpeed = speed; + } + // Log the speed to chat + ModAPI.displayToChat("[Timescale] Set world timescale to " + speed.toFixed(2) + "."); + } + }); +})() +``` +Now for the serverside part. +```javascript +(function TimescaleCommand() { + //... + + ModAPI.dedicatedServer.appendCode(function () { // Run on the server + globalThis.timeScale = 1n; // Initialize globalThis.timeScale + globalThis.timeScaleDividing = false; // Initialize globalThis.timeScaleDividing + + ModAPI.addEventListener("processcommand", (event) => { // when the server receives a command + if (event.command.toLowerCase().startsWith("/timescale")) { // if it is a timescale command + var speed = parseFloat(event.command.split(" ")[1]); // get the second part of the command (the speed of time) + if (!speed) { // If it doesn't exist, set it to 1. + globalThis.timeScale = 1n; + globalThis.timeScaleDividing = false; + } else { // If it does exist: + if (speed < 1) { + // When the speed is less than 1, round the denominator (1 over x) + // And enable division mode + globalThis.timeScaleDividing = true; + globalThis.timeScale = BigInt(Math.round(1 / speed)); + } else { + // When the speed is greater or equal to 1, round the numerator (x over 1) + // And disable division mode + globalThis.timeScaleDividing = false; + globalThis.timeScale = BigInt(Math.round(speed)); + } + } + if (ModAPI.server) { //If the server is initialized + //Bump the current time forward so the server doesn't try to play catch-up + ModAPI.server.currentTime = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.server.MinecraftServer", "getCurrentTimeMillis")](); + } + + //Prevent the command not found error from appearing + event.preventDefault = true; + } + }); + + //Monkey patch the getCurrentTime function. + const original_getCurrentTime = ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.server.MinecraftServer", "getCurrentTimeMillis")]; + ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.server.MinecraftServer", "getCurrentTimeMillis")] = function () { + if (globalThis.timeScaleDividing) { //If we are in divide mode + return original_getCurrentTime() / globalThis.timeScale; //Return the current time divided by the time scale + } else { + return original_getCurrentTime() * globalThis.timeScale; //Else, return the current time multiplied by the time scale + } + }; + }); +})() +``` \ No newline at end of file diff --git a/examplemods/timescale_command.js b/examplemods/timescale_command.js index 0b44021..4a66a16 100644 --- a/examplemods/timescale_command.js +++ b/examplemods/timescale_command.js @@ -6,6 +6,7 @@ if (event.message.toLowerCase().startsWith("/timescale")) { var speed = parseFloat(event.message.split(" ")[1]); if (!speed) { + speed = 1; PluginAPI.mc.timer.timerSpeed = 1; } else { if (speed < 1) {