EaglerForgeInjector/index.html
2024-09-01 13:01:52 +08:00

323 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EaglerForge Injector</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" rel="stylesheet">
<style>
body {
background-color: #2b2b2b;
color: #ffffff;
}
.container {
text-align: center;
margin-top: 50px;
}
.github-button-container {
position: absolute;
top: 10px;
right: 10px;
}
.github-button-container a.btn {
color: #ffffff;
border-color: #ffffff;
}
.github-button-container a.btn:hover {
background-color: #ffffff;
color: #000000;
}
#info {
text-align: left;
min-width: 50vw;
}
details {
text-align: left;
padding: 6px;
padding-left: 8px;
border-radius: 1rem;
background-color: rgba(0,0,0,0.5);
margin-bottom: 1rem;
}
summary {
background-color: rgba(255,255,255,0.1);
padding: 4px;
border-radius: 0.6rem;
}
</style>
</head>
<body>
<div class="container">
<h1>EaglerForge Injector</h1>
<div class="github-button-container">
<a href="https://github.com/eaglerforge/EaglerForgeInjector" class="btn btn-default" target="_blank">
<i class="fab fa-github"></i> GitHub
</a>
</div>
<h6>
Adds ModAPI with more functionality (adds hooking into functions, exposes
all classes, etc) to unminified unobfuscated EaglercraftX offline downloads (web support coming soon).
</h6>
<br />
<div class="custom-file mb-3">
<input class="custom-file-input" type="file" id="htmlFile" accept=".html,.js" />
<label class="custom-file-label" for="htmlFile">Choose .html file...</label>
<br /><br />
<button class="btn btn-primary" id="giveme">Make modded build</button>
</div>
<br><br><br>
<span>Info:</span>
<div id="#info">
<details>
<summary>What .html file do I choose?</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>How does this tool work?</summary>
The injector works by analysing your uploaded file for patterns that appear in TeaVM's compiled JavaScript code. Then, it will replace all functions with proxies to the original code, which it moves into
<code>ModAPI.hooks.methods</code>. It does similar things with static properties and constructors, and then hooks into <code>$rt_metadata</code> to access auxilary information.
</details>
</div>
</div>
<script src="filesaver.min.js"></script>
<script>
var modapi_preinit = `globalThis.ModAPI ||= {};
ModAPI.hooks ||= {};
ModAPI.hooks.freezeCallstack = false;
ModAPI.hooks._rippedData ||= [];
ModAPI.hooks._teavm ||= {};
ModAPI.hooks._rippedConstructors ||= {};
ModAPI.hooks.methods ||= {};
ModAPI.hooks._rippedMethodTypeMap ||= {};
ModAPI.hooks._postInit ||= ()=>{};
ModAPI.hooks._rippedStaticProperties ||= {};
ModAPI.hooks._rippedStaticIndexer ||= {};
`;
var freezeCallstack = `if(ModAPI.hooks.freezeCallstack){return false};`;
document.querySelector("#giveme").addEventListener("click", () => {
if (
!document.querySelector("input").files ||
!document.querySelector("input").files[0]
) {
return;
}
// @type File
var file = document.querySelector("input").files[0];
var fileType = file.name.split(".");
fileType = fileType[fileType.length - 1];
file.text().then((string) => {
var patchedFile = string;
patchedFile = patchedFile.replaceAll(
`(function(root, module) {`,
`${modapi_preinit}
(function(root, module) {`
);
patchedFile = patchedFile.replaceAll(
`var main;(function(){`,
`${modapi_preinit}
var main;(function(){`
);
patchedFile = patchedFile
.replace("\r", "")
.replace(
"var main;\n(function() {",
modapi_preinit + "var main;\n(function() {"
);
patchedFile = patchedFile.replace(
/function \$rt_metadata\(data\)( ?){/gm,
`function $rt_metadata(data) {
/*/EaglerForge Client Patch/*/
ModAPI.hooks._rippedData.push(data);
/*/EaglerForge Client Patch/*/`
);
patchedFile = patchedFile.replaceAll(
`return thread != null && thread.isResuming()`,
(match) => {
return freezeCallstack + match;
}
);
patchedFile = patchedFile.replaceAll(
`return thread != null && thread.isSuspending();`,
(match) => {
return freezeCallstack + match;
}
);
patchedFile = patchedFile.replaceAll(
`return $rt_currentNativeThread;`,
(match) => {
return (
`if(ModAPI.hooks.freezeCallstack){return {push: (a)=>{console.log("Data pushed to stack: ") + a}, pop: ()=>{console.warn("Frozen stack was popped, context is now unstable.")}}};` +
match
);
}
);
patchedFile = patchedFile.replace(
` id="game_frame">`,
` id="game_frame">
\<script id="modapi_postinit"\>${globalThis.modapi_postinit}\<\/script\>
\<script id="modapi_modloader"\>${globalThis.modapi_modloader}\<\/script\>
\<script id="modapi_guikit"\>${globalThis.modapi_guikit}\<\/script\>
\<script id="modapi_postinit_data"\>globalThis.modapi_postinit = \`${globalThis.modapi_postinit}\`;\<\/script\>`
);
const extractConstructorRegex =
/^\s*function (\S*?)__init_\d*?\((?!\$)/gm;
const extractConstructorFullNameRegex =
/function (\S*?)__init_[0-9]*/gm;
patchedFile = patchedFile.replaceAll(
extractConstructorRegex,
(match) => {
var fullName = match.match(extractConstructorFullNameRegex);
fullName = fullName[0].replace("function ", "");
return (
`ModAPI.hooks._rippedConstructors[\`${fullName}\`] = ${fullName};
` + match
);
}
);
const extractInstanceMethodRegex =
/^[\t ]*function \S+?_\S+?_\S+?\((\$this)?/gm; // /^[\t ]*function \S+?_\S+?_\S+?\(\$this/gm
const extractInstanceMethodFullNameRegex = /function (\S*?)\(/gm; // /function (\S*?)\(\$this/gm
patchedFile = patchedFile.replaceAll(
extractInstanceMethodRegex,
(match) => {
if (
match.includes("__init_") ||
match.includes("__clinit_") ||
match.includes("_$callClinit")
) {
return match;
}
var fullName = match.match(extractInstanceMethodFullNameRegex);
fullName = fullName[0].replace("function ", "").replace("(", "");
return (
`function ${fullName}(...args) {
return ModAPI.hooks.methods[\`${fullName}\`].apply(this, args);
}
ModAPI.hooks._rippedMethodTypeMap[\`${fullName}\`] = \`${
match.endsWith("($this") ? "instance" : "static"
}\`;
ModAPI.hooks.methods[\`${fullName}\`]=` +
match.replace(fullName + "(", "(")
);
return match;
}
);
var staticVariables = [
...patchedFile.matchAll(/var \S+?_\S+?_\S+? = null;/gm),
].flatMap((x) => {
return x[0];
});
//Todo: add support for static properties on classes with constructors like this: function nmcg_GuiMainMenu() {
patchedFile = patchedFile.replaceAll(
/var \S+?_\S+? = \$rt_classWithoutFields\(\S*?\);/gm,
function (match) {
var prefix = match.replaceAll(
/ = \$rt_classWithoutFields\(\S*?\);/gm,
""
);
var entries = [];
staticVariables.forEach((entry) => {
if (entry.startsWith(prefix)) {
var variableName = entry
.replace("var ", "")
.replace(" = null;", "");
var segments = variableName.split("_");
segments.splice(0, 2);
var name = segments.join("_");
entries.push({
name: name,
variable: variableName,
});
}
});
var getComponents = "";
entries.forEach((entry) => {
getComponents += `
case \`${entry.name}\`:
return ${entry.variable};
break;`;
});
var setComponents = "";
entries.forEach((entry) => {
setComponents += `
case \`${entry.name}\`:
${entry.variable} = c;
break;`;
});
var proxy = `
ModAPI.hooks._rippedStaticIndexer[\`${prefix.replace(
"var ",
""
)}\`] = [${entries
.flatMap((x) => {
return '"' + x.name + '"';
})
.join(",")}];
ModAPI.hooks._rippedStaticProperties[\`${prefix.replace(
"var ",
""
)}\`] = new Proxy({${
entries
.flatMap((x) => {
return '"' + x.name + '"';
})
.join(":null,") + (entries.length > 0 ? ":null" : "")
}}, {
get: function (a,b,c) {
switch (b) {
${getComponents}
}
},
set: function (a,b,c) {
switch (b) {
${setComponents}
}
}
});`;
return match + proxy;
}
);
patchedFile = patchedFile.replaceAll(
/function \$rt_\S+?\(/gm,
(match) => {
var name = match.replace("function ", "");
name = name.substring(0, name.length - 1);
return (
`ModAPI.hooks._teavm[\`${name}\`]=${name};
` + match
);
}
);
patchedFile = patchedFile.replaceAll(/main\(\);\s*?}/gm, (match) => {
return match.replace("main();", "main();ModAPI.hooks._postInit();");
});
var blob = new Blob([patchedFile], { type: file.type });
saveAs(blob, "processed." + fileType);
});
});
</script>
<!-- Code assets -->
<script src="postinit.injector.js"></script>
<script src="modloader.injector.js"></script>
<script src="modgui.injector.js"></script>
</body>
</html>