DEVELOPMENT ENVIRONMENT

~liljamo/deck-builder

a8f056d1523e00e69662e2868f3976103ead82ff — Jonni Liljamo 1 year, 9 months ago 85f88d2
Move register/login to plugins, implement mostly
7 files changed, 427 insertions(+), 53 deletions(-)

M sdbclient/src/main.rs
A sdbclient/src/plugins/menu/accountlogin/mod.rs
R sdbclient/src/plugins/menu/{accountloginui.rs => accountlogin/ui.rs}
A sdbclient/src/plugins/menu/accountregister/mod.rs
R sdbclient/src/plugins/menu/{accountregisterui.rs => accountregister/ui.rs}
M sdbclient/src/plugins/menu/accountscreenloggedin.rs
M sdbclient/src/plugins/menu/mod.rs
M sdbclient/src/main.rs => sdbclient/src/main.rs +1 -16
@@ 28,14 28,6 @@ pub enum GameState {
    Game,
}

// Input structs
/// Login inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct InputsUserLogin(String, String);
/// Register inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct InputsUserRegister(String, String, String, String);

fn main() {
    let mut app = App::new();



@@ 97,14 89,7 @@ fn main() {
    })
    .insert_resource(cfg::CfgHidden {
        api_server: "http://localhost:8080/api".to_string(),
    })
    .insert_resource(InputsUserLogin("".to_string(), "".to_string()))
    .insert_resource(InputsUserRegister(
        "".to_string(),
        "".to_string(),
        "".to_string(),
        "".to_string(),
    ));
    });

    app.add_startup_system(setup).add_state(GameState::Splash);


A sdbclient/src/plugins/menu/accountlogin/mod.rs => sdbclient/src/plugins/menu/accountlogin/mod.rs +123 -0
@@ 0,0 1,123 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    tasks::{AsyncComputeTaskPool, Task},
};
use bevy_console::PrintConsoleLine;

use futures_lite::future;

use crate::{
    api::{self, user::ResponseToken},
    cfg::{self, CfgDirs, CfgHidden, CfgUser},
    util,
};

use super::MenuState;

pub mod ui;

pub struct AccountLoginPlugin;

impl Plugin for AccountLoginPlugin {
    fn build(&self, app: &mut App) {
        app.add_state(LoginState::None)
            // UI system
            .insert_resource(ui::InputsUserLogin::new())
            .add_system_set(
                SystemSet::on_update(LoginState::Input).with_system(ui::account_login_ui),
            )
            // Login system, as in calling the API
            .add_system_set(
                SystemSet::on_enter(LoginState::LoggingIn).with_system(start_login_call),
            )
            .add_system_set(
                SystemSet::on_update(LoginState::LoggingIn).with_system(handle_login_call),
            );
    }
}

/// Login State
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub enum LoginState {
    None,
    Input,
    LoggingIn,
}

#[derive(Component)]
struct LoginCall(Task<ResponseToken>);

fn start_login_call(
    mut commands: Commands,
    cfg_hidden: Res<CfgHidden>,
    inputs: Res<ui::InputsUserLogin>,
) {
    let api_address = cfg_hidden.api_server.clone();
    let i = inputs.clone();

    let thread_pool = AsyncComputeTaskPool::get();
    let task = thread_pool.spawn(async move {
        let token_response = api::user::token(api_address, i.email, i.password);

        token_response
    });
    commands.spawn(LoginCall(task));
}

fn handle_login_call(
    mut commands: Commands,
    mut login_call_tasks: Query<(Entity, &mut LoginCall)>,
    mut inputs: ResMut<ui::InputsUserLogin>,
    mut login_state: ResMut<State<LoginState>>,
    mut menu_state: ResMut<State<MenuState>>,
    mut cfg_user: ResMut<CfgUser>,
    cfg_dirs: Res<CfgDirs>,
    mut console: EventWriter<PrintConsoleLine>,
) {
    let (entity, mut task) = login_call_tasks.single_mut();
    if let Some(login_response) = future::block_on(future::poll_once(&mut task.0)) {
        match login_response {
            ResponseToken::Valid(res) => {
                console.send(PrintConsoleLine::new(format!(
                    "Logged in with: {}",
                    inputs.email
                )));

                // TODO: We need to fetch the user details at some point.
                *cfg_user = CfgUser {
                    logged_in: true,
                    user_token: res.token,
                    id: res.id,
                    username: "NOT SET".to_string(),
                    email: "NOT SET".to_string(),
                };

                util::sl::save(
                    cfg_dirs.0.config_dir().to_str().unwrap(),
                    cfg::FILE_CFG_USER,
                    &cfg_user.into_inner(),
                    console,
                );

                login_state.set(LoginState::None).unwrap();
                menu_state.set(MenuState::AccountLoggedIn).unwrap();
            }
            ResponseToken::Error { error } => {
                inputs.error = error;
                login_state.set(LoginState::Input).unwrap();
            }
        }

        // Remove the task, since it's done now
        commands.entity(entity).remove::<LoginCall>();
        commands.entity(entity).despawn_recursive();
    }
}

R sdbclient/src/plugins/menu/accountloginui.rs => sdbclient/src/plugins/menu/accountlogin/ui.rs +37 -6
@@ 9,13 9,34 @@
use bevy::prelude::*;
use bevy_egui::{egui, EguiContext};

use crate::{util::eguipwd, InputsUserLogin};
use crate::util::eguipwd;

use super::MenuState;
use crate::plugins::menu::MenuState;

use super::LoginState;

/// Login inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct InputsUserLogin {
    pub email: String,
    pub password: String,
    pub error: String,
}

impl InputsUserLogin {
    pub fn new() -> Self {
        Self {
            email: "".to_string(),
            password: "".to_string(),
            error: "".to_string(),
        }
    }
}

pub fn account_login_ui(
    mut egui_context: ResMut<EguiContext>,
    mut menu_state: ResMut<State<MenuState>>,
    mut login_state: ResMut<State<LoginState>>,
    mut inputs: ResMut<InputsUserLogin>,
) {
    egui::Window::new("Login")


@@ 23,20 44,30 @@ pub fn account_login_ui(
        .show(egui_context.ctx_mut(), |ui| {
            ui.horizontal(|ui| {
                ui.label("Email: ");
                ui.text_edit_singleline(&mut inputs.0);
                ui.text_edit_singleline(&mut inputs.email);
            });

            ui.horizontal(|ui| {
                ui.label("Password: ");
                ui.add(eguipwd::password(&mut inputs.1));
                ui.add(eguipwd::password(&mut inputs.password));
            });

            // Show an error if there is one
            if !inputs.error.is_empty() {
                ui.horizontal(|ui| {
                    ui.label(egui::RichText::new(inputs.error.clone()).color(egui::Color32::RED));
                });
            }

            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
                if ui.button("Cancel").clicked() {
                    menu_state.set(MenuState::Main).unwrap();
                    login_state.set(LoginState::None).unwrap();
                    menu_state.set(MenuState::AccountLoggedOut).unwrap();
                }
                if ui.button("Login").clicked() {
                    info!("todo");
                    login_state.set(LoginState::LoggingIn).unwrap();
                    // Reset error field
                    inputs.error = "".to_string();
                }
            })
        });

A sdbclient/src/plugins/menu/accountregister/mod.rs => sdbclient/src/plugins/menu/accountregister/mod.rs +153 -0
@@ 0,0 1,153 @@
/*
 * This file is part of sdbclient
 * Copyright (C) 2022 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::{
    prelude::*,
    tasks::{AsyncComputeTaskPool, Task},
};
use bevy_console::PrintConsoleLine;

use futures_lite::future;

use crate::{
    api::{
        self,
        user::{ResponseRegister, ResponseToken},
    },
    cfg::{self, CfgDirs, CfgHidden, CfgUser},
    util,
};

use super::MenuState;

pub mod ui;

pub struct AccountRegisterPlugin;

impl Plugin for AccountRegisterPlugin {
    fn build(&self, app: &mut App) {
        app.add_state(RegisterState::None)
            // UI system
            .insert_resource(ui::InputsUserRegister::new())
            .add_system_set(
                SystemSet::on_update(RegisterState::Input).with_system(ui::account_register_ui),
            )
            // Register system, as in calling the API
            .add_system_set(
                SystemSet::on_enter(RegisterState::Registering).with_system(start_register_call),
            )
            .add_system_set(
                SystemSet::on_update(RegisterState::Registering).with_system(handle_register_call),
            );
    }
}

/// Register State
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub enum RegisterState {
    None,
    Input,
    Registering,
}

struct RegisterCallResponse {
    register: ResponseRegister,
    token: ResponseToken,
}

#[derive(Component)]
struct RegisterCall(Task<RegisterCallResponse>);

fn start_register_call(
    mut commands: Commands,
    cfg_hidden: Res<CfgHidden>,
    inputs: Res<ui::InputsUserRegister>,
) {
    let api_address = cfg_hidden.api_server.clone();
    let i = inputs.clone();

    let thread_pool = AsyncComputeTaskPool::get();
    let task = thread_pool.spawn(async move {
        let register_response = api::user::register(
            api_address.clone(),
            i.username.clone(),
            i.email.clone(),
            i.password.clone(),
        );

        // TODO: This is bad, if the above fails, this will too. Or maybe that doesn't matter?
        let token_response =
            api::user::token(api_address.clone(), i.email.clone(), i.password.clone());

        RegisterCallResponse {
            register: register_response,
            token: token_response,
        }
    });
    commands.spawn(RegisterCall(task));
}

fn handle_register_call(
    mut commands: Commands,
    mut register_call_tasks: Query<(Entity, &mut RegisterCall)>,
    mut inputs: ResMut<ui::InputsUserRegister>,
    mut register_state: ResMut<State<RegisterState>>,
    mut menu_state: ResMut<State<MenuState>>,
    mut cfg_user: ResMut<CfgUser>,
    cfg_dirs: Res<CfgDirs>,
    mut console: EventWriter<PrintConsoleLine>,
) {
    let (entity, mut task) = register_call_tasks.single_mut();
    if let Some(register_call_response) = future::block_on(future::poll_once(&mut task.0)) {
        match register_call_response.register {
            ResponseRegister::Valid(register_res) => {
                console.send(PrintConsoleLine::new(format!(
                    "Registered user: {}",
                    register_res.username,
                )));

                match register_call_response.token {
                    ResponseToken::Valid(token_res) => {
                        *cfg_user = CfgUser {
                            logged_in: true,
                            user_token: token_res.token,
                            id: register_res.id,
                            username: register_res.username,
                            email: register_res.email,
                        };

                        util::sl::save(
                            cfg_dirs.0.config_dir().to_str().unwrap(),
                            cfg::FILE_CFG_USER,
                            &cfg_user.into_inner(),
                            console,
                        );
                    }
                    ResponseToken::Error { error } => {
                        // TODO: Handle? Is it possible to even get here without the server shitting itself between the register and token calls?
                        // And if the server does indeed shit itself between those calls, the user can just login normally, so 🤷‍♀️
                        console.send(PrintConsoleLine::new(format!(
                            "Something went wrong with getting the user token after registering, got error: '{}'", error
                        )));
                    }
                }

                register_state.set(RegisterState::None).unwrap();
                menu_state.set(MenuState::AccountLoggedIn).unwrap();
            }
            ResponseRegister::Error { error } => {
                inputs.error = error;
                register_state.set(RegisterState::Input).unwrap();
            }
        }

        // Remove the task, since it's done now
        commands.entity(entity).remove::<RegisterCall>();
        commands.entity(entity).despawn_recursive();
    }
}

R sdbclient/src/plugins/menu/accountregisterui.rs => sdbclient/src/plugins/menu/accountregister/ui.rs +58 -10
@@ 9,13 9,40 @@
use bevy::prelude::*;
use bevy_egui::{egui, EguiContext};

use crate::{util::eguipwd, InputsUserRegister};
use crate::util::eguipwd;

use super::MenuState;
use crate::plugins::menu::MenuState;

use super::RegisterState;

/// Register inputs
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone)]
pub struct InputsUserRegister {
    pub username: String,
    pub email: String,
    pub password: String,
    pub password_confirm: String,
    pub passwords_match: bool,
    pub error: String,
}

impl InputsUserRegister {
    pub fn new() -> Self {
        Self {
            username: "".to_string(),
            email: "".to_string(),
            password: "".to_string(),
            password_confirm: "".to_string(),
            passwords_match: true,
            error: "".to_string(),
        }
    }
}

pub fn account_register_ui(
    mut egui_context: ResMut<EguiContext>,
    mut menu_state: ResMut<State<MenuState>>,
    mut register_state: ResMut<State<RegisterState>>,
    mut inputs: ResMut<InputsUserRegister>,
) {
    egui::Window::new("Register")


@@ 23,31 50,52 @@ pub fn account_register_ui(
        .show(egui_context.ctx_mut(), |ui| {
            ui.horizontal(|ui| {
                ui.label("Username: ");
                ui.text_edit_singleline(&mut inputs.0);
                ui.text_edit_singleline(&mut inputs.username);
            });

            ui.horizontal(|ui| {
                ui.label("Email: ");
                ui.text_edit_singleline(&mut inputs.1);
                ui.text_edit_singleline(&mut inputs.email);
            });

            ui.horizontal(|ui| {
                ui.label("Password: ");
                ui.add(eguipwd::password(&mut inputs.2));
                ui.add(eguipwd::password(&mut inputs.password));
            });

            ui.horizontal(|ui| {
                ui.label("Confirm password: ");
                ui.add(eguipwd::password(&mut inputs.3));
                ui.add(eguipwd::password(&mut inputs.password_confirm));
            });

            inputs.passwords_match = inputs.password == inputs.password_confirm;

            // Show an error if there is one
            if !inputs.passwords_match {
                ui.horizontal(|ui| {
                    ui.label(
                        egui::RichText::new("passwords don't match").color(egui::Color32::RED),
                    );
                });
            } else if !inputs.error.is_empty() {
                ui.horizontal(|ui| {
                    ui.label(egui::RichText::new(inputs.error.clone()).color(egui::Color32::RED));
                });
            }

            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
                if ui.button("Cancel").clicked() {
                    menu_state.set(MenuState::Main).unwrap();
                }
                if ui.button("Register").clicked() {
                    info!("todo");
                    register_state.set(RegisterState::None).unwrap();
                    menu_state.set(MenuState::AccountLoggedOut).unwrap();
                }

                ui.add_enabled_ui(inputs.passwords_match, |ui| {
                    if ui.button("Register").clicked() {
                        register_state.set(RegisterState::Registering).unwrap();
                        // Reset error field
                        inputs.error = "".to_string();
                    }
                });
            })
        });
}

M sdbclient/src/plugins/menu/accountscreenloggedin.rs => sdbclient/src/plugins/menu/accountscreenloggedin.rs +37 -2
@@ 11,14 11,21 @@ use bevy::{
    ui::{JustifyContent, Size, Style, Val},
};

use crate::cfg::CfgUser;

use super::{MenuButtonAction, NORMAL_BUTTON, TEXT_COLOR};

/// Tag component for tagging entities on settings menu screen
#[derive(Component)]
pub struct OnAccountLoggedInScreen;

pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
pub fn account_loggedin_setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    cfg_user: ResMut<CfgUser>,
) {
    let font = asset_server.load("fonts/FiraMono-Regular.ttf");
    let font_bold = asset_server.load("fonts/FiraMono-Bold.ttf");

    let button_style = Style {
        size: Size::new(Val::Px(200.0), Val::Px(65.0)),


@@ 63,6 70,34 @@ pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetSer
                    ..Default::default()
                }),
            );
            parent.spawn(
                TextBundle::from_section(
                    "Logged in as: ",
                    TextStyle {
                        font: font.clone(),
                        font_size: 25.0,
                        color: TEXT_COLOR,
                    },
                )
                .with_style(Style {
                    margin: UiRect::all(Val::Px(50.)),
                    ..Default::default()
                }),
            );
            parent.spawn(
                TextBundle::from_section(
                    cfg_user.username.clone(),
                    TextStyle {
                        font: font_bold.clone(),
                        font_size: 40.0,
                        color: TEXT_COLOR,
                    },
                )
                .with_style(Style {
                    margin: UiRect::all(Val::Px(50.)),
                    ..Default::default()
                }),
            );
            parent
                .spawn(NodeBundle {
                    style: Style {


@@ 75,7 110,7 @@ pub fn account_loggedin_setup(mut commands: Commands, asset_server: Res<AssetSer
                })
                .with_children(|parent| {
                    for (action, text) in [
                        (MenuButtonAction::AccountLogin, "Logout"),
                        (MenuButtonAction::AccountLogout, "Logout"),
                        (MenuButtonAction::BackToMainMenu, "Back"),
                    ] {
                        parent

M sdbclient/src/plugins/menu/mod.rs => sdbclient/src/plugins/menu/mod.rs +18 -19
@@ 33,11 33,8 @@ use accountscreenloggedout::*;
mod accountscreenloggedin;
use accountscreenloggedin::*;

mod accountloginui;
use accountloginui::*;

mod accountregisterui;
use accountregisterui::*;
mod accountlogin;
mod accountregister;

const TEXT_COLOR: Color = Color::rgb(0.9, 0.9, 0.9);



@@ 70,11 67,11 @@ impl Plugin for MenuPlugin {
            // Systems for account loggedin screen
            .add_system_set(SystemSet::on_enter(MenuState::AccountLoggedIn).with_system(account_loggedin_setup))
            .add_system_set(SystemSet::on_exit(MenuState::AccountLoggedIn).with_system(despawn_screen::<OnAccountLoggedInScreen>))
            // Account login and register systems
            .add_system_set(SystemSet::on_update(MenuState::AccountLogin).with_system(account_login_ui))
            .add_system_set(SystemSet::on_update(MenuState::AccountRegister).with_system(account_register_ui))
            // Common systems
            .add_system_set(SystemSet::on_update(GameState::MainMenu).with_system(menu_action).with_system(button_system));

        app.add_plugin(accountregister::AccountRegisterPlugin)
            .add_plugin(accountlogin::AccountLoginPlugin);
    }
}



@@ 97,14 94,6 @@ pub enum MenuState {
#[derive(Component)]
struct OnAccountScreen;

/// Tag component for tagging entities on login screen
#[derive(Component)]
struct OnLoginScreen;

/// Tag component for tagging entities on register screen
#[derive(Component)]
struct OnRegisterScreen;

const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25);
const HOVERED_PRESSED_BUTTON: Color = Color::rgb(0.25, 0.65, 0.25);


@@ 125,6 114,7 @@ enum MenuButtonAction {
    Account,
    AccountLogin,
    AccountRegister,
    AccountLogout,
    BackToMainMenu,
    BackToSettings,
    Exit,


@@ 137,6 127,8 @@ fn menu_action(
    >,
    mut app_exit_events: EventWriter<AppExit>,
    mut menu_state: ResMut<State<MenuState>>,
    mut register_state: ResMut<State<accountregister::RegisterState>>,
    mut login_state: ResMut<State<accountlogin::LoginState>>,
    //mut game_state: ResMut<State<GameState>>,
    cfg_user: Res<CfgUser>,
) {


@@ 144,7 136,7 @@ fn menu_action(
        if *interaction == Interaction::Clicked {
            match menu_button_action {
                MenuButtonAction::Exit => app_exit_events.send(AppExit),
                MenuButtonAction::Play => println!("todo"),
                MenuButtonAction::Play => warn!("todo"),
                MenuButtonAction::Settings => menu_state.set(MenuState::Settings).unwrap(),
                MenuButtonAction::SettingsDisplay => {
                    menu_state.set(MenuState::SettingsDisplay).unwrap()


@@ 160,10 152,17 @@ fn menu_action(
                        menu_state.set(MenuState::AccountLoggedOut).unwrap()
                    }
                }
                MenuButtonAction::AccountLogin => menu_state.set(MenuState::AccountLogin).unwrap(),
                MenuButtonAction::AccountLogin => {
                    menu_state.set(MenuState::AccountLogin).unwrap();
                    login_state.set(accountlogin::LoginState::Input).unwrap();
                }
                MenuButtonAction::AccountRegister => {
                    menu_state.set(MenuState::AccountRegister).unwrap()
                    menu_state.set(MenuState::AccountRegister).unwrap();
                    register_state
                        .set(accountregister::RegisterState::Input)
                        .unwrap();
                }
                MenuButtonAction::AccountLogout => warn!("todo"),
                MenuButtonAction::BackToSettings => menu_state.set(MenuState::Settings).unwrap(),
                MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main).unwrap(),
            }