Display player avatars in proxy

This commit is contained in:
IQuant 2024-05-27 16:08:21 +03:00
parent 86f1ed7e24
commit ed8959fb36
5 changed files with 194 additions and 86 deletions

View file

@ -1,5 +1,5 @@
use std::{
env, fmt::Display, net::SocketAddr, sync::{atomic::Ordering, Arc}, thread, time::Duration
fmt::Display, net::SocketAddr, sync::{atomic::Ordering, Arc}, time::Duration
};
use bitcode::{Decode, Encode};
@ -17,6 +17,8 @@ pub mod messages;
mod mod_manager;
mod self_update;
pub mod releases;
pub mod net;
pub mod steam_helper;
#[derive(Debug, Decode, Encode, Clone)]
pub struct GameSettings {
@ -24,8 +26,6 @@ pub struct GameSettings {
debug_mode: bool,
}
pub mod net;
enum AppState {
Connect,
ModManager,
@ -34,28 +34,6 @@ enum AppState {
SelfUpdate,
}
struct SteamState {
pub client: steamworks::Client,
}
impl SteamState {
fn new() -> Result<Self, SteamAPIInitError> {
if env::var_os("NP_DISABLE_STEAM").is_some() {
return Err(SteamAPIInitError::FailedGeneric("Disabled by env variable".to_string()))
}
let app_id = env::var("NP_APPID").ok().and_then(|x| x.parse().ok());
let (client, single) = steamworks::Client::init_app(app_id.unwrap_or(881100))?;
thread::spawn(move || {
info!("Spawned steam callback thread");
loop {
single.run_callbacks();
thread::sleep(Duration::from_millis(3));
}
});
Ok(SteamState { client })
}
}
#[derive(Debug, Serialize, Deserialize)]
struct AppSavedState {
addr: String,
@ -76,7 +54,7 @@ impl Default for AppSavedState {
pub struct App {
state: AppState,
modmanager: Modmanager,
steam_state: Result<SteamState, SteamAPIInitError>,
steam_state: Result<steam_helper::SteamState, SteamAPIInitError>,
saved_state: AppSavedState,
modmanager_settings: ModmanagerSettings,
self_update: SelfUpdateManager,
@ -95,7 +73,7 @@ impl App {
Self {
state: AppState::ModManager,
modmanager: Modmanager::default(),
steam_state: SteamState::new(),saved_state,
steam_state: steam_helper::SteamState::new(),saved_state,
modmanager_settings,
self_update: SelfUpdateManager::new(),
}
@ -151,6 +129,57 @@ impl App {
netman.clone().start();
self.state = AppState::Netman { netman };
}
fn connect_screen(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Noita Entangled Worlds proxy");
ui.checkbox(&mut self.saved_state.debug_mode, "Debug mode");
ui.checkbox(&mut self.saved_state.use_constant_seed, "Use specified seed");
if ui.button("Host").clicked() {
self.start_server();
}
ui.separator();
ui.text_edit_singleline(&mut self.saved_state.addr);
let addr = self.saved_state.addr.parse();
ui.add_enabled_ui(addr.is_ok(), |ui| {
if ui.button("Connect").clicked() {
if let Ok(addr) = addr {
self.start_connect(addr);
}
}
});
ui.separator();
ui.heading("Steam networking");
match &self.steam_state {
Ok(_) => {
if ui.button("Create lobby").clicked() {
self.start_steam_host();
}
if ui.button("Connect to lobby in clipboard").clicked() {
let id = ClipboardProvider::new()
.and_then(|mut ctx: ClipboardContext| ctx.get_contents());
match id {
Ok(id) => {
let id = id.parse().map(LobbyId::from_raw);
match id {
Ok(id) => self.start_steam_connect(id),
Err(error) => self.notify_error(error),
}
}
Err(error) => self.notify_error(error),
}
}
}
Err(err) => {
ui.label(format!("Could not init steam networking: {}", err));
}
}
self.self_update.display_version(ui);
if self.self_update.request_update {
self.state = AppState::SelfUpdate;
}
});
}
}
impl eframe::App for App {
@ -158,59 +187,34 @@ impl eframe::App for App {
ctx.request_repaint_after(Duration::from_secs(1));
match &self.state {
AppState::Connect => {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Noita Entangled Worlds proxy");
ui.checkbox(&mut self.saved_state.debug_mode, "Debug mode");
ui.checkbox(&mut self.saved_state.use_constant_seed, "Use specified seed");
if ui.button("Host").clicked() {
self.start_server();
}
ui.separator();
ui.text_edit_singleline(&mut self.saved_state.addr);
let addr = self.saved_state.addr.parse();
ui.add_enabled_ui(addr.is_ok(), |ui| {
if ui.button("Connect").clicked() {
if let Ok(addr) = addr {
self.start_connect(addr);
}
}
});
ui.separator();
ui.heading("Steam networking");
match &self.steam_state {
Ok(_) => {
if ui.button("Create lobby").clicked() {
self.start_steam_host();
}
if ui.button("Connect to lobby in clipboard").clicked() {
let id = ClipboardProvider::new()
.and_then(|mut ctx: ClipboardContext| ctx.get_contents());
match id {
Ok(id) => {
let id = id.parse().map(LobbyId::from_raw);
match id {
Ok(id) => self.start_steam_connect(id),
Err(error) => self.notify_error(error),
}
}
Err(error) => self.notify_error(error),
}
}
}
Err(err) => {
ui.label(format!("Could not init steam networking: {}", err));
}
}
self.self_update.display_version(ui);
if self.self_update.request_update {
self.state = AppState::SelfUpdate;
}
});
self.connect_screen(ctx);
}
AppState::Netman { netman } => {
let stopped = netman.stopped.load(Ordering::Relaxed);
let accept_local = netman.accept_local.load(Ordering::Relaxed);
let local_connected = netman.local_connected.load(Ordering::Relaxed);
egui::SidePanel::left("players").resizable(false).exact_width(200.0).show(ctx, |ui| {
ui.heading("Players");
if netman.peer.is_steam() {
let steam = self.steam_state.as_mut().expect("steam should be available, as we are using steam networking");
for peer in netman.peer.iter_peer_ids() {
let role = peer_role(peer, netman);
let username = steam.get_user_name(peer.into());
let avatar = steam.get_avatar(ctx, peer.into());
if let Some(avatar) = avatar {
avatar.display_with_labels(ui, &username, role);
ui.add_space(5.0);
} else {
ui.label(&username);
}
}
} else {
for peer in netman.peer.iter_peer_ids() {
ui.label(peer.to_string());
}
}
});
egui::CentralPanel::default().show(ctx, |ui| {
if stopped {
ui.colored_label(Color32::LIGHT_RED, "Netmanager thread has stopped");
@ -231,17 +235,15 @@ impl eframe::App for App {
ui.label("Not yet ready");
}
ui.separator();
if let Some(id) = netman.peer.lobby_id() {
if ui.button("Save lobby id to clipboard").clicked() {
let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
let _ = ctx.set_contents(id.raw().to_string());
if netman.peer.is_steam() {
if let Some(id) = netman.peer.lobby_id() {
if ui.button("Save lobby id to clipboard").clicked() {
let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
let _ = ctx.set_contents(id.raw().to_string());
}
} else {
ui.label("Lobby id not available");
}
} else {
ui.label("Lobby id not available");
}
ui.heading("Current users");
for peer in netman.peer.iter_peer_ids() {
ui.label(peer.to_string());
}
ui.label(format!("Peer state: {}", netman.peer.state()));
});
@ -281,3 +283,15 @@ impl eframe::App for App {
eframe::set_value(storage, MODMANAGER, &self.modmanager_settings);
}
}
fn peer_role(peer: net::omni::OmniPeerId, netman: &Arc<net::NetManager>) -> &str {
if peer == netman.peer.host_id() {
"Host"
} else {
if Some(peer) == netman.peer.my_id() {
"Me"
} else {
"Player"
}
}
}

View file

@ -14,7 +14,7 @@ use tracing::{error, info};
use crate::{
releases::{get_release_by_tag, Downloader, ReleasesError, Version},
SteamState,
steam_helper::SteamState,
};
#[derive(Default)]

View file

@ -133,4 +133,11 @@ impl PeerVariant {
PeerVariant::Steam(p) => p.lobby_id(),
}
}
pub fn is_steam(&self) -> bool {
match self {
PeerVariant::Steam(_) => true,
_ => false,
}
}
}

View file

@ -0,0 +1,86 @@
use std::{collections::HashMap, env, thread, time::Duration};
use eframe::egui::{self, ColorImage, RichText, TextureHandle, TextureOptions, Ui, Widget};
use steamworks::{SteamAPIInitError, SteamId};
use tracing::info;
pub struct SteamUserAvatar {
avatar: TextureHandle,
}
impl SteamUserAvatar {
pub fn display(&self, ui: &mut Ui) -> egui::Response {
egui::Image::new(&self.avatar).ui(ui)
}
pub fn display_with_labels(&self, ui: &mut Ui, label_top: &str, label_bottom: &str) {
let image = egui::Image::new(&self.avatar).fit_to_exact_size([32.0, 32.0].into());
ui.group(|ui| {
ui.set_min_width(200.0);
ui.horizontal(|ui| {
ui.add(image);
ui.vertical(|ui| {
ui.label(RichText::new(label_top).size(14.0));
ui.label(RichText::new(label_bottom).size(11.0));
});
});
});
}
}
pub struct SteamState {
pub client: steamworks::Client,
avatar_cache: HashMap<SteamId, SteamUserAvatar>,
}
impl SteamState {
pub(crate) fn new() -> Result<Self, SteamAPIInitError> {
if env::var_os("NP_DISABLE_STEAM").is_some() {
return Err(SteamAPIInitError::FailedGeneric(
"Disabled by env variable".to_string(),
));
}
let app_id = env::var("NP_APPID").ok().and_then(|x| x.parse().ok());
let (client, single) = steamworks::Client::init_app(app_id.unwrap_or(881100))?;
thread::spawn(move || {
info!("Spawned steam callback thread");
loop {
single.run_callbacks();
thread::sleep(Duration::from_millis(3));
}
});
Ok(SteamState {
client,
avatar_cache: HashMap::new(),
})
}
pub fn get_user_name(&self, id: SteamId) -> String {
let friends = self.client.friends();
friends.get_friend(id).name()
}
pub fn get_avatar(&mut self, ctx: &egui::Context, id: SteamId) -> Option<&SteamUserAvatar> {
let friends = self.client.friends();
if self.avatar_cache.contains_key(&id) {
self.avatar_cache.get(&id)
} else {
friends
.get_friend(id)
.small_avatar()
.map(|data| {
ctx.load_texture(
format!("steam_avatar_for_{:?}", id),
ColorImage::from_rgba_unmultiplied([32, 32], &data),
TextureOptions::LINEAR,
)
})
.map(|avatar| {
let avatar = SteamUserAvatar { avatar };
&*self.avatar_cache.entry(id).or_insert(avatar)
})
}
// ctx.load_texture(name, image, options)
}
}