← 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