diff --git a/src/js/net/minecraft/client/GameWindow.js b/src/js/net/minecraft/client/GameWindow.js index 89e1908..f77f241 100644 --- a/src/js/net/minecraft/client/GameWindow.js +++ b/src/js/net/minecraft/client/GameWindow.js @@ -1,22 +1,48 @@ +import Minecraft from "./Minecraft.js"; +import FocusStateType from "../util/FocusStateType.js"; import GuiIngameMenu from "./gui/screens/GuiIngameMenu.js"; import Keyboard from "../util/Keyboard.js"; -import Minecraft from "./Minecraft.js"; +import GuiLoadingScreen from "./gui/screens/GuiLoadingScreen.js"; export default class GameWindow { constructor(minecraft, canvasWrapperId) { this.minecraft = minecraft; - this.canvasWrapperId = canvasWrapperId; + + this.width = 0; + this.height = 0; + + this.mouseX = 0; + this.mouseY = 0; this.mouseMotionX = 0; this.mouseMotionY = 0; - this.mouseLocked = false; - this.actualMouseLocked = false; - this.isMobile = this.detectTouchDevice(); + this.mouseInsideWindow = false; + this.mouseDownInterval = null; + this.focusState = FocusStateType.EXITED; + this.lastIngameSwitchTime = 0; + + this.mobileDevice = this.detectTouchDevice(); + + // Initialize canvas elements + this.initializeElements(canvasWrapperId); + + // Register listeners + if (this.mobileDevice) { + this.registerMobileListeners(); + } else { + this.registerDesktopListeners(); + } + + // Create keyboard + Keyboard.create(); + } + + initializeElements(canvasWrapperId) { // Get canvas wrapper - this.wrapper = document.getElementById(this.canvasWrapperId); + this.wrapper = document.getElementById(canvasWrapperId); // Remove all children of wrapper while (this.wrapper.firstChild) { @@ -29,143 +55,193 @@ export default class GameWindow { // Create screen renderer this.canvas2d = document.createElement('canvas'); + this.canvas2d.debugCanvas = document.createElement('canvas'); this.wrapper.appendChild(this.canvas2d); // Create screen item renderer this.canvasItems = document.createElement('canvas'); this.wrapper.appendChild(this.canvasItems); + } - this.canvas.addEventListener("webglcontextlost", function (event) { - event.preventDefault(); - }, false); - - let mouseDownInterval = null; - - // Request focus - document.onclick = () => { - if (this.minecraft.currentScreen === null) { - this.requestFocus(); - } - } - - window.addEventListener('resize', _ => this.updateWindowSize(), false); - - // Focus listener - document.addEventListener('pointerlockchange', _ => this.onFocusChanged(), false); - document.addEventListener('pointerlockerror', e => { - e.preventDefault() - }, false); - - // Mouse motion - document.addEventListener('mousemove', event => { - this.onMouseMove(event); - - // Handle mouse move on screen - if (minecraft.currentScreen !== null) { - minecraft.currentScreen.mouseDragged(event.x / this.scaleFactor, event.y / this.scaleFactor, event.code); - } - }, false); - - // Mouse release - document.addEventListener('mouseup', event => { - // Handle mouse release on screen - if (minecraft.currentScreen !== null) { - minecraft.currentScreen.mouseReleased(event.x / this.scaleFactor, event.y / this.scaleFactor, event.code); - } - - clearInterval(mouseDownInterval); - }, false); - - // Losing focus event - this.canvas.addEventListener("mouseout", () => { - if (minecraft.currentScreen === null && !this.actualMouseLocked) { - minecraft.displayScreen(new GuiIngameMenu()); - } - - clearInterval(mouseDownInterval); + registerDesktopListeners() { + this.registerListener(window, 'resize', event => { + this.updateWindowSize(); }); + this.registerListener(document, 'mousedown', event => { + // In-Game mouse click + this.minecraft.onMouseClicked(event.button); - // Right click - document.addEventListener('contextmenu', event => { - event.preventDefault(); - }); - - // Mouse buttons - document.addEventListener('mousedown', event => { - event.preventDefault(); - - // Create sound engine (It has to be created after user interaction) - if (!minecraft.soundManager.isCreated()) { - minecraft.soundManager.create(minecraft.worldRenderer); - } - - // Handle in-game mouse click - if (!this.isMobile) { - minecraft.onMouseClicked(event.button); - - // Start interval to repeat the mouse event - clearInterval(mouseDownInterval); - mouseDownInterval = setInterval(() => minecraft.onMouseClicked(event.button), 250); + // Start interval to repeat the mouse event + if (this.mouseDownInterval !== null) { + clearInterval(this.mouseDownInterval); } + this.mouseDownInterval = setInterval(_ => this.minecraft.onMouseClicked(event.button), 250); // Handle mouse click on screen - if (minecraft.currentScreen !== null) { - minecraft.currentScreen.mouseClicked(event.x / this.scaleFactor, event.y / this.scaleFactor, event.code); + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.mouseClicked( + event.x / this.scaleFactor, + event.y / this.scaleFactor, + event.code + ); } - }, false); - // Mouse scroll - this.wrapper.addEventListener('wheel', (event) => { - event.preventDefault(); + // Fix cursor lock state + this.requestCursorUpdate(); + + // Request lock on click + if (this.minecraft.currentScreen === null && this.focusState === FocusStateType.EXITED) { + this.updateFocusState(FocusStateType.REQUEST_LOCK); + } + + this.initialSoundEngine(); + }); + this.registerListener(document, 'mousemove', event => { + this.mouseX = event.clientX / this.scaleFactor; + this.mouseY = event.clientY / this.scaleFactor; + + this.mouseMotionX = event.movementX; + this.mouseMotionY = -event.movementY; + + // Handle mouse move on screen + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.mouseDragged(event.x / this.scaleFactor, event.y / this.scaleFactor, event.code); + } + + this.requestCursorUpdate(); + }); + this.registerListener(document, 'mouseup', event => { + // Handle mouse release on screen + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.mouseReleased( + event.x / this.scaleFactor, + event.y / this.scaleFactor, + event.code + ); + } + + if (this.mouseDownInterval !== null) { + clearInterval(this.mouseDownInterval); + } + }); + this.registerListener(document, 'pointerlockchange', event => { + let intentState = this.focusState.getIntent(); // Get target state we want to switch into + let isCursorLocked = this.isCursorLockedToCanvas(); // Get current state of the canvas lock + let isLockIntent = intentState === FocusStateType.LOCKED; // Check if we want to lock the cursor + + let lastSwitchDuration = Date.now() - this.lastIngameSwitchTime; + if (this.focusState === FocusStateType.LOCKED && !isCursorLocked && lastSwitchDuration < 200) { + // If the user exists the inventory by using the escape key, the cursor unlocks from the canvas, + // so we have to prevent that by switching immediately to the request state + this.focusState = FocusStateType.REQUEST_LOCK; + } else { + if (intentState === null) { + // The state changed unintentionally, so we have to choose a new state from the current canvas lock + this.updateFocusState(isCursorLocked ? FocusStateType.LOCKED : FocusStateType.EXITED); + } else if (isCursorLocked === isLockIntent) { + // Check if the canvas completed the lock operation like intended and change the state to its final state + this.updateFocusState(intentState); + } + } + }); + this.registerListener(this.wrapper, 'mouseover', event => { + // Enable keyboard util handling + Keyboard.setEnabled(true); + this.mouseInsideWindow = true; + + // Update cursor lock + this.requestCursorUpdate(); + }); + this.registerListener(this.wrapper, 'mouseleave', event => { + // Disable keyboard util handling + Keyboard.setEnabled(false); + this.mouseInsideWindow = false; + + // Update cursor lock + this.requestCursorUpdate(); + }); + this.registerListener(document, 'mouseout', event => { + this.requestCursorUpdate(); + }); + this.registerListener(document, 'mouseenter', event => { + this.requestCursorUpdate(); + }); + this.registerListener(window, 'keydown', event => { + // Prevent browser functions except fullscreen + if (event.key !== 'F11') { + event.preventDefault(); + } + + // Ignore key input if mouse is not inside window + if (!this.mouseInsideWindow) { + return; + } + + // Handle escape press if focus is still in requesting state + if (event.key === 'Escape' && this.minecraft.currentScreen === null) { + this.updateFocusState(FocusStateType.REQUEST_EXIT); + return; + } + + let currentScreen = this.minecraft.currentScreen; + if (currentScreen === null) { + // Handle in-game key press + this.minecraft.onKeyPressed(event.code); + } else { + // Handle key type on screen + currentScreen.keyTyped(event.code, event.key); + } + + this.requestCursorUpdate(); + }, false); + this.registerListener(window, 'keyup', event => { + // Handle key release on screen + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.keyReleased(event.code); + } + }); + this.registerListener(document, 'contextmenu'); + this.registerListener(this.wrapper, 'wheel', event => { event.stopPropagation(); + // Handle mouse scroll let delta = Math.sign(event.deltaY); - minecraft.onMouseScroll(delta); - }, false); - - // Keyboard interaction with screen - window.addEventListener('keydown', event => { - if (event.code === "F11") { - return; // Toggle fullscreen - } - - // Prevent key - event.preventDefault(); - - if (minecraft.currentScreen !== null) { - // Handle key type on screen - minecraft.currentScreen.keyTyped(event.code, event.key); - } else if (event.code === 'Escape') { - minecraft.displayScreen(new GuiIngameMenu()); - } else { - minecraft.onKeyPressed(event.code); - } + this.minecraft.onMouseScroll(delta); }); + } - // Keyboard interaction with screen - window.addEventListener('keyup', (event) => { - // Prevent key - event.preventDefault(); + registerMobileListeners() { + let touchStartTime = 0; + let prevTouched = false; - if (minecraft.currentScreen !== null) { - // Handle key release on screen - minecraft.currentScreen.keyReleased(event.code); - } + this.registerListener(window, 'resize', event => { + this.updateWindowSize(); }); - - // Touch interaction - let touchStart; - window.addEventListener('touchstart', (event) => { + this.registerListener(document, 'touchstart', event => { for (let i = 0; i < event.touches.length; i++) { let touch = event.touches[i]; - let x = touch.pageX; let y = touch.pageY; + // Handle mouse click on screen + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.mouseClicked( + x / this.scaleFactor, + y / this.scaleFactor, + 0 + ); + } + let isRightHand = x > this.wrapper.offsetWidth / 2; + // Handle player movement if (isRightHand) { - touchStart = Date.now(); + touchStartTime = Date.now(); } else { let tileSize = this.wrapper.offsetWidth / 8; @@ -201,48 +277,61 @@ export default class GameWindow { } } } - - // Create sound engine (It has to be created after user interaction) - if (!minecraft.soundManager.isCreated()) { - minecraft.soundManager.create(minecraft.worldRenderer); - } - }); - - // Touch movement - let prevTouch; - window.addEventListener('touchmove', (event) => { + }, false); + this.registerListener(document, 'touchmove', event => { for (let i = 0; i < event.touches.length; i++) { let touch = event.touches[i]; - let x = touch.pageX; let y = touch.pageY; + // Handle mouse move on screen + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.mouseDragged( + x / this.scaleFactor, + y / this.scaleFactor, + 0 + ); + } + // Right hand let isRightHand = x > this.wrapper.offsetWidth / 2; + // Handle player movement if (isRightHand) { - // Player movement - if (prevTouch) { - this.mouseMotionX = (x - prevTouch.pageX) * 10; - this.mouseMotionY = -(y - prevTouch.pageY) * 10; + if (prevTouched) { + this.mouseMotionX = (x - prevTouched.pageX) * 10; + this.mouseMotionY = -(y - prevTouched.pageY) * 10; } - - prevTouch = touch; + prevTouched = touch; + touchStartTime = Date.now(); } } - }); - window.addEventListener('touchend', (event) => { + }, false); + this.registerListener(document, 'touchend', event => { // Break block - if (!prevTouch && touchStart && (Date.now() - touchStart) < 1000) { - minecraft.onMouseClicked(2); + if (!prevTouched && touchStartTime !== 0 && (Date.now() - touchStartTime) < 1000) { + this.minecraft.onMouseClicked(2); } - prevTouch = null; - touchStart = null; + prevTouched = false; + touchStartTime = 0; - // Stop pressing keys + // Handle touches for (let i = 0; i < event.changedTouches.length; i++) { let touch = event.changedTouches[i]; + let x = touch.pageX; + let y = touch.pageY; + + // Handle mouse release on screen + let currentScreen = this.minecraft.currentScreen; + if (currentScreen !== null) { + currentScreen.mouseReleased( + x / this.scaleFactor, + y / this.scaleFactor, + 0 + ); + } // Left hand let isLeftHand = touch.pageX < this.wrapper.offsetWidth / 2; @@ -253,41 +342,18 @@ export default class GameWindow { break; } } - }); + + this.initialSoundEngine(); + }, false); + this.registerListener(document, 'contextmenu'); // Break block listener - if (this.isMobile) { - setInterval(() => { - if (touchStart && (Date.now() - touchStart) > 1000) { - touchStart = Date.now(); - minecraft.onMouseClicked(0); - } - }, 200); - } - - // Create keyboard - Keyboard.create(); - } - - requestFocus() { - if (this.isMobile) { - document.body.requestFullscreen(); - } else { - window.focus(); - this.canvas.requestPointerLock(); - document.body.style.cursor = 'none'; - } - - this.mouseLocked = true; - } - - exitFocus() { - if (this.isMobile) { - return; - } - - document.exitPointerLock(); - document.body.style.cursor = 'default'; + setInterval(() => { + if (touchStartTime !== 0 && (Date.now() - touchStartTime) > 250) { + touchStartTime = Date.now(); + this.minecraft.onMouseClicked(0); + } + }, 200); } updateWindowSize() { @@ -313,11 +379,15 @@ export default class GameWindow { this.canvas2d.style.width = wrapperWidth + "px"; this.canvas2d.style.height = wrapperHeight + "px"; + let debugCanvas = this.canvas2d["debugCanvas"]; + debugCanvas.width = this.canvas2d.width; + debugCanvas.height = this.canvas2d.height; + // Reinitialize gui this.minecraft.screenRenderer.initialize(); // Reinitialize current screen - if (!(this.minecraft.currentScreen === null)) { + if (this.minecraft.currentScreen !== null) { this.minecraft.currentScreen.setup(this.minecraft, this.width, this.height); } } @@ -332,29 +402,63 @@ export default class GameWindow { } this.scaleFactor = scale; - this.width = wrapperWidth / scale; - this.height = wrapperHeight / scale; + this.width = Math.ceil(wrapperWidth / scale); + this.height = Math.ceil(wrapperHeight / scale); } - onFocusChanged() { - this.actualMouseLocked = document.pointerLockElement === this.canvas; + isCursorLockedToCanvas() { + // The actual state of the browser cursor lock + return document.pointerLockElement === this.canvas; } - onMouseMove(event) { - this.mouseX = event.clientX / this.scaleFactor; - this.mouseY = event.clientY / this.scaleFactor; + isLocked() { + // The actual definition for the game if the cursor is locked or not + return this.focusState.isLock() && this.minecraft.currentScreen === null; + } - if (document.pointerLockElement !== this.canvas) { - this.mouseLocked = false; - - if (this.minecraft.currentScreen === null) { - this.requestFocus(); - } + updateFocusState(state) { + if (state.getIntent() === this.focusState || state === this.focusState) { + return; } - if (this.actualMouseLocked || this.mouseLocked) { - this.mouseMotionX = event.movementX; - this.mouseMotionY = -event.movementY; + let prevLock = this.focusState.isLock(); + let nextLock = state.isLock(); + + // Update state + this.focusState = state; + + // Update cursor visibility + document.body.style.cursor = nextLock ? 'none' : 'default'; + + // Request lock state + this.requestCursorUpdate(); + + // Open menu on exit + if (prevLock !== nextLock) { + let currentScreen = this.minecraft.currentScreen; + + // Open in-game menu + if (currentScreen === null && !nextLock) { + this.minecraft.displayScreen(new GuiIngameMenu()); + } + + // Close current screen + if (!(currentScreen instanceof GuiLoadingScreen) && nextLock) { + this.minecraft.displayScreen(null); + this.lastIngameSwitchTime = Date.now(); + } + } + } + + requestCursorUpdate() { + // Check if the current state doesn't match the canvas lock + if (this.mouseInsideWindow && this.focusState.isLock() !== this.isCursorLockedToCanvas()) { + // Request cursor lock depending on the state + if (this.focusState.isLock()) { + this.canvas.requestPointerLock(); + } else { + document.exitPointerLock(); + } } } @@ -367,8 +471,30 @@ export default class GameWindow { return false; } - close() { - this.openUrl(Minecraft.URL_GITHUB); + getMemoryLimit() { + return this.getMemoryValue("jsHeapSizeLimit", 1); + } + + getMemoryAllocated() { + return this.getMemoryValue("totalJSHeapSize", 0); + } + + getMemoryUsed() { + return this.getMemoryValue("usedJSHeapSize", 0); + } + + getMemoryValue(key, fallbackValue = 0) { + let performance = window.performance || window.msPerformance || window.webkitPerformance || window.mozPerformance; + if (performance && performance.memory && performance.memory[key]) { + return performance.memory[key]; + } + return fallbackValue; + } + + getGPUName() { + let gl = this.canvas.getContext("webgl2"); + const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); + return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); } openUrl(url, newTab) { @@ -379,8 +505,46 @@ export default class GameWindow { } } + close() { + this.openUrl(Minecraft.URL_GITHUB); + } + async getClipboardText() { return navigator.clipboard.readText(); } -} \ No newline at end of file + isMobileDevice() { + return this.mobileDevice; + } + + pullMouseMotionX() { + let value = this.mouseMotionX; + this.mouseMotionX = 0; + return value; + } + + pullMouseMotionY() { + let value = this.mouseMotionY; + this.mouseMotionY = 0; + return value; + } + + initialSoundEngine() { + // Create sound engine (It has to be created after user interaction) + if (!this.minecraft.soundManager.isCreated()) { + this.minecraft.soundManager.create(this.minecraft.worldRenderer); + } + } + + registerListener(parent, event, listener = null, preventDefaults = true) { + parent.addEventListener(event, event => { + if (preventDefaults) { + event.preventDefault(); + } + + if (listener !== null) { + listener(event); + } + }); + } +} diff --git a/src/js/net/minecraft/client/Minecraft.js b/src/js/net/minecraft/client/Minecraft.js index 3584ada..a80ed9a 100644 --- a/src/js/net/minecraft/client/Minecraft.js +++ b/src/js/net/minecraft/client/Minecraft.js @@ -21,10 +21,11 @@ import CommandHandler from "./command/CommandHandler.js"; import GuiContainerCreative from "./gui/screens/container/GuiContainerCreative.js"; import GameProfile from "../util/GameProfile.js"; import UUID from "../util/UUID.js"; +import FocusStateType from "../util/FocusStateType.js"; export default class Minecraft { - static VERSION = "1.1.1" + static VERSION = "1.1.2" static URL_GITHUB = "https://github.com/labystudio/js-minecraft"; static PROTOCOL_VERSION = 758; @@ -46,6 +47,7 @@ export default class Minecraft { this.player = null; this.fps = 0; + this.maxFps = 0; let username = "Player" + Math.floor(Math.random() * 100); this.profile = new GameProfile(username, UUID.randomUUID()); @@ -137,7 +139,7 @@ export default class Minecraft { } hasInGameFocus() { - return this.window.mouseLocked && this.currentScreen === null; + return this.window.isLocked() && this.currentScreen === null; } isInGame() { @@ -167,7 +169,7 @@ export default class Minecraft { } // Render the game - this.onRender(this.timer.partialTicks); + this.onRender(this.isPaused() ? 0 : this.timer.partialTicks); // Increase rendered frame this.frames++; @@ -175,6 +177,7 @@ export default class Minecraft { // Loop if a second passed while (Date.now() >= this.lastTime + 1000) { this.fps = this.frames; + this.maxFps = Math.max(this.maxFps, this.fps); this.lastTime += 1000; this.frames = 0; } @@ -184,10 +187,9 @@ export default class Minecraft { if (this.isInGame()) { // Player rotation if (!this.isPaused()) { - this.player.turn(this.window.mouseMotionX, this.window.mouseMotionY); - - this.window.mouseMotionX = 0; - this.window.mouseMotionY = 0; + let deltaX = this.window.pullMouseMotionX(); + let deltaY = this.window.pullMouseMotionY(); + this.player.turn(deltaX, deltaY); } // Update lights @@ -196,7 +198,7 @@ export default class Minecraft { } // Render the game - if (this.hasInGameFocus()) { + if (this.isInGame() && !this.isPaused()) { this.worldRenderer.render(partialTicks); } } @@ -207,6 +209,10 @@ export default class Minecraft { } displayScreen(screen) { + if (screen === this.currentScreen) { + return; + } + if (typeof screen === "undefined") { console.error("Tried to display an undefined screen"); return; @@ -230,9 +236,9 @@ export default class Minecraft { // Initialize new screen if (screen === null) { - this.window.requestFocus(); + this.window.updateFocusState(FocusStateType.REQUEST_LOCK); } else { - this.window.exitFocus(); + this.window.updateFocusState(FocusStateType.REQUEST_EXIT); screen.setup(this, this.window.width, this.window.height); } @@ -242,6 +248,9 @@ export default class Minecraft { onTick() { if (this.isInGame() && !this.isPaused()) { + // Tick overlay + this.ingameOverlay.onTick(); + // Tick world this.world.onTick(); @@ -253,9 +262,6 @@ export default class Minecraft { // Tick particle renderer this.particleRenderer.onTick(); - - // Tick overlay - this.ingameOverlay.onTick(); } // Tick the screen @@ -315,6 +321,7 @@ export default class Minecraft { // Toggle debug overlay if (button === "F3") { this.settings.debugOverlay = !this.settings.debugOverlay; + this.settings.save(); } // Open inventory @@ -324,7 +331,7 @@ export default class Minecraft { } onMouseClicked(button) { - if (this.window.mouseLocked) { + if (this.window.isLocked()) { let hitResult = this.player.rayTrace(5, this.timer.partialTicks); // Destroy block diff --git a/src/js/net/minecraft/client/entity/Entity.js b/src/js/net/minecraft/client/entity/Entity.js index e68008b..c1c641b 100644 --- a/src/js/net/minecraft/client/entity/Entity.js +++ b/src/js/net/minecraft/client/entity/Entity.js @@ -195,4 +195,12 @@ export default class Entity { this.isDead = true; } + isMoving() { + return this.motionX !== 0.0 + || this.motionY !== 0.0 && !this.onGround + || this.motionZ !== 0.0 + || this.rotationYaw !== this.prevRotationYaw + || this.rotationPitch !== this.prevRotationPitch; + } + } \ No newline at end of file diff --git a/src/js/net/minecraft/client/gui/Gui.js b/src/js/net/minecraft/client/gui/Gui.js index f1d2161..4148f04 100644 --- a/src/js/net/minecraft/client/gui/Gui.js +++ b/src/js/net/minecraft/client/gui/Gui.js @@ -16,8 +16,8 @@ export default class Gui { this.minecraft.fontRenderer.drawString(stack, string, x - this.getStringWidth(stack, string) / 2, y, color); } - drawRightString(stack, string, x, y, color = -1) { - this.minecraft.fontRenderer.drawString(stack, string, x - this.getStringWidth(stack, string), y, color); + drawRightString(stack, string, x, y, color = -1, shadow = true) { + this.minecraft.fontRenderer.drawString(stack, string, x - this.getStringWidth(stack, string), y, color, shadow); } drawString(stack, string, x, y, color = -1, shadow = true) { diff --git a/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js b/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js index c720c76..5b42614 100644 --- a/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js +++ b/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js @@ -2,6 +2,10 @@ import Gui from "../Gui.js"; import Block from "../../world/block/Block.js"; import ChatOverlay from "./ChatOverlay.js"; import Minecraft from "../../Minecraft.js"; +import EnumBlockFace from "../../../util/EnumBlockFace.js"; +import MathHelper from "../../../util/MathHelper.js"; +import FontRenderer from "../../render/gui/FontRenderer.js"; +import EnumSkyBlock from "../../../util/EnumSkyBlock.js"; export default class IngameOverlay extends Gui { @@ -14,6 +18,8 @@ export default class IngameOverlay extends Gui { this.textureCrosshair = minecraft.resources["gui/icons.png"]; this.textureHotbar = minecraft.resources["gui/gui.png"]; + + this.ticksRendered = 0; } render(stack, mouseX, mouseY, partialTicks) { @@ -28,29 +34,32 @@ export default class IngameOverlay extends Gui { // Render chat this.chatOverlay.render(stack, mouseX, mouseY, partialTicks); - let world = this.minecraft.world; - let player = this.minecraft.player; - - let x = Math.floor(player.x); - let y = Math.floor(player.y); - let z = Math.floor(player.z); - - let fps = Math.floor(this.minecraft.fps); - let lightUpdates = world.lightUpdateQueue.length; - let chunkUpdates = this.minecraft.worldRenderer.chunkSectionUpdateQueue.length; - let lightLevel = world.getTotalLightAt(x, y, z); - - // Debug + // Render debug canvas on stack if (this.minecraft.settings.debugOverlay) { - this.drawString(stack, "js-minecraft " + Minecraft.VERSION, 1, 1); - this.drawString(stack, fps + " fps," + " " + lightUpdates + " light updates," + " " + chunkUpdates + " chunk updates", 1, 1 + 9 * 1); - this.drawString(stack, x + ", " + y + ", " + z + " (" + (x >> 4) + ", " + (y >> 4) + ", " + (z >> 4) + ")", 1, 1 + 9 * 2); - this.drawString(stack, "Light: " + lightLevel, 1, 1 + 9 * 3); + stack.drawImage(this.window.canvas2d.debugCanvas, 0, 0); } } onTick() { this.chatOverlay.onTick(); + + // Render debug overlay on tick + if (this.minecraft.settings.debugOverlay) { + let stack = this.window.canvas2d.debugCanvas.getContext('2d'); + let moving = this.minecraft.player.isMoving(); + + // Render debug overlay each tick if the player is moving + if (this.ticksRendered % (moving ? 1 : 20) === 0) { + // Clear debug canvas + stack.clearRect(0, 0, this.window.width, this.window.height); + + // Render debug information + this.renderLeftDebugOverlay(stack); + this.renderRightDebugOverlay(stack); + } + + this.ticksRendered++; + } } renderCrosshair(stack, x, y) { @@ -85,4 +94,181 @@ export default class IngameOverlay extends Gui { } } + renderLeftDebugOverlay(stack) { + let world = this.minecraft.world; + let player = this.minecraft.player; + let worldRenderer = this.minecraft.worldRenderer; + + let x = player.x; + let y = player.y; + let z = player.z; + + let yaw = MathHelper.wrapAngleTo180(player.rotationYaw); + let pitch = player.rotationPitch; + + let facingIndex = (((yaw + 180) * 4.0 / 360.0) + 0.5) & 3; + let facing = EnumBlockFace.values()[facingIndex + 2]; + + let fixedX = x.toFixed(2); + let fixedY = y.toFixed(2); + let fixedZ = z.toFixed(2); + + let blockX = Math.floor(x); + let blockY = Math.floor(y); + let blockZ = Math.floor(z); + + let chunkX = blockX >> 4; + let chunkY = blockY >> 4; + let chunkZ = blockZ >> 4; + + let inChunkX = blockX & 0xF; + let inChunkY = blockY & 0xF; + let inChunkZ = blockZ & 0xF; + + let visibleChunks = 0; + let loadedChunks = 0; + for (let [index, chunk] of world.chunks) { + for (let y in chunk.sections) { + let chunkSection = chunk.sections[y]; + if (chunkSection.group.visible) { + visibleChunks++; + } + loadedChunks++; + } + } + let visibleEntities = 0; + for (let index in world.entities) { + let entity = world.entities[index]; + if (entity.renderer.group.visible) { + visibleEntities++; + } + } + + let fps = Math.floor(this.minecraft.fps); + let viewDistance = this.minecraft.settings.viewDistance; + let lightUpdates = world.lightUpdateQueue.length; + let chunkUpdates = worldRenderer.chunkSectionUpdateQueue.length; + let entities = world.entities.length; + let particles = this.minecraft.particleRenderer.particles.length; + let skyLight = world.getSavedLightValue(EnumSkyBlock.SKY, blockX, blockY, blockZ); + let blockLight = world.getSavedLightValue(EnumSkyBlock.BLOCK, blockX, blockY, blockZ); + let lightLevel = world.getTotalLightAt(blockX, blockY, blockZ); + let biome = "T: " + world.getTemperature(blockX, blockY, blockZ) + " H: " + world.getHumidity(blockX, blockY, blockZ); + + let soundsLoaded = 0; + let soundsPlaying = 0; + let soundPool = this.minecraft.soundManager.soundPool; + for (let [id, sounds] of Object.entries(soundPool)) { + for (let sound of sounds) { + soundsLoaded++; + + if (sound.isPlaying) { + soundsPlaying++; + } + } + } + + let towards = "Towards " + (facing.isPositive() ? "positive" : "negative") + " " + (facing.isXAxis() ? "X" : "Z"); + + let lines = [ + "js-minecraft " + Minecraft.VERSION, + fps + " fps (" + chunkUpdates + " chunk updates) T: " + this.minecraft.maxFps, + "C: " + visibleChunks + "/" + loadedChunks + " D: " + viewDistance + ", L: " + lightUpdates, + "E: " + visibleEntities + "/" + entities + ", P: " + particles, + "", + "XYZ: " + fixedX + " / " + fixedY + " / " + fixedZ, + "Block: " + blockX + " " + blockY + " " + blockZ, + "Chunk: " + chunkX + " " + chunkY + " " + chunkZ + " in " + inChunkX + " " + inChunkY + " " + inChunkZ, + "Facing: " + facing.getName() + " (" + towards + ") (" + yaw.toFixed(1) + " / " + pitch.toFixed(1) + ")", + "Light: " + lightLevel + " (" + skyLight + " sky, " + blockLight + " block)", + // "Biome: " + biome, + "", + "Sounds: " + soundsPlaying + "/" + soundsLoaded, + "Time: " + world.time % 24000 + " (Day " + Math.floor(world.time / 24000) + ")", + "Cursor: " + this.minecraft.window.focusState.getName() + ] + + // Hit result + let hit = worldRenderer.lastHitResult; + if (hit !== null && hit.type !== 0) { + lines.push("Looking at: " + hit.x + " " + hit.y + " " + hit.z); + } + + // Draw lines + for (let i = 0; i < lines.length; i++) { + if (lines[i].length === 0) { + continue; + } + + // Draw background + this.drawRect(stack, + 1, + 1 + FontRenderer.FONT_HEIGHT * i, + 1 + this.getStringWidth(stack, lines[i]) + 1, + 1 + FontRenderer.FONT_HEIGHT * i + FontRenderer.FONT_HEIGHT, + '#50505090' + ); + + // Draw line + this.drawString(stack, lines[i], 2, 2 + FontRenderer.FONT_HEIGHT * i, 0xffe0e0e0, false); + } + } + + renderRightDebugOverlay(stack) { + let memoryLimit = this.minecraft.window.getMemoryLimit(); + let memoryUsed = this.minecraft.window.getMemoryUsed(); + let memoryAllocated = this.minecraft.window.getMemoryAllocated(); + + let usedPercentage = Math.floor(memoryUsed / memoryLimit * 100); + let allocatedPercentage = Math.floor(memoryAllocated / memoryLimit * 100); + + let width = this.window.canvas.width; + let height = this.window.canvas.height; + + let lines = [ + "Mem: " + usedPercentage + "% " + this.humanFileSize(memoryUsed, memoryLimit), + "Allocated: " + allocatedPercentage + "% " + this.humanFileSize(null, memoryAllocated), + "", + "Display: " + width + "x" + height, + this.window.getGPUName() + ]; + + // Draw lines + for (let i = 0; i < lines.length; i++) { + if (lines[i].length === 0) { + continue; + } + + // Draw background + this.drawRect(stack, + this.window.width - this.getStringWidth(stack, lines[i]) - 3, + 1 + FontRenderer.FONT_HEIGHT * i, + this.window.width - 1, + 1 + FontRenderer.FONT_HEIGHT * i + FontRenderer.FONT_HEIGHT, + '#50505090' + ); + + // Draw line + this.drawRightString(stack, lines[i], this.window.width - 2, 2 + FontRenderer.FONT_HEIGHT * i, 0xffe0e0e0, false); + } + } + + humanFileSize(bytesUsed, bytesMax) { + if (Math.abs(bytesMax) < 1000) { + return (bytesUsed === null ? "" : bytesUsed + "/") + bytesMax + "B"; + } + const units = ['kB', 'MB']; + let u = -1; + const r = 10; + const thresh = 1000; + + do { + if (bytesUsed !== null) { + bytesUsed /= thresh; + } + bytesMax /= thresh; + ++u; + } while (Math.round(Math.abs(bytesMax) * r) / r >= thresh && u < units.length - 1); + return (bytesUsed === null ? "" : bytesUsed.toFixed(0) + "/") + bytesMax.toFixed(0) + units[u]; + } } \ No newline at end of file diff --git a/src/js/net/minecraft/client/gui/screens/GuiContainer.js b/src/js/net/minecraft/client/gui/screens/GuiContainer.js index b92172b..1258cda 100644 --- a/src/js/net/minecraft/client/gui/screens/GuiContainer.js +++ b/src/js/net/minecraft/client/gui/screens/GuiContainer.js @@ -76,7 +76,7 @@ export default class GuiContainer extends GuiScreen { keyTyped(key, character) { // Swap to slot for (let i = 1; i <= 9; i++) { - if (key === 'Digit' + i) { + if (key === 'Digit' + i && this.hoverSlot !== null) { this.container.swapWithHotbar(this.hoverSlot, this.minecraft.player.inventory, i - 1); } } diff --git a/src/js/net/minecraft/client/gui/screens/GuiLoadingScreen.js b/src/js/net/minecraft/client/gui/screens/GuiLoadingScreen.js index 739fdb8..1bd7bbf 100644 --- a/src/js/net/minecraft/client/gui/screens/GuiLoadingScreen.js +++ b/src/js/net/minecraft/client/gui/screens/GuiLoadingScreen.js @@ -48,7 +48,7 @@ export default class GuiLoadingScreen extends GuiScreen { } setProgress(progress) { - if (progress < this.progress) { + if (progress < this.progress || progress > 1) { return; } this.progress = progress; diff --git a/src/js/net/minecraft/client/network/NetworkManager.js b/src/js/net/minecraft/client/network/NetworkManager.js index cb0e2db..1e1ed96 100644 --- a/src/js/net/minecraft/client/network/NetworkManager.js +++ b/src/js/net/minecraft/client/network/NetworkManager.js @@ -87,7 +87,7 @@ export default class NetworkManager { wrapper.write(array); let chunk = wrapper.getArray().buffer; - // Decrypt chunk + // Encrypt chunk if (this.isEncrypted) { chunk = this.encryption.encrypt(new Uint8Array(chunk)); } diff --git a/src/js/net/minecraft/client/render/WorldRenderer.js b/src/js/net/minecraft/client/render/WorldRenderer.js index 0d42dce..4608fdc 100644 --- a/src/js/net/minecraft/client/render/WorldRenderer.js +++ b/src/js/net/minecraft/client/render/WorldRenderer.js @@ -49,6 +49,8 @@ export default class WorldRenderer { this.flushRebuild = false; + this.lastHitResult = null; + this.initialize(); } @@ -763,6 +765,8 @@ export default class WorldRenderer { ); } } + + this.lastHitResult = hitResult; } translate(stack, x, y, z) { diff --git a/src/js/net/minecraft/client/render/gui/ScreenRenderer.js b/src/js/net/minecraft/client/render/gui/ScreenRenderer.js index 3152396..956d764 100644 --- a/src/js/net/minecraft/client/render/gui/ScreenRenderer.js +++ b/src/js/net/minecraft/client/render/gui/ScreenRenderer.js @@ -1,18 +1,18 @@ export default class ScreenRenderer { - static UPSCALE = 3; - constructor(minecraft, window) { this.minecraft = minecraft; this.window = window; + + this.upscale = window.isMobileDevice() ? 1 : 3; } initialize() { this.resolution = this.minecraft.isInGame() ? 1 : this.minecraft.window.scaleFactor; // Increase resolution for the splash text // Update camera size - this.window.canvas2d.width = this.window.width * ScreenRenderer.UPSCALE; - this.window.canvas2d.height = this.window.height * ScreenRenderer.UPSCALE; + this.window.canvas2d.width = this.window.width * this.upscale; + this.window.canvas2d.height = this.window.height * this.upscale; // Get context stack of 2d canvas this.stack2d = this.window.canvas2d.getContext('2d'); @@ -26,7 +26,7 @@ export default class ScreenRenderer { let mouseY = this.minecraft.window.mouseY; this.stack2d.save(); - this.stack2d.scale(ScreenRenderer.UPSCALE, ScreenRenderer.UPSCALE, ScreenRenderer.UPSCALE); + this.stack2d.scale(this.upscale, this.upscale, this.upscale); // Reset 2d canvas this.stack2d.clearRect(0, 0, this.window.width, this.window.height); diff --git a/src/js/net/minecraft/client/sound/SoundManager.js b/src/js/net/minecraft/client/sound/SoundManager.js index 11d855d..b8a31f4 100644 --- a/src/js/net/minecraft/client/sound/SoundManager.js +++ b/src/js/net/minecraft/client/sound/SoundManager.js @@ -7,7 +7,7 @@ export default class SoundManager { this.audioLoader = new THREE.AudioLoader(); this.audioListener = null; - this.soundPool = []; + this.soundPool = {}; } create(worldRenderer) { @@ -70,7 +70,7 @@ export default class SoundManager { } else if (pool.length > 0) { // Play random sound in pool let sound = pool[Math.floor(Math.random() * pool.length)]; - if (typeof volume === "undefined") { + if (typeof volume === "undefined" || typeof sound === "undefined") { return; } diff --git a/src/js/net/minecraft/util/EnumBlockFace.js b/src/js/net/minecraft/util/EnumBlockFace.js index 0ea0157..e556e79 100644 --- a/src/js/net/minecraft/util/EnumBlockFace.js +++ b/src/js/net/minecraft/util/EnumBlockFace.js @@ -17,6 +17,10 @@ export default class EnumBlockFace { return this.isXAxis() ? 0.6 : this.isYAxis() ? 1.0 : 0.8; } + isPositive() { + return this.x > 0 || this.y > 0 || this.z > 0; + } + isXAxis() { return this.x !== 0; } @@ -58,6 +62,25 @@ export default class EnumBlockFace { return this.x === other.x && this.y === other.y && this.z === other.z; } + getName() { + switch (this) { + case EnumBlockFace.TOP: + return "top"; + case EnumBlockFace.BOTTOM: + return "bottom"; + case EnumBlockFace.NORTH: + return "north"; + case EnumBlockFace.EAST: + return "east"; + case EnumBlockFace.SOUTH: + return "south"; + case EnumBlockFace.WEST: + return "west"; + default: + return "unknown"; + } + } + static values() { return [EnumBlockFace.TOP, EnumBlockFace.BOTTOM, EnumBlockFace.NORTH, EnumBlockFace.EAST, EnumBlockFace.SOUTH, EnumBlockFace.WEST]; } diff --git a/src/js/net/minecraft/util/FocusStateType.js b/src/js/net/minecraft/util/FocusStateType.js new file mode 100644 index 0000000..356d33e --- /dev/null +++ b/src/js/net/minecraft/util/FocusStateType.js @@ -0,0 +1,74 @@ +export default class FocusStateType { + + static REQUEST_EXIT = new FocusStateType(0, 1); + static EXITED = new FocusStateType(1, -1); + + static REQUEST_LOCK = new FocusStateType(2, 3); + static LOCKED = new FocusStateType(3, -1); + + constructor(id, intentId) { + this.id = id; + this.intentId = intentId; + } + + getIntent() { + return this.intentId === -1 ? null : FocusStateType.getById(this.intentId); + } + + isLock() { + return this === FocusStateType.REQUEST_LOCK || this === FocusStateType.LOCKED; + } + + isRequest() { + return this === FocusStateType.REQUEST_LOCK || this === FocusStateType.REQUEST_EXIT; + } + + isIntent() { + return !this.isRequest(); + } + + opposite() { + switch (this) { + case FocusStateType.REQUEST_EXIT: + return FocusStateType.REQUEST_LOCK; + case FocusStateType.REQUEST_LOCK: + return FocusStateType.REQUEST_EXIT; + case FocusStateType.EXITED: + return FocusStateType.LOCKED; + case FocusStateType.LOCKED: + return FocusStateType.EXITED; + default: + return null; + } + } + + getName() { + switch (this) { + case FocusStateType.REQUEST_EXIT: + return "REQUEST_EXIT"; + case FocusStateType.REQUEST_LOCK: + return "REQUEST_LOCK"; + case FocusStateType.EXITED: + return "EXITED"; + case FocusStateType.LOCKED: + return "LOCKED"; + default: + return "UNKNOWN"; + } + } + + static getById(id) { + switch (id) { + case 0: + return FocusStateType.REQUEST_EXIT; + case 1: + return FocusStateType.EXITED; + case 2: + return FocusStateType.REQUEST_LOCK; + case 3: + return FocusStateType.LOCKED; + default: + return null; + } + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/util/Keyboard.js b/src/js/net/minecraft/util/Keyboard.js index 57e06bd..a19f4d3 100644 --- a/src/js/net/minecraft/util/Keyboard.js +++ b/src/js/net/minecraft/util/Keyboard.js @@ -1,15 +1,18 @@ export default class Keyboard { static state = {}; + static enabled = false; static create() { - window.addEventListener('keydown', function (event) { + window.addEventListener('keydown', event => { Keyboard.state[event.code] = true; }); - window.addEventListener('keyup', function (event) { + window.addEventListener('keyup', event => { event.preventDefault(); delete Keyboard.state[event.code]; }); + + Keyboard.setEnabled(true); }; static setState(key, state) { @@ -21,7 +24,15 @@ export default class Keyboard { } static isKeyDown(key) { - return Keyboard.state[key]; + return Keyboard.state[key] && Keyboard.enabled; + } + + static setEnabled(enabled) { + Keyboard.enabled = enabled; + + if (!enabled) { + Keyboard.unPressAll(); + } } } \ No newline at end of file diff --git a/src/js/net/minecraft/util/MathHelper.js b/src/js/net/minecraft/util/MathHelper.js index a6a9de0..e508e21 100644 --- a/src/js/net/minecraft/util/MathHelper.js +++ b/src/js/net/minecraft/util/MathHelper.js @@ -99,4 +99,5 @@ export default class MathHelper { let h = c && ((v === r) ? (g - b) / c : ((v === g) ? 2 + (b - r) / c : 4 + (r - g) / c)); return [60 * (h < 0 ? h + 6 : h), v && c / v, v]; } + } \ No newline at end of file