M server/src/main.rs => server/src/main.rs +61 -22
@@ 60,22 60,16 @@ fn on_message(
player_query: Query<(Entity, &Player)>,
part_query: Query<(Entity, &PartType, &Transform)>,
mut packet_recv: Local<ManualEventReader<ServerEvent>>,
- mut packet_send: ResMut<Events<ServerEvent>>,
+ mut packet_event_send: ResMut<Events<ServerEvent>>,
) {
- let mut packets = Vec::new();
- for ev in packet_recv.read(&packet_send) {
+ let mut event_queue = Vec::new();
+ for ev in packet_recv.read(&packet_event_send) {
if let ServerEvent::Recv(addr, MessageType::Text, data) = ev {
- let data = String::from_utf8_lossy(&data);
- let json: serde_json::Value = err_or_cont!(serde_json::from_str(&data));
- let packet_type = json["t"].clone();
- let data = json["c"].clone();
- let packet_type = some_or_cont!(packet_type.as_str());
- match packet_type {
- // handshake
- "ClientLogin" => {
- // spawn player
- let username = data["username"].clone();
- let username = some_or_cont!(username.as_str());
+ let data = String::from_utf8_lossy(data);
+ let packet: Packet = err_or_cont!(serde_json::from_str(&data));
+
+ match packet {
+ Packet::ClientLogin { username, .. } => {
let angle: f32 = {
let mut rng = rand::thread_rng();
rng.gen::<f32>() * std::f32::consts::PI * 2.
@@ 108,7 102,7 @@ fn on_message(
}
let packet = Packet::PlanetPositions { planets };
let buf = serde_json::to_vec(&packet).unwrap();
- packets.push(ServerEvent::Send(*addr, MessageType::Text, buf));
+ event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf));
// tell the player already existing users
let mut players = Vec::new();
@@ 119,7 113,7 @@ fn on_message(
players,
};
let buf = serde_json::to_vec(&packet).unwrap();
- packets.push(ServerEvent::Send(*addr, MessageType::Text, buf));
+ event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf));
// tell other players that a player has spawned in
let packet = Packet::SpawnPlayer {
@@ 127,7 121,7 @@ fn on_message(
username: username.to_string(),
};
let buf = serde_json::to_vec(&packet).unwrap();
- packets.push(ServerEvent::Broadcast(MessageType::Text, buf));
+ event_queue.push(ServerEvent::Broadcast(MessageType::Text, buf));
// tell the player where parts are
let mut parts = Vec::new();
@@ 142,17 136,62 @@ fn on_message(
transform: proto_transform!(Transform::from_translation(transform.translation * SCALE).with_rotation(transform.rotation)),
}));
let packet = Packet::PartPositions {
- parts
+ parts
};
let buf = serde_json::to_vec(&packet).unwrap();
- packets.push(ServerEvent::Send(*addr, MessageType::Text, buf));
+ event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf));
+
+ // and send the welcome message :)
+ let packet = Packet::Message {
+ message_type: packet::MessageType::Server,
+ actor: "SERVER".to_string(),
+ content: "Welcome to StarKingdoms.IO! Have fun!".to_string(),
+ };
+ let buf = serde_json::to_vec(&packet).unwrap();
+ event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf));
+ }
+ Packet::SendMessage { target, content } => {
+ // find our player
+ let mut player = None;
+ for (_, q_player) in &player_query {
+ if q_player.addr == *addr {
+ player = Some(q_player);
+ }
+ }
+ let player = player.unwrap();
+ if let Some(target_username) = target {
+ let mut target_player = None;
+ for (_, q_player) in &player_query {
+ if q_player.username == target_username {
+ target_player = Some(q_player);
+ }
+ }
+ let target_player = target_player.unwrap();
+ let packet = Packet::Message {
+ message_type: packet::MessageType::Direct,
+ actor: player.username.clone(),
+ content,
+ };
+ let buf = serde_json::to_vec(&packet).unwrap();
+ event_queue.push(ServerEvent::Send(target_player.addr, MessageType::Text, buf.clone()));
+ event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf));
+ } else {
+ // send to general chat
+ let packet = Packet::Message {
+ message_type: packet::MessageType::Chat,
+ actor: player.username.clone(),
+ content,
+ };
+ let buf = serde_json::to_vec(&packet).unwrap();
+ event_queue.push(ServerEvent::Broadcast(MessageType::Text, buf));
+ }
}
_ => continue
- };
+ }
}
}
- for packet in packets {
- packet_send.send(packet);
+ for event in event_queue {
+ packet_event_send.send(event);
}
}
M server/src/packet.rs => server/src/packet.rs +17 -0
@@ 31,6 31,14 @@ pub struct Part {
}
#[derive(Debug, Serialize, Deserialize)]
+pub enum MessageType {
+ Server,
+ Error,
+ Chat,
+ Direct
+}
+
+#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
pub enum Packet {
// serverbound
@@ 38,6 46,10 @@ pub enum Packet {
username: String,
jwt: Option<String>,
},
+ SendMessage {
+ target: Option<String>,
+ content: String
+ },
// clientbound
SpawnPlayer {
id: u32,
@@ 55,4 67,9 @@ pub enum Packet {
PlayerLeave {
id: u32,
},
+ Message {
+ message_type: MessageType,
+ actor: String,
+ content: String
+ }
}
M starkingdoms-client/index.html => starkingdoms-client/index.html +84 -71
@@ 1,39 1,40 @@
<!doctype html>
<html lang="en">
-<head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <title>StarKingdoms</title>
-</head>
-<body class="bg-grid">
+ <head>
+ <meta charset="UTF-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+ <title>StarKingdoms</title>
+ </head>
+ <body class="bg-grid">
- <div id="gamewindow">
- <!-- Canvas gets added here by the game script -->
- </div>
+ <div id="gamewindow">
+ <!-- Canvas gets added here by the game script -->
+ </div>
- <div class="popup popup-center" id="server_selector">
+ <div class="popup popup-center popup-max-width-300" id="server_selector">
- <h1>StarKingdoms</h1>
- <h2>Join Game</h2>
+ <h1>StarKingdoms</h1>
+ <h2>Join Game</h2>
- <form id="join-fm">
- <label>Choose server</label>
+ <form id="join-fm">
+ <label>Choose server</label>
- <div class="fm-select">
- <button class="fm-select-button" role="combobox" aria-labelledby="server selector" aria-haspopup="listbox"
- aria-expanded="false" aria-controls="fm-select-dropdown">
- <span class="fm-selected-value">Loading servers list</span>
- <span class="fm-arrow"></span>
- </button>
- <ul class="fm-select-dropdown" role="listbox" id="fm-select-dropdown">
- <!-- Filled by TS -->
- </ul>
- </div>
+ <div class="fm-select">
+ <button class="fm-select-button" role="combobox" aria-labelledby="server selector"
+ aria-haspopup="listbox"
+ aria-expanded="false" aria-controls="fm-select-dropdown">
+ <span class="fm-selected-value">Loading servers list</span>
+ <span class="fm-arrow"></span>
+ </button>
+ <ul class="fm-select-dropdown" role="listbox" id="fm-select-dropdown">
+ <!-- Filled by TS -->
+ </ul>
+ </div>
- <label for="username" class="username-label">Username</label>
- <input class="username-box" id="username" required autocomplete="off"/>
- <button id="launch-btn" class="launch-btn">Launch!</button>
- <span id="server-danger" class="server-danger hidden">
+ <label for="username" class="username-label">Username</label>
+ <input class="username-box" id="username" required autocomplete="off"/>
+ <button id="launch-btn" class="launch-btn">Launch!</button>
+ <span id="server-danger" class="server-danger hidden">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="server-danger-icon">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
@@ 41,52 42,64 @@
</svg>
Here be dragons! You have a <b>prerelease server</b> selected. Expect bugs, and save data on this server may be wiped at any time.
</span>
- </form>
-</div>
+ </form>
+ </div>
-<div class="popup popup-wmin log-hidden log-container" id="packet_log">
- <h1>Packet Log</h1>
- <table class="log">
- <thead>
- <tr class="log-wfull">
- <th class="log-wfull log-lalign log-header log-w50">#</th>
- <th class="log-wfull log-lalign log-header">Dir</th>
- <th class="log-wfull log-lalign log-header log-w240">Type</th>
- <th class="log-wfull log-lalign log-header">Delta</th>
- </tr>
- </thead>
- <tbody id="log_body">
+ <div class="popup popup-wmin log-hidden log-container popup-max-width-300" id="packet_log">
+ <h1>Packet Log</h1>
+ <table class="log">
+ <thead>
+ <tr class="log-wfull">
+ <th class="log-wfull log-lalign log-header log-w50">#</th>
+ <th class="log-wfull log-lalign log-header">Dir</th>
+ <th class="log-wfull log-lalign log-header log-w240">Type</th>
+ <th class="log-wfull log-lalign log-header">Delta</th>
+ </tr>
+ </thead>
+ <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>
+ </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 class="popup hud hidden" id="hud">
- <table>
- <thead>
- <tr>
- <th class="hud-d"></th>
- <th class="hud-d"></th>
- <th class="hud-d"></th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td id="pos">Position: --, --</td>
- <td id="velocity">Velocity: --</td>
- <td id="track">Track Angle: --</td>
- </tr>
- </tbody>
- </table>
- </div>
+ <div class="popup chat-container hidden" id="chat">
+ <h1>Chat</h1>
+ <div id="chatbox" class="chat-table mono">
+ <p class="message server-message">[SERVER] Welcome to StarKingdoms.IO! Have fun!</p>
+ <p class="message global-message">core: this is a global chat message</p>
+ <p class="message direct-message">[DM core2] core this is a direct message</p>
+ <p class="message direct-message">[DM core2] core2: and this is a reply</p>
+ <p class="message server-error">[SERVER] Disconnected</p>
+ </div>
+ <input placeholder="Enter message or command here..." class="chat-box" id="chatentry" required autocomplete="off"/>
+ </div>
+
+ <div class="popup hud hidden popup-max-width-300" id="hud">
+ <table>
+ <thead>
+ <tr>
+ <th class="hud-d"></th>
+ <th class="hud-d"></th>
+ <th class="hud-d"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td id="pos">Position: --, --</td>
+ <td id="velocity">Velocity: --</td>
+ <td id="track">Track Angle: --</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
-<span class="footer-left" id="footer-left"></span>
- <span class="footer-right" id="footer-right"></span>
+ <span class="footer-left" id="footer-left"></span>
+ <span class="footer-right" id="footer-right"></span>
-<script type="module" src="src/main.ts"></script>
-</body>
+ <script type="module" src="src/main.ts"></script>
+ </body>
</html>
A starkingdoms-client/src/css/chat.css => starkingdoms-client/src/css/chat.css +48 -0
@@ 0,0 1,48 @@
+.chat-container {
+ position: absolute;
+ top: 0.5em;
+ right: 0.5em;
+ width: 30vw;
+ height: min-content;
+ font-size: 0.875rem; /* 14px */
+ line-height: 1.25rem; /* 20px */
+ font-weight: 500;
+}
+.chat-table {
+ height: 23vh;
+ display: block;
+ overflow: auto;
+}
+.chat-box {
+ appearance: none;
+ background: transparent;
+ color: var(--text);
+ padding: 0.675em 1em;
+ border: 1px solid var(--links);
+ border-radius: 0.25rem;
+ cursor: text;
+ width: 100%;
+ max-width: 100%;
+}
+.chat-box:focus {
+ outline: none;
+ background-color: var(--links-ultratransparent);
+}
+.message {
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
+.server-message {
+ color: #facb61;
+}
+.server-error {
+ color: #ff2222;
+}
+.global-message {
+ color: #54fa46;
+}
+.direct-message {
+ color: #66d8ed;
+}<
\ No newline at end of file
M => +4 -1
@@ 4,10 4,13 @@
background-color: var(--bg-secondary-1);
height: min-content;
max-width: 300px;
border-radius: 5px;
z-index: 100000;
}
.popup-max-width-300 {
max-width: 300px;
}
.popup-wmin {
width: min-content;
}
M starkingdoms-client/src/css/style.css => starkingdoms-client/src/css/style.css +2 -1
@@ 6,4 6,5 @@
@import "json.css";
@import "log.css";
@import "game.css";
-@import "hud.css";>
\ No newline at end of file
+@import "hud.css";
+@import "chat.css";<
\ No newline at end of file
M starkingdoms-client/src/css/themes/catppuccin-common/definitions.css => starkingdoms-client/src/css/themes/catppuccin-common/definitions.css +1 -0
@@ 31,4 31,5 @@
--pill: rgb(var(--blue));
--sel-bg: rgba(var(--surface2), 0.4);
--cursor: rgb(var(--rosewater));
+ --dm: rgb(var(--teal));
}=
\ No newline at end of file
M starkingdoms-client/src/hub.ts => starkingdoms-client/src/hub.ts +11 -2
@@ 1,7 1,13 @@
import createDebug from "debug";
import {
+ MessagePacket,
Packet,
- PacketType, PartPositionsPacket, PlanetPositionsPacket, PlayerLeavePacket, PlayerListPacket, SpawnPlayerPacket,
+ PacketType,
+ PartPositionsPacket,
+ PlanetPositionsPacket,
+ PlayerLeavePacket,
+ PlayerListPacket,
+ SpawnPlayerPacket,
} from "./protocol.ts";
import {appendPacket} from "./packet_ui.ts";
import {global} from "./main.ts";
@@ 81,11 87,14 @@ export async function hub_connect(url: string, username: string): Promise<Client
global.parts_map.set(p.parts[i][0], p.parts[i][1]);
}
} else if (packet.t == PacketType.PlayerLeave) {
- let p = <PlayerLeavePacket> packet.c;
+ let p = <PlayerLeavePacket>packet.c;
let username = global.players_map.get(p.id)!;
global.inverse_players_map.delete(username);
global.players_map.delete(p.id);
logger(`player removed (id=${p.id})`);
+ } else if (packet.t == PacketType.Message) {
+ let p = <MessagePacket>packet.c;
+ logger(`message type=${p.message_type} actor=${p.actor} content=${p.content}`);
} else {
logger(`unrecognized packet type ${packet.t}`);
}
M starkingdoms-client/src/protocol.ts => starkingdoms-client/src/protocol.ts +21 -4
@@ 38,25 38,42 @@ export interface PlayerListPacket {
export interface PlayerLeavePacket {
id: number
}
+export interface SendMessagePacket {
+ target: string | null,
+ content: string
+}
+export enum MessageType {
+ Server = "Server",
+ Error = "Error",
+ Chat = "Chat",
+ Direct = "Direct"
+}
+export interface MessagePacket {
+ message_type: MessageType,
+ actor: string,
+ content: string
+}
export enum PacketType {
// serverbound
ClientLogin = "ClientLogin",
+ SendMessage = "SendMessage",
// clientbound
SpawnPlayer = "SpawnPlayer",
PlayerList = "PlayerList",
PlanetPositions = "PlanetPositions",
PartPositions = "PartPositions",
- PlayerLeave = "PlayerLeave"
+ PlayerLeave = "PlayerLeave",
+ Message = "Message"
}
export interface Packet {
t: PacketType,
- c: ClientLoginPacket | SpawnPlayerPacket | PlayerListPacket | PlanetPositionsPacket | PartPositionsPacket | PlayerLeavePacket
+ c: ClientLoginPacket | SpawnPlayerPacket | PlayerListPacket | PlanetPositionsPacket | PartPositionsPacket | PlayerLeavePacket | SendMessagePacket | MessagePacket
}
-export const SERVERBOUND = [PacketType.ClientLogin];
-export const CLIENTBOUND = [PacketType.SpawnPlayer, PacketType.PlayerList, PacketType.PlanetPositions, PacketType.PartPositions, PacketType.PlayerLeave];
+export const SERVERBOUND = [PacketType.ClientLogin, PacketType.SendMessage];
+export const CLIENTBOUND = [PacketType.SpawnPlayer, PacketType.PlayerList, PacketType.PlanetPositions, PacketType.PartPositions, PacketType.PlayerLeave, PacketType.Message];
export enum Direction {
Serverbound = "Serverbound",
M starkingdoms-client/src/rendering.ts => starkingdoms-client/src/rendering.ts +2 -0
@@ 10,6 10,8 @@ export function startRender() {
document.getElementById("server_selector")!.classList.add("hidden");
// show the HUD
document.getElementById("hud")!.classList.remove("hidden");
+ // and chat
+ document.getElementById("chat")!.classList.remove("hidden");
// create the canvas
let canvas = document.createElement("canvas");
canvas.classList.add("game");