M starkingdoms-client/index.html => starkingdoms-client/index.html +14 -1
@@ 6,7 6,7 @@
<title>StarKingdoms</title>
</head>
<body class="bg-grid">
-<div class="popup" id="server_selector">
+<div class="popup popup-center popup-w19" id="server_selector">
<h1>StarKingdoms</h1>
<h2>Join Game</h2>
@@ 39,6 39,19 @@ Here be dragons! You have a <b>prerelease server</b> selected. Expect bugs, and
</form>
</div>
+<div class="popup popup-wmin log-hidden" id="packet_log">
+ <h1>Packet Log</h1>
+ <table class="log">
+ <tbody id="log_body">
+
+ </tbody>
+ </table>
+ <hr>
+ <h1>Packet Explorer</h1>
+ <p id="explorer_selected" class="mono">Selected: --</p>
+ <table class="mono json" id="explorer_json"></table>
+</div>
+
<div id="gamewindow" class="game">
<!-- Canvas gets added here by the game script -->
</div>
M starkingdoms-client/src/css/globals.css => starkingdoms-client/src/css/globals.css +5 -2
@@ 5,7 5,6 @@ html {
box-sizing: inherit;
}
-
body {
background-color: var(--bg);
color: var(--body);
@@ 15,6 14,10 @@ body {
line-height: 1.5rem;
}
+.mono {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
h1 {
font-size: 1.25rem;
line-height: 1.75rem;
@@ 22,4 25,4 @@ h1 {
h2 {
font-size: 1.125rem;
line-height: 1.75rem;
-}>
\ No newline at end of file
+}
A starkingdoms-client/src/css/json.css => starkingdoms-client/src/css/json.css +21 -0
@@ 0,0 1,21 @@
+:root {
+ --ident: #75bfff;
+ --string: #ff7de9;
+ --literal: #86de74;
+ --indent-spacing: 16px;
+}
+
+.json-ident {
+ color: var(--ident);
+}
+.json-string {
+ color: var(--string);
+}
+.json-literal {
+ color: var(--literal);
+}
+.json {
+ max-height: 200px;
+ display: block;
+ overflow: auto;
+}
A starkingdoms-client/src/css/log.css => starkingdoms-client/src/css/log.css +35 -0
@@ 0,0 1,35 @@
+.log-icon {
+ width: 16px
+}
+.log-lalign {
+ text-align: left;
+}
+.log-item {
+ cursor: pointer;
+}
+.log-item:hover {
+ background-color: #4a4a4f;
+}
+.log {
+ border: none;
+ border-collapse: collapse;
+ display: block;
+ max-height: 200px;
+ overflow: auto;
+}
+.log-selected {
+ background-color: #204e8a;
+ cursor: default;
+}
+.log-leaving {
+ color: var(--error);
+}
+.log-arriving {
+ color: var(--success);
+}
+.log > tbody > tr > td {
+ padding: 2px;
+}
+.log-hidden {
+ display: none;
+}
M => +15 -8
@@ 1,17 1,24 @@
.popup {
padding: 2vh 1vw;
background-color: var(--bg-secondary-1);
height: min-content;
border-radius: 5px;
}
.popup-w19 {
width: 19%;
}
.popup-wmin {
width: min-content;
}
.popup-center {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
padding: 2vh 1vw;
background-color: var(--bg-secondary-1);
width: 19%;
height: min-content;
border-radius: 5px;
}
.popup > h1 {
margin: 0;
@@ 19,4 26,4 @@
.popup > h2 {
margin: 0 0 1vh;
color: var(--sub-headline);
}
\ No newline at end of file
}
M starkingdoms-client/src/css/style.css => starkingdoms-client/src/css/style.css +3 -1
@@ 2,4 2,6 @@
@import "grid.css";
@import "popup.css";
@import "footer.css";
-@import "form.css";>
\ No newline at end of file
+@import "form.css";
+@import "json.css";
+@import "log.css";
M starkingdoms-client/src/hub.ts => starkingdoms-client/src/hub.ts +31 -3
@@ 1,5 1,13 @@
import createDebug from "debug";
-import {Packet, PacketType} from "./protocol.ts";
+import {
+ Packet,
+ PacketType,
+ PartPositionsPacket,
+ PlanetPositionsPacket, PlayerLeavePacket,
+ PlayerListPacket,
+ SpawnPlayerPacket
+} from "./protocol.ts";
+import {appendPacket} from "./packet_ui.ts";
const logger = createDebug("hub");
@@ 7,6 15,11 @@ export interface ClientHub {
socket: WebSocket;
}
+export function sendPacket(client: ClientHub, packet: Packet) {
+ client.socket.send(JSON.stringify(packet));
+ appendPacket(packet);
+}
+
export async function hub_connect(url: string, username: string) {
logger("connecting to client hub at " + url)
@@ 25,11 38,26 @@ export async function hub_connect(url: string, username: string) {
jwt: null
}
};
- ws.send(JSON.stringify(packet));
+ sendPacket(client, packet);
ws.onmessage = (e) => {
let packet: Packet = JSON.parse(e.data);
- logger(packet);
+
+ appendPacket(packet);
+
+ if (packet.t == PacketType.SpawnPlayer) {
+ let p = <SpawnPlayerPacket> packet.c;
+ } else if (packet.t == PacketType.PlayerList) {
+ let p = <PlayerListPacket> packet.c;
+ } else if (packet.t == PacketType.PlanetPositions) {
+ let p = <PlanetPositionsPacket> packet.c;
+ } else if (packet.t == PacketType.PartPositions) {
+ let p = <PartPositionsPacket> packet.c;
+ } else if (packet.t == PacketType.PlayerLeave) {
+ let p = <PlayerLeavePacket> packet.c;
+ } else {
+ logger(`unrecognized packet type ${packet.t}`);
+ }
}
return client;
M starkingdoms-client/src/main.ts => starkingdoms-client/src/main.ts +5 -1
@@ 1,5 1,5 @@
import createDebug from "debug";
-import {hub_connect, ClientHub} from "./hub.ts";
+import {ClientHub, hub_connect} from "./hub.ts";
import {ConfigServer, loadConfig} from "./config.ts";
import "./css/style.css";
import "./css/themes/catppuccin-mocha/colors.css";
@@ 9,6 9,10 @@ let config = await loadConfig();
const logger = createDebug("main");
logger(`Hello, world! StarKingdoms ${APP_VERSION} (${COMMIT_HASH}) at your service`);
+if (window.localStorage.getItem("stk-packet-mode") === "debug") {
+ document.getElementById("packet_log")!.classList.remove("log-hidden");
+}
+
export interface GlobalData {
client: ClientHub | null
}
A starkingdoms-client/src/packet_ui.ts => starkingdoms-client/src/packet_ui.ts +170 -0
@@ 0,0 1,170 @@
+import {Direction, Packet, type_direction} from "./protocol.ts";
+import createDebug from "debug";
+
+const logger = createDebug("jsonview");
+
+let selected = document.getElementById("explorer_selected")!;
+let table = document.getElementById("explorer_json")!;
+
+export function show_packet(maybe_packet: Packet | null, sequence_number: number) {
+ logger(`selected packet ${sequence_number}`);
+ if (maybe_packet === null) {
+ selected.textContent = "Selected: --";
+ table.innerHTML = "";
+ return;
+ }
+ let packet = maybe_packet!;
+ let direction = type_direction(packet.t);
+
+ selected.textContent = `Selected: #${sequence_number} ${direction} ${packet.t}`;
+
+ // iterate over everything and calculate a max depth
+ let max_depth = depthOf(packet.c);
+ logger(`indent depth ${max_depth}`);
+
+ // generate a tree
+ let tree = generateTree(packet.c, 0);
+
+ logger(`generating tree of ${tree.length} items`);
+
+ let table_html = "";
+
+ for (let i = 0; i < tree.length; i++) {
+ let [indent, key, value, is_text] = tree[i];
+ table_html += `
+ <tr>
+ <td style="padding-left: calc(var(--indent-spacing) * ${indent})" class="json-ident">${key}</td>
+ <td style="padding-left: calc(var(--indent-spacing) * ${max_depth + 1})" class="${is_text ? "json-string" : "json-literal"}">${value}</td>
+ </tr>
+ `
+ }
+
+ table.innerHTML = `
+<!-- Rendered tree of ${tree.length} items -->
+ <thead>
+ <tr></tr>
+ <tr></tr>
+ </thead>
+ <tbody>
+ ${table_html}
+</tbody>
+ `;
+}
+
+function depthOf(object: any) {
+ let level = 0;
+ for(let key in object) {
+ if (!object.hasOwnProperty(key)) continue;
+
+ if(typeof object[key] == 'object'){
+ let depth = depthOf(object[key]) + 1;
+ level = Math.max(depth, level);
+ }
+ }
+ return level;
+}
+
+
+function generateTree(object: any, level: number): [indent: number, key: string, value: string, value_string: boolean][] {
+ let items: [indent: number, key: string, value: string, value_string: boolean][] = [];
+
+ for (let key in object) {
+ if (!object.hasOwnProperty(key)) continue;
+
+ if (object[key] === null) {
+ items.push([level, key, "null", false]);
+ } else if (object[key] === undefined) {
+ items.push([level, key, "undefined", false]);
+ } else if (typeof object[key] === 'object') {
+ if (Array.isArray(object[key])) {
+ if (object[key].length == 0) {
+ items.push([level, key + ":", "[]", false]);
+ } else {
+ items.push([level, key + ":", "", false]);
+ }
+ for (let i = 0; i < object[key].length; i++) {
+ items.push([level+1, i.toString() + ":", "", false]);
+ items = items.concat(generateTree(object[key][i], level+2));
+ }
+ continue;
+ }
+ items.push([level, key + ":", "", false]);
+ items = items.concat(generateTree(object[key], level + 1));
+ } else if (Array.isArray(object[key])) {
+ for (let i = 0; i < object[key].length; i++) {
+ items.push([level, i.toString() + ":", "", false]);
+ items = items.concat(generateTree(object[key][i], level));
+ }
+ } else if (typeof object[key] === 'string') {
+ items.push([level, key + ":", "\"" + object[key] + "\"", true]);
+ } else {
+ items.push([level, key + ":", object[key].toString(), false]);
+ }
+ }
+
+ return items;
+}
+
+export let packets: Packet[] = [];
+export let selected_packet: number | null = null;
+let last_packet: Date | null = null;
+const log_body = document.getElementById("log_body")!;
+
+const up_arrow = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="log-icon log-leaving">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
+ </svg>`;
+const down_arrow = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="log-icon log-arriving">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" />
+ </svg>`;
+
+export function selectPacket(id: number) {
+ console.log("selecting packet " + id);
+ if (selected_packet !== null) {
+ document.getElementById("packet-" + selected_packet)!.classList.remove("log-selected");
+ document.getElementById("packet-" + selected_packet)!.classList.add("log-item");
+ }
+ document.getElementById("packet-" + id)!.classList.add("log-selected");
+ document.getElementById("packet-" + id)!.classList.remove("log-item");
+ selected_packet = id;
+ show_packet(packets[id], id);
+}
+
+export function appendPacket(packet: Packet) {
+ packets.push(packet);
+
+ let duration = "--";
+ if (last_packet !== null) {
+ duration = (new Date().getTime() - last_packet!.getTime()).toString() + "ms";
+ }
+ last_packet = new Date();
+
+ let index_deepcopy = packets.length + 1;
+ let index = index_deepcopy - 2;
+
+ let tr = document.createElement("tr");
+ tr.classList.add("log-item");
+ tr.id = "packet-" + index;
+
+ let td_idx = document.createElement("td");
+ td_idx.innerHTML = index.toString();
+ tr.appendChild(td_idx);
+
+ let td_direction = document.createElement("td");
+ td_direction.innerHTML = type_direction(packet.t) == Direction.Clientbound ? down_arrow : up_arrow;
+ tr.appendChild(td_direction);
+
+ let td_type = document.createElement("td");
+ td_type.innerHTML = packet.t;
+ tr.appendChild(td_type);
+
+ let td_ts = document.createElement("td");
+ td_ts.innerHTML = duration;
+ td_ts.classList.add(type_direction(packet.t) == Direction.Clientbound ? "log-arriving" : "log-leaving")
+ tr.appendChild(td_ts);
+
+ tr.addEventListener("click", () => {
+ selectPacket(index);
+ });
+
+ log_body.appendChild(tr);
+}
M starkingdoms-client/src/protocol.ts => starkingdoms-client/src/protocol.ts +32 -4
@@ 23,8 23,7 @@ export interface ClientLoginPacket {
}
export interface SpawnPlayerPacket {
id: number,
- username: string,
- position: ProtoTransform
+ username: string
}
export interface PlanetPositionsPacket {
planets: [number, Planet][]
@@ 32,15 31,44 @@ export interface PlanetPositionsPacket {
export interface PartPositionsPacket {
parts: [number, Part][]
}
+export interface PlayerListPacket {
+ players: [number, String][]
+}
+export interface PlayerLeavePacket {
+ id: number
+}
export enum PacketType {
+ // serverbound
ClientLogin = "ClientLogin",
+ // clientbound
SpawnPlayer = "SpawnPlayer",
+ PlayerList = "PlayerList",
PlanetPositions = "PlanetPositions",
- PartPositions = "PartPositions"
+ PartPositions = "PartPositions",
+ PlayerLeave = "PlayerLeave"
}
export interface Packet {
t: PacketType,
- c: ClientLoginPacket | SpawnPlayerPacket | PlanetPositionsPacket | PartPositionsPacket
+ c: ClientLoginPacket | SpawnPlayerPacket | PlayerListPacket | PlanetPositionsPacket | PartPositionsPacket | PlayerLeavePacket
+}
+
+export const SERVERBOUND = [PacketType.ClientLogin];
+export const CLIENTBOUND = [PacketType.SpawnPlayer, PacketType.PlayerList, PacketType.PlanetPositions, PacketType.PartPositions, PacketType.PlayerLeave];
+
+export enum Direction {
+ Serverbound = "Serverbound",
+ Clientbound = "Clientbound",
+ InvalidType = "InvalidType"
+}
+
+export function type_direction(type: PacketType): Direction {
+ if (SERVERBOUND.includes(type)) {
+ return Direction.Serverbound;
+ } else if (CLIENTBOUND.includes(type)) {
+ return Direction.Clientbound;
+ } else {
+ return Direction.InvalidType;
+ }
}