wip
This commit is contained in:
@@ -61,6 +61,7 @@ impl From<EtiGame> for Game {
|
|||||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||||
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
|
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
|
||||||
thumbnail: None,
|
thumbnail: None,
|
||||||
|
downloaded: false,
|
||||||
installed: false,
|
installed: false,
|
||||||
eti_game_version: None,
|
eti_game_version: None,
|
||||||
local_version: None,
|
local_version: None,
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ pub struct Game {
|
|||||||
pub size: u64,
|
pub size: u64,
|
||||||
/// thumbnail image
|
/// thumbnail image
|
||||||
pub thumbnail: Option<Bytes>,
|
pub thumbnail: Option<Bytes>,
|
||||||
|
/// indicates that the ETI bundle exists locally
|
||||||
|
#[serde(default)]
|
||||||
|
pub downloaded: bool,
|
||||||
/// only relevant for client (yeah... I know)
|
/// only relevant for client (yeah... I know)
|
||||||
pub installed: bool,
|
pub installed: bool,
|
||||||
/// ETI game version from version.ini (YYYYMMDD format) (server)
|
/// ETI game version from version.ini (YYYYMMDD format) (server)
|
||||||
@@ -165,7 +168,9 @@ impl GameDB {
|
|||||||
|
|
||||||
pub fn set_all_uninstalled(&mut self) {
|
pub fn set_all_uninstalled(&mut self) {
|
||||||
for game in self.games.values_mut() {
|
for game in self.games.values_mut() {
|
||||||
|
game.downloaded = false;
|
||||||
game.installed = false;
|
game.installed = false;
|
||||||
|
game.local_version = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1111,7 +1111,16 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result<GameDB> {
|
|||||||
if path.is_dir()
|
if path.is_dir()
|
||||||
&& let Some(game_id) = path.file_name().and_then(|n| n.to_str())
|
&& let Some(game_id) = path.file_name().and_then(|n| n.to_str())
|
||||||
{
|
{
|
||||||
// Check if this game has a version.ini file
|
let eti_path = path.join(format!("{game_id}.eti"));
|
||||||
|
let downloaded = tokio::fs::metadata(&eti_path).await.is_ok();
|
||||||
|
if !downloaded {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !local_dir_has_content(&path).await {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(version) = lanspread_db::db::read_version_from_ini(&path) {
|
if let Ok(version) = lanspread_db::db::read_version_from_ini(&path) {
|
||||||
let size = calculate_directory_size(&path).await?;
|
let size = calculate_directory_size(&path).await?;
|
||||||
let game = Game {
|
let game = Game {
|
||||||
@@ -1125,6 +1134,7 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result<GameDB> {
|
|||||||
genre: String::new(),
|
genre: String::new(),
|
||||||
size,
|
size,
|
||||||
thumbnail: None,
|
thumbnail: None,
|
||||||
|
downloaded,
|
||||||
installed: true,
|
installed: true,
|
||||||
eti_game_version: version.clone(),
|
eti_game_version: version.clone(),
|
||||||
local_version: version,
|
local_version: version,
|
||||||
@@ -1138,6 +1148,30 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result<GameDB> {
|
|||||||
Ok(GameDB::from(games))
|
Ok(GameDB::from(games))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn local_dir_has_content(path: &Path) -> bool {
|
||||||
|
let local_dir = path.join("local");
|
||||||
|
if tokio::fs::metadata(&local_dir).await.is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = match tokio::fs::read_dir(&local_dir).await {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to read local dir {}: {e}", local_dir.display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match entries.next_entry().await {
|
||||||
|
Ok(Some(_)) => true,
|
||||||
|
Ok(None) => false,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to iterate local dir {}: {e}", local_dir.display());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn calculate_directory_size(dir: &Path) -> eyre::Result<u64> {
|
async fn calculate_directory_size(dir: &Path) -> eyre::Result<u64> {
|
||||||
let mut total_size = 0u64;
|
let mut total_size = 0u64;
|
||||||
let mut entries = tokio::fs::read_dir(dir).await?;
|
let mut entries = tokio::fs::read_dir(dir).await?;
|
||||||
|
|||||||
@@ -233,10 +233,6 @@ async fn run_game_windows(
|
|||||||
id: String,
|
id: String,
|
||||||
state: tauri::State<'_, LanSpreadState>,
|
state: tauri::State<'_, LanSpreadState>,
|
||||||
) -> tauri::Result<()> {
|
) -> tauri::Result<()> {
|
||||||
use std::fs::File;
|
|
||||||
|
|
||||||
const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done";
|
|
||||||
|
|
||||||
let games_folder_lock = state.inner().games_folder.clone();
|
let games_folder_lock = state.inner().games_folder.clone();
|
||||||
let games_folder = {
|
let games_folder = {
|
||||||
let guard = games_folder_lock.read().await;
|
let guard = games_folder_lock.read().await;
|
||||||
@@ -254,8 +250,8 @@ async fn run_game_windows(
|
|||||||
let game_setup_bin = game_path.join("game_setup.cmd");
|
let game_setup_bin = game_path.join("game_setup.cmd");
|
||||||
let game_start_bin = game_path.join("game_start.cmd");
|
let game_start_bin = game_path.join("game_start.cmd");
|
||||||
|
|
||||||
let first_start_done_file = game_path.join(FIRST_START_DONE_FILE);
|
let local_ready = local_install_is_ready(&game_path);
|
||||||
if !first_start_done_file.exists() && game_setup_bin.exists() {
|
if !local_ready && game_setup_bin.exists() {
|
||||||
let result = run_as_admin(
|
let result = run_as_admin(
|
||||||
"cmd.exe",
|
"cmd.exe",
|
||||||
&format!(
|
&format!(
|
||||||
@@ -270,10 +266,6 @@ async fn run_game_windows(
|
|||||||
log::error!("failed to run game_setup.cmd");
|
log::error!("failed to run game_setup.cmd");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = File::create(&first_start_done_file) {
|
|
||||||
log::error!("failed to create {first_start_done_file:?}: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if game_start_bin.exists() {
|
if game_start_bin.exists() {
|
||||||
@@ -311,31 +303,71 @@ async fn run_game(id: String, state: tauri::State<'_, LanSpreadState>) -> tauri:
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) {
|
fn eti_package_exists(game_path: &Path, game_id: &str) -> bool {
|
||||||
if let Some(file_name) = path.file_name()
|
game_path.is_dir() && game_path.join(format!("{game_id}.eti")).is_file()
|
||||||
&& let Some(file_name) = file_name.to_str()
|
|
||||||
&& let Some(game) = game_db.get_mut_game_by_id(file_name)
|
|
||||||
{
|
|
||||||
if installed {
|
|
||||||
log::debug!("Set {game} to installed");
|
|
||||||
} else {
|
|
||||||
log::debug!("Set {game} to uninstalled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local_install_is_ready(game_path: &Path) -> bool {
|
||||||
|
let local_dir = game_path.join("local");
|
||||||
|
if !local_dir.is_dir() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::read_dir(&local_dir) {
|
||||||
|
Ok(mut entries) => match entries.next() {
|
||||||
|
Some(Ok(_)) => true,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to inspect entry in local dir {}: {e}",
|
||||||
|
local_dir.display()
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to enumerate local dir for game {}: {e}",
|
||||||
|
game_path.display()
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_game_installation_state(game: &mut Game, games_root: &Path) {
|
||||||
|
let game_path = games_root.join(&game.id);
|
||||||
|
if !game_path.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloaded = eti_package_exists(&game_path, &game.id);
|
||||||
|
game.downloaded = downloaded;
|
||||||
|
|
||||||
|
let installed = downloaded && local_install_is_ready(&game_path);
|
||||||
game.installed = installed;
|
game.installed = installed;
|
||||||
|
|
||||||
// Read local version.ini if installed
|
|
||||||
if installed {
|
if installed {
|
||||||
if let Ok(version) = lanspread_db::db::read_version_from_ini(path) {
|
log::debug!("Set {game} to installed");
|
||||||
|
match lanspread_db::db::read_version_from_ini(&game_path) {
|
||||||
|
Ok(version) => {
|
||||||
game.local_version = version;
|
game.local_version = version;
|
||||||
if let Some(ref version) = game.local_version {
|
if let Some(ref version) = game.local_version {
|
||||||
log::debug!("Read local version for game {}: {}", game.id, version);
|
log::debug!("Read local version for game {}: {}", game.id, version);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
log::warn!("Failed to read local version.ini for game: {}", game.id);
|
Err(e) => {
|
||||||
|
log::warn!("Failed to read local version.ini for game {}: {e}", game.id);
|
||||||
|
game.local_version = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clear local version when uninstalled
|
|
||||||
game.local_version = None;
|
game.local_version = None;
|
||||||
|
if downloaded {
|
||||||
|
log::debug!(
|
||||||
|
"Game {} is downloaded but awaiting local install contents",
|
||||||
|
game.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,41 +387,6 @@ async fn refresh_games_list(app_handle: &AppHandle) {
|
|||||||
|
|
||||||
let path = PathBuf::from(&games_folder);
|
let path = PathBuf::from(&games_folder);
|
||||||
|
|
||||||
let mut installed_paths: Vec<PathBuf> = Vec::new();
|
|
||||||
if path.exists() {
|
|
||||||
match std::fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
for entry_result in entries {
|
|
||||||
match entry_result {
|
|
||||||
Ok(entry) => match entry.file_type() {
|
|
||||||
Ok(file_type) if file_type.is_dir() => {
|
|
||||||
let dir_path = entry.path();
|
|
||||||
if dir_path.join("version.ini").exists() {
|
|
||||||
installed_paths.push(dir_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(
|
|
||||||
"Failed to read file type for entry {}: {e}",
|
|
||||||
entry.path().display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to read directory entry in {}: {e}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to read game dir {}: {e}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::error!("game dir {} does not exist", path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut game_db = games_db_lock.write().await;
|
let mut game_db = games_db_lock.write().await;
|
||||||
|
|
||||||
if game_db.games.is_empty() {
|
if game_db.games.is_empty() {
|
||||||
@@ -399,8 +396,12 @@ async fn refresh_games_list(app_handle: &AppHandle) {
|
|||||||
|
|
||||||
game_db.set_all_uninstalled();
|
game_db.set_all_uninstalled();
|
||||||
|
|
||||||
for dir_path in &installed_paths {
|
if path.exists() {
|
||||||
set_game_install_state_from_path(&mut game_db, dir_path, true);
|
for game in game_db.games.values_mut() {
|
||||||
|
update_game_installation_state(game, &path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("game dir {} does not exist", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
let games_to_emit = game_db
|
let games_to_emit = game_db
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface Game {
|
|||||||
description: string;
|
description: string;
|
||||||
size: number;
|
size: number;
|
||||||
thumbnail: Uint8Array | number[];
|
thumbnail: Uint8Array | number[];
|
||||||
|
downloaded: boolean;
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
install_status: InstallStatus;
|
install_status: InstallStatus;
|
||||||
eti_game_version?: string;
|
eti_game_version?: string;
|
||||||
@@ -49,7 +50,7 @@ const App = () => {
|
|||||||
switch (filter) {
|
switch (filter) {
|
||||||
case 'available':
|
case 'available':
|
||||||
// Show union of installed games and games with peers
|
// Show union of installed games and games with peers
|
||||||
return games.filter(game => game.installed || game.peer_count > 0);
|
return games.filter(game => game.installed || game.downloaded || game.peer_count > 0);
|
||||||
case 'installed':
|
case 'installed':
|
||||||
return games.filter(game => game.installed);
|
return games.filter(game => game.installed);
|
||||||
case 'all':
|
case 'all':
|
||||||
@@ -381,6 +382,36 @@ const App = () => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInProgressLabel = (game: Game): string | undefined => {
|
||||||
|
switch (game.install_status) {
|
||||||
|
case InstallStatus.CheckingPeers:
|
||||||
|
return 'Checking peers...';
|
||||||
|
case InstallStatus.Downloading:
|
||||||
|
return 'Downloading...';
|
||||||
|
case InstallStatus.Unpacking:
|
||||||
|
return 'Unpacking...';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionLabel = (game: Game): string => {
|
||||||
|
const inProgress = getInProgressLabel(game);
|
||||||
|
if (inProgress) {
|
||||||
|
return inProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!game.installed) {
|
||||||
|
return game.downloaded ? 'Install' : 'Download';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsUpdate(game)) {
|
||||||
|
return 'Update';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Play';
|
||||||
|
};
|
||||||
|
|
||||||
const dialogGameDir = async () => {
|
const dialogGameDir = async () => {
|
||||||
const file = await open({
|
const file = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
@@ -484,21 +515,11 @@ const App = () => {
|
|||||||
runGame(item.id);
|
runGame(item.id);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{!item.installed
|
{getActionLabel(item)}
|
||||||
? item.install_status === InstallStatus.CheckingPeers ? 'Checking peers...'
|
|
||||||
: item.install_status === InstallStatus.Downloading ? 'Downloading...'
|
|
||||||
: item.install_status === InstallStatus.Unpacking ? 'Unpacking...'
|
|
||||||
: 'Install'
|
|
||||||
: needsUpdate(item)
|
|
||||||
? item.install_status === InstallStatus.CheckingPeers ? 'Checking peers...'
|
|
||||||
: item.install_status === InstallStatus.Downloading ? 'Downloading...'
|
|
||||||
: item.install_status === InstallStatus.Unpacking ? 'Unpacking...'
|
|
||||||
: 'Update'
|
|
||||||
: 'Play'}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}>
|
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}>
|
||||||
<div className="status-left">
|
<div className="status-left">
|
||||||
{item.status_message && item.peer_count === 0 && !item.installed ? item.status_message : ''}
|
{item.status_message && item.peer_count === 0 && !item.installed && !item.downloaded ? item.status_message : ''}
|
||||||
</div>
|
</div>
|
||||||
<div className="status-right">
|
<div className="status-right">
|
||||||
{item.peer_count > 0 && (
|
{item.peer_count > 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user