Basic Client server combo

This commit is contained in:
Elias Stepanik 2025-04-04 04:01:57 +02:00
parent bc18013601
commit 8f86a7997f
20 changed files with 304 additions and 136 deletions

View File

@ -17,4 +17,5 @@ bevy_render = "0.15.0"
bevy_window = "0.15.0" bevy_window = "0.15.0"
egui_dock = "0.14.0" egui_dock = "0.14.0"
spacetimedb-sdk = "1.0" spacetimedb-sdk = "1.0"
hex = "0.4" hex = "0.4"
random_word = { version = "0.5.0", features = ["en"] }

View File

@ -7,7 +7,7 @@ use crate::helper::*;
use bevy_egui::EguiSet; use bevy_egui::EguiSet;
use bevy_render::extract_resource::ExtractResourcePlugin; use bevy_render::extract_resource::ExtractResourcePlugin;
use spacetimedb_sdk::{credentials, DbContext, Error, Event, Identity, Status, Table, TableWithPrimaryKey}; use spacetimedb_sdk::{credentials, DbContext, Error, Event, Identity, Status, Table, TableWithPrimaryKey};
use crate::helper::database::setup_database; use crate::plugins::network::systems::database::setup_database;
use crate::module_bindings::DbConnection; use crate::module_bindings::DbConnection;
pub struct AppPlugin; pub struct AppPlugin;

View File

@ -1,119 +0,0 @@
use bevy::prelude::{Commands, Resource};
use bevy::utils::info;
use spacetimedb_sdk::{credentials, DbContext, Error, Event, Identity, Status, Table, TableWithPrimaryKey};
use crate::module_bindings::*;
/// The URI of the SpacetimeDB instance hosting our chat module.
const HOST: &str = "http://192.168.178.10:3000";
/// The database name we chose when we published our module.
const DB_NAME: &str = "horror-game";
#[derive(Resource)]
pub struct DbConnectionResource(pub(crate) DbConnection);
pub fn setup_database(mut commands: Commands) {
// Call your connection function and insert the connection as a resource.
let ctx = connect_to_db();
register_callbacks(&ctx);
// Subscribe to SQL queries in order to construct a local partial replica of the database.
subscribe_to_tables(&ctx);
// Spawn a thread, where the connection will process messages and invoke callbacks.
ctx.run_threaded();
commands.insert_resource(DbConnectionResource(ctx));
}
/// Register subscriptions for all rows of both tables.
fn subscribe_to_tables(ctx: &DbConnection) {
ctx.subscription_builder()
.on_applied(on_sub_applied)
.on_error(on_sub_error)
.subscribe(["SELECT * FROM physics_world"]);
}
/// Our `on_subscription_applied` callback:
/// sort all past messages and print them in timestamp order.
fn on_sub_applied(ctx: &SubscriptionEventContext) {
println!("Fully connected and all subscriptions applied.");
println!("Use /name to set your name, or type a message!");
}
/// Or `on_error` callback:
/// print the error, then exit the process.
fn on_sub_error(_ctx: &ErrorContext, err: Error) {
eprintln!("Subscription failed: {}", err);
std::process::exit(1);
}
fn connect_to_db() -> DbConnection {
DbConnection::builder()
// Register our `on_connect` callback, which will save our auth token.
.on_connect(on_connected)
// Register our `on_connect_error` callback, which will print a message, then exit the process.
/*.on_connect_error(on_connect_error)*/
// Our `on_disconnect` callback, which will print a message, then exit the process.
.on_disconnect( on_disconnected)
// If the user has previously connected, we'll have saved a token in the `on_connect` callback.
// In that case, we'll load it and pass it to `with_token`,
// so we can re-authenticate as the same `Identity`.
.with_token(creds_store().load().expect("Error loading credentials"))
// Set the database name we chose when we called `spacetime publish`.
.with_module_name(DB_NAME)
// Set the URI of the SpacetimeDB host that's running our database.
.with_uri(HOST)
// Finalize configuration and connect!
.build()
.expect("Failed to connect")
}
/// Register all the callbacks our app will use to respond to database events.
fn register_callbacks(ctx: &DbConnection) {
/*// When a new message is received, print it.
ctx.db.message().on_insert(on_message_inserted);*/
/*// When we fail to set our name, print a warning.
ctx.reducers.on_set_name(on_name_set);*/
// When we fail to send a message, print a warning.
/*ctx.reducers.on_send_message(on_message_sent);*/
}
fn creds_store() -> credentials::File {
credentials::File::new("quickstart-chat")
}
/// Our `on_connect` callback: save our credentials to a file.
fn on_connected(_ctx: &DbConnection, _identity: Identity, token: &str) {
if let Err(e) = creds_store().save(token) {
eprintln!("Failed to save credentials: {:?}", e);
}
}
/// Our `on_connect_error` callback: print the error, then exit the process.
fn on_connect_error(_ctx: &ErrorContext, err: Error) {
eprintln!("Connection error: {:?}", err);
std::process::exit(1);
}
/// Our `on_disconnect` callback: print a note, then exit the process.
fn on_disconnected(_ctx: &ErrorContext, err: Option<Error>) {
if let Some(err) = err {
eprintln!("Disconnected: {}", err);
std::process::exit(1);
} else {
println!("Disconnected.");
std::process::exit(0);
}
}

View File

@ -1,3 +1,2 @@
pub mod debug_gizmos; pub mod debug_gizmos;
pub mod egui_dock; pub mod egui_dock;
pub mod database;

View File

@ -4,6 +4,9 @@ use bevy::math::Vec3;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_render::camera::{Exposure, PhysicalCameraParameters, Projection}; use bevy_render::camera::{Exposure, PhysicalCameraParameters, Projection};
use bevy_window::CursorGrabMode; use bevy_window::CursorGrabMode;
use random_word::Lang;
use crate::module_bindings::set_name;
use crate::plugins::network::systems::database::DbConnectionResource;
#[derive(Component)] #[derive(Component)]
pub struct CameraController { pub struct CameraController {
@ -54,6 +57,7 @@ pub fn camera_controller_system(
mut windows: Query<&mut Window>, mut windows: Query<&mut Window>,
mut query: Query<(&mut Transform, &mut CameraController)>, mut query: Query<(&mut Transform, &mut CameraController)>,
mut app_exit_events: EventWriter<AppExit>, mut app_exit_events: EventWriter<AppExit>,
mut ctx: ResMut<DbConnectionResource>,
) { ) {
let mut window = windows.single_mut(); let mut window = windows.single_mut();
let (mut transform, mut controller) = query.single_mut(); let (mut transform, mut controller) = query.single_mut();
@ -92,6 +96,12 @@ pub fn camera_controller_system(
} }
} }
let word = random_word::get(Lang::En);
if keyboard_input.just_pressed(KeyCode::KeyQ) {
ctx.0.reducers.set_name(word.to_string()).unwrap();
}
// ==================== // ====================
// 3) Handle Keyboard Movement (WASD, Space, Shift) // 3) Handle Keyboard Movement (WASD, Space, Shift)
// ==================== // ====================
@ -147,6 +157,8 @@ pub fn camera_controller_system(
} }
} }
// ======================= // =======================
// 7) Exit on Escape // 7) Exit on Escape
// ======================= // =======================

View File

@ -1,18 +1,15 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::helper::database::DbConnectionResource;
pub fn init(mut commands: Commands) {
pub fn init(mut commands: Commands, db_connection: Res<DbConnectionResource>) {
} }
pub fn fixed_update(time: Res<Time>, db_connection: Res<DbConnectionResource>) { pub fn fixed_update(time: Res<Time>,) {
} }
pub fn update(time: Res<Time>, db_connection: Res<DbConnectionResource>) { pub fn update(time: Res<Time>,) {
} }

View File

@ -1,3 +1,5 @@
pub mod camera; pub mod camera;
pub mod environment; pub mod environment;
pub mod ui; pub mod ui;
pub mod network;

View File

@ -0,0 +1,2 @@
pub(crate) mod systems;
mod network_plugin;

View File

@ -0,0 +1,13 @@
use bevy::app::{App, Plugin, Startup};
use bevy::color::palettes::basic::{GREEN, YELLOW};
use bevy::color::palettes::css::RED;
use bevy::prelude::*;
use crate::plugins::environment::systems::environment_system::*;
use crate::plugins::network::systems::database::setup_database;
pub struct NetworkPlugin;
impl Plugin for NetworkPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup_database);
}
}

View File

@ -0,0 +1,50 @@
use bevy::log::{error, info};
use spacetimedb_sdk::Status;
use crate::module_bindings::{EventContext, Player, ReducerEventContext, RemoteDbContext};
/// Our `User::on_insert` callback:
/// if the user is online, print a notification.
pub fn on_user_inserted(_ctx: &EventContext, user: &Player) {
if user.online {
info!("User {} connected.", user_name_or_identity(user));
}
}
pub fn user_name_or_identity(user: &Player) -> String {
user.name
.clone()
.unwrap_or_else(|| user.identity.to_hex().to_string())
}
/// Our `User::on_update` callback:
/// print a notification about name and status changes.
pub fn on_user_updated(_ctx: &EventContext, old: &Player, new: &Player) {
if old.name != new.name {
info!(
"User {} renamed to {}.",
user_name_or_identity(old),
user_name_or_identity(new)
);
}
if old.online && !new.online {
info!("User {} disconnected.", user_name_or_identity(new));
}
if !old.online && new.online {
info!("User {} connected.", user_name_or_identity(new));
}
}
/// Our `on_set_name` callback: print a warning if the reducer failed.
pub fn on_name_set(ctx: &ReducerEventContext, name: &String) {
if let Status::Failed(err) = &ctx.event.status {
error!("Failed to change name to {:?}: {}", name, err);
}
}
/// Our `on_send_message` callback: print a warning if the reducer failed.
pub fn on_message_sent(ctx: &ReducerEventContext, text: &String) {
if let Status::Failed(err) = &ctx.event.status {
error!("Failed to send message {:?}: {}", text, err);
}
}

View File

@ -0,0 +1,31 @@
use spacetimedb_sdk::{credentials, Error, Identity};
use crate::module_bindings::{DbConnection, ErrorContext};
pub fn creds_store() -> credentials::File {
credentials::File::new("token")
}
/// Our `on_connect` callback: save our credentials to a file.
pub fn on_connected(_ctx: &DbConnection, _identity: Identity, token: &str) {
if let Err(e) = creds_store().save(token) {
eprintln!("Failed to save credentials: {:?}", e);
}
}
/// Our `on_connect_error` callback: print the error, then exit the process.
pub fn on_connect_error(_ctx: &ErrorContext, err: Error) {
eprintln!("Connection error: {:?}", err);
std::process::exit(1);
}
/// Our `on_disconnect` callback: print a note, then exit the process.
pub fn on_disconnected(_ctx: &ErrorContext, err: Option<Error>) {
if let Some(err) = err {
eprintln!("Disconnected: {}", err);
std::process::exit(1);
} else {
println!("Disconnected.");
std::process::exit(0);
}
}

View File

@ -0,0 +1,61 @@
use bevy::prelude::{Commands, Resource};
use bevy::utils::info;
use spacetimedb_sdk::{credentials, DbContext, Error, Event, Identity, Status, Table, TableWithPrimaryKey};
use crate::module_bindings::*;
use crate::plugins::network::systems::callbacks::*;
use crate::plugins::network::systems::connection::*;
use crate::plugins::network::systems::subscriptions::*;
/// The URI of the SpacetimeDB instance hosting our chat module.
const HOST: &str = "http://192.168.178.10:3000";
/// The database name we chose when we published our module.
const DB_NAME: &str = "horror-game-test";
#[derive(Resource)]
pub struct DbConnectionResource(pub(crate) DbConnection);
pub fn setup_database(mut commands: Commands) {
// Call your connection function and insert the connection as a resource.
let ctx = connect_to_db();
register_callbacks(&ctx);
subscribe_to_tables(&ctx);
ctx.run_threaded();
commands.insert_resource(DbConnectionResource(ctx));
}
/// Register subscriptions for all rows of both tables
fn connect_to_db() -> DbConnection {
DbConnection::builder()
.on_connect(on_connected)
.on_connect_error(on_connect_error)
.on_disconnect( on_disconnected)
.with_module_name(DB_NAME)
.with_uri(HOST)
.build()
.expect("Failed to connect")
}
/// Register all the callbacks our app will use to respond to database events.
fn register_callbacks(ctx: &DbConnection) {
// When a new user joins, print a notification.
ctx.db.player().on_insert(on_user_inserted);
// When a user's status changes, print a notification.
ctx.db.player().on_update(on_user_updated);
// When we fail to set our name, print a warning.
ctx.reducers.on_set_name(on_name_set);
}
fn subscribe_to_tables(ctx: &DbConnection) {
ctx.subscription_builder()
.on_applied(on_sub_applied)
.on_error(on_sub_error)
.subscribe(["SELECT * FROM player"]);
}

View File

@ -0,0 +1,4 @@
pub mod database;
mod connection;
mod callbacks;
mod subscriptions;

View File

@ -0,0 +1,23 @@
use bevy::prelude::info;
use spacetimedb_sdk::{Error, Table};
use crate::module_bindings::{ErrorContext, PlayerTableAccess, SubscriptionEventContext};
/// Our `on_subscription_applied` callback:
/// sort all past messages and print them in timestamp order.
pub fn on_sub_applied(ctx: &SubscriptionEventContext) {
let mut players = ctx.db.player().iter().collect::<Vec<_>>();
players.sort_by_key(|p| p.name.clone());
for player in players {
println!("Player {:?} online", player.name);
}
println!("Fully connected and all subscriptions applied.");
println!("Use /name to set your name, or type a message!");
}
/// Or `on_error` callback:
/// print the error, then exit the process.
pub fn on_sub_error(_ctx: &ErrorContext, err: Error) {
eprintln!("Subscription failed: {}", err);
std::process::exit(1);
}

View File

@ -2,6 +2,6 @@
@echo off @echo off
REM Script to publish the horror-game project using spacetime REM Script to publish the horror-game project using spacetime
spacetime publish -c --project-path server horror-game -y spacetime publish -c --project-path server horror-game-test -y
rm client\src\module_bindings\* rm client\src\module_bindings\*
spacetime generate --lang rust --out-dir client/src/module_bindings --project-path server spacetime generate --lang rust --out-dir client/src/module_bindings --project-path server

View File

@ -1,10 +1,47 @@
mod types;
use spacetimedb::{reducer, ReducerContext, SpacetimeType, Table};
use crate::types::player::{player, Player};
#[spacetimedb::table(name = config, public)]
pub struct Config {
#[primary_key]
pub id: u32,
}
#[spacetimedb::reducer]
pub fn test(ctx: &ReducerContext) -> Result<(), String> {
log::debug!("This reducer was called by {}.", ctx.sender);
Ok(())
}
use std::time::Duration; #[reducer(client_connected)]
use spacetimedb::{ReducerContext, ScheduleAt, Table}; // Called when a client connects to the SpacetimeDB
pub fn client_connected(ctx: &ReducerContext) {
#[spacetimedb::reducer(init)] if let Some(player) = ctx.db.player().identity().find(ctx.sender) {
pub fn init(ctx: &ReducerContext) { // If this is a returning player, i.e. we already have a `player` with this `Identity`,
log::info!("Initializing..."); // set `online: true`, but leave `name` and `identity` unchanged.
ctx.db.player().identity().update(Player { online: true, ..player });
} else {
// If this is a new player, create a `player` row for the `Identity`,
// which is online, but hasn't set a name.
ctx.db.player().insert(Player {
name: None,
identity: ctx.sender,
online: true,
});
}
}
#[reducer(client_disconnected)]
// Called when a client disconnects from SpacetimeDB
pub fn identity_disconnected(ctx: &ReducerContext) {
if let Some(player) = ctx.db.player().identity().find(ctx.sender) {
ctx.db.player().identity().update(Player { online: false, ..player });
} else {
// This branch should be unreachable,
// as it doesn't make sense for a client to disconnect without connecting first.
log::warn!("Disconnect event for unknown Player with identity {:?}", ctx.sender);
}
} }

2
server/src/types/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod vec3;
pub mod player;

View File

@ -0,0 +1,31 @@
use spacetimedb::{reducer, Identity, ReducerContext};
#[spacetimedb::table(name = player, public)]
#[derive(Debug, Clone)]
pub struct Player {
#[primary_key]
pub identity: Identity,
pub name: Option<String>,
pub online: bool,
}
#[reducer]
/// Clients invoke this reducer to set their user names.
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
let name = validate_name(name)?;
if let Some(user) = ctx.db.player().identity().find(ctx.sender) {
ctx.db.player().identity().update(Player { name: Some(name), ..user });
Ok(())
} else {
Err("Cannot set name for unknown user".to_string())
}
}
/// Takes a name and checks if it's acceptable as a user's name.
fn validate_name(name: String) -> Result<String, String> {
if name.is_empty() {
Err("Names must not be empty".to_string())
} else {
Ok(name)
}
}

20
server/src/types/vec3.rs Normal file
View File

@ -0,0 +1,20 @@
use spacetimedb::SpacetimeType;
#[derive(SpacetimeType, Clone, Debug)]
pub struct DbVector3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[spacetimedb::table(name = entity, public)]
#[derive(Debug, Clone)]
pub struct Entity {
// The `auto_inc` attribute indicates to SpacetimeDB that
// this value should be determined by SpacetimeDB on insert.
#[auto_inc]
#[primary_key]
pub entity_id: u32,
pub position: DbVector3,
pub mass: u32,
}

2
watch_logs.bat Normal file
View File

@ -0,0 +1,2 @@
for /l %g in () do @( spacetime logs horror-game-test )