mirror of
https://github.com/eaglerforge/EaglerForgeInjector
synced 2025-07-26 23:39:25 -09:00
Merge pull request #33 from eaglerforge/main
add self-compile instructions to documentation
This commit is contained in:
commit
acc4df76ea
53
docs/compiling_client.md
Normal file
53
docs/compiling_client.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Compiling Eaglercraft with support for EFI
|
||||
In recent updates of eaglercraft, compiling for EaglerForgeInjector has become a great deal more complicated. To enable reflection and disable obfuscation, follow these steps once you have an EaglercraftX workspace set up:
|
||||
|
||||
|
||||
1. In any files named `build.gradle`, set the `obfuscate` property to `false`.
|
||||
2. In any files named `build.gradle`, find any code that looks like this:
|
||||
```javascript
|
||||
tasks.named("generateJavaScript") {
|
||||
doLast {
|
||||
|
||||
// NOTE: This step may break at any time, and is not required for 99% of browsers
|
||||
|
||||
def phile = file(folder + "/" + name)
|
||||
def dest = phile.getText("UTF-8")
|
||||
def i = dest.substring(0, dest.indexOf("=\$rt_globals.Symbol('jsoClass');")).lastIndexOf("let ")
|
||||
dest = dest.substring(0, i) + "var" + dest.substring(i + 3)
|
||||
def j = dest.indexOf("function(\$rt_globals,\$rt_exports){")
|
||||
dest = dest.substring(0, j + 34) + "\n" + file(folder + "/ES6ShimScript.txt").getText("UTF-8") + "\n" + dest.substring(j + 34)
|
||||
phile.write(dest, "UTF-8")
|
||||
}
|
||||
}
|
||||
```
|
||||
and delete it.
|
||||
3. Inside of the `src/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/` folder, create a new file called `ForceReflection.java`, with these contents:
|
||||
```java
|
||||
package net.lax1dude.eaglercraft.v1_8.internal.teavm;
|
||||
|
||||
public class ForceReflection {
|
||||
public static Object myObject;
|
||||
|
||||
public static Object forceInit(Class iClass) {
|
||||
myObject = new ReflectiveClass();
|
||||
try {
|
||||
myObject = iClass.newInstance();
|
||||
} catch (Exception e) {
|
||||
// TODO: handle exception
|
||||
}
|
||||
return myObject;
|
||||
}
|
||||
|
||||
public static class ReflectiveClass {
|
||||
}
|
||||
}
|
||||
```
|
||||
4. In the same folder, edit `MainClass.java` edit the start of the `main(String[] args)` method to look like this:
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
ForceReflection.forceInit(ForceReflection.class);
|
||||
if(args.length == 1) {
|
||||
//... rest of method
|
||||
```
|
||||
5. Finally, build an offline download by using `CompileJS.bat`/`CompileJS.sh` and then `MakeOfflineDownload.bat`/`MakeOfflineDownload.sh`.
|
||||
6. You can then upload the `EaglercraftX_1.8_Offline_en_US.html` into EaglerForgeInjector.
|
@ -180,6 +180,58 @@ ModAPI.addEventListener("lib:libcustomitems:loaded", () => {
|
||||
}
|
||||
event.preventDefault = true;
|
||||
}
|
||||
// "this command was made by EymenWSMC. comments included" - radmanplays
|
||||
if (event.command.toLowerCase().startsWith("//replacenear")) {
|
||||
const args = event.command.split(" ").slice(1);
|
||||
if (args.length < 3) {
|
||||
event.sender.addChatMessage(ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText").constructors[0](ModAPI.util.str(prefix + "Usage: //replacenear <radius> <targetBlock> <replacementBlock>")));
|
||||
event.preventDefault = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const radius = parseInt(args[0]);
|
||||
const targetBlockName = args[1];
|
||||
const replacementBlockName = args[2];
|
||||
|
||||
const targetBlock = ModAPI.blocks[targetBlockName];
|
||||
const replacementBlock = ModAPI.blocks[replacementBlockName];
|
||||
if (!targetBlock || !replacementBlock) {
|
||||
event.sender.addChatMessage(ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText").constructors[0](ModAPI.util.str(prefix + "Invalid block names!")));
|
||||
event.preventDefault = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//Replacing logic
|
||||
const targetBlockState = targetBlock.getDefaultState().getRef();
|
||||
const replacementBlockState = replacementBlock.getDefaultState().getRef();
|
||||
|
||||
const playerPos = event.sender.getPosition();
|
||||
const xStart = Math.floor(playerPos.x - radius);
|
||||
const xEnd = Math.floor(playerPos.x + radius);
|
||||
const yStart = Math.floor(playerPos.y - radius);
|
||||
const yEnd = Math.floor(playerPos.y + radius);
|
||||
const zStart = Math.floor(playerPos.z - radius);
|
||||
const zEnd = Math.floor(playerPos.z + radius);
|
||||
|
||||
//Replace ity with radoius
|
||||
for (let x = xStart; x <= xEnd; x++) {
|
||||
for (let y = yStart; y <= yEnd; y++) {
|
||||
for (let z = zStart; z <= zEnd; z++) {
|
||||
const blockPos = blockPosConstructor(x, y, z);
|
||||
const currentBlock = event.sender.getServerForPlayer().getBlockState(blockPos);
|
||||
|
||||
if (currentBlock.equals(targetBlockState)) {
|
||||
event.sender.getServerForPlayer().setBlockState(blockPos, replacementBlockState, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Send messages shit
|
||||
event.sender.addChatMessage(ModAPI.reflect.getClassById("net.minecraft.util.ChatComponentText").constructors[0](ModAPI.util.str(prefix + `Replaced ${targetBlockName} with ${replacementBlockName} within radius ${radius}`)));
|
||||
event.preventDefault = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -44,12 +44,11 @@ function registerSteveClientSide() {
|
||||
ModAPI.util.str("steve")
|
||||
);
|
||||
blockClass.staticMethods.registerBlock0.method(
|
||||
198, //use blockid 198
|
||||
198, //use blockid 198. MAKE SURE TO CHANGE IF YOU ARE MAKING A MOD USING THIS, MAXIMUM BLOCK ID IS 4095.
|
||||
ModAPI.util.str("steve"),
|
||||
block_of_steve
|
||||
);
|
||||
itemClass.staticMethods.registerItemBlock0.method(block_of_steve);
|
||||
ModAPI.mc.renderItem.registerBlock(block_of_steve, ModAPI.util.str("steve"));
|
||||
ModAPI.addEventListener("lib:asyncsink", async () => {
|
||||
ModAPI.addEventListener("custom:asyncsink_reloaded", ()=>{
|
||||
ModAPI.mc.renderItem.registerBlock(block_of_steve, ModAPI.util.str("steve"));
|
||||
@ -88,6 +87,7 @@ function registerSteveClientSide() {
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE0SURBVDhPpdO9S8NAHMbxy3sVfJmMg6h7FRXXkkUUX0addSjo4OAfIDqLIoiLi3+BRRx0EIQOnV0EcVAIWkR0KIFgrcEktX6vcXD0nuE+5Afhnhw5bWy4qylaidOfVQhT0zFKYozjBHVdzi3TwCZvteaS/0fLD8oGf5OzTeyxNUyE3Ln2HmGctpuxKuS3wd76CgPHsrEj142NeojCkHsFry+4c3aJ6g1OtlZp0Ok4DD4i+Y2GIZ+DMMAhtw+fHu8xi3IDM9t5YfMQF71dLHo+ZjsfXbh4WtnH0vYaqp/BcXGGM3D7BxiYTi+el8uYZWm2gM/VB/Tfaqje4GB5iga2Jv+sUuUa5/ITmOXq7gbnC+MY1r9QvcHG9AgN0lRex1u/ilr7ehqWvBNZvMlRbESfqNhAiG/Pb1bHXpMbFgAAAABJRU5ErkJggg=="
|
||||
)).arrayBuffer());
|
||||
});
|
||||
ModAPI.blocks["steve"] = block_of_steve;
|
||||
}
|
||||
function registerSteveServerSide() {
|
||||
function fixupBlockIds() {
|
||||
@ -112,16 +112,18 @@ function registerSteveServerSide() {
|
||||
ModAPI.util.str("steve")
|
||||
);
|
||||
blockClass.staticMethods.registerBlock0.method(
|
||||
198,
|
||||
198, //use blockid 198. MAKE SURE TO CHANGE IF YOU ARE MAKING A MOD USING THIS, MAXIMUM BLOCK ID IS 4095.
|
||||
ModAPI.util.str("steve"),
|
||||
block_of_steve
|
||||
);
|
||||
itemClass.staticMethods.registerItemBlock0.method(block_of_steve);
|
||||
fixupBlockIds();
|
||||
ModAPI.blocks["steve"] = block_of_steve;
|
||||
});
|
||||
}
|
||||
ModAPI.dedicatedServer.appendCode(makeSteveBlock);
|
||||
makeSteveBlock();
|
||||
registerSteveClientSide();
|
||||
fixupBlockIds();
|
||||
|
||||
ModAPI.dedicatedServer.appendCode(registerSteveServerSide);
|
@ -26,13 +26,11 @@ function registerSteveClientSide() {
|
||||
ModAPI.util.str("steve")
|
||||
).$setCreativeTab(creativeBlockTab);
|
||||
blockClass.staticMethods.registerBlock0.method(
|
||||
198, //use blockid 198
|
||||
198, //use blockid 198. MAKE SURE TO CHANGE IF YOU ARE MAKING A MOD USING THIS, MAXIMUM BLOCK ID IS 4095.
|
||||
ModAPI.util.str("steve"),
|
||||
block_of_steve
|
||||
);
|
||||
itemClass.staticMethods.registerItemBlock0.method(block_of_steve);
|
||||
ModAPI.mc.renderItem.registerBlock(block_of_steve, ModAPI.util.str("steve"));
|
||||
|
||||
|
||||
ModAPI.addEventListener("lib:asyncsink", async () => {
|
||||
ModAPI.addEventListener("custom:asyncsink_reloaded", ()=>{
|
||||
@ -72,6 +70,7 @@ function registerSteveClientSide() {
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE0SURBVDhPpdO9S8NAHMbxy3sVfJmMg6h7FRXXkkUUX0addSjo4OAfIDqLIoiLi3+BRRx0EIQOnV0EcVAIWkR0KIFgrcEktX6vcXD0nuE+5Afhnhw5bWy4qylaidOfVQhT0zFKYozjBHVdzi3TwCZvteaS/0fLD8oGf5OzTeyxNUyE3Ln2HmGctpuxKuS3wd76CgPHsrEj142NeojCkHsFry+4c3aJ6g1OtlZp0Ok4DD4i+Y2GIZ+DMMAhtw+fHu8xi3IDM9t5YfMQF71dLHo+ZjsfXbh4WtnH0vYaqp/BcXGGM3D7BxiYTi+el8uYZWm2gM/VB/Tfaqje4GB5iga2Jv+sUuUa5/ITmOXq7gbnC+MY1r9QvcHG9AgN0lRex1u/ilr7ehqWvBNZvMlRbESfqNhAiG/Pb1bHXpMbFgAAAABJRU5ErkJggg=="
|
||||
)).arrayBuffer());
|
||||
});
|
||||
ModAPI.blocks["steve"] = block_of_steve;
|
||||
}
|
||||
function registerSteveServerSide() {
|
||||
function fixupBlockIds() {
|
||||
@ -98,12 +97,13 @@ function registerSteveServerSide() {
|
||||
ModAPI.util.str("steve")
|
||||
).$setCreativeTab(creativeBlockTab);
|
||||
blockClass.staticMethods.registerBlock0.method(
|
||||
198,
|
||||
198, //use blockid 198. MAKE SURE TO CHANGE IF YOU ARE MAKING A MOD USING THIS, MAXIMUM BLOCK ID IS 4095.
|
||||
ModAPI.util.str("steve"),
|
||||
block_of_steve
|
||||
);
|
||||
itemClass.staticMethods.registerItemBlock0.method(block_of_steve);
|
||||
fixupBlockIds();
|
||||
ModAPI.blocks["steve"] = block_of_steve;
|
||||
});
|
||||
}
|
||||
registerSteveClientSide();
|
||||
|
51
examplemods/diamondBlockCustomCraft.js
Normal file
51
examplemods/diamondBlockCustomCraft.js
Normal file
@ -0,0 +1,51 @@
|
||||
(function AddDiamondRecipe() {
|
||||
ModAPI.meta.title("DiamondCraftingRecipeMod");
|
||||
ModAPI.meta.description("Adds a crafting recipe to create diamond blocks from dirt.");
|
||||
|
||||
async function addDiamondRecipe() {
|
||||
await new Promise((res,rej)=>{var x = setInterval(()=>{if(ModAPI.blocks){clearInterval(x);res();}}, 100);})
|
||||
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 = {
|
||||
"D": {
|
||||
type: "block",
|
||||
id: "dirt" // Using dirt blocks
|
||||
}
|
||||
};
|
||||
|
||||
// Define the crafting grid pattern for the recipe
|
||||
var recipePattern = [
|
||||
"DDD",
|
||||
"DDD",
|
||||
"DDD"
|
||||
];
|
||||
|
||||
// Convert the recipe pattern and legend into the required format
|
||||
var recipeInternal = [];
|
||||
Object.keys(recipeLegend).forEach((key) => {
|
||||
recipeInternal.push(ToChar(key));
|
||||
var ingredient = ModAPI.blocks[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));
|
||||
|
||||
// Define the output item as diamond_block
|
||||
var resultItem = ModAPI.reflect.getClassById("net.minecraft.item.ItemStack").constructors[1](ModAPI.blocks["diamond_block"].getRef(), 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);
|
||||
}
|
||||
|
||||
ModAPI.dedicatedServer.appendCode(addDiamondRecipe);
|
||||
|
||||
addDiamondRecipe();
|
||||
})();
|
@ -15,7 +15,7 @@
|
||||
globalThis.LCI_ITEMDB ||= {};
|
||||
globalThis.LibCustomItems = {
|
||||
makeItemStack: function makeItemStack(tag) {
|
||||
return globalThis.LCI_ITEMBD[tag] || null;
|
||||
return globalThis.LCI_ITEMDB[tag] || null;
|
||||
}
|
||||
};
|
||||
var useName = ModAPI.util.getMethodFromPackage("net.minecraft.network.NetHandlerPlayServer", "processPlayerBlockPlacement");
|
||||
@ -148,7 +148,7 @@
|
||||
if (globalThis.LCI_RECIPEEVENTS[data.tag]) {
|
||||
globalThis.LCI_RECIPEEVENTS[data.tag](new Proxy(testItem, ModAPI.util.TeaVM_to_Recursive_BaseData_ProxyConf));
|
||||
}
|
||||
globalThis.LCI_ITEMBD[data.tag] = new Proxy(testItem, ModAPI.util.TeaVM_to_Recursive_BaseData_ProxyConf);
|
||||
globalThis.LCI_ITEMDB[data.tag] = new Proxy(testItem, ModAPI.util.TeaVM_to_Recursive_BaseData_ProxyConf);
|
||||
|
||||
var craftingManager = ModAPI.reflect.getClassById("net.minecraft.item.crafting.CraftingManager").staticMethods.getInstance.method();
|
||||
if((data.useRecipe !== false) || (data.useRecipe !== "false")) {
|
||||
@ -162,7 +162,7 @@
|
||||
LCI_registerItem(data);
|
||||
}
|
||||
LibCustomItems.makeItemStack = function makeItemStack(tag) {
|
||||
return globalThis.LCI_ITEMBD[tag] || null;
|
||||
return globalThis.LCI_ITEMDB[tag] || null;
|
||||
}
|
||||
ModAPI.events.callEvent("lib:libcustomitems:loaded", {});
|
||||
})();
|
||||
|
4
examplemods/no_particles.js
Normal file
4
examplemods/no_particles.js
Normal file
@ -0,0 +1,4 @@
|
||||
ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.particle.EffectRenderer", "renderParticles")] = ()=>{};
|
||||
ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.particle.EffectRenderer", "hasParticlesInAlphaLayer")] = ()=>{return 0};
|
||||
ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.particle.EffectRenderer", "addEffect")] = ()=>{};
|
||||
ModAPI.hooks.methods[ModAPI.util.getMethodFromPackage("net.minecraft.client.particle.EffectRenderer", "addBlockDestroyEffects")] = ()=>{};
|
@ -56,7 +56,7 @@
|
||||
ModAPI.util.str("unluckiness")
|
||||
);
|
||||
blockClass.staticMethods.registerBlock0.method(
|
||||
544,
|
||||
544, //use blockid 544. MAKE SURE TO CHANGE IF YOU ARE MAKING A MOD USING THIS, MAXIMUM BLOCK ID IS 4095.
|
||||
ModAPI.util.str("unluckiness"),
|
||||
block_of_unluckiness
|
||||
);
|
||||
|
72
examplemods/useless_item_example_mod.js
Normal file
72
examplemods/useless_item_example_mod.js
Normal file
@ -0,0 +1,72 @@
|
||||
// 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.
|
||||
ModAPI.meta.title("Adding items demo.");
|
||||
ModAPI.meta.version("v1.0");
|
||||
ModAPI.meta.icon(itemTexture);
|
||||
ModAPI.meta.description("Requires AsyncSink.");
|
||||
|
||||
function ExampleItem() {
|
||||
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() {
|
||||
itemSuper(this); //Use super function to get block properties on this class.
|
||||
this.$setCreativeTab(creativeMiscTab);
|
||||
}
|
||||
ModAPI.reflect.prototypeStack(itemClass, nmi_ItemExample);
|
||||
nmi_ItemExample.prototype.$onItemRightClick = function ($itemstack, $world, $player) { //example of how to override a method
|
||||
return $itemstack;
|
||||
}
|
||||
|
||||
function internal_reg() {
|
||||
var example_item = (new nmi_ItemExample()).$setUnlocalizedName(
|
||||
ModAPI.util.str("exampleitem")
|
||||
);
|
||||
itemClass.staticMethods.registerItem0.method(432, ModAPI.util.str("exampleitem"), example_item);
|
||||
ModAPI.items["exampleitem"] = example_item;
|
||||
|
||||
return example_item;
|
||||
}
|
||||
|
||||
if (ModAPI.items) {
|
||||
return internal_reg();
|
||||
} else {
|
||||
ModAPI.addEventListener("bootstrap", internal_reg);
|
||||
}
|
||||
}
|
||||
|
||||
ModAPI.dedicatedServer.appendCode(ExampleItem);
|
||||
var example_item = ExampleItem();
|
||||
|
||||
ModAPI.addEventListener("lib:asyncsink", async () => {
|
||||
ModAPI.addEventListener("custom:asyncsink_reloaded", ()=>{
|
||||
ModAPI.mc.renderItem.registerItem(example_item, ModAPI.util.str("exampleitem"));
|
||||
});
|
||||
AsyncSink.L10N.set("item.exampleitem.name", "Example Item");
|
||||
AsyncSink.setFile("resourcepacks/AsyncSinkLib/assets/minecraft/models/item/exampleitem.json", JSON.stringify(
|
||||
{
|
||||
"parent": "builtin/generated",
|
||||
"textures": {
|
||||
"layer0": "items/exampleitem"
|
||||
},
|
||||
"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/exampleitem.png", await (await fetch(
|
||||
itemTexture
|
||||
)).arrayBuffer());
|
||||
});
|
||||
})();
|
15
index.html
15
index.html
@ -117,20 +117,7 @@
|
||||
<summary>
|
||||
How do I compile my own unobfuscated unsigned Eaglercraft build?
|
||||
</summary>
|
||||
Once you have a local EaglercraftX workspace setup, in
|
||||
<code>build.gradle</code>, set the <code>obfuscate</code> property to
|
||||
<code>false</code>. Then, run <code>CompileJS.bat</code> (or .sh if on
|
||||
a unix-based os), and then run <code>MakeOfflineDownload.bat</code>.
|
||||
The outputted offline download will have a much larger file size than
|
||||
other offline builds. This is the file you should select. (it should
|
||||
have a naming convention similar to
|
||||
<code>EaglercraftX_1.8_Offline_en_US.html</code>)
|
||||
</details>
|
||||
<details>
|
||||
<summary>Roadmap?</summary>
|
||||
<a href="https://eaglerforge.github.io/EaglerForgeInjector/roadmap/"
|
||||
>roadmap.</a
|
||||
>
|
||||
<a href="docs/compiling_client.md">tutorial here</a>
|
||||
</details>
|
||||
<details>
|
||||
<summary>How does this tool work?</summary>
|
||||
|
Loading…
x
Reference in New Issue
Block a user