Multiplayer client relay bridge and match lobby menu: WebSocket bridge, match lobby page, order relay, confirmed turns, checksums, chat, abort/resume
This commit is contained in:
1 parent
848ab0f677
commit
583c3fcdaf
9 files changed
+1063
-6
No files matched your search
+164
-3
@@ -11,6 +11,7 @@ mod fortress;
|
||||
mod game;
|
||||
mod gfx;
|
||||
mod i18n;
|
||||
mod lobby;
|
||||
mod map;
|
||||
mod menu;
|
||||
mod netplay;
|
||||
@@ -43,6 +44,7 @@ use gfx::Gfx;
|
||||
use macroquad::prelude::*;
|
||||
use map::{inb, mh, mw};
|
||||
use menu::{Menu, MenuAction, Page};
|
||||
use lobby::{MatchClient, Phase, StartInfo};
|
||||
use render3d::{Cam, Overlay, R3d};
|
||||
use settings::{bindable_keys, Action, Settings};
|
||||
use sound::SoundBank;
|
||||
@@ -230,10 +232,49 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuAction::OpenMatch => {
|
||||
if !app.connect_match(cloud) {
|
||||
menu.error = Some(i18n::tr("net.status_offline"));
|
||||
}
|
||||
}
|
||||
MenuAction::MatchJoin(code) => app.client.join(&code),
|
||||
MenuAction::MatchCreate => app.client.create(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty),
|
||||
MenuAction::MatchTakeSide(side) => app.client.take_side(side),
|
||||
MenuAction::MatchReady(ready) => app.client.ready(ready),
|
||||
MenuAction::MatchStart => app.client.start(),
|
||||
MenuAction::MatchResume => app.client.resume(),
|
||||
MenuAction::MatchAbort => {
|
||||
app.client.abort();
|
||||
app.match_mode = false;
|
||||
}
|
||||
MenuAction::MatchLeave => {
|
||||
app.client.leave();
|
||||
app.match_mode = false;
|
||||
}
|
||||
MenuAction::MatchChat(text) => app.client.chat(&text),
|
||||
MenuAction::MatchEditField(i) => {
|
||||
if ui::touch_ui() {
|
||||
let title = i18n::tr(if i == 0 { "net.join_hint" } else { "net.chat" });
|
||||
if let Some(t) = platform::prompt(&title, &menu.fields[i]) {
|
||||
menu.fields[i] = t.chars().filter(|c| !c.is_control()).take(if i == 0 { 16 } else { 80 }).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn url_encode(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' => out.push(b as char),
|
||||
_ => out.push_str(&format!("%{b:02X}")),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn next_seed(seed: u32) -> u32 {
|
||||
seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
|
||||
}
|
||||
@@ -336,6 +377,9 @@ struct App {
|
||||
seed: u32,
|
||||
status: Option<(String, f64)>,
|
||||
recorded: bool, // the end of the current mission has been recorded in the campaign progress
|
||||
client: MatchClient,
|
||||
ws_id: i32,
|
||||
match_mode: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -371,6 +415,92 @@ impl App {
|
||||
fn autosave_json(&self) -> String {
|
||||
save::to_json(&self.game, &self.cam, &self.settings)
|
||||
}
|
||||
|
||||
fn connect_match(&mut self, cloud: &Cloud) -> bool {
|
||||
if self.ws_id > 0 || self.client.connected {
|
||||
return true;
|
||||
}
|
||||
let name = cloud.session.as_ref().map(|s| s.name.clone()).unwrap_or_else(|| "Guest".into());
|
||||
let token = cloud.session.as_ref().map(|s| s.token.clone()).unwrap_or_default();
|
||||
let mut url = format!("/api/netplay/ws?name={}", url_encode(&name));
|
||||
if !token.is_empty() {
|
||||
url.push_str("&token=");
|
||||
url.push_str(&url_encode(&token));
|
||||
}
|
||||
self.ws_id = platform::ws_connect(&url);
|
||||
if self.ws_id <= 0 {
|
||||
return false;
|
||||
}
|
||||
self.client.on_connect(&name);
|
||||
true
|
||||
}
|
||||
|
||||
fn poll_match(&mut self) {
|
||||
if self.ws_id <= 0 {
|
||||
if self.client.connected {
|
||||
self.client.on_disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
while let Some(msg) = platform::ws_recv(self.ws_id) {
|
||||
self.client.feed(&msg);
|
||||
}
|
||||
if self.client.phase() == Phase::Closed && self.game.in_match() {
|
||||
self.game.end_match();
|
||||
self.match_mode = false;
|
||||
}
|
||||
let state = platform::ws_state(self.ws_id);
|
||||
if state == 0 || state == 1 {
|
||||
while let Some(text) = self.client.outbox.pop_front() {
|
||||
if !platform::ws_send(self.ws_id, &text) {
|
||||
self.client.outbox.push_front(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
platform::ws_close(self.ws_id);
|
||||
self.ws_id = 0;
|
||||
self.match_mode = false;
|
||||
self.client.on_disconnect();
|
||||
}
|
||||
|
||||
fn start_match_game(&mut self, info: StartInfo) -> bool {
|
||||
if info.map_w < 72 || info.map_h < 72 || !info.sides.contains(&0) {
|
||||
return false;
|
||||
}
|
||||
self.game = Game::new(info.seed, info.map_w, info.map_h);
|
||||
let local = info.local_side.unwrap_or(0);
|
||||
if !self.game.start_match(&info.sides, local) {
|
||||
return false;
|
||||
}
|
||||
self.game.hints = self.settings.hints;
|
||||
let (cx, cy) = self.game.map.core;
|
||||
self.cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
|
||||
self.match_mode = true;
|
||||
self.seed = info.seed.wrapping_mul(31) | 1;
|
||||
true
|
||||
}
|
||||
|
||||
fn tick_match_game(&mut self) {
|
||||
if !self.match_mode || !self.game.in_match() {
|
||||
return;
|
||||
}
|
||||
for (_, cmds) in self.client.take_turns() {
|
||||
self.game.receive_turn(&cmds);
|
||||
}
|
||||
for (_, turn, sum) in self.client.take_checks() {
|
||||
self.game.check_peer(turn, sum);
|
||||
}
|
||||
let cmds = self.game.take_orders();
|
||||
if !cmds.is_empty() {
|
||||
self.client.send_commands(&cmds);
|
||||
}
|
||||
if let Some(turn) = self.game.checksum_due() {
|
||||
let sum = self.game.checksum();
|
||||
self.client.send_checksum(turn, sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macroquad::main(conf)]
|
||||
@@ -400,7 +530,7 @@ async fn main() {
|
||||
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
|
||||
// behind the title menu: the endless bridge battle
|
||||
let (game, cam) = Game::new_demo(seed);
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false };
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false };
|
||||
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
|
||||
app.game.hints = app.settings.hints;
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
@@ -506,6 +636,25 @@ async fn main() {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ---------------- multiplayer relay: receive messages, send queued requests, update the menu state
|
||||
app.poll_match();
|
||||
if matches!(menu.page, Some(Page::Match)) {
|
||||
menu.set_match_ui(app.client.ui());
|
||||
}
|
||||
if let Some(info) = app.client.take_start() {
|
||||
if app.start_match_game(info) {
|
||||
menu.page = None;
|
||||
menu.error = None;
|
||||
ui.log.clear();
|
||||
autosave_t = 0.0;
|
||||
cache_t = 0.0;
|
||||
app.set_status(i18n::tr("net.title"), now);
|
||||
} else {
|
||||
menu.error = Some(i18n::tr("net.status_closed"));
|
||||
app.match_mode = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- text fields (savegame name, login, account)
|
||||
if menu.typing() {
|
||||
// Tab switches the field right away (the following characters go to the next one)
|
||||
@@ -831,7 +980,18 @@ async fn main() {
|
||||
| MenuAction::CreateUser(..)
|
||||
| MenuAction::DeleteUser(..)
|
||||
| MenuAction::DownloadStandalone
|
||||
| MenuAction::EditField(..)) => account_action(a, &mut cloud, &mut menu, &mut app, now),
|
||||
| MenuAction::EditField(..)
|
||||
| MenuAction::OpenMatch
|
||||
| MenuAction::MatchJoin(..)
|
||||
| MenuAction::MatchCreate
|
||||
| MenuAction::MatchTakeSide(..)
|
||||
| MenuAction::MatchReady(..)
|
||||
| MenuAction::MatchStart
|
||||
| MenuAction::MatchResume
|
||||
| MenuAction::MatchAbort
|
||||
| MenuAction::MatchLeave
|
||||
| MenuAction::MatchChat(..)
|
||||
| MenuAction::MatchEditField(..)) => account_action(a, &mut cloud, &mut menu, &mut app, now),
|
||||
MenuAction::EditName => {
|
||||
if let Some(n) = platform::prompt(&i18n::tr("saves.name"), &menu.save_name) {
|
||||
menu.save_name = n.chars().filter(|c| !c.is_control()).take(40).collect();
|
||||
@@ -991,8 +1151,9 @@ async fn main() {
|
||||
if playing {
|
||||
// the world runs in fixed steps: the speed setting only feeds more time into them
|
||||
app.game.update(dt * app.settings.game_speed.max(1.0));
|
||||
app.tick_match_game();
|
||||
// autosave: snapshot for leaving the tab + regular autosave
|
||||
if app.game.state == 0 {
|
||||
if app.game.state == 0 && !app.match_mode {
|
||||
cache_t += dt;
|
||||
autosave_t += dt;
|
||||
if cache_t >= AUTOSAVE_CACHE_EVERY {
|
||||
|
||||
Reference in new issue
Block a user