[feat] use eti game.db, commit not working, something is wrong with game.id in the client/frontend

This commit is contained in:
2024-11-12 22:12:49 +01:00
parent ba2177abf0
commit 1388bc2115
17 changed files with 790 additions and 310 deletions

View File

@ -16,14 +16,14 @@ pub enum ClientEvent {
#[derive(Debug)]
pub enum ClientCommand {
ListGames,
GetGame(u64),
GetGame(String),
ServerAddr(SocketAddr),
}
/// # Errors
pub async fn run(
mut rx_control: UnboundedReceiver<ClientCommand>,
tx_event: UnboundedSender<ClientEvent>,
tx_notify_ui: UnboundedSender<ClientEvent>,
) -> eyre::Result<()> {
// blocking wait for remote address
log::debug!("waiting for server address");
@ -75,7 +75,7 @@ pub async fn run(
};
let data = request.encode();
log::error!("encoded data: {}", String::from_utf8_lossy(&data));
log::debug!("encoded data: {}", String::from_utf8_lossy(&data));
let stream = match conn.open_bidirectional_stream().await {
Ok(stream) => stream,
@ -91,104 +91,52 @@ pub async fn run(
log::error!("failed to send request to server {:?}", e);
}
if let Ok(Some(data)) = rx.receive().await {
log::trace!("server response (raw): {}", String::from_utf8_lossy(&data));
let mut all_data: Vec<u8> = Vec::new();
while let Ok(Some(data)) = rx.receive().await {
log::trace!("msg from server (raw): {}", String::from_utf8_lossy(&data));
all_data.extend_from_slice(&data);
}
log::debug!("{} bytes received from server", all_data.len());
log::trace!(
"server response (RAW): {}",
String::from_utf8_lossy(&all_data)
);
let response = Response::decode(&data);
log::trace!(
"server response (decoded): {}",
String::from_utf8_lossy(&data)
);
match response {
Response::Games(games) => {
for game in &games {
log::debug!("{game:?}");
}
let response = Response::decode(&all_data);
log::trace!("server response (DECODED): {response:?}");
if let Err(e) = tx_event.send(ClientEvent::ListGames(games)) {
log::error!("failed to send ClientEvent::ListGames to client {e:?}");
}
match response {
Response::Games(games) => {
for game in &games {
log::debug!("{game:?}");
}
Response::Game(game) => log::debug!("game received: {game:?}"),
Response::GameNotFound(id) => log::debug!("game not found {id}"),
Response::InvalidRequest(request_bytes, err) => log::error!(
"server says our request was invalid (error: {}): {}",
if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) {
log::debug!("failed to send ClientEvent::ListGames to client {e:?}");
}
}
Response::Game(game) => log::debug!("game received: {game:?}"),
Response::GameNotFound(id) => log::debug!("game not found {id}"),
Response::InvalidRequest(request_bytes, err) => log::error!(
"server says our request was invalid (error: {}): {}",
err,
String::from_utf8_lossy(&request_bytes)
),
Response::EncodingError(err) => {
log::error!("server encoding error: {err}");
}
Response::DecodingError(data, err) => {
log::error!(
"response decoding error: {} (data: {})",
err,
String::from_utf8_lossy(&request_bytes)
),
Response::EncodingError(err) => {
log::error!("server encoding error: {err}");
}
Response::DecodingError(data, err) => {
log::error!(
"response decoding error: {} (data: {})",
err,
String::from_utf8_lossy(&data)
);
}
String::from_utf8_lossy(&data)
);
}
}
if let Err(err) = tx.close().await {
log::error!("failed to close stream: {err}");
}
if let Err(err) = tx.close().await {
log::error!("failed to close stream: {err}");
}
}
}
// log::info!("server closed connection");
// Ok(())
}
// #[derive(Debug, Parser)]
// struct Cli {
// /// Server IP address.
// #[clap(long, default_value = "127.0.0.1")]
// ip: IpAddr,
// /// Server port.
// #[clap(long, default_value = "13337")]
// port: u16,
// }
// #[tokio::main]
// async fn main() -> eyre::Result<()> {
// let cli = Cli::parse();
// let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel::<ControlMessage>();
// // Spawn client in a separate task
// let client_handle = tokio::spawn(async move {
// let remote_addr = SocketAddr::from((cli.ip, cli.port));
// Client::run(remote_addr, rx_control).await
// });
// // Handle stdin commands in the main task
// let mut stdin = BufReader::new(tokio::io::stdin());
// let mut line = String::new();
// loop {
// line.clear();
// if stdin.read_line(&mut line).await? == 0 {
// break; // EOF reached
// }
// // Trim whitespace and handle commands
// match line.trim() {
// "list" => {
// tx_control.send(ControlMessage::ListGames)?;
// }
// cmd if cmd.starts_with("get ") => {
// if let Ok(id) = cmd[4..].trim().parse::<u64>() {
// tx_control.send(ControlMessage::GetGame(id))?;
// } else {
// println!("Invalid game ID");
// }
// }
// "quit" | "exit" => break,
// "" => continue,
// _ => println!("Unknown command. Available commands: list, get <id>, quit"),
// }
// }
// client_handle.await??;
// Ok(())
// }

View File

@ -0,0 +1,10 @@
[package]
name = "lanspread-compat"
version = "0.1.0"
edition = "2021"
[dependencies]
eyre = { workspace = true }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] }
serde = { workspace = true }
tracing = { workspace = true }

View File

@ -0,0 +1,47 @@
use std::path::Path;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePool;
#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct EtiGame {
pub game_id: String,
pub game_title: String,
pub game_key: String,
pub game_release: String,
pub game_publisher: String,
pub game_size: f64,
pub game_readme_de: String,
pub game_readme_en: String,
pub game_readme_fr: String,
pub game_maxplayers: u32,
pub game_master_req: i32,
pub genre_de: String,
pub game_version: String,
}
/// # Errors
pub async fn get_games(db: &Path) -> eyre::Result<Vec<EtiGame>> {
let pool = SqlitePool::connect(format!("sqlite:{}", db.to_string_lossy()).as_str()).await?;
let mut games = sqlx::query_as::<_, EtiGame>(
"SELECT
g.game_id, g.game_title, g.game_key, g.game_release,
g.game_publisher, CAST(g.game_size AS REAL) as game_size, g.game_readme_de,
g.game_readme_en, g.game_readme_fr, CAST(g.game_maxplayers AS INTEGER) as game_maxplayers,
g.game_master_req, ge.genre_de, g.game_version
FROM games g
JOIN genre ge ON g.genre_id = ge.genre_id",
)
.fetch_all(&pool)
.await?;
games.sort_by(|a, b| a.game_title.cmp(&b.game_title));
tracing::info!("Found {} games in game.db", games.len());
for game in &games {
tracing::debug!("{}: {}", game.game_id, game.game_title);
}
Ok(games)
}

View File

@ -0,0 +1 @@
pub mod eti;

View File

@ -13,7 +13,8 @@ unwrap_used = "warn"
[dependencies]
# external
eyre = { workspace = true}
semver = { workspace = true}
serde = { workspace = true}
serde_json = { workspace = true}
eyre = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }

View File

@ -1,33 +1,30 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::doc_markdown)]
use std::{
collections::HashMap,
fmt,
fs::{File, OpenOptions},
path::Path,
};
use std::{collections::HashMap, fmt};
use serde::{Deserialize, Serialize};
use crate::serialization::version_serde;
/// A game
#[derive(Clone, Serialize, Deserialize)]
pub struct Game {
/// example: 1
pub id: u64,
/// example: Call of Duty 3
/// example: aoe2
pub id: String,
/// example: Age of Empires 2
pub name: String,
/// example: A shooter game in war.
/// example: Dieses Paket enthält die original AoE 2 Version,...
pub description: String,
/// example: `call_of_duty.tar.zst`
pub install_archive: String,
/// example: 1999
pub release_year: String,
/// Microsoft
pub publisher: String,
/// example: 8
pub max_players: u32,
/// example: 1.0.0
#[serde(with = "version_serde")]
pub version: semver::Version,
/// size (bytes)
/// example: 3.5
pub version: String,
/// example: Echtzeit-Strategie
pub genre: String,
/// size in bytes: example: 3455063152
pub size: u64,
}
@ -35,14 +32,10 @@ impl fmt::Debug for Game {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {} {} ({} players) ({}: {} MB) {}",
"{}: {} ({} MB)",
self.id,
self.name,
self.version,
self.max_players,
self.install_archive,
self.size,
self.description,
self.size / 1024 / 1024,
)
}
}
@ -75,8 +68,7 @@ impl Ord for Game {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameDB {
pub games: HashMap<u64, Game>,
next_id: u64,
pub games: HashMap<String, Game>,
}
impl GameDB {
@ -84,7 +76,6 @@ impl GameDB {
pub fn new() -> Self {
GameDB {
games: HashMap::new(),
next_id: 1,
}
}
@ -92,39 +83,17 @@ impl GameDB {
pub fn from(games: Vec<Game>) -> Self {
let mut db = GameDB::new();
for game in games {
let id = game.id;
db.games.insert(game.id, game);
db.next_id = db.next_id.max(id + 1);
db.games.insert(game.id.clone(), game);
}
db
}
pub fn add_game<S: Into<String>>(
&mut self,
name: S,
description: S,
install_archive: S,
max_players: u32,
version: semver::Version,
) -> u64 {
let id = self.next_id;
self.next_id += 1;
let game = Game {
id,
name: name.into(),
description: description.into(),
install_archive: install_archive.into(),
max_players,
version,
size: 0,
};
self.games.insert(id, game);
id
}
#[must_use]
pub fn get_game_by_id(&self, id: u64) -> Option<&Game> {
self.games.get(&id)
pub fn get_game_by_id<S>(&self, id: S) -> Option<&Game>
where
S: AsRef<str>,
{
self.games.get(id.as_ref())
}
#[must_use]
@ -132,56 +101,10 @@ impl GameDB {
self.games.values().find(|game| game.name == name)
}
pub fn update_game<S: Into<String>>(
&mut self,
id: u64,
name: Option<S>,
description: Option<S>,
install_archive: Option<S>,
) -> bool {
if let Some(game) = self.games.get_mut(&id) {
if let Some(new_name) = name {
game.name = new_name.into();
}
if let Some(new_description) = description {
game.description = new_description.into();
}
if let Some(archive) = install_archive {
game.install_archive = archive.into();
}
true
} else {
false
}
}
pub fn delete_game(&mut self, id: u64) -> bool {
self.games.remove(&id).is_some()
}
#[must_use]
pub fn all_games(&self) -> Vec<&Game> {
self.games.values().collect()
}
pub fn save_to_file(&self, path: &Path) -> eyre::Result<()> {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
let games: Vec<&Game> = self.games.values().collect();
serde_json::to_writer(file, &games)?;
Ok(())
}
pub fn load_from_file(path: &Path) -> eyre::Result<Self> {
let file = File::open(path)?;
let games: Vec<Game> = serde_json::from_reader(file)?;
let db = GameDB::from(games);
Ok(db)
}
}
impl Default for GameDB {

View File

@ -1,2 +1 @@
pub mod db;
mod serialization;

View File

@ -1,19 +0,0 @@
pub(crate) mod version_serde {
use semver::Version;
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&version.to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Version::parse(&s).map_err(serde::de::Error::custom)
}
}

View File

@ -6,7 +6,7 @@ use tracing::error;
#[derive(Debug, Serialize, Deserialize)]
pub enum Request {
ListGames,
GetGame { id: u64 },
GetGame { id: String },
Invalid(Vec<u8>, String),
}
@ -14,7 +14,7 @@ pub enum Request {
pub enum Response {
Games(Vec<Game>),
Game(Game),
GameNotFound(u64),
GameNotFound(String),
InvalidRequest(Vec<u8>, String),
EncodingError(String),
DecodingError(Vec<u8>, String),

View File

@ -13,6 +13,7 @@ unwrap_used = "warn"
[dependencies]
# local
lanspread-compat = { path = "../lanspread-compat" }
lanspread-db = { path = "../lanspread-db" }
lanspread-mdns = { path = "../lanspread-mdns" }
lanspread-proto = { path = "../lanspread-proto" }

View File

@ -1,11 +1,14 @@
#![allow(clippy::doc_markdown)]
use std::{
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
path::PathBuf,
sync::Arc,
};
use clap::Parser;
use lanspread_db::db::GameDB;
use lanspread_compat::eti::{self, EtiGame};
use lanspread_db::db::{Game, GameDB};
use lanspread_mdns::{
DaemonEvent,
MdnsAdvertiser,
@ -15,18 +18,13 @@ use lanspread_mdns::{
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::Server as QuicServer;
use testing::generate_test_db;
use tokio::{io::AsyncWriteExt, sync::Mutex};
use tracing_subscriber::EnvFilter;
mod testing;
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem"));
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
pub(crate) struct Server {
db_path: PathBuf,
}
pub(crate) struct Server;
#[derive(Clone, Debug)]
struct ServerCtx {
@ -40,20 +38,14 @@ struct ConnectionCtx {
}
impl Server {
fn new<S: Into<PathBuf>>(db_path: S) -> Self {
Server {
db_path: db_path.into(),
}
}
async fn run(&mut self, addr: SocketAddr) -> eyre::Result<()> {
async fn run(addr: SocketAddr, db: GameDB) -> eyre::Result<()> {
let mut server = QuicServer::builder()
.with_tls((CERT_PEM, KEY_PEM))?
.with_io(addr)?
.start()?;
let server_ctx = Arc::new(ServerCtx {
handler: RequestHandler::new(&self.db_path)?,
handler: RequestHandler::new(db),
});
while let Some(mut connection) = server.accept().await {
@ -126,11 +118,10 @@ struct RequestHandler {
}
impl RequestHandler {
fn new(db_path: &Path) -> eyre::Result<Self> {
let db = GameDB::load_from_file(db_path)?;
Ok(RequestHandler {
db: Arc::new(Mutex::new(db)),
})
fn new(games: GameDB) -> RequestHandler {
RequestHandler {
db: Arc::new(Mutex::new(games)),
}
}
async fn handle_request(&self, request: Request) -> Response {
@ -141,7 +132,7 @@ impl RequestHandler {
}
Request::GetGame { id } => {
let db = self.db.lock().await;
match db.get_game_by_id(id) {
match db.get_game_by_id(&id) {
Some(game) => Response::Game(game.clone()),
None => Response::GameNotFound(id),
}
@ -158,16 +149,33 @@ impl RequestHandler {
}
}
const GAME_DB_PATH: &str = "/home/pfs/shm/game.db";
#[derive(Debug, Parser)]
struct Cli {
/// The IP address to bind to.
#[clap(long, default_value = "127.0.0.1")]
/// IP address to bind to.
#[clap(long)]
ip: IpAddr,
/// The listen port.
#[clap(long, default_value = "13337")]
/// Listen port.
#[clap(long)]
port: u16,
/// Game database path (SQLite).
#[clap(long)]
db: PathBuf,
}
fn eti_game_to_game(eti_game: EtiGame) -> Game {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
Game {
id: eti_game.game_id,
name: eti_game.game_title,
description: eti_game.game_readme_de,
release_year: eti_game.game_release,
publisher: eti_game.game_publisher,
max_players: eti_game.game_maxplayers,
version: eti_game.game_version,
genre: eti_game.genre_de,
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
}
}
#[tokio::main]
@ -178,7 +186,9 @@ async fn main() -> eyre::Result<()> {
let cli = Cli::parse();
generate_test_db(GAME_DB_PATH);
let eti_games = eti::get_games(&cli.db).await?;
let games: Vec<Game> = eti_games.into_iter().map(eti_game_to_game).collect();
let game_db = GameDB::from(games);
let mdns = MdnsAdvertiser::new(
LANSPREAD_SERVICE_TYPE,
@ -190,7 +200,7 @@ async fn main() -> eyre::Result<()> {
while let Ok(event) = mdns.monitor.recv() {
tracing::info!("mDNS: {:?}", &event);
if let DaemonEvent::Error(e) = event {
tracing::info!("Failed: {e}");
tracing::error!("mDNS: {e}");
break;
}
}
@ -198,6 +208,5 @@ async fn main() -> eyre::Result<()> {
tracing::info!("Server listening on {}:{}", cli.ip, cli.port);
let mut server = Server::new(GAME_DB_PATH);
server.run(SocketAddr::from((cli.ip, cli.port))).await
Server::run(SocketAddr::from((cli.ip, cli.port)), game_db).await
}

View File

@ -1,35 +0,0 @@
#![allow(clippy::unwrap_used)]
use std::path::PathBuf;
use lanspread_db::db::GameDB;
pub(crate) fn generate_test_db<P: Into<PathBuf>>(db_path: P) {
let db_path = db_path.into();
let mut db = GameDB::new();
db.add_game(
"Call of Duty 3",
"A shooter game in war.",
"call_of_duty.tar.zst",
64,
semver::Version::new(1, 0, 0),
);
db.add_game(
"Counter-Strike Source",
"Valve's iconic shooter.",
"cstrike.tar.zst",
32,
semver::Version::new(1, 0, 0),
);
db.add_game(
"Factorio",
"Best game of all time, seriously.",
"factorio.tar.zst",
128,
semver::Version::new(1, 0, 0),
);
db.update_game(1, Some("Call of Duty 4"), None, None);
db.save_to_file(&db_path).unwrap();
}

View File

@ -1,4 +1,4 @@
use std::{net::SocketAddr, process::Command};
use std::net::SocketAddr;
use lanspread_client::{ClientCommand, ClientEvent};
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
@ -22,24 +22,26 @@ fn request_games(state: tauri::State<LanSpreadState>) {
}
#[tauri::command]
fn run_game_backend(id: u64, state: tauri::State<LanSpreadState>) -> String {
fn run_game_backend(id: String, state: tauri::State<LanSpreadState>) -> String {
log::error!("Running game with id {id}");
let result = Command::new(r#"C:\Users\ddidderr\scoop\apps\mpv\0.39.0\mpv.exe"#).spawn();
// let result = Command::new(r#"C:\Users\ddidderr\scoop\apps\mpv\0.39.0\mpv.exe"#).spawn();
if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) {
log::error!("Failed to send message to client: {e:?}");
}
if result.is_ok() {
"Ok".to_string()
} else {
"Failed to run game".to_string()
}
"TODO".to_string()
// if result.is_ok() {
// "Ok".to_string()
// } else {
// "Failed to run game".to_string()
// }
}
async fn find_server(app: AppHandle) {
log::error!("Looking for server...");
log::info!("Looking for server...");
loop {
match discover_service(LANSPREAD_SERVICE_TYPE, Some(LANSPREAD_INSTANCE_NAME)) {
@ -73,7 +75,7 @@ pub fn run() {
tauri_plugin_log::TargetKind::Stdout,
))
.level(log::LevelFilter::Info)
.level_for("lanspread_client", log::LevelFilter::Debug)
.level_for("lanspread_client", log::LevelFilter::Trace)
.level_for("lanspread_tauri_leptos_lib", log::LevelFilter::Debug)
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
@ -106,7 +108,7 @@ pub fn run() {
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
while let Some(event) = rx_client_event.recv().await {
log::debug!("Received client event: {event:?}");
log::debug!("Received client event");
match event {
ClientEvent::ListGames(games) => {

View File

@ -16,7 +16,7 @@ extern "C" {
#[derive(Serialize, Deserialize)]
struct RunGameArgs {
id: u64,
id: String,
}
#[derive(Serialize, Deserialize)]
@ -59,14 +59,14 @@ pub fn App() -> impl IntoView {
});
// Call list_games on component mount
create_effect(move |_| {
spawn_local(async move {
let args = serde_wasm_bindgen::to_value(&()).unwrap();
invoke("request_games", args).await;
});
});
// create_effect(move |_| {
// spawn_local(async move {
// let args = serde_wasm_bindgen::to_value(&()).unwrap();
// invoke("request_games", args).await;
// });
// });
let run_game = move |id: u64| {
let run_game = move |id: String| {
log_1(&JsValue::from_str(format!("id={id}").as_str()));
spawn_local(async move {
let args = serde_wasm_bindgen::to_value(&RunGameArgs { id }).unwrap();
@ -87,7 +87,7 @@ pub fn App() -> impl IntoView {
{move || game_items.get().into_iter()
.map(|item|
view! {
<div class="item" on:click=move |_| run_game(item.id)>
<div class="item" on:click=move |_| run_game(item.id.clone())>
<img src="https://via.placeholder.com/200x150" alt="Item Image" />
<div class="item-name">{ &item.name }</div>
<div class="description">