Skip to content

Commit

Permalink
First implementation of network layer
Browse files Browse the repository at this point in the history
  • Loading branch information
snendev committed Aug 14, 2024
1 parent d9736c2 commit 76ac9d6
Show file tree
Hide file tree
Showing 19 changed files with 1,654 additions and 98 deletions.
989 changes: 973 additions & 16 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ members = ["apps/*", "plugins/*"]

[workspace.dependencies]
active_game = { path = "plugins/active_game" }
client = { path = "plugins/client" }
game = { path = "plugins/game" }
server = { path = "plugins/server" }
ui = { path = "plugins/ui" }

bevy = { version = "0.14", default-features = false }
bevy_mod_try_system = "0.2"
bevy-inspector-egui = "0.25"
bevy_renet2 = { git = "https://github.com/UkoeHB/renet2", rev = "0.0.5" }
bevy_replicon = "0.27"
bevy_replicon_renet2 = { git = "https://github.com/UkoeHB/renet2", rev = "0.0.5" }
renet2 = { git = "https://github.com/UkoeHB/renet2", rev = "0.0.5", default-features = false, features = [
"bevy",
"serde",
] }
sickle_ui = "0.2"

anyhow = "1.0"
Expand Down Expand Up @@ -50,14 +57,12 @@ strip = "none"
[features]
default = []
dev = ["dep:bevy-inspector-egui"]
server = ["dep:server"]
ui = ["dep:ui"]

[dependencies]
# plugins
active_game = { workspace = true }
game = { workspace = true }
server = { workspace = true, optional = true }
ui = { workspace = true, optional = true }

# bevy
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

WORDFIGHT is a simple text-based "fighting" game. The only mechanic is spelling.

## How To Run

To run the webserver (be sure to have [`trunk`](https://trunkrs.dev/) installed!):

```sh
trunk serve --release
```

To run the game-server:

```sh
cargo run -p wordfight-server --release
```

You can configure the server IP, port, and the port for accessing the server token using the environment variables `SERVER_IP`, `SERVER_PORT`, and `SERVER_TOKENS_PORT` respectively.

## Gameplay

Two players battle by typing any substring of a valid English word into the shared input space of a fixed size (for the time being, that size is 7 characters). Each player's word extends from one "side" of the input, and both perspectives are shown to both players.
Expand Down
12 changes: 12 additions & 0 deletions apps/server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "wordfight-server"
version = "0.1.0"
authors = ["Sean Sullivan <[email protected]>"]
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false

[dependencies]
wordfight = { path = "../../" }
server = { workspace = true }
bevy = { workspace = true }
36 changes: 36 additions & 0 deletions apps/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use bevy::{
app::ScheduleRunnerPlugin,
log::{Level, LogPlugin},
prelude::*,
};

use server::ServerPlugin;
use wordfight::WordFightPlugins;

fn main() {
App::default()
.add_plugins((
ScheduleRunnerPlugin::run_loop(
// need some wait duration so that async tasks are not entirely outcompeted by the main loop
std::time::Duration::from_millis(10),
),
WordFightPlugins.build().set(LogPlugin {
filter: "wgpu=error,naga=warn,h3=error".to_string(),
level: Level::INFO,
..Default::default()
}),
))
.add_plugins(ServerPlugin {
port: option_env!("SERVER_PORT").unwrap_or("7636").to_string(),
wt_tokens_port: option_env!("SERVER_TOKENS_PORT")
.unwrap_or("7637")
.to_string(),
// native_host: option_env!("SERVER_IP")
// .unwrap_or("127.0.0.1")
// .to_string(),
// native_port: option_env!("SERVER_IP")
// .unwrap_or("127.0.0.1")
// .to_string(),
})
.run();
}
11 changes: 10 additions & 1 deletion apps/web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ log = []
[dependencies]
# plugins
wordfight = { path = "../../" }
client = { workspace = true }

# bevy
bevy = { workspace = true }
Expand All @@ -19,21 +20,29 @@ leptos = { version = "0.6", features = ["csr"] }

console_error_panic_hook = "0.1"
js-sys = "0.3"
futures-lite = "2.3"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
"Blob",
"BlobPropertyBag",
"console",
"DedicatedWorkerGlobalScope",
"Document",
"Headers",
"HtmlElement",
"Location",
"MessageEvent",
"KeyboardEvent",
"Node",
"RequestInit",
"RequestMode",
"Response",
"Text",
"Url",
"Window",
"Worker",
"WorkerGlobalScope",
] }
gloo-worker = "0.5"

gloo-worker = { version = "0.5", features = ["futures"] }
3 changes: 2 additions & 1 deletion apps/web/src/bin/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ use wordfight_web::BevyWorker;
fn main() {
console_error_panic_hook::set_once();

BevyWorker::registrar().register();
let registrar = BevyWorker::registrar();
registrar.register();
}
9 changes: 9 additions & 0 deletions apps/web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ pub use wordfight::*;
mod worker;
pub use worker::*;

pub const SERVER_IP: Option<&'static str> = option_env!("SERVER_IP");
pub const SERVER_DEFAULT_IP: &'static str = "127.0.0.1";

pub const SERVER_PORT: Option<&'static str> = option_env!("SERVER_PORT");
pub const SERVER_DEFAULT_PORT: &'static str = "7636";

pub const SERVER_TOKENS_PORT: Option<&'static str> = option_env!("SERVER_TOKENS_PORT");
pub const SERVER_DEFAULT_TOKENS_PORT: &'static str = "7637";

#[derive(Debug)]
#[derive(Deserialize, Serialize)]
pub enum AppMessage {
Expand Down
147 changes: 102 additions & 45 deletions apps/web/src/worker.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
use gloo_worker::{HandlerId, Worker, WorkerScope};
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};

use bevy::{prelude::*, utils::HashSet};

use wordfight::{ActiveGameUpdate, WordFightPlugins};
use client::ClientPlugin;
use wordfight::{ActiveGameUpdate, PlayerSide, WordFightPlugins};

use crate::{AppMessage, UpdateStateMessage, WorkerMessage};
use crate::{
AppMessage, UpdateStateMessage, WorkerMessage, SERVER_DEFAULT_IP, SERVER_DEFAULT_PORT,
SERVER_DEFAULT_TOKENS_PORT, SERVER_IP, SERVER_PORT, SERVER_TOKENS_PORT,
};

// Use this to enable console logging
#[wasm_bindgen]
Expand All @@ -19,10 +26,8 @@ extern "C" {
fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
fn clearInterval(token: f64);
}

pub struct BevyWorker {
app: App,
my_player: Entity,
game: Option<App>,
subscriptions: HashSet<HandlerId>,
_trigger_update: Closure<dyn FnMut()>,
_interval: Interval,
Expand All @@ -31,75 +36,127 @@ pub struct BevyWorker {
impl Worker for BevyWorker {
type Input = AppMessage;
type Output = WorkerMessage;
type Message = ();
type Message = WorkerUpdateMessage;

fn create(scope: &WorkerScope<Self>) -> Self {
let mut app = App::new();
app.add_plugins(WordFightPlugins);
app.update();
app.update();

let events = app.world().resource::<Events<ActiveGameUpdate>>();
let mut reader = events.get_reader();
let update = reader.read(&events).last().unwrap();
let my_player = update.player_left;

scope
.send_future(async { WorkerUpdateMessage::Token(fetch_server_token().await.unwrap()) });
let scope_clone = scope.clone();
let trigger_update = Closure::new(move || {
scope_clone.send_message(());
scope_clone.send_message(WorkerUpdateMessage::Update);
});
let interval = setInterval(&trigger_update, 10);
Self {
app,
my_player,
game: None,
subscriptions: HashSet::default(),
_trigger_update: trigger_update,
_interval: Interval(interval),
}
}

fn update(&mut self, scope: &WorkerScope<Self>, _: Self::Message) {
fn update(&mut self, scope: &WorkerScope<Self>, message: Self::Message) {
#[cfg(feature = "log")]
log("Update".to_string());
self.app.update();
let events = self.app.world().resource::<Events<ActiveGameUpdate>>();
let mut reader = events.get_reader();
if let Some(update) = reader.read(&events).last() {
for id in &self.subscriptions {
scope.respond(
*id,
WorkerMessage::UpdateState(UpdateStateMessage {
left_word: update.left_word.to_string(),
left_score: *update.left_score,
right_word: update.right_word.to_string(),
right_score: *update.right_score,
arena_size: update.arena_size,
}),
);
if let Some(app) = self.game.as_mut() {
let WorkerUpdateMessage::Update = message else {
return;
};
app.update();
let events = app.world().resource::<Events<ActiveGameUpdate>>();
let mut reader = events.get_reader();
if let Some(update) = reader.read(&events).last() {
for id in &self.subscriptions {
scope.respond(
*id,
WorkerMessage::UpdateState(UpdateStateMessage {
left_word: update.left_word.to_string(),
left_score: *update.left_score,
right_word: update.right_word.to_string(),
right_score: *update.right_score,
arena_size: update.arena_size,
}),
);
}
}
} else if let WorkerUpdateMessage::Token(token) = message {
let app = build_app(token);
self.game = Some(app);
}
}

fn received(&mut self, _: &WorkerScope<Self>, message: Self::Input, id: HandlerId) {
#[cfg(feature = "log")]
log(format!("Message received! {:?}", message));
self.subscriptions.insert(id);
let action: wordfight::Action = match message {
AppMessage::AddLetter(letter) => wordfight::Action::Append(letter),
AppMessage::Backspace => wordfight::Action::Delete,
};
self.app
.world_mut()
.send_event(action.made_by(self.my_player, wordfight::PlayerSide::Left));

self.app.update();
if let Some(app) = self.game.as_mut() {
self.subscriptions.insert(id);
let action: wordfight::Action = match message {
AppMessage::AddLetter(letter) => wordfight::Action::Append(letter),
AppMessage::Backspace => wordfight::Action::Delete,
};
let mut query = app.world_mut().query::<(Entity, &PlayerSide)>();
let Some((my_player, _)) = query
.iter(app.world())
.find(|(_, side)| **side == PlayerSide::Left)
else {
return;
};
app.world_mut()
.send_event(action.made_by(my_player, wordfight::PlayerSide::Left));

app.update();
}
}
}

pub enum WorkerUpdateMessage {
Token(String),
Update,
}

pub struct Interval(f64);

impl Drop for Interval {
fn drop(&mut self) {
clearInterval(self.0);
}
}

fn build_app(server_token: String) -> App {
let mut app = App::new();
let server_origin = SERVER_IP.unwrap_or(SERVER_DEFAULT_IP).to_string();
let server_port = SERVER_PORT.unwrap_or(SERVER_DEFAULT_PORT).to_string();

app.add_plugins(WordFightPlugins);
app.add_plugins(ClientPlugin {
server_origin,
server_port,
server_token,
});
app.update();
app.update();
app
}

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen]
fn fetch(input: &Request) -> Promise;
}

async fn fetch_server_token() -> Result<String, JsValue> {
let server_origin = SERVER_IP.unwrap_or(SERVER_DEFAULT_IP);
let server_token_port = SERVER_TOKENS_PORT.unwrap_or(SERVER_DEFAULT_TOKENS_PORT);
let server_url = format!("http://{server_origin}:{server_token_port}");
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::Cors);
let request = Request::new_with_str_and_init(&server_url, &opts)?;

let response = JsFuture::from(fetch(&request)).await?;

assert!(response.is_instance_of::<Response>());
let response: Response = response.dyn_into().unwrap();
let text = JsFuture::from(response.text()?).await?;

Ok(text.as_string().unwrap())
}
21 changes: 21 additions & 0 deletions plugins/client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "client"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"


[dependencies]
game = { workspace = true }

bevy = { workspace = true }
bevy_replicon = { workspace = true, features = ["client"] }
bevy_replicon_renet2 = { workspace = true, features = ["wt_client_transport"] }
bevy_renet2 = { workspace = true }
renet2 = { workspace = true, features = ["wt_client_transport"] }

serde = { workspace = true }
bincode = "1.3"
base64 = { version = "0.22" }
url = "2.5"
wasm-timer = { version = "0.2" }
Loading

0 comments on commit 76ac9d6

Please sign in to comment.