diff --git a/src/js/net/minecraft/client/GameSettings.js b/src/js/net/minecraft/client/GameSettings.js index c9a9adf..4d9a556 100644 --- a/src/js/net/minecraft/client/GameSettings.js +++ b/src/js/net/minecraft/client/GameSettings.js @@ -6,6 +6,7 @@ export default class GameSettings { this.keyTogglePerspective = 'F5'; this.keyOpenChat = 'KeyT'; this.keyOpenInventory = 'KeyE'; + this.keyPlayerList = 'Tab'; this.thirdPersonView = 0; this.fov = 70; diff --git a/src/js/net/minecraft/client/GameWindow.js b/src/js/net/minecraft/client/GameWindow.js index 5049198..748d083 100644 --- a/src/js/net/minecraft/client/GameWindow.js +++ b/src/js/net/minecraft/client/GameWindow.js @@ -52,6 +52,7 @@ export default class GameWindow { // Create render layers this.canvasWorld = document.createElement('canvas'); this.canvasDebug = document.createElement('canvas'); + this.canvasPlayerList = document.createElement('canvas'); this.canvasItems = document.createElement('canvas'); // Create canvas renderer @@ -380,6 +381,11 @@ export default class GameWindow { this.canvasDebug.height = this.canvas.height; } + if (this.canvasPlayerList.width !== this.canvas.width || this.canvasPlayerList.height !== this.canvas.height) { + this.canvasPlayerList.width = this.canvas.width; + this.canvasPlayerList.height = this.canvas.height; + } + // Reinitialize gui this.minecraft.screenRenderer.initialize(); diff --git a/src/js/net/minecraft/client/Minecraft.js b/src/js/net/minecraft/client/Minecraft.js index 8a0e951..2aea1c1 100644 --- a/src/js/net/minecraft/client/Minecraft.js +++ b/src/js/net/minecraft/client/Minecraft.js @@ -51,7 +51,7 @@ export default class Minecraft { this.maxFps = 0; let username = "Player" + Math.floor(Math.random() * 100); - let profile = new GameProfile(username, UUID.randomUUID()); + let profile = new GameProfile(UUID.randomUUID(), username); this.session = new Session(profile, ""); // Tick timer diff --git a/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js b/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js index 4c58799..a4b4eb3 100644 --- a/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js +++ b/src/js/net/minecraft/client/gui/overlay/IngameOverlay.js @@ -6,6 +6,8 @@ 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"; +import PlayerListOverlay from "./PlayerListOverlay.js"; +import Keyboard from "../../../util/Keyboard.js"; export default class IngameOverlay extends Gui { @@ -15,6 +17,7 @@ export default class IngameOverlay extends Gui { this.window = window; this.chatOverlay = new ChatOverlay(minecraft); + this.playerListOverlay = new PlayerListOverlay(minecraft, this); this.textureCrosshair = minecraft.resources["gui/icons.png"]; this.textureHotbar = minecraft.resources["gui/gui.png"]; @@ -38,6 +41,11 @@ export default class IngameOverlay extends Gui { if (this.minecraft.settings.debugOverlay) { stack.drawImage(this.window.canvasDebug, 0, 0); } + + // Render player list + if (Keyboard.isKeyDown(this.minecraft.settings.keyPlayerList) && !this.minecraft.isSingleplayer()) { + this.playerListOverlay.renderPlayerList(stack, this.window.width); + } } onTick() { @@ -224,6 +232,7 @@ export default class IngameOverlay extends Gui { // Draw line this.drawString(stack, lines[i], 2, 2 + FontRenderer.FONT_HEIGHT * i, 0xffe0e0e0, false); } + } renderRightDebugOverlay(stack) { diff --git a/src/js/net/minecraft/client/gui/overlay/PlayerListOverlay.js b/src/js/net/minecraft/client/gui/overlay/PlayerListOverlay.js new file mode 100644 index 0000000..bbc7f9e --- /dev/null +++ b/src/js/net/minecraft/client/gui/overlay/PlayerListOverlay.js @@ -0,0 +1,224 @@ +import Gui from "../Gui.js"; +import FontRenderer from "../../render/gui/FontRenderer.js"; + +export default class PlayerListOverlay extends Gui { + + constructor(minecraft, ingameOverlay) { + super(); + + this.minecraft = minecraft; + this.window = minecraft.window; + this.ingameOverlay = ingameOverlay; + this.dirty = true; + + this.header = null; + this.footer = null; + } + + renderPlayerList(stack, width) { + if (this.dirty) { + let subStack = this.window.canvasPlayerList.getContext("2d"); + this.reinitialize(subStack, width); + } + + stack.drawImage(this.window.canvasPlayerList, 0, 0); + } + + reinitialize(stack, width) { + this.dirty = false; + + let playerInfoMap = this.minecraft.playerController.getNetworkHandler().getPlayerInfoMap(); + let maxPlayerNameWidth = 0; + let maxScoreValueWidth = 0; // TODO + + // Calculate max scoreboard width entry + for (let [uuid, playerInfo] of playerInfoMap) { + let displayName = playerInfo.displayName === null ? playerInfo.profile.getUsername() : playerInfo.displayName; + let playerLength = this.getStringWidth(stack, displayName); + + // Find max player length + maxPlayerNameWidth = Math.max(maxPlayerNameWidth, playerLength); + } + + // Get the max width of the scoreboard + let maxScoreWidth = 0; // TODO + + let screenWidth = this.window.width; + let playerAmount = playerInfoMap.size; + let rows = playerAmount; + + // Calculate max columns + let columns; + for (columns = 1; rows > 20; rows = Math.floor((playerAmount + columns - 1) / columns)) { + columns++; + } + + let isOnlineMode = true; // TODO + let columnWidth = Math.min( + columns * ((isOnlineMode ? 9 : 0) + maxPlayerNameWidth + maxScoreWidth + 13), + screenWidth - 50 + ) / columns; + + // Clear canvas + stack.clearRect(0, 0, this.window.width, this.window.height); + + let x = Math.floor(screenWidth / 2) - Math.floor((columnWidth * columns + (columns - 1) * 5) / 2); + let y = 10; + + // Calculate background with of columns + let backgroundWidth = columnWidth * columns + (columns - 1) * 5; + + // Calculate header + let headerLines = null; + if (this.header !== null) { + headerLines = this.minecraft.fontRenderer.listFormattedStringToWidth(this.header, width - 50); + + for (let line of headerLines) { + backgroundWidth = Math.max(backgroundWidth, this.getStringWidth(stack, line)); + } + } + + // Calculate footer + let footerLines = null; + if (this.footer !== null) { + footerLines = this.minecraft.fontRenderer.listFormattedStringToWidth(this.footer, width - 50); + + for (let line of footerLines) { + backgroundWidth = Math.max(backgroundWidth, this.getStringWidth(stack, line)); + } + } + + if (headerLines !== null) { + this.drawRect( + stack, + Math.floor(width / 2) - Math.floor(backgroundWidth / 2) - 1, + y - 1, + Math.floor(width / 2) + Math.floor(backgroundWidth / 2) + 1, + y + headerLines.length * FontRenderer.FONT_HEIGHT, + 'rgba(0,0,0,0.5)' + ); + + for (let i = 0; i < headerLines.length; i++) { + this.drawCenteredString( + stack, + headerLines[i], + Math.floor(width / 2) + 1, + y + ); + y += FontRenderer.FONT_HEIGHT; + } + y++; + } + + // Render player list background + this.drawRect( + stack, + Math.floor(screenWidth / 2) - Math.floor(backgroundWidth / 2) - 1, + y - 1, + Math.floor(screenWidth / 2) + Math.floor(backgroundWidth / 2) + 1, + y + rows * FontRenderer.FONT_HEIGHT, + 'rgba(0,0,0,0.5)' + ); + + let i = 0; + for (let [uuid, playerInfo] of playerInfoMap) { + let indexX = Math.floor(i / rows); + let indexY = i % rows; + + let entryX = x + indexX * columnWidth + indexX * 5 - 1; + let entryY = y + indexY * FontRenderer.FONT_HEIGHT; + + let rightX = entryX + columnWidth - 1; + + // Check if index is inside of range + if (i < playerInfoMap.size) { + let pingX = entryX + columnWidth - 1; + let displayName = playerInfo.displayName === null ? playerInfo.profile.getUsername() : playerInfo.displayName; + + // Render player entry background + this.drawRect( + stack, + entryX, + entryY, + entryX + columnWidth, + entryY + (FontRenderer.FONT_HEIGHT - 1), + 'rgba(255,255,255,0.13)' + ); + + // Render player head + if (isOnlineMode) { + let size = FontRenderer.FONT_HEIGHT - 1; + + this.drawRect( + stack, + entryX, + entryY, + entryX + size, + entryY + size, + 'rgb(0,0,0)' + ); + + entryX += size + 1; + } + + // Render player name + this.drawString(stack, displayName, entryX + 1.0, entryY - (FontRenderer.FONT_HEIGHT > 9 ? 0.5 : 0.0)) + + // Render ping + let ping = playerInfo.ping; + let spriteOffset = ping < 0 ? 5 : ping < 150 ? 0 : ping < 300 ? 1 : ping < 600 ? 2 : ping < 1000 ? 3 : 4; + this.drawSprite( + stack, + this.ingameOverlay.textureCrosshair, + 0, + 16 + spriteOffset * 8, + 10, + 8, + pingX - FontRenderer.FONT_HEIGHT - 1, + entryY + 1, + 10, + 8, + 'rgb(0,0,0)' + ); + } + + i++; + } + + if (footerLines !== null) { + y = y + rows * 9 + 1; + + this.drawRect( + stack, + Math.floor(width / 2) - Math.floor(backgroundWidth / 2) - 1, + y - 1, + Math.floor(width / 2) + Math.floor(backgroundWidth / 2) + 1, + y + footerLines.length * FontRenderer.FONT_HEIGHT, + 'rgba(0,0,0,0.5)' + ); + + for (let i = 0; i < footerLines.length; i++) { + this.drawCenteredString( + stack, + footerLines[i], + Math.floor(width / 2) + 1, + y + ); + y += FontRenderer.FONT_HEIGHT; + } + y++; + } + } + + setDirty() { + this.dirty = true; + } + + setHeader(header) { + this.header = header; + } + + setFooter(footer) { + this.footer = footer; + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/network/PacketRegistry.js b/src/js/net/minecraft/client/network/PacketRegistry.js index b9715a5..e05baef 100644 --- a/src/js/net/minecraft/client/network/PacketRegistry.js +++ b/src/js/net/minecraft/client/network/PacketRegistry.js @@ -22,6 +22,8 @@ import ServerBlockChangePacket from "./packet/play/server/ServerBlockChangePacke import ServerChatPacket from "./packet/play/server/ServerChatPacket.js"; import ServerDisconnectPacket from "./packet/play/server/ServerDisconnectPacket.js"; import ServerPlayerPositionRotationPacket from "./packet/play/server/ServerPlayerPositionRotationPacket.js"; +import ServerPlayerListEntryPacket from "./packet/play/server/ServerPlayerListEntryPacket.js"; +import ServerPlayerListDataPacket from "./packet/play/server/ServerPlayerListDataPacket.js"; export default class PacketRegistry { @@ -53,7 +55,9 @@ export default class PacketRegistry { this.registerServer(ProtocolState.PLAY, 0x21, ServerChunkDataPacket); this.registerServer(ProtocolState.PLAY, 0x23, ServerBlockChangePacket); this.registerServer(ProtocolState.PLAY, 0x26, ServerMultiChunkDataPacket); + this.registerServer(ProtocolState.PLAY, 0x38, ServerPlayerListEntryPacket); this.registerServer(ProtocolState.PLAY, 0x40, ServerDisconnectPacket); + this.registerServer(ProtocolState.PLAY, 0x47, ServerPlayerListDataPacket); this.registerClient(ProtocolState.PLAY, 0x00, ClientKeepAlivePacket); this.registerClient(ProtocolState.PLAY, 0x01, ClientChatPacket); diff --git a/src/js/net/minecraft/client/network/handler/NetworkPlayHandler.js b/src/js/net/minecraft/client/network/handler/NetworkPlayHandler.js index 751e3f7..9fe1a57 100644 --- a/src/js/net/minecraft/client/network/handler/NetworkPlayHandler.js +++ b/src/js/net/minecraft/client/network/handler/NetworkPlayHandler.js @@ -13,6 +13,8 @@ export default class NetworkPlayHandler extends PacketHandler { this.minecraft = networkManager.minecraft; this.networkManager = networkManager; this.profile = profile; + + this.playerInfoMap = new Map(); } handleKeepAlive(packet) { @@ -31,6 +33,46 @@ export default class NetworkPlayHandler extends PacketHandler { } } + handleServerPlayerListEntry(packet) { + for (let entry of packet.getPlayers()) { + let uuid = entry.profile.getId().toString(); + + if (packet.getAction() === 4) { // REMOVE_PLAYER + this.playerInfoMap.delete(uuid); + } else { + if (packet.getAction() === 0) { // ADD_PLAYER + this.playerInfoMap.set(uuid, entry); + } + + let playerInfo = this.playerInfoMap.get(uuid); + if (playerInfo !== null && typeof playerInfo !== "undefined") { + switch (packet.getAction()) { + case 0: // ADD_PLAYER + playerInfo.gameType = entry.gameType; + playerInfo.ping = entry.ping; + break; + case 1: // UPDATE_GAMEMODE + playerInfo.gameType = entry.gameType; + break; + case 2: // UPDATE_LATENCY + playerInfo.ping = entry.ping; + break; + case 3: // UPDATE_DISPLAY_NAME + playerInfo.displayName = entry.displayName; + break; + } + } + } + } + + this.minecraft.ingameOverlay.playerListOverlay.setDirty(); + } + + handleServerPlayerListData(packet) { + this.minecraft.ingameOverlay.playerListOverlay.setHeader(packet.getHeader()); + this.minecraft.ingameOverlay.playerListOverlay.setFooter(packet.getFooter()); + } + handleServerPlayerPositionRotation(packet) { let player = this.minecraft.player; @@ -117,6 +159,10 @@ export default class NetworkPlayHandler extends PacketHandler { return this.networkManager; } + getPlayerInfoMap() { + return this.playerInfoMap; + } + sendPacket(packet) { this.networkManager.sendPacket(packet); } diff --git a/src/js/net/minecraft/client/network/packet/login/server/LoginDisconnectPacket.js b/src/js/net/minecraft/client/network/packet/login/server/LoginDisconnectPacket.js index 094003d..664cc4c 100644 --- a/src/js/net/minecraft/client/network/packet/login/server/LoginDisconnectPacket.js +++ b/src/js/net/minecraft/client/network/packet/login/server/LoginDisconnectPacket.js @@ -1,5 +1,4 @@ import Packet from "../../../Packet.js"; -import {format} from "../../../../../../../../../libraries/chat.js"; export default class LoginDisconnectPacket extends Packet { @@ -14,7 +13,7 @@ export default class LoginDisconnectPacket extends Packet { } read(buffer) { - this.message = format(JSON.parse(buffer.readString(32767))); + this.message = buffer.readTextComponent(); } handle(handler) { diff --git a/src/js/net/minecraft/client/network/packet/play/server/ServerChatPacket.js b/src/js/net/minecraft/client/network/packet/play/server/ServerChatPacket.js index b93d50f..8633ec9 100644 --- a/src/js/net/minecraft/client/network/packet/play/server/ServerChatPacket.js +++ b/src/js/net/minecraft/client/network/packet/play/server/ServerChatPacket.js @@ -1,5 +1,4 @@ import Packet from "../../../Packet.js"; -import {format} from "../../../../../../../../../libraries/chat.js"; export default class ServerChatPacket extends Packet { @@ -11,7 +10,7 @@ export default class ServerChatPacket extends Packet { } read(buffer) { - this.message = format(JSON.parse(buffer.readString(32767))); + this.message = buffer.readTextComponent(); this.type = buffer.readByte(); } diff --git a/src/js/net/minecraft/client/network/packet/play/server/ServerDisconnectPacket.js b/src/js/net/minecraft/client/network/packet/play/server/ServerDisconnectPacket.js index ee3ae3c..bc3de13 100644 --- a/src/js/net/minecraft/client/network/packet/play/server/ServerDisconnectPacket.js +++ b/src/js/net/minecraft/client/network/packet/play/server/ServerDisconnectPacket.js @@ -1,5 +1,4 @@ import Packet from "../../../Packet.js"; -import {format} from "../../../../../../../../../libraries/chat.js"; export default class ServerDisconnectPacket extends Packet { @@ -10,7 +9,7 @@ export default class ServerDisconnectPacket extends Packet { } read(buffer) { - this.reason = format(JSON.parse(buffer.readString(32767))); + this.reason = buffer.readTextComponent(); } handle(handler) { diff --git a/src/js/net/minecraft/client/network/packet/play/server/ServerPlayerListDataPacket.js b/src/js/net/minecraft/client/network/packet/play/server/ServerPlayerListDataPacket.js new file mode 100644 index 0000000..b6a866d --- /dev/null +++ b/src/js/net/minecraft/client/network/packet/play/server/ServerPlayerListDataPacket.js @@ -0,0 +1,28 @@ +import Packet from "../../../Packet.js"; + +export default class ServerPlayerListDataPacket extends Packet { + + constructor() { + super(); + + this.header = null; + this.footer = null; + } + + read(buffer) { + this.header = buffer.readTextComponent(); + this.footer = buffer.readTextComponent(); + } + + handle(handler) { + handler.handleServerPlayerListData(this); + } + + getHeader() { + return this.header; + } + + getFooter() { + return this.footer; + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/network/packet/play/server/ServerPlayerListEntryPacket.js b/src/js/net/minecraft/client/network/packet/play/server/ServerPlayerListEntryPacket.js new file mode 100644 index 0000000..151dd1f --- /dev/null +++ b/src/js/net/minecraft/client/network/packet/play/server/ServerPlayerListEntryPacket.js @@ -0,0 +1,86 @@ +import Packet from "../../../Packet.js"; +import GameProfile from "../../../../../util/GameProfile.js"; + +export default class ServerPlayerListEntryPacket extends Packet { + + constructor() { + super(); + + this.players = []; + } + + read(buffer) { + this.action = buffer.readVarInt(); + let amount = buffer.readVarInt(); + + for (let i = 0; i < amount; i++) { + let profile = null; + let gameType = 0; + let ping = 0; + let displayName = null; + + switch (this.action) { + case 0: // ADD_PLAYER + profile = new GameProfile(buffer.readUUID(), buffer.readString(16)); + + let propertiesCount = buffer.readVarInt(); + for (let propIndex = 0; propIndex < propertiesCount; propIndex++) { + let key = buffer.readString(32767); + let value = buffer.readString(32767); + + if (buffer.readBoolean()) { + let signature = buffer.readString(32767); + // TODO implement properties + } else { + // TODO implement properties + } + } + + gameType = buffer.readVarInt(); + ping = buffer.readVarInt(); + + if (buffer.readBoolean()) { + displayName = buffer.readTextComponent(); + } + break; + case 1: // UPDATE_GAME_MODE + profile = new GameProfile(buffer.readUUID(), null); + gameType = buffer.readVarInt(); + break; + case 2: // UPDATE_LATENCY + profile = new GameProfile(buffer.readUUID(), null); + ping = buffer.readVarInt(); + break; + case 3: // UPDATE_DISPLAY_NAME + profile = new GameProfile(buffer.readUUID(), null); + + if (buffer.readBoolean()) { + displayName = buffer.readTextComponent(); + } + break; + case 4: // REMOVE_PLAYER + profile = new GameProfile(buffer.readUUID(), null); + break; + } + + this.players.push({ + profile: profile, + ping: ping, + gameType: gameType, + displayName: displayName + }) + } + } + + handle(handler) { + handler.handleServerPlayerListEntry(this); + } + + getAction() { + return this.action; + } + + getPlayers() { + return this.players; + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/network/util/ByteBuf.js b/src/js/net/minecraft/client/network/util/ByteBuf.js index 19a270a..6f1492a 100644 --- a/src/js/net/minecraft/client/network/util/ByteBuf.js +++ b/src/js/net/minecraft/client/network/util/ByteBuf.js @@ -1,5 +1,7 @@ import Long from "../../../../../../../libraries/long.js"; import BlockPosition from "../../../util/BlockPosition.js"; +import UUID from "../../../util/UUID.js"; +import {format} from "../../../../../../../libraries/chat.js"; export default class ByteBuf { @@ -213,6 +215,19 @@ export default class ByteBuf { return BlockPosition.fromLong(this.readLong()); } + readUUID() { + return new UUID(this.readLong(), this.readLong()); + } + + writeUUID(uuid) { + this.writeLong(uuid.getMostSignificantBits()); + this.writeLong(uuid.getLeastSignificantBits()); + } + + readTextComponent() { + return format(JSON.parse(this.readString(32767))); + } + readableBytes() { return this.array.length - this.pos; } diff --git a/src/js/net/minecraft/client/render/gui/ScreenRenderer.js b/src/js/net/minecraft/client/render/gui/ScreenRenderer.js index 14d18f8..7557864 100644 --- a/src/js/net/minecraft/client/render/gui/ScreenRenderer.js +++ b/src/js/net/minecraft/client/render/gui/ScreenRenderer.js @@ -49,6 +49,7 @@ export default class ScreenRenderer { } } catch (e) { console.error(e); + console.log(e.stack); } // Scale GUI back diff --git a/src/js/net/minecraft/util/GameProfile.js b/src/js/net/minecraft/util/GameProfile.js index 16d24a3..39304a2 100644 --- a/src/js/net/minecraft/util/GameProfile.js +++ b/src/js/net/minecraft/util/GameProfile.js @@ -1,8 +1,8 @@ export default class GameProfile { - constructor(username, uuid) { - this.username = username; + constructor(uuid, username) { this.uuid = uuid; + this.username = username; } getCompactUUID() { diff --git a/src/js/net/minecraft/util/UUID.js b/src/js/net/minecraft/util/UUID.js index bdbe5d8..63ec4e2 100644 --- a/src/js/net/minecraft/util/UUID.js +++ b/src/js/net/minecraft/util/UUID.js @@ -16,6 +16,14 @@ export default class UUID { UUID.digits(this.leastSigBits, 12)); } + getMostSignificantBits() { + return this.mostSigBits; + } + + getLeastSignificantBits() { + return this.leastSigBits; + } + static randomUUID() { let random = new Random(); diff --git a/src/resources/gui/icons.png b/src/resources/gui/icons.png index d746b17..10a5b05 100644 Binary files a/src/resources/gui/icons.png and b/src/resources/gui/icons.png differ