← All Workshops

MudEngine Part 11: Peer-to-Peer (Hybrid)

Step 7 / 12

Rewrite main.rs

The old main.rs launched a fullstack server (with #[cfg(feature = "server")]) or a Blitz client (without). Now there is no server — the host and client are both native apps that use dioxus_native::launch.

The new App component shows a connection screen where the player chooses Host or Join, then enters the game.

mud-engine/src/main.rs
use std::sync::Arc;
use dioxus::prelude::*;
use dioxus_native::prelude::*;

mod game_engine;
mod p2p;

fn main() {
    dioxus_native::launch(App);
}

fn random_name() -> String {
    let id = uuid::Uuid::new_v4();
    const ADJS: [&str; 10] = [
        "Great", "Intelligent", "Cute", "Brave", "Mighty",
        "Sly", "Ancient", "Mysterious", "Swift", "Gentle",
    ];
    const NOUNS: [&str; 10] = [
        "Dragon", "Elf", "Phoenix", "Goblin", "Griffin",
        "Unicorn", "Wizard", "Knight", "Wyrm", "Fae",
    ];
    let adj = ADJS[id.as_bytes()[0] as usize % 10];
    let noun = NOUNS[id.as_bytes()[1] as usize % 10];
    format!("{} {}", adj, noun)
}

#[component]
fn App() -> Element {
    let mut mode = use_signal(|| String::new());
    let mut host_id_input = use_signal(|| String::new());
    let mut player_name = use_signal(random_name);

    // ── Connection screen ──
    rsx! {
        div { class: "p2p-screen",
            h1 { "🧙 MudEngine — P2P" }
            p { "Start a game or join an existing one." }

            if mode.read().is_empty() {
                div { class: "p2p-choices",
                    button {
                        class: "p2p-btn host",
                        onclick: move |_| mode.set("host".into()),
                        "🎬 Start as Host"
                    }
                    button {
                        class: "p2p-btn join",
                        onclick: move |_| mode.set("join".into()),
                        "🔗 Join a Game"
                    }
                }
            } else if *mode.read() == "host" {
                div { class: "p2p-form",
                    input {
                        placeholder: "Your name...",
                        value: "{player_name}",
                        oninput: move |e| player_name.set(e.value()),
                    }
                    button {
                        class: "p2p-btn",
                        onclick: move |_| {
                            let name = player_name.read().clone();
                            if !name.trim().is_empty() {
                                // Launch host — see step below
                            }
                        },
                        "Start Hosting"
                    }
                }
            } else {
                div { class: "p2p-form",
                    input {
                        placeholder: "Your name...",
                        value: "{player_name}",
                        oninput: move |e| player_name.set(e.value()),
                    }
                    input {
                        placeholder: "Host EndpointId...",
                        value: "{host_id_input}",
                        oninput: move |e| host_id_input.set(e.value()),
                    }
                    button {
                        class: "p2p-btn",
                        onclick: move |_| {
                            let name = player_name.read().clone();
                            let host = host_id_input.read().clone();
                            if !name.trim().is_empty() && !host.trim().is_empty() {
                                // Launch client — see next step
                            }
                        },
                        "Connect"
                    }
                }
            }

            // mDNS discovery hint
            if *mode.read() == "join" {
                div { class: "mdns-hint",
                    "💡 Players on the same network are discovered automatically via mDNS. Check the host's terminal for their EndpointId."
                }
            }
        }
    }
}
🔁 The full Game component

The Game component from Parts 6–10 is reused almost unchanged. The key difference: instead of use_websocket (which sends messages to an Axum server), it uses a channel to the iroh gossip layer.

Replace the WebSocket-based receive loop with an mpsc receiver that the P2P layer feeds ServerMessage values into. The RSX (grid, sidebar, chat, D-pad, quest UI) stays identical to Part 9 — only the transport layer changes.

Step 7 / 12