diff --git a/semag/README.md b/semag/README.md new file mode 100644 index 00000000..3e7469a1 --- /dev/null +++ b/semag/README.md @@ -0,0 +1,5 @@ +# Browsercraft + +This is a proof of concept of Minecraft running unmodified in the browser, using [CheerpJ](https://labs.leaningtech.com/cheerpj). + +See [the website](https://browsercraft.cheerpj.com) for a live demo and more information. diff --git a/semag/lwjgl-2.9.0.jar b/semag/lwjgl-2.9.0.jar new file mode 100644 index 00000000..4cb7cda8 Binary files /dev/null and b/semag/lwjgl-2.9.0.jar differ diff --git a/semag/mc_server.html b/semag/mc_server.html new file mode 100644 index 00000000..0325afd8 --- /dev/null +++ b/semag/mc_server.html @@ -0,0 +1,31 @@ + + + + + CheerpJ test + + + + +
+

+ + + diff --git a/semag/minecraft-web.js b/semag/minecraft-web.js new file mode 100644 index 00000000..1217d4a1 --- /dev/null +++ b/semag/minecraft-web.js @@ -0,0 +1,192 @@ +/** + * Downloads a file from a url and writes it to the CheerpJ filesystem. + * @param {string} url + * @param {string} destPath + * @param {(downloadedBytes: number, totalBytes: number) => void} [progressCallback] + * @returns {Promise} + */ +async function downloadFileToCheerpJ(url, destPath, progressCallback) { + const response = await fetch(url); + const reader = response.body.getReader(); + const contentLength = +response.headers.get('Content-Length'); + + const bytes = new Uint8Array(contentLength); + progressCallback?.(0, contentLength); + + let pos = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + bytes.set(value, pos); + pos += value.length; + progressCallback?.(pos, contentLength); + } + + // Write to CheerpJ filesystem + return new Promise((resolve, reject) => { + cheerpOSOpen(cjFDs, destPath, "w", fd => { + cheerpOSWrite(cjFDs, fd, bytes, 0, bytes.length, w => { + cheerpOSClose(cjFDs, fd); + resolve(); + }); + }); + }); +} + +const template = document.createElement('template'); +template.innerHTML = ` + + +
+
+

+ This is a proof-of-concept demo of Minecraft 1.2.5 running unmodified in the browser. +

+

+ Clicking the button below will download the client from mojang.com. + By clicking it, you agree to the Minecraft EULA. +

+ +
+ This is not an official Minecraft product. It is not approved by or associated with Mojang or Microsoft. +
+
+ +`; + +export default class MinecraftClient extends HTMLElement { + #canvas; + #progress; + #button; + #display; + #intro; + #isRunning; + + constructor() { + super(); + + const shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(template.content.cloneNode(true)); + + this.#button = shadowRoot.querySelector('button'); + this.#button.addEventListener('click', () => this.run()); + + this.#canvas = shadowRoot.querySelector('canvas'); + this.#canvas.width = 854; + this.#canvas.height = 480; + this.#canvas.tabIndex = -1; + this.#canvas.style.display = 'none'; + + this.#progress = shadowRoot.querySelector('progress'); + this.#progress.style.display = 'none'; + + this.#intro = shadowRoot.querySelector('.intro'); + + // CheerpJ needs an element to render to, but we are going to render to own canvas + this.#display = shadowRoot.querySelector('.display'); + this.#display.setAttribute('style', 'width:100%;height:100%;position:absolute;top:0;left:0px;visibility:hidden;'); + cheerpjCreateDisplay(-1, -1, this.#display); + + this.#isRunning = false; + } + + static register() { + customElements.define('minecraft-client', this); + } + + /** @returns {Promise} Exit code */ + async run() { + if (this.#isRunning) { + throw new Error('Already running'); + } + + this.#intro.style.display = 'none'; + + this.#progress.style.display = 'unset'; + const jarPath = "/files/client_1.2.5.jar" + await downloadFileToCheerpJ( + "https://piston-data.mojang.com/v1/objects/4a2fac7504182a97dcbcd7560c6392d7c8139928/client.jar", + jarPath, + (downloadedBytes, totalBytes) => { + this.#progress.value = downloadedBytes; + this.#progress.max = totalBytes; + } + ); + this.#progress.style.display = 'none'; + + this.#canvas.style.display = 'unset'; + window.lwjglCanvasElement = this.#canvas; + const exitCode = await cheerpjRunMain("net.minecraft.client.Minecraft", `/app/lwjgl-2.9.0.jar:/app/lwjgl_util-2.9.0.jar:${jarPath}`) + + this.#canvas.style.display = 'none'; + this.#isRunning = false; + + return exitCode; + } + + /** @returns {boolean} */ + get isRunning() { + return this.#isRunning; + } +} diff --git a/semag/style.css b/semag/style.css new file mode 100644 index 00000000..d11f5ef7 --- /dev/null +++ b/semag/style.css @@ -0,0 +1,92 @@ +html, body { + height: 100%; + margin: 0; + padding: 0; + + font-family: system-ui, sans-serif; +} + +@font-face { + font-family: "Minecrafter"; + src: url("fonts/minecrafter/Minecrafter.Reg.woff2"), sans-serif; +} + +header { + background: #171615; + color: white; + padding: 1rem; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +header h1 { + color: #c6b8b4; + font: 48px Minecrafter; + transform: perspective(24px) translateZ(0) rotate3d(1, 0, 0, 2deg); + text-shadow: 0px 5px 4px black; +} + +main { + margin: 0 auto; + padding: 1rem; + max-width: 60ch; + width: 100%; + box-sizing: border-box; + line-height: 1.4; +} + +main h2 { + font-size: 1.5rem; + margin: 1rem 0 0.5rem; +} + +main li { + margin: 0.5rem 0; +} + +@media (max-width: 900px) { + minecraft-client { + width: 100%; + } +} + +.controls { + margin: 0.5rem 0; +} + +.controls > * { + appearance: none; + background: transparent; + border: 0; + cursor: pointer; + + width: 24px; + height: 24px; + + margin-right: 0.5rem; +} + +.controls svg { + stroke: #c6b8b4; +} + +.controls > *:hover svg { + stroke: white; +} + +.mobile-only { + display: none; +} + +@media (max-width: 854px) { + .mobile-only { + display: block; + } + + .desktop-only { + display: none; + } +}