Direct World Joining (#3457)

* Begin work on worlds backend

* Finish implementing get_profile_worlds and get_server_status (except pinning)

* Create TS types and manually copy unparsed chat components

* Clippy fix

* Update types.d.ts

* Initial worlds UI work

* Fix api::get_profile_worlds to take in a relative path

* sanitize & security update

* Fix sanitizePotentialFileUrl

* Fix sanitizePotentialFileUrl (for real)

* Fix empty motd causing error

* Finally actually fix world icons

* Fix world icon not being visible on non-Windows

* Use the correct generics to take in AppHandle

* Implement start_join_singleplayer_world and start_join_server for modern versions

* Don't error if server has no cached icon

* Migrate to own server pinging

* Ignore missing server hidden field and missing saves dir

* Update world list frontend

* More frontend work

* Server status player sample can be absent

* Fix refresh state

* Add get_profile_protocol_version

* Add protocol_version column to database

* SQL INTEGER is i64 in sqlx

* sqlx prepare

* Cache protocol version in database

* Continue worlds UI work

* Fix motds being bold

* Remove legacy pinging and add a 30-second timeout

* Remove pinned for now and match world (and server) parsing closer to spec

* Move type ServerStatus to worlds.ts

* Implement add_server_to_profile

* Fix pack_status being ignored when joining from launcher

* Make World path field be relative

* Implement rename_world and reset_world_icon

* Clippy fix

* Fix rename_world

* UI enhancements

* Implement backup_world, which returns the backup size in bytes

* Clippy fix

* Return index when adding servers to profile

* Fix backup

* Implement delete_world

* Implement edit_server_in_profile and remove_server_from_profile

* Clippy fix

* Log server joins

* Add edit and delete support

* Fix ts errors

* Fix minecraft font

* Switch font out for non-monospaced.

* Fix font proper

* Some more world cleanup, handle play state, check quickplay compatibility

* Clear the cached protocol version when a profile's game version is changed

* Fix tint colors in navbar

* Fix server protocol version pinging

* UI fixes

* Fix protocol version handler

* Fix MOTD parsing

* Add worlds_updated profile event

* fix pkg

* Functional home screen with worlds

* lint

* Fix incorrect folder creation

* Make items clickable

* Add locked field to SingleplayerWorld indicating whether the world is locked by the game

* Implement locking frontend

* Fix locking condition

* Split worlds_updated profile event into servers_updated and world_updated

* Fix compile error

* Use port from resolve SRV record

* Fix serialization of ProfilePayload and ProfilePayloadType

* Individual singleplayer world refreshing

* Log when worlds are perceived to be updated

* Push logging + total refresh lock

* Unlisten fixes

* Highlight current world when clicked

* Launcher logs refactor (#3444)

* Switch live log to use STDOUT

* fix clippy, legacy logs support

* Fix lint

* Handle non-XML log messages in XML logging, and don't escape log messages into XML

---------

Co-authored-by: Josiah Glosson <soujournme@gmail.com>

* Update incompatibility text

* Home page fixes, and unlock after close

* Remove logging

* Add join log database migration

* Switch server join timing to being in the database instead of in a separate log file

* Create optimized get_recent_worlds function that takes in a limit

* Update dependencies and fix Cargo.lock

* temp disable overflow menus

* revert home page changes

* Enable overflow menus again

* Remove list

* Revert

* Push dev tools

* Remove default filter

* Disable debug renderer

* Fix random app errors

* Refactor

* Fix missing computed import

* Fix light mode issues

* Fix TS errors

* Lint

* Fix bad link in change modpack version modal

* fix lint

* fix intl

---------

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Prospector
2025-04-26 18:09:58 -07:00
committed by GitHub
parent 25016053ca
commit ff4c7f47b2
106 changed files with 5852 additions and 1346 deletions

View File

@@ -19,6 +19,7 @@ pub mod utils;
pub mod ads;
pub mod cache;
pub mod friends;
pub mod worlds;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;

View File

@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use theseus::prelude::*;
use theseus::profile::QuickPlayType;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("profile")
@@ -250,7 +251,7 @@ pub async fn profile_get_pack_export_candidates(
// invoke('plugin:profile|profile_run', path)
#[tauri::command]
pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
let process = profile::run(path).await?;
let process = profile::run(path, &QuickPlayType::None).await?;
Ok(process)
}
@@ -264,7 +265,9 @@ pub async fn profile_run_credentials(
path: &str,
credentials: Credentials,
) -> Result<ProcessMetadata> {
let process = profile::run_credentials(path, &credentials).await?;
let process =
profile::run_credentials(path, &credentials, &QuickPlayType::None)
.await?;
Ok(process)
}
@@ -347,6 +350,9 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
prof.name = name;
}
if let Some(game_version) = edit_profile.game_version.clone() {
if game_version != prof.game_version {
prof.protocol_version = None;
}
prof.game_version = game_version;
}
if let Some(loader) = edit_profile.loader {

View File

@@ -4,9 +4,11 @@ use theseus::{
prelude::{CommandPayload, DirectoryInfo},
};
use crate::api::Result;
use crate::api::{Result, TheseusSerializableError};
use dashmap::DashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use theseus::prelude::canonicalize;
use url::Url;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("utils")
@@ -140,3 +142,28 @@ pub async fn handle_command(command: String) -> Result<()> {
tracing::info!("handle command: {command}");
Ok(theseus::handler::parse_and_emit_command(&command).await?)
}
// Remove when (and if) https://github.com/tauri-apps/tauri/issues/12022 is implemented
pub(crate) fn tauri_convert_file_src(path: &Path) -> Result<Url> {
#[cfg(any(windows, target_os = "android"))]
const BASE: &str = "http://asset.localhost/";
#[cfg(not(any(windows, target_os = "android")))]
const BASE: &str = "asset://localhost/";
macro_rules! theseus_try {
($test:expr) => {
match $test {
Ok(val) => val,
Err(e) => {
return Err(TheseusSerializableError::Theseus(e.into()))
}
}
};
}
let path = theseus_try!(canonicalize(path));
let path = path.to_string_lossy();
let encoded = urlencoding::encode(&path);
Ok(theseus_try!(Url::parse(&format!("{BASE}{encoded}"))))
}

195
apps/app/src/api/worlds.rs Normal file
View File

@@ -0,0 +1,195 @@
use crate::api::Result;
use either::Either;
use tauri::{AppHandle, Manager, Runtime};
use theseus::prelude::ProcessMetadata;
use theseus::profile::{get_full_path, QuickPlayType};
use theseus::worlds::{
ServerPackStatus, ServerStatus, World, WorldWithProfile,
};
use theseus::{profile, worlds};
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("worlds")
.invoke_handler(tauri::generate_handler![
get_recent_worlds,
get_profile_worlds,
get_singleplayer_world,
rename_world,
reset_world_icon,
backup_world,
delete_world,
add_server_to_profile,
edit_server_in_profile,
remove_server_from_profile,
get_profile_protocol_version,
get_server_status,
start_join_singleplayer_world,
start_join_server,
])
.build()
}
#[tauri::command]
pub async fn get_recent_worlds<R: Runtime>(
app_handle: AppHandle<R>,
limit: usize,
) -> Result<Vec<WorldWithProfile>> {
let mut result = worlds::get_recent_worlds(limit).await?;
for world in result.iter_mut() {
adapt_world_icon(&app_handle, &mut world.world);
}
Ok(result)
}
#[tauri::command]
pub async fn get_profile_worlds<R: Runtime>(
app_handle: AppHandle<R>,
path: &str,
) -> Result<Vec<World>> {
let mut result = worlds::get_profile_worlds(path).await?;
for world in result.iter_mut() {
adapt_world_icon(&app_handle, world);
}
Ok(result)
}
#[tauri::command]
pub async fn get_singleplayer_world<R: Runtime>(
app_handle: AppHandle<R>,
instance: &str,
world: &str,
) -> Result<World> {
let instance = get_full_path(instance).await?;
let mut world = worlds::get_singleplayer_world(&instance, world).await?;
adapt_world_icon(&app_handle, &mut world);
Ok(world)
}
fn adapt_world_icon<R: Runtime>(app_handle: &AppHandle<R>, world: &mut World) {
if let Some(Either::Left(icon_path)) = &world.icon {
let icon_path = icon_path.clone();
if let Ok(new_url) = super::utils::tauri_convert_file_src(&icon_path) {
world.icon = Some(Either::Right(new_url));
if let Err(e) =
app_handle.asset_protocol_scope().allow_file(&icon_path)
{
tracing::warn!(
"Failed to allow file access for icon {}: {}",
icon_path.display(),
e
);
}
} else {
tracing::warn!(
"Encountered invalid icon path for world {}: {}",
world.name,
icon_path.display()
);
world.icon = None;
}
}
}
#[tauri::command]
pub async fn rename_world(
instance: &str,
world: &str,
new_name: &str,
) -> Result<()> {
let instance = get_full_path(instance).await?;
worlds::rename_world(&instance, world, new_name).await?;
Ok(())
}
#[tauri::command]
pub async fn reset_world_icon(instance: &str, world: &str) -> Result<()> {
let instance = get_full_path(instance).await?;
worlds::reset_world_icon(&instance, world).await?;
Ok(())
}
#[tauri::command]
pub async fn backup_world(instance: &str, world: &str) -> Result<u64> {
let instance = get_full_path(instance).await?;
Ok(worlds::backup_world(&instance, world).await?)
}
#[tauri::command]
pub async fn delete_world(instance: &str, world: &str) -> Result<()> {
let instance = get_full_path(instance).await?;
worlds::delete_world(&instance, world).await?;
Ok(())
}
#[tauri::command]
pub async fn add_server_to_profile(
path: &str,
name: String,
address: String,
pack_status: ServerPackStatus,
) -> Result<usize> {
let path = get_full_path(path).await?;
Ok(
worlds::add_server_to_profile(&path, name, address, pack_status)
.await?,
)
}
#[tauri::command]
pub async fn edit_server_in_profile(
path: &str,
index: usize,
name: String,
address: String,
pack_status: ServerPackStatus,
) -> Result<()> {
let path = get_full_path(path).await?;
worlds::edit_server_in_profile(&path, index, name, address, pack_status)
.await?;
Ok(())
}
#[tauri::command]
pub async fn remove_server_from_profile(
path: &str,
index: usize,
) -> Result<()> {
let path = get_full_path(path).await?;
worlds::remove_server_from_profile(&path, index).await?;
Ok(())
}
#[tauri::command]
pub async fn get_profile_protocol_version(path: &str) -> Result<Option<i32>> {
Ok(worlds::get_profile_protocol_version(path).await?)
}
#[tauri::command]
pub async fn get_server_status(
address: &str,
protocol_version: Option<i32>,
) -> Result<ServerStatus> {
Ok(worlds::get_server_status(address, protocol_version).await?)
}
#[tauri::command]
pub async fn start_join_singleplayer_world(
path: &str,
world: String,
) -> Result<ProcessMetadata> {
let process =
profile::run(path, &QuickPlayType::Singleplayer(world)).await?;
Ok(process)
}
#[tauri::command]
pub async fn start_join_server(
path: &str,
address: &str,
) -> Result<ProcessMetadata> {
let process =
profile::run(path, &QuickPlayType::Server(address.to_owned())).await?;
Ok(process)
}