~starkingdoms/starkingdoms

a116b645d54aae52e567a720bdb566f0f1dff4e8 — core 2 years ago 323f80a
chat functionality
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 starkingdoms-client/src/css/popup.css => starkingdoms-client/src/css/popup.css +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");