You've already forked AstralRinth
forked from didirus/AstralRinth
Refactor Library
The launcher code was in a position ripe for sphagetti, so this rewrites it in a more robust way. In addition to cleaner code, this provides the following changes: - Removal of obsolete Mojang authentication - The rebasing of some internal state into a Sled database - Tweaks which make some internal mechanisms more robust (e.g. profiles which fail to load can be removed) - Additional tooling integration such as direnv - Distinct public API to avoid messing with too much internal code - Unified error handling in the form of `theseus::Error` and `theseus::Result`
This commit is contained in:
129
theseus/src/state/dirs.rs
Normal file
129
theseus/src/state/dirs.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
//! Theseus directory information
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryInfo {
|
||||
pub config_dir: PathBuf,
|
||||
pub working_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DirectoryInfo {
|
||||
/// Get all paths needed for Theseus to operate properly
|
||||
pub async fn init() -> crate::Result<Self> {
|
||||
// Working directory
|
||||
let working_dir = std::env::current_dir().map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Could not open working directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Config directory
|
||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
|
||||
.ok_or(crate::Error::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
))?;
|
||||
|
||||
fs::create_dir_all(&config_dir).await.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Error creating Theseus config directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
config_dir,
|
||||
working_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Minecraft instance metadata directory
|
||||
#[inline]
|
||||
pub fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("meta")
|
||||
}
|
||||
|
||||
/// Get the Minecraft versions metadata directory
|
||||
#[inline]
|
||||
pub fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("versions")
|
||||
}
|
||||
|
||||
/// Get the metadata directory for a given version
|
||||
#[inline]
|
||||
pub fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().join(version)
|
||||
}
|
||||
|
||||
/// Get the Minecraft libraries metadata directory
|
||||
#[inline]
|
||||
pub fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("libraries")
|
||||
}
|
||||
|
||||
/// Get the Minecraft assets metadata directory
|
||||
#[inline]
|
||||
pub fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("assets")
|
||||
}
|
||||
|
||||
/// Get the assets index directory
|
||||
#[inline]
|
||||
pub fn assets_index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("indexes")
|
||||
}
|
||||
|
||||
/// Get the assets objects directory
|
||||
#[inline]
|
||||
pub fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("objects")
|
||||
}
|
||||
|
||||
/// Get the directory for a specific object
|
||||
#[inline]
|
||||
pub fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().join(&hash[..2]).join(hash)
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("resources")
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("natives")
|
||||
}
|
||||
|
||||
/// Get the natives directory for a version of Minecraft
|
||||
#[inline]
|
||||
pub fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().join(version)
|
||||
}
|
||||
|
||||
/// Get the directory containing instance icons
|
||||
#[inline]
|
||||
pub fn icon_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("icons")
|
||||
}
|
||||
|
||||
/// Get the file containing the global database
|
||||
#[inline]
|
||||
pub fn database_file(&self) -> PathBuf {
|
||||
self.config_dir.join("data.bin")
|
||||
}
|
||||
|
||||
/// Get the settings file for Theseus
|
||||
#[inline]
|
||||
pub fn settings_file(&self) -> PathBuf {
|
||||
self.config_dir.join("settings.json")
|
||||
}
|
||||
|
||||
/// Get path from environment variable
|
||||
#[inline]
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
}
|
||||
90
theseus/src/state/metadata.rs
Normal file
90
theseus/src/state/metadata.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Theseus metadata
|
||||
use crate::config::BINCODE_CONFIG;
|
||||
use bincode::{Decode, Encode};
|
||||
use daedalus::{
|
||||
minecraft::{fetch_version_manifest, VersionManifest as MinecraftManifest},
|
||||
modded::{
|
||||
fetch_manifest as fetch_loader_manifest, Manifest as LoaderManifest,
|
||||
},
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::collections::LinkedList;
|
||||
|
||||
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
||||
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct Metadata {
|
||||
pub minecraft: MinecraftManifest,
|
||||
pub forge: LoaderManifest,
|
||||
pub fabric: LoaderManifest,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
fn get_manifest(name: &str) -> String {
|
||||
format!("{METADATA_URL}/{name}/v0/manifest.json")
|
||||
}
|
||||
|
||||
async fn fetch() -> crate::Result<Self> {
|
||||
let (minecraft, forge, fabric) = tokio::try_join! {
|
||||
async {
|
||||
let url = Self::get_manifest("minecraft");
|
||||
fetch_version_manifest(Some(&url)).await
|
||||
},
|
||||
async {
|
||||
let url = Self::get_manifest("forge");
|
||||
fetch_loader_manifest(&url).await
|
||||
},
|
||||
async {
|
||||
let url = Self::get_manifest("fabric");
|
||||
fetch_loader_manifest(&url).await
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(Self {
|
||||
minecraft,
|
||||
forge,
|
||||
fabric,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
|
||||
if let Some(ref meta_bin) = db.get(METADATA_DB_FIELD)? {
|
||||
match bincode::decode_from_slice::<Self, _>(
|
||||
&meta_bin,
|
||||
*BINCODE_CONFIG,
|
||||
) {
|
||||
Ok((meta, _)) => metadata = Some(meta),
|
||||
Err(err) => {
|
||||
log::warn!("Could not read launcher metadata: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut fetch_futures = LinkedList::new();
|
||||
for _ in 0..3 {
|
||||
fetch_futures.push_back(Self::fetch().boxed());
|
||||
}
|
||||
|
||||
match future::select_ok(fetch_futures).await {
|
||||
Ok(meta) => metadata = Some(meta.0),
|
||||
Err(err) => log::warn!("Unable to fetch launcher metadata: {err}"),
|
||||
}
|
||||
|
||||
if let Some(meta) = metadata {
|
||||
db.insert(
|
||||
METADATA_DB_FIELD,
|
||||
sled::IVec::from(bincode::encode_to_vec(
|
||||
&meta,
|
||||
*BINCODE_CONFIG,
|
||||
)?),
|
||||
)?;
|
||||
db.flush_async().await?;
|
||||
Ok(meta)
|
||||
} else {
|
||||
Err(crate::Error::NoValueFor(String::from("launcher metadata")))
|
||||
}
|
||||
}
|
||||
}
|
||||
118
theseus/src/state/mod.rs
Normal file
118
theseus/src/state/mod.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
//! Theseus state management system
|
||||
use crate::config::sled_config;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, OnceCell, RwLock, Semaphore};
|
||||
|
||||
// Submodules
|
||||
mod dirs;
|
||||
pub use self::dirs::*;
|
||||
|
||||
mod metadata;
|
||||
pub use metadata::*;
|
||||
|
||||
mod settings;
|
||||
pub use settings::*;
|
||||
|
||||
mod profiles;
|
||||
pub use profiles::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
/// Database, used to store some information
|
||||
pub(self) database: sled::Db,
|
||||
/// Information on the location of files used in the launcher
|
||||
pub directories: DirectoryInfo,
|
||||
/// Semaphore used to limit concurrent I/O and avoid errors
|
||||
pub io_semaphore: Semaphore,
|
||||
/// Launcher metadata
|
||||
pub metadata: Metadata,
|
||||
/// Launcher configuration
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Launcher profile metadata
|
||||
pub profiles: RwLock<Profiles>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(|| async {
|
||||
// Directories
|
||||
let directories = DirectoryInfo::init().await?;
|
||||
|
||||
// Database
|
||||
// TODO: make database versioned
|
||||
let database =
|
||||
sled_config().path(directories.database_file()).open()?;
|
||||
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
|
||||
// Metadata
|
||||
let metadata = Metadata::init(&database).await?;
|
||||
|
||||
// Profiles
|
||||
let profiles = Profiles::init(&database).await?;
|
||||
|
||||
// Loose initializations
|
||||
let io_semaphore =
|
||||
Semaphore::new(settings.max_concurrent_downloads);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
io_semaphore,
|
||||
metadata,
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
/// Synchronize in-memory state with persistent state
|
||||
pub async fn sync() -> crate::Result<()> {
|
||||
let state = Self::get().await?;
|
||||
let batch = Arc::new(Mutex::new(sled::Batch::default()));
|
||||
|
||||
let sync_settings = async {
|
||||
let state = Arc::clone(&state);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let reader = state.settings.read().await;
|
||||
reader.sync(&state.directories.settings_file()).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let sync_profiles = async {
|
||||
let state = Arc::clone(&state);
|
||||
let batch = Arc::clone(&batch);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let profiles = state.profiles.read().await;
|
||||
let mut batch = batch.lock().await;
|
||||
|
||||
profiles.sync(&mut batch).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
tokio::try_join!(sync_settings, sync_profiles)?;
|
||||
|
||||
state
|
||||
.database
|
||||
.apply_batch(Arc::try_unwrap(batch).unwrap().into_inner())?;
|
||||
state.database.flush_async().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
340
theseus/src/state/profiles.rs
Normal file
340
theseus/src/state/profiles.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
use super::settings::{Hooks, MemorySettings, WindowSize};
|
||||
use crate::config::BINCODE_CONFIG;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use futures::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||
|
||||
// TODO: possibly add defaults to some of these values
|
||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
pub const SUPPORTED_ICON_FORMATS: &[&'static str] = &[
|
||||
"bmp", "gif", "jpeg", "jpg", "jpe", "png", "svg", "svgz", "webp", "rgb",
|
||||
"mp4",
|
||||
];
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub metadata: ProfileMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<MemorySettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<WindowSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hooks: Option<Hooks>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ProfileMetadata {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<PathBuf>,
|
||||
pub game_version: String,
|
||||
#[serde(default)]
|
||||
pub loader: ModLoader,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loader_version: Option<LoaderVersion>,
|
||||
pub format_version: u32,
|
||||
}
|
||||
|
||||
// TODO: Quilt?
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ModLoader {
|
||||
Vanilla,
|
||||
Forge,
|
||||
Fabric,
|
||||
}
|
||||
|
||||
impl Default for ModLoader {
|
||||
fn default() -> Self {
|
||||
ModLoader::Vanilla
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModLoader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
&Self::Vanilla => "Vanilla",
|
||||
&Self::Forge => "Forge",
|
||||
&Self::Fabric => "Fabric",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct JavaSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub install: Option<PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_arguments: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub async fn new(
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(crate::Error::InputError(String::from(
|
||||
"Empty name for instance!",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
path: path.canonicalize()?,
|
||||
metadata: ProfileMetadata {
|
||||
name,
|
||||
icon: None,
|
||||
game_version: version,
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: None,
|
||||
memory: None,
|
||||
resolution: None,
|
||||
hooks: None,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: deduplicate these builder methods
|
||||
// They are flat like this in order to allow builder-style usage
|
||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||
self.metadata.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn with_icon<'a>(
|
||||
&'a mut self,
|
||||
icon: &'a Path,
|
||||
) -> crate::Result<&'a mut Self> {
|
||||
let ext = icon
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("");
|
||||
|
||||
if SUPPORTED_ICON_FORMATS.contains(&ext) {
|
||||
let file_name = format!("icon.{ext}");
|
||||
fs::copy(icon, &self.path.join(&file_name)).await?;
|
||||
self.metadata.icon =
|
||||
Some(Path::new(&format!("./{file_name}")).to_owned());
|
||||
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(crate::Error::InputError(format!(
|
||||
"Unsupported image type: {ext}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||
self.metadata.game_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_loader(
|
||||
&mut self,
|
||||
loader: ModLoader,
|
||||
version: Option<LoaderVersion>,
|
||||
) -> &mut Self {
|
||||
self.metadata.loader = loader;
|
||||
self.metadata.loader_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_java_settings(
|
||||
&mut self,
|
||||
settings: Option<JavaSettings>,
|
||||
) -> &mut Self {
|
||||
self.java = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_memory(
|
||||
&mut self,
|
||||
settings: Option<MemorySettings>,
|
||||
) -> &mut Self {
|
||||
self.memory = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_resolution(
|
||||
&mut self,
|
||||
resolution: Option<WindowSize>,
|
||||
) -> &mut Self {
|
||||
self.resolution = resolution;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
||||
self.hooks = hooks;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
||||
Ok(Default::default()),
|
||||
|bytes| {
|
||||
bincode::decode_from_slice::<Box<[PathBuf]>, _>(
|
||||
&bytes,
|
||||
*BINCODE_CONFIG,
|
||||
)
|
||||
.map(|it| it.0)
|
||||
},
|
||||
)?;
|
||||
|
||||
let profiles = stream::iter(profile_db.iter())
|
||||
.then(|it| async move {
|
||||
let path = PathBuf::from(it);
|
||||
let prof = match Self::read_profile_from_dir(&path).await {
|
||||
Ok(prof) => Some(prof),
|
||||
Err(err) => {
|
||||
log::warn!("Error loading profile: {err}. Skipping...");
|
||||
None
|
||||
}
|
||||
};
|
||||
(path, prof)
|
||||
})
|
||||
.collect::<HashMap<PathBuf, Option<Profile>>>()
|
||||
.await;
|
||||
|
||||
Ok(Self(profiles))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
self.0.insert(
|
||||
profile
|
||||
.path
|
||||
.canonicalize()?
|
||||
.to_str()
|
||||
.ok_or(crate::Error::UTFError(profile.path.clone()))?
|
||||
.into(),
|
||||
Some(profile),
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn insert_from<'a>(
|
||||
&'a mut self,
|
||||
path: &'a Path,
|
||||
) -> crate::Result<&Self> {
|
||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
||||
self.0.remove(&path);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn sync<'a>(
|
||||
&'a self,
|
||||
batch: &'a mut sled::Batch,
|
||||
) -> crate::Result<&Self> {
|
||||
stream::iter(self.0.iter())
|
||||
.map(Ok::<_, crate::Error>)
|
||||
.try_for_each_concurrent(None, |(path, profile)| async move {
|
||||
let json = serde_json::to_vec_pretty(&profile)?;
|
||||
|
||||
let json_path =
|
||||
Path::new(path.to_str().unwrap()).join(PROFILE_JSON_PATH);
|
||||
|
||||
fs::write(json_path, json).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
batch.insert(
|
||||
PROFILE_SUBTREE,
|
||||
bincode::encode_to_vec(
|
||||
self.0.keys().collect::<Box<[_]>>(),
|
||||
*BINCODE_CONFIG,
|
||||
)?,
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> {
|
||||
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = PathBuf::from(path);
|
||||
Ok(profile)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn profile_test() -> Result<(), serde_json::Error> {
|
||||
let profile = Profile {
|
||||
path: PathBuf::new(),
|
||||
metadata: ProfileMetadata {
|
||||
name: String::from("Example Pack"),
|
||||
icon: None,
|
||||
game_version: String::from("1.18.2"),
|
||||
loader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
format_version: CURRENT_FORMAT_VERSION,
|
||||
},
|
||||
java: Some(JavaSettings {
|
||||
install: Some(PathBuf::from("/usr/bin/java")),
|
||||
extra_arguments: Some(Vec::new()),
|
||||
}),
|
||||
memory: Some(MemorySettings {
|
||||
minimum: None,
|
||||
maximum: 8192,
|
||||
}),
|
||||
resolution: Some(WindowSize(1920, 1080)),
|
||||
hooks: Some(Hooks {
|
||||
pre_launch: HashSet::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::new(),
|
||||
}),
|
||||
};
|
||||
let json = serde_json::json!({
|
||||
"metadata": {
|
||||
"name": "Example Pack",
|
||||
"game_version": "1.18.2",
|
||||
"format_version": 1u32,
|
||||
"loader": "vanilla",
|
||||
},
|
||||
"java": {
|
||||
"extra_arguments": [],
|
||||
"install": "/usr/bin/java",
|
||||
},
|
||||
"memory": {
|
||||
"maximum": 8192u32,
|
||||
},
|
||||
"resolution": (1920u16, 1080u16),
|
||||
"hooks": {},
|
||||
});
|
||||
|
||||
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
|
||||
assert_str_eq!(
|
||||
format!("{:?}", serde_json::from_value::<Profile>(json)?),
|
||||
format!("{:?}", profile),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
119
theseus/src/state/settings.rs
Normal file
119
theseus/src/state/settings.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! Theseus settings file
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
// TODO: convert to semver?
|
||||
const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
// Types
|
||||
/// Global Theseus settings
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub memory: MemorySettings,
|
||||
pub game_resolution: WindowSize,
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub java_8_path: Option<PathBuf>,
|
||||
pub java_17_path: Option<PathBuf>,
|
||||
pub hooks: Hooks,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
java_8_path: None,
|
||||
java_17_path: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 64,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub async fn init(file: &Path) -> crate::Result<Self> {
|
||||
if file.exists() {
|
||||
fs::read(&file)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Error reading settings file: {err}"
|
||||
))
|
||||
})
|
||||
.and_then(|it| {
|
||||
serde_json::from_slice::<Settings>(&it)
|
||||
.map_err(crate::Error::from)
|
||||
})
|
||||
} else {
|
||||
Ok(Settings::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
fs::write(to, serde_json::to_vec_pretty(self)?)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::FSError(format!(
|
||||
"Error saving settings to file: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Minecraft memory settings
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct MemorySettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<u32>,
|
||||
pub maximum: u32,
|
||||
}
|
||||
|
||||
impl Default for MemorySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
minimum: None,
|
||||
maximum: 2048,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Game window size
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct WindowSize(pub u16, pub u16);
|
||||
|
||||
impl Default for WindowSize {
|
||||
fn default() -> Self {
|
||||
Self(854, 480)
|
||||
}
|
||||
}
|
||||
|
||||
/// Game initialization hooks
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Hooks {
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
pub pre_launch: HashSet<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
pub post_exit: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Hooks {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pre_launch: HashSet::<String>::new(),
|
||||
wrapper: None,
|
||||
post_exit: HashSet::<String>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user