Deno desktop stuff
This commit is contained in:
@@ -20,8 +20,8 @@ const TITLE_SCALE = 8;
|
||||
// for the player when it fails
|
||||
export type JoinServer = (address: ServerAddress, name: string, status: (text: string) => void) => Promise<void>;
|
||||
|
||||
// the first thing players see, like minecraft's title and multiplayer screens rolled into one:
|
||||
// a name, a server, and a button to join it
|
||||
// the first thing players see, like minecraft's title and multiplayer screens rolled into one: a name, a server,
|
||||
// and a button to join it. in the desktop app there's also singleplayer, which joins the app's own server
|
||||
export class TitleScreen extends GuiScreen {
|
||||
#join_server: JoinServer;
|
||||
|
||||
@@ -33,6 +33,10 @@ export class TitleScreen extends GuiScreen {
|
||||
);
|
||||
server = new EditBox(WIDTH, ROW_HEIGHT, new TextInput(initial_server(), 256, (char) => char !== " "), "host:port");
|
||||
join_button = new Button("Join server", WIDTH, ROW_HEIGHT, () => this.#join());
|
||||
// the page was served by a game server of its own to play on alone, see desktop/main.ts
|
||||
singleplayer_button = has_singleplayer()
|
||||
? new Button("Singleplayer", WIDTH, ROW_HEIGHT, () => this.#join_singleplayer())
|
||||
: undefined;
|
||||
credits_button = new Button("Credits", WIDTH, ROW_HEIGHT, () => this.#open_credits());
|
||||
// open over the title screen, it gets the input while it's there
|
||||
#credits: CreditsScreen | undefined;
|
||||
@@ -75,6 +79,7 @@ export class TitleScreen extends GuiScreen {
|
||||
if (InputManager.is_key_pressed("Enter")) {
|
||||
this.#join();
|
||||
}
|
||||
this.singleplayer_button?.handle_input();
|
||||
this.join_button.handle_input();
|
||||
this.credits_button.handle_input();
|
||||
}
|
||||
@@ -92,6 +97,7 @@ export class TitleScreen extends GuiScreen {
|
||||
this.#label("Name", this.name);
|
||||
this.#label("Server", this.server);
|
||||
this.name.render();
|
||||
this.singleplayer_button?.render();
|
||||
this.server.render();
|
||||
this.join_button.render();
|
||||
this.credits_button.render();
|
||||
@@ -121,12 +127,18 @@ export class TitleScreen extends GuiScreen {
|
||||
|
||||
#layout() {
|
||||
const x = (canvas.width - WIDTH) / 2;
|
||||
const total = 2 * (LABEL_GAP + ROW_HEIGHT + GAP) + 2 * ROW_HEIGHT + GAP;
|
||||
const singleplayer_height = this.singleplayer_button ? ROW_HEIGHT + GAP : 0;
|
||||
const total = 2 * (LABEL_GAP + ROW_HEIGHT + GAP) + singleplayer_height + 2 * ROW_HEIGHT + GAP;
|
||||
let y = (canvas.height - total) / 2 + 40;
|
||||
for (const field of [this.name, this.server]) {
|
||||
field.x = x;
|
||||
field.y = y + LABEL_GAP;
|
||||
y += LABEL_GAP + ROW_HEIGHT + GAP;
|
||||
if (field === this.name && this.singleplayer_button) {
|
||||
this.singleplayer_button.x = x;
|
||||
this.singleplayer_button.y = y;
|
||||
y += singleplayer_height;
|
||||
}
|
||||
}
|
||||
this.join_button.x = x;
|
||||
this.join_button.y = y;
|
||||
@@ -151,10 +163,21 @@ export class TitleScreen extends GuiScreen {
|
||||
return;
|
||||
}
|
||||
remember(LAST_SERVER_KEY, this.server.value.trim());
|
||||
remember(LAST_NAME_KEY, this.name.value);
|
||||
await this.#connect(address);
|
||||
}
|
||||
|
||||
// the server that served this page
|
||||
async #join_singleplayer() {
|
||||
if (this.#joining) {
|
||||
return;
|
||||
}
|
||||
await this.#connect(server_address(location.host), "Starting singleplayer...");
|
||||
}
|
||||
|
||||
async #connect(address: ServerAddress, status = `Connecting to ${address.base.host}...`) {
|
||||
remember(LAST_NAME_KEY, this.name.value);
|
||||
this.#set_joining(true);
|
||||
this.status = `Connecting to ${address.base.host}...`;
|
||||
this.status = status;
|
||||
this.status_is_error = false;
|
||||
try {
|
||||
await this.#join_server(address, this.name.value, (text) => this.status = text);
|
||||
@@ -167,6 +190,9 @@ export class TitleScreen extends GuiScreen {
|
||||
#set_joining(joining: boolean) {
|
||||
this.#joining = joining;
|
||||
this.name.active = this.server.active = this.join_button.active = !joining;
|
||||
if (this.singleplayer_button) {
|
||||
this.singleplayer_button.active = !joining;
|
||||
}
|
||||
}
|
||||
|
||||
#show_error(message: string) {
|
||||
@@ -202,8 +228,19 @@ function remember(key: string, value: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// a server's mods can't be unloaded, so going back to the title screen starts the page over. without ?server=
|
||||
// or ?name=, so the fields show what was used last
|
||||
export function back_to_title() {
|
||||
location.href = location.pathname;
|
||||
function has_singleplayer() {
|
||||
return new URL(location.href).searchParams.has("singleplayer");
|
||||
}
|
||||
|
||||
// a server's mods can't be unloaded, so going back to the title screen starts the page over. without ?server=
|
||||
// or ?name=, so the fields show what was used last. the desktop app keeps its ?singleplayer and ?server=, its saved
|
||||
// fields don't last between launches and the page's own address isn't a server to join
|
||||
export function back_to_title() {
|
||||
const page = new URL(location.href);
|
||||
const next = new URL(location.pathname, location.href);
|
||||
if (has_singleplayer()) {
|
||||
next.searchParams.set("server", page.searchParams.get("server") ?? "");
|
||||
next.searchParams.set("singleplayer", "");
|
||||
}
|
||||
location.href = next.href;
|
||||
}
|
||||
|
||||
+19
-3
@@ -25,12 +25,11 @@ export function default_server(page = new URL(location.href)): string {
|
||||
return page.searchParams.get("server") ?? page.host;
|
||||
}
|
||||
|
||||
// what a player typed as the server: host:port, or a full http(s) or ws(s) url
|
||||
// what a player typed as the server: host:port, or a full http(s) or ws(s) url to pick the scheme
|
||||
export function server_address(input: string, page = new URL(location.href)): ServerAddress {
|
||||
const text = input.trim();
|
||||
let host = text;
|
||||
// without a scheme it's as secure as the page, browsers block insecure sockets from secure pages anyway
|
||||
let secure = page.protocol === "https:";
|
||||
let secure: boolean;
|
||||
if (text.includes("://")) {
|
||||
let url: URL;
|
||||
try {
|
||||
@@ -40,6 +39,10 @@ export function server_address(input: string, page = new URL(location.href)): Se
|
||||
}
|
||||
host = url.host;
|
||||
secure = url.protocol === "https:" || url.protocol === "wss:";
|
||||
} else {
|
||||
// servers on this machine are plain ws and everything else is wss, whatever the page was served over. the
|
||||
// desktop app's page is plain http on localhost, but the servers it joins are real domains behind tls
|
||||
secure = !is_local_host(host);
|
||||
}
|
||||
if (!host || /[\s/?#]/.test(host)) {
|
||||
throw new HandshakeError(`${text || "An empty address"} isn't a server address`);
|
||||
@@ -52,6 +55,19 @@ export function server_address(input: string, page = new URL(location.href)): Se
|
||||
};
|
||||
}
|
||||
|
||||
const LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]", "0.0.0.0"];
|
||||
|
||||
// host is host:port, or just a host
|
||||
function is_local_host(host: string) {
|
||||
let hostname: string;
|
||||
try {
|
||||
hostname = new URL(`http://${host}/`).hostname;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return LOCAL_HOSTS.includes(hostname);
|
||||
}
|
||||
|
||||
// a socket whose messages all land in one queue, so none get lost between the handshake and the game
|
||||
export class ServerSocket {
|
||||
socket: WebSocket;
|
||||
|
||||
Reference in New Issue
Block a user