Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
1432 lines
49 KiB
Rust
1432 lines
49 KiB
Rust
//! Theseus settings file
|
|
|
|
use crate::util::download::DownloadEngine;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{Pool, Row, Sqlite};
|
|
use std::collections::HashMap;
|
|
|
|
// Types
|
|
#[derive(
|
|
Serialize, Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq,
|
|
)]
|
|
#[repr(u8)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum DownloadSourceMode {
|
|
#[default]
|
|
Auto = 0,
|
|
OfficialOnly = 1,
|
|
MirrorPreferred = 2,
|
|
OfficialPreferred = 3,
|
|
}
|
|
|
|
impl DownloadSourceMode {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Auto => "auto",
|
|
Self::OfficialOnly => "official_only",
|
|
Self::MirrorPreferred => "mirror_preferred",
|
|
Self::OfficialPreferred => "official_preferred",
|
|
}
|
|
}
|
|
|
|
pub fn from_string(value: &str) -> Self {
|
|
match value {
|
|
"official_only" => Self::OfficialOnly,
|
|
"mirror_preferred" => Self::MirrorPreferred,
|
|
"official_preferred" => Self::OfficialPreferred,
|
|
_ => Self::Auto,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn from_u8(value: u8) -> Self {
|
|
match value {
|
|
1 => Self::OfficialOnly,
|
|
2 => Self::MirrorPreferred,
|
|
3 => Self::OfficialPreferred,
|
|
_ => Self::Auto,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn prefers_mirror(self, auto_prefers_mirror: bool) -> bool {
|
|
match self {
|
|
Self::Auto => auto_prefers_mirror,
|
|
Self::OfficialOnly | Self::OfficialPreferred => false,
|
|
Self::MirrorPreferred => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
|
|
pub enum HomeLayout {
|
|
#[default]
|
|
Standard,
|
|
Minimal,
|
|
}
|
|
|
|
impl HomeLayout {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Standard => "standard",
|
|
Self::Minimal => "minimal",
|
|
}
|
|
}
|
|
|
|
pub fn from_string(value: &str) -> Self {
|
|
match value {
|
|
"minimal" => Self::Minimal,
|
|
_ => Self::Standard,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Serialize for HomeLayout {
|
|
fn serialize<S: serde::Serializer>(
|
|
&self,
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error> {
|
|
serializer.serialize_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for HomeLayout {
|
|
fn deserialize<D: serde::Deserializer<'de>>(
|
|
deserializer: D,
|
|
) -> Result<Self, D::Error> {
|
|
let value = String::deserialize(deserializer)?;
|
|
Ok(Self::from_string(&value))
|
|
}
|
|
}
|
|
|
|
/// Global Theseus settings
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct Settings {
|
|
pub max_concurrent_downloads: usize,
|
|
pub max_concurrent_writes: usize,
|
|
#[serde(default)]
|
|
pub download_engine: DownloadEngine,
|
|
#[serde(default)]
|
|
pub auto_concurrent_downloads: bool,
|
|
#[serde(default)]
|
|
pub minecraft_metadata_source: DownloadSourceMode,
|
|
#[serde(default)]
|
|
pub minecraft_file_source: DownloadSourceMode,
|
|
#[serde(default)]
|
|
pub modrinth_source: DownloadSourceMode,
|
|
#[serde(default)]
|
|
pub curseforge_source: DownloadSourceMode,
|
|
#[serde(default = "default_true")]
|
|
pub bypass_curseforge_download_restrictions: bool,
|
|
#[serde(default)]
|
|
pub mojang_auth_source: DownloadSourceMode,
|
|
#[serde(default, rename = "use_minecraft_mirror", skip_serializing)]
|
|
legacy_use_minecraft_mirror: Option<bool>,
|
|
#[serde(default, rename = "use_modrinth_mirror", skip_serializing)]
|
|
legacy_use_modrinth_mirror: Option<bool>,
|
|
#[serde(default, rename = "use_curseforge_mirror", skip_serializing)]
|
|
legacy_use_curseforge_mirror: Option<bool>,
|
|
|
|
pub theme: Theme,
|
|
pub accent_color: AccentColor,
|
|
pub locale: String,
|
|
pub default_page: DefaultPage,
|
|
pub collapsed_navigation: bool,
|
|
pub hide_nametag_skins_page: bool,
|
|
pub advanced_rendering: bool,
|
|
pub native_decorations: bool,
|
|
pub toggle_sidebar: bool,
|
|
pub custom_background_path: Option<String>,
|
|
pub custom_background_blur: u32,
|
|
pub custom_background_opacity: u32,
|
|
pub transparent_background: bool,
|
|
pub transparent_background_opacity: u32,
|
|
pub transparent_background_blur: bool,
|
|
pub sidebar_instance_count: u32,
|
|
#[serde(default)]
|
|
pub close_behavior: String,
|
|
#[serde(default)]
|
|
pub auto_hide_downloads_button: bool,
|
|
#[serde(default)]
|
|
pub home_layout: HomeLayout,
|
|
#[serde(default)]
|
|
pub minimal_home_instance_id: Option<String>,
|
|
#[serde(default)]
|
|
pub home_widgets: Option<serde_json::Value>,
|
|
#[serde(default = "default_terracotta_public_nodes")]
|
|
pub terracotta_public_nodes: Vec<String>,
|
|
|
|
#[serde(skip, default)]
|
|
pub personalized_ads: bool,
|
|
|
|
pub onboarded: bool,
|
|
pub onboarding_version: usize,
|
|
pub onboarding_instance_tour_completed: bool,
|
|
|
|
pub extra_launch_args: Vec<String>,
|
|
pub custom_env_vars: Vec<(String, String)>,
|
|
pub memory: MemorySettings,
|
|
pub force_fullscreen: bool,
|
|
pub maximize_window: bool,
|
|
#[serde(default)]
|
|
pub force_unicode_font: bool,
|
|
pub game_resolution: WindowSize,
|
|
pub hide_on_process_start: bool,
|
|
pub enter_lightweight_mode_on_game_launch: bool,
|
|
pub auto_set_java_high_performance_mode: bool,
|
|
pub hooks: Hooks,
|
|
|
|
pub custom_dir: Option<String>,
|
|
pub prev_custom_dir: Option<String>,
|
|
pub migrated: bool,
|
|
|
|
pub developer_mode: bool,
|
|
pub feature_flags: HashMap<FeatureFlag, bool>,
|
|
|
|
pub skipped_update: Option<String>,
|
|
pub pending_update_toast_for_version: Option<String>,
|
|
pub auto_download_updates: Option<bool>,
|
|
|
|
pub version: usize,
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum FeatureFlag {
|
|
PagePath,
|
|
ProjectBackground,
|
|
WorldsTab,
|
|
WorldsInHome,
|
|
ServerRamAsBytesAlwaysOn,
|
|
AlwaysShowAppControls,
|
|
SkipUnknownPackWarning,
|
|
PrideFundraiser,
|
|
ServersInApp,
|
|
ServerProjectQa,
|
|
I18nDebug,
|
|
ShowInstancePlayTime,
|
|
SkipNonEssentialWarnings,
|
|
AdvancedFiltersCollapsed,
|
|
PageTransitions,
|
|
ShowVersionEnvironmentColumn,
|
|
XmclDownloadEngine,
|
|
AutoInstallDependencies,
|
|
}
|
|
|
|
impl Settings {
|
|
const CURRENT_VERSION: usize = 3;
|
|
|
|
pub async fn get<'a, E>(exec: E) -> crate::Result<Self>
|
|
where
|
|
E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy,
|
|
{
|
|
let res = sqlx::query!(
|
|
"
|
|
SELECT
|
|
max_concurrent_writes, max_concurrent_downloads,
|
|
auto_concurrent_downloads, minecraft_metadata_source,
|
|
minecraft_file_source, modrinth_source, curseforge_source, mojang_auth_source,
|
|
theme, locale, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,
|
|
discord_rpc, developer_mode, telemetry, telemetry_consent_version, personalized_ads,
|
|
onboarded, onboarding_version, onboarding_instance_tour_completed,
|
|
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
|
|
mc_memory_max, mc_memory_auto, mc_force_fullscreen, mc_maximize_window, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start, enter_lightweight_mode_on_game_launch,
|
|
auto_set_java_high_performance_mode,
|
|
hook_pre_launch, hook_wrapper, hook_post_exit,
|
|
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,
|
|
skipped_update, pending_update_toast_for_version, auto_download_updates, accent_color,
|
|
custom_background_path, custom_background_blur, custom_background_opacity,
|
|
transparent_background, transparent_background_opacity, transparent_background_blur,
|
|
sidebar_instance_count, home_layout, minimal_home_instance_id,
|
|
json(home_widgets) as \"home_widgets?: String\", auto_hide_downloads_button,
|
|
json(terracotta_public_nodes) terracotta_public_nodes,
|
|
version
|
|
FROM settings
|
|
"
|
|
)
|
|
.fetch_one(exec)
|
|
.await?;
|
|
|
|
let close_behavior: String = sqlx::query_scalar(
|
|
"SELECT close_behavior FROM settings WHERE id = 0",
|
|
)
|
|
.fetch_one(exec)
|
|
.await?;
|
|
|
|
let engine_row =
|
|
sqlx::query("SELECT download_engine FROM settings WHERE id = 0")
|
|
.fetch_one(exec)
|
|
.await?;
|
|
let download_engine = DownloadEngine::from_str(
|
|
&engine_row.get::<String, _>("download_engine"),
|
|
);
|
|
let bypass_curseforge_download_restrictions: bool = sqlx::query_scalar(
|
|
"SELECT bypass_curseforge_download_restrictions FROM settings WHERE id = 0",
|
|
)
|
|
.fetch_one(exec)
|
|
.await?;
|
|
let settings = Self {
|
|
max_concurrent_downloads: res.max_concurrent_downloads as usize,
|
|
max_concurrent_writes: res.max_concurrent_writes as usize,
|
|
download_engine,
|
|
auto_concurrent_downloads: res.auto_concurrent_downloads == 1,
|
|
minecraft_metadata_source: DownloadSourceMode::from_string(
|
|
&res.minecraft_metadata_source,
|
|
),
|
|
minecraft_file_source: DownloadSourceMode::from_string(
|
|
&res.minecraft_file_source,
|
|
),
|
|
modrinth_source: DownloadSourceMode::from_string(
|
|
&res.modrinth_source,
|
|
),
|
|
curseforge_source: DownloadSourceMode::from_string(
|
|
&res.curseforge_source,
|
|
),
|
|
bypass_curseforge_download_restrictions,
|
|
mojang_auth_source: DownloadSourceMode::from_string(
|
|
&res.mojang_auth_source,
|
|
),
|
|
legacy_use_minecraft_mirror: None,
|
|
legacy_use_modrinth_mirror: None,
|
|
legacy_use_curseforge_mirror: None,
|
|
theme: Theme::from_string(&res.theme),
|
|
accent_color: AccentColor::from_string(&res.accent_color),
|
|
locale: res.locale,
|
|
default_page: DefaultPage::from_string(&res.default_page),
|
|
collapsed_navigation: res.collapsed_navigation == 1,
|
|
hide_nametag_skins_page: res.hide_nametag_skins_page == 1,
|
|
advanced_rendering: res.advanced_rendering == 1,
|
|
native_decorations: res.native_decorations == 1,
|
|
toggle_sidebar: res.toggle_sidebar == 1,
|
|
custom_background_path: res.custom_background_path,
|
|
custom_background_blur: res.custom_background_blur as u32,
|
|
custom_background_opacity: res.custom_background_opacity as u32,
|
|
transparent_background: res.transparent_background == 1,
|
|
transparent_background_opacity: res.transparent_background_opacity
|
|
as u32,
|
|
transparent_background_blur: res.transparent_background_blur == 1,
|
|
sidebar_instance_count: res.sidebar_instance_count as u32,
|
|
close_behavior,
|
|
auto_hide_downloads_button: res.auto_hide_downloads_button == 1,
|
|
home_layout: HomeLayout::from_string(&res.home_layout),
|
|
minimal_home_instance_id: res.minimal_home_instance_id,
|
|
home_widgets: res
|
|
.home_widgets
|
|
.as_ref()
|
|
.and_then(|value| serde_json::from_str(value).ok()),
|
|
terracotta_public_nodes: res
|
|
.terracotta_public_nodes
|
|
.as_ref()
|
|
.and_then(|value| serde_json::from_str(value).ok())
|
|
.unwrap_or_else(default_terracotta_public_nodes),
|
|
developer_mode: res.developer_mode == 1,
|
|
personalized_ads: res.personalized_ads == 1,
|
|
onboarded: res.onboarded == 1,
|
|
onboarding_version: res.onboarding_version as usize,
|
|
onboarding_instance_tour_completed: res
|
|
.onboarding_instance_tour_completed
|
|
== 1,
|
|
extra_launch_args: res
|
|
.extra_launch_args
|
|
.as_ref()
|
|
.and_then(|x| serde_json::from_str(x).ok())
|
|
.unwrap_or_default(),
|
|
custom_env_vars: res
|
|
.custom_env_vars
|
|
.as_ref()
|
|
.and_then(|x| serde_json::from_str(x).ok())
|
|
.unwrap_or_default(),
|
|
memory: MemorySettings {
|
|
maximum: res.mc_memory_max as u32,
|
|
automatic: res.mc_memory_auto == 1,
|
|
optimize_before_launch: sqlx::query_scalar(
|
|
"SELECT mc_memory_optimize FROM settings WHERE id = 0",
|
|
)
|
|
.fetch_one(exec)
|
|
.await?,
|
|
},
|
|
force_fullscreen: res.mc_force_fullscreen == 1,
|
|
maximize_window: res.mc_maximize_window == 1,
|
|
force_unicode_font: sqlx::query_scalar(
|
|
"SELECT mc_force_unicode_font FROM settings WHERE id = 0",
|
|
)
|
|
.fetch_one(exec)
|
|
.await?,
|
|
game_resolution: WindowSize(
|
|
res.mc_game_resolution_x as u16,
|
|
res.mc_game_resolution_y as u16,
|
|
),
|
|
hide_on_process_start: res.hide_on_process_start == 1,
|
|
enter_lightweight_mode_on_game_launch: res
|
|
.enter_lightweight_mode_on_game_launch
|
|
== 1,
|
|
auto_set_java_high_performance_mode: res
|
|
.auto_set_java_high_performance_mode
|
|
== 1,
|
|
hooks: Hooks {
|
|
pre_launch: res.hook_pre_launch,
|
|
wrapper: res.hook_wrapper,
|
|
post_exit: res.hook_post_exit,
|
|
},
|
|
custom_dir: res.custom_dir,
|
|
prev_custom_dir: res.prev_custom_dir,
|
|
migrated: res.migrated == 1,
|
|
feature_flags: res
|
|
.feature_flags
|
|
.as_ref()
|
|
.and_then(|x| serde_json::from_str(x).ok())
|
|
.unwrap_or_default(),
|
|
skipped_update: res.skipped_update,
|
|
pending_update_toast_for_version: res
|
|
.pending_update_toast_for_version,
|
|
auto_download_updates: res.auto_download_updates.map(|x| x == 1),
|
|
version: res.version as usize,
|
|
};
|
|
crate::util::download::set_active_engine(settings.download_engine);
|
|
Ok(settings)
|
|
}
|
|
|
|
pub async fn update<'a, E>(&self, exec: E) -> crate::Result<()>
|
|
where
|
|
E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy,
|
|
{
|
|
let max_concurrent_writes = self.max_concurrent_writes as i32;
|
|
let max_concurrent_downloads =
|
|
self.max_concurrent_downloads.clamp(1, 256) as i32;
|
|
let theme = self.theme.as_str();
|
|
let accent_color = self.accent_color.as_str();
|
|
let default_page = self.default_page.as_str();
|
|
let extra_launch_args = serde_json::to_string(&self.extra_launch_args)?;
|
|
let custom_env_vars = serde_json::to_string(&self.custom_env_vars)?;
|
|
let feature_flags = serde_json::to_string(&self.feature_flags)?;
|
|
let custom_background_blur = self.custom_background_blur.min(40) as i32;
|
|
let custom_background_opacity =
|
|
self.custom_background_opacity.clamp(10, 100) as i32;
|
|
let transparent_background_opacity =
|
|
self.transparent_background_opacity.min(100) as i32;
|
|
let sidebar_instance_count = self.sidebar_instance_count.min(50) as i32;
|
|
let home_layout = self.home_layout.as_str();
|
|
let home_widgets = self
|
|
.home_widgets
|
|
.as_ref()
|
|
.map(serde_json::to_string)
|
|
.transpose()?;
|
|
let terracotta_public_nodes =
|
|
serde_json::to_string(&self.terracotta_public_nodes)?;
|
|
let version = self.version as i64;
|
|
let onboarding_version = self.onboarding_version as i64;
|
|
let minecraft_metadata_source = self.minecraft_metadata_source.as_str();
|
|
let minecraft_file_source = self.minecraft_file_source.as_str();
|
|
let modrinth_source = self.modrinth_source.as_str();
|
|
let curseforge_source = self.curseforge_source.as_str();
|
|
let mojang_auth_source = self.mojang_auth_source.as_str();
|
|
let auto_prefers_mirror = self.auto_prefers_mirror();
|
|
let use_minecraft_mirror = self
|
|
.minecraft_file_source
|
|
.prefers_mirror(auto_prefers_mirror);
|
|
let use_modrinth_mirror =
|
|
self.modrinth_source.prefers_mirror(auto_prefers_mirror);
|
|
let use_curseforge_mirror =
|
|
self.curseforge_source.prefers_mirror(auto_prefers_mirror);
|
|
|
|
sqlx::query!(
|
|
"
|
|
UPDATE settings
|
|
SET
|
|
max_concurrent_writes = $1,
|
|
max_concurrent_downloads = $2,
|
|
|
|
theme = $3,
|
|
locale = $4,
|
|
default_page = $5,
|
|
collapsed_navigation = $6,
|
|
advanced_rendering = $7,
|
|
native_decorations = $8,
|
|
|
|
discord_rpc = $9,
|
|
developer_mode = $10,
|
|
telemetry = $11,
|
|
personalized_ads = $12,
|
|
|
|
onboarded = $13,
|
|
|
|
extra_launch_args = jsonb($14),
|
|
custom_env_vars = jsonb($15),
|
|
mc_memory_max = $16,
|
|
mc_memory_auto = $17,
|
|
mc_force_fullscreen = $18,
|
|
mc_maximize_window = $19,
|
|
mc_game_resolution_x = $20,
|
|
mc_game_resolution_y = $21,
|
|
hide_on_process_start = $22,
|
|
auto_set_java_high_performance_mode = $23,
|
|
|
|
hook_pre_launch = $24,
|
|
hook_wrapper = $25,
|
|
hook_post_exit = $26,
|
|
|
|
custom_dir = $27,
|
|
prev_custom_dir = $28,
|
|
migrated = $29,
|
|
|
|
toggle_sidebar = $30,
|
|
feature_flags = $31,
|
|
hide_nametag_skins_page = $32,
|
|
|
|
skipped_update = $33,
|
|
pending_update_toast_for_version = $34,
|
|
auto_download_updates = $35,
|
|
accent_color = $36,
|
|
custom_background_path = $37,
|
|
custom_background_blur = $38,
|
|
custom_background_opacity = $39,
|
|
|
|
version = $40,
|
|
auto_concurrent_downloads = $41,
|
|
minecraft_metadata_source = $42,
|
|
minecraft_file_source = $43,
|
|
modrinth_source = $44,
|
|
curseforge_source = $45,
|
|
use_minecraft_mirror = $46,
|
|
use_modrinth_mirror = $47,
|
|
use_curseforge_mirror = $48,
|
|
onboarding_version = $49,
|
|
onboarding_instance_tour_completed = $50,
|
|
sidebar_instance_count = $51,
|
|
transparent_background = $52,
|
|
transparent_background_opacity = $53,
|
|
transparent_background_blur = $54,
|
|
home_layout = $55,
|
|
minimal_home_instance_id = $56,
|
|
auto_hide_downloads_button = $57,
|
|
home_widgets = jsonb($58),
|
|
mojang_auth_source = $59,
|
|
terracotta_public_nodes = jsonb($60),
|
|
telemetry_consent_version = $61
|
|
",
|
|
max_concurrent_writes,
|
|
max_concurrent_downloads,
|
|
theme,
|
|
self.locale,
|
|
default_page,
|
|
self.collapsed_navigation,
|
|
self.advanced_rendering,
|
|
self.native_decorations,
|
|
false,
|
|
self.developer_mode,
|
|
false,
|
|
self.personalized_ads,
|
|
self.onboarded,
|
|
extra_launch_args,
|
|
custom_env_vars,
|
|
self.memory.maximum,
|
|
self.memory.automatic,
|
|
self.force_fullscreen,
|
|
self.maximize_window,
|
|
self.game_resolution.0,
|
|
self.game_resolution.1,
|
|
self.hide_on_process_start,
|
|
self.auto_set_java_high_performance_mode,
|
|
self.hooks.pre_launch,
|
|
self.hooks.wrapper,
|
|
self.hooks.post_exit,
|
|
self.custom_dir,
|
|
self.prev_custom_dir,
|
|
self.migrated,
|
|
self.toggle_sidebar,
|
|
feature_flags,
|
|
self.hide_nametag_skins_page,
|
|
self.skipped_update,
|
|
self.pending_update_toast_for_version,
|
|
self.auto_download_updates,
|
|
accent_color,
|
|
self.custom_background_path,
|
|
custom_background_blur,
|
|
custom_background_opacity,
|
|
version,
|
|
self.auto_concurrent_downloads,
|
|
minecraft_metadata_source,
|
|
minecraft_file_source,
|
|
modrinth_source,
|
|
curseforge_source,
|
|
use_minecraft_mirror,
|
|
use_modrinth_mirror,
|
|
use_curseforge_mirror,
|
|
onboarding_version,
|
|
self.onboarding_instance_tour_completed,
|
|
sidebar_instance_count,
|
|
self.transparent_background,
|
|
transparent_background_opacity,
|
|
self.transparent_background_blur,
|
|
home_layout,
|
|
self.minimal_home_instance_id,
|
|
self.auto_hide_downloads_button,
|
|
home_widgets,
|
|
mojang_auth_source,
|
|
terracotta_public_nodes,
|
|
0_i64,
|
|
)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"UPDATE settings SET enter_lightweight_mode_on_game_launch = ? WHERE id = 0",
|
|
)
|
|
.bind(self.enter_lightweight_mode_on_game_launch)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
sqlx::query("UPDATE settings SET close_behavior = ? WHERE id = 0")
|
|
.bind(&self.close_behavior)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
sqlx::query("UPDATE settings SET download_engine = ? WHERE id = 0")
|
|
.bind(self.download_engine.as_str())
|
|
.execute(exec)
|
|
.await?;
|
|
sqlx::query(
|
|
"UPDATE settings SET bypass_curseforge_download_restrictions = ? WHERE id = 0",
|
|
)
|
|
.bind(self.bypass_curseforge_download_restrictions)
|
|
.execute(exec)
|
|
.await?;
|
|
sqlx::query("UPDATE settings SET mc_memory_optimize = ? WHERE id = 0")
|
|
.bind(self.memory.optimize_before_launch)
|
|
.execute(exec)
|
|
.await?;
|
|
sqlx::query(
|
|
"UPDATE settings SET mc_force_unicode_font = ? WHERE id = 0",
|
|
)
|
|
.bind(self.force_unicode_font)
|
|
.execute(exec)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn effective_max_concurrent_downloads(&self) -> usize {
|
|
if self.auto_concurrent_downloads {
|
|
64
|
|
} else {
|
|
self.max_concurrent_downloads.clamp(1, 256)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn apply_legacy_download_source_settings(&mut self) {
|
|
let has_legacy_settings = self.legacy_use_minecraft_mirror.is_some()
|
|
|| self.legacy_use_modrinth_mirror.is_some()
|
|
|| self.legacy_use_curseforge_mirror.is_some();
|
|
let has_explicit_source_settings = self.minecraft_metadata_source
|
|
!= DownloadSourceMode::Auto
|
|
|| self.minecraft_file_source != DownloadSourceMode::Auto
|
|
|| self.modrinth_source != DownloadSourceMode::Auto
|
|
|| self.curseforge_source != DownloadSourceMode::Auto;
|
|
if !has_legacy_settings || has_explicit_source_settings {
|
|
return;
|
|
}
|
|
|
|
if self.legacy_use_minecraft_mirror == Some(false)
|
|
&& self.legacy_use_modrinth_mirror == Some(false)
|
|
&& self.legacy_use_curseforge_mirror == Some(true)
|
|
{
|
|
self.minecraft_metadata_source = DownloadSourceMode::Auto;
|
|
self.minecraft_file_source = DownloadSourceMode::Auto;
|
|
self.modrinth_source = DownloadSourceMode::Auto;
|
|
self.curseforge_source = DownloadSourceMode::Auto;
|
|
return;
|
|
}
|
|
|
|
if let Some(enabled) = self.legacy_use_minecraft_mirror {
|
|
let source = legacy_download_source(enabled);
|
|
self.minecraft_metadata_source = source;
|
|
self.minecraft_file_source = source;
|
|
}
|
|
if let Some(enabled) = self.legacy_use_modrinth_mirror {
|
|
self.modrinth_source = legacy_download_source(enabled);
|
|
}
|
|
if let Some(enabled) = self.legacy_use_curseforge_mirror {
|
|
self.curseforge_source = legacy_download_source(enabled);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn auto_prefers_mirror(&self) -> bool {
|
|
let timezone = std::env::var("TZ").ok().or_else(|| {
|
|
std::fs::read_link("/etc/localtime")
|
|
.ok()
|
|
.map(|path| path.to_string_lossy().into_owned())
|
|
});
|
|
|
|
if let Some(timezone) = timezone {
|
|
return locale_prefers_mirror(&timezone);
|
|
}
|
|
|
|
locale_prefers_mirror(&self.locale)
|
|
|| ["LC_ALL", "LC_MESSAGES", "LANG"]
|
|
.into_iter()
|
|
.filter_map(|key| std::env::var(key).ok())
|
|
.any(|value| locale_prefers_mirror(&value))
|
|
}
|
|
|
|
pub async fn migrate(exec: &Pool<Sqlite>) -> crate::Result<()> {
|
|
let mut settings = Self::get(exec).await?;
|
|
|
|
if settings.version < Settings::CURRENT_VERSION {
|
|
tracing::info!(
|
|
"Migrating settings version {} to {:?}",
|
|
settings.version,
|
|
Settings::CURRENT_VERSION
|
|
);
|
|
}
|
|
while settings.version < Settings::CURRENT_VERSION {
|
|
if let Err(err) = settings.perform_migration() {
|
|
tracing::error!(
|
|
"Failed to migrate settings from version {}: {}",
|
|
settings.version,
|
|
err
|
|
);
|
|
return Err(err);
|
|
}
|
|
}
|
|
|
|
settings.update(exec).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn perform_migration(&mut self) -> crate::Result<()> {
|
|
match self.version {
|
|
1 => {
|
|
let quoter = shlex::Quoter::new().allow_nul(true);
|
|
|
|
// Previously split by spaces
|
|
if let Some(pre_launch) = self.hooks.pre_launch.as_ref() {
|
|
self.hooks.pre_launch =
|
|
Some(quoter.join(pre_launch.split(' ')).unwrap())
|
|
}
|
|
|
|
// Previously treated as complete path to command
|
|
if let Some(wrapper) = self.hooks.wrapper.as_ref() {
|
|
self.hooks.wrapper =
|
|
Some(quoter.quote(wrapper).unwrap().to_string())
|
|
}
|
|
|
|
// Previously split by spaces
|
|
if let Some(post_exit) = self.hooks.post_exit.as_ref() {
|
|
self.hooks.post_exit =
|
|
Some(quoter.join(post_exit.split(' ')).unwrap())
|
|
}
|
|
|
|
self.version = 2;
|
|
}
|
|
2 => {
|
|
// Update old default memory setting from 2GB to 4GB (depending on system memory)
|
|
const LEGACY_DEFAULT_MEMORY_MB: u32 = 2048;
|
|
if self.memory.maximum == LEGACY_DEFAULT_MEMORY_MB {
|
|
self.memory.maximum =
|
|
crate::api::jre::default_memory_max_mb();
|
|
}
|
|
|
|
self.version = 3;
|
|
}
|
|
version => {
|
|
return Err(crate::ErrorKind::OtherError(format!(
|
|
"Invalid settings version: {version}"
|
|
))
|
|
.into());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn default_terracotta_public_nodes() -> Vec<String> {
|
|
vec!["wss://center.node.1tmc.top".to_string()]
|
|
}
|
|
|
|
fn locale_prefers_mirror(value: &str) -> bool {
|
|
let normalized = value.to_ascii_lowercase().replace('_', "-");
|
|
|
|
normalized.starts_with("zh-cn")
|
|
|| normalized.starts_with("zh-hans")
|
|
|| normalized.contains("asia/shanghai")
|
|
|| normalized.contains("asia/chongqing")
|
|
|| normalized.contains("asia/harbin")
|
|
|| normalized.contains("asia/urumqi")
|
|
}
|
|
|
|
fn legacy_download_source(enabled: bool) -> DownloadSourceMode {
|
|
if enabled {
|
|
DownloadSourceMode::MirrorPreferred
|
|
} else {
|
|
DownloadSourceMode::OfficialOnly
|
|
}
|
|
}
|
|
|
|
/// Accent color used for interactive controls and highlights.
|
|
///
|
|
/// Serialized as a plain string: either a preset name (`pink`, `orange`, ...)
|
|
/// or `custom:#rrggbb` for a user-defined color. Unknown values fall back to
|
|
/// [`AccentColor::Pink`], keeping older builds forward-compatible.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum AccentColor {
|
|
Pink,
|
|
Orange,
|
|
Green,
|
|
Blue,
|
|
Purple,
|
|
System,
|
|
Custom(String),
|
|
}
|
|
|
|
impl AccentColor {
|
|
pub fn as_str(&self) -> &str {
|
|
match self {
|
|
AccentColor::Pink => "pink",
|
|
AccentColor::Orange => "orange",
|
|
AccentColor::Green => "green",
|
|
AccentColor::Blue => "blue",
|
|
AccentColor::Purple => "purple",
|
|
AccentColor::System => "system",
|
|
AccentColor::Custom(value) => {
|
|
if Self::is_valid_custom(value) {
|
|
value
|
|
} else {
|
|
"pink"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn from_string(string: &str) -> AccentColor {
|
|
match string {
|
|
"orange" => AccentColor::Orange,
|
|
"green" => AccentColor::Green,
|
|
"blue" => AccentColor::Blue,
|
|
"purple" => AccentColor::Purple,
|
|
"system" => AccentColor::System,
|
|
other => match Self::parse_custom(other) {
|
|
Some(custom) => custom,
|
|
None => AccentColor::Pink,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn is_valid_custom(string: &str) -> bool {
|
|
string.strip_prefix("custom:#").is_some_and(|hex| {
|
|
hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit())
|
|
})
|
|
}
|
|
|
|
fn parse_custom(string: &str) -> Option<AccentColor> {
|
|
if Self::is_valid_custom(string) {
|
|
Some(AccentColor::Custom(string.to_ascii_lowercase()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Serialize for AccentColor {
|
|
fn serialize<S: serde::Serializer>(
|
|
&self,
|
|
serializer: S,
|
|
) -> Result<S::Ok, S::Error> {
|
|
serializer.serialize_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for AccentColor {
|
|
fn deserialize<D: serde::Deserializer<'de>>(
|
|
deserializer: D,
|
|
) -> Result<Self, D::Error> {
|
|
let value = String::deserialize(deserializer)?;
|
|
Ok(AccentColor::from_string(&value))
|
|
}
|
|
}
|
|
|
|
/// Theseus theme
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Theme {
|
|
Dark,
|
|
Light,
|
|
Oled,
|
|
System,
|
|
}
|
|
|
|
impl Theme {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Theme::Dark => "dark",
|
|
Theme::Light => "light",
|
|
Theme::Oled => "oled",
|
|
Theme::System => "system",
|
|
}
|
|
}
|
|
|
|
pub fn from_string(string: &str) -> Theme {
|
|
match string {
|
|
"dark" => Theme::Dark,
|
|
"light" => Theme::Light,
|
|
"oled" => Theme::Oled,
|
|
"system" => Theme::System,
|
|
_ => Theme::Dark,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Minecraft memory settings
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
|
pub struct MemorySettings {
|
|
pub maximum: u32,
|
|
#[serde(default = "default_true")]
|
|
pub automatic: bool,
|
|
#[serde(default)]
|
|
pub optimize_before_launch: bool,
|
|
}
|
|
|
|
/// Game window size
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
|
pub struct WindowSize(pub u16, pub u16);
|
|
|
|
/// Game initialization hooks
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
|
#[serde_with::serde_as]
|
|
pub struct Hooks {
|
|
#[serde_as(as = "serde_with::NoneAsEmptyString")]
|
|
pub pre_launch: Option<String>,
|
|
#[serde_as(as = "serde_with::NoneAsEmptyString")]
|
|
pub wrapper: Option<String>,
|
|
#[serde_as(as = "serde_with::NoneAsEmptyString")]
|
|
pub post_exit: Option<String>,
|
|
}
|
|
|
|
/// Opening window to start with
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
|
pub enum DefaultPage {
|
|
Home,
|
|
DiscoverContent,
|
|
Library,
|
|
}
|
|
|
|
impl DefaultPage {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
DefaultPage::Home => "home",
|
|
DefaultPage::DiscoverContent => "discover_content",
|
|
DefaultPage::Library => "library",
|
|
}
|
|
}
|
|
|
|
pub fn from_string(string: &str) -> Self {
|
|
match string {
|
|
"home" => Self::Home,
|
|
"discover_content" => Self::DiscoverContent,
|
|
"library" => Self::Library,
|
|
_ => Self::Home,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn home_layout_uses_stable_wire_values() {
|
|
assert_eq!(HomeLayout::from_string("standard"), HomeLayout::Standard);
|
|
assert_eq!(HomeLayout::from_string("minimal"), HomeLayout::Minimal);
|
|
assert_eq!(HomeLayout::from_string("unknown"), HomeLayout::Standard);
|
|
assert_eq!(
|
|
serde_json::to_string(&HomeLayout::Standard).unwrap(),
|
|
"\"standard\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&HomeLayout::Minimal).unwrap(),
|
|
"\"minimal\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<HomeLayout>("\"future-layout\"").unwrap(),
|
|
HomeLayout::Standard
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn accent_color_parses_preset_values() {
|
|
assert_eq!(AccentColor::from_string("pink"), AccentColor::Pink);
|
|
assert_eq!(AccentColor::from_string("orange"), AccentColor::Orange);
|
|
assert_eq!(AccentColor::from_string("green"), AccentColor::Green);
|
|
assert_eq!(AccentColor::from_string("blue"), AccentColor::Blue);
|
|
assert_eq!(AccentColor::from_string("purple"), AccentColor::Purple);
|
|
assert_eq!(AccentColor::from_string("system"), AccentColor::System);
|
|
}
|
|
|
|
#[test]
|
|
fn accent_color_normalizes_custom_hex_to_lowercase() {
|
|
assert_eq!(
|
|
AccentColor::from_string("custom:#DB2777"),
|
|
AccentColor::Custom("custom:#db2777".to_owned())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn accent_color_falls_back_to_pink_on_invalid_values() {
|
|
for value in [
|
|
"",
|
|
"magenta",
|
|
"custom:",
|
|
"custom:#db27",
|
|
"custom:#db2777aa",
|
|
"custom:#db277g",
|
|
] {
|
|
assert_eq!(AccentColor::from_string(value), AccentColor::Pink);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn accent_color_serializes_as_plain_strings() {
|
|
let custom = AccentColor::Custom("custom:#db2777".to_owned());
|
|
assert_eq!(
|
|
serde_json::to_string(&AccentColor::Blue).unwrap(),
|
|
"\"blue\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&AccentColor::System).unwrap(),
|
|
"\"system\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&custom).unwrap(),
|
|
"\"custom:#db2777\""
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn accent_color_serializes_invalid_custom_as_pink() {
|
|
let invalid = AccentColor::Custom("not-a-color".to_owned());
|
|
assert_eq!(invalid.as_str(), "pink");
|
|
assert_eq!(serde_json::to_string(&invalid).unwrap(), "\"pink\"");
|
|
}
|
|
|
|
#[test]
|
|
fn accent_color_deserializes_from_plain_strings() {
|
|
let color: AccentColor =
|
|
serde_json::from_str("\"custom:#1bd96a\"").unwrap();
|
|
assert_eq!(color, AccentColor::Custom("custom:#1bd96a".to_owned()));
|
|
let preset: AccentColor = serde_json::from_str("\"purple\"").unwrap();
|
|
assert_eq!(preset, AccentColor::Purple);
|
|
let system: AccentColor = serde_json::from_str("\"system\"").unwrap();
|
|
assert_eq!(system, AccentColor::System);
|
|
}
|
|
|
|
#[test]
|
|
fn download_source_mode_uses_stable_wire_values() {
|
|
assert_eq!(
|
|
serde_json::to_string(&DownloadSourceMode::Auto).unwrap(),
|
|
"\"auto\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&DownloadSourceMode::OfficialOnly).unwrap(),
|
|
"\"official_only\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&DownloadSourceMode::MirrorPreferred)
|
|
.unwrap(),
|
|
"\"mirror_preferred\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&DownloadSourceMode::OfficialPreferred)
|
|
.unwrap(),
|
|
"\"official_preferred\""
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn official_preferred_sources_round_trip_in_a_fresh_database() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
sqlx::query(
|
|
"
|
|
UPDATE settings
|
|
SET
|
|
minecraft_metadata_source = 'official_preferred',
|
|
minecraft_file_source = 'official_preferred',
|
|
modrinth_source = 'official_preferred',
|
|
curseforge_source = 'official_preferred'
|
|
WHERE id = 0
|
|
",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
let settings = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(
|
|
settings.minecraft_metadata_source,
|
|
DownloadSourceMode::OfficialPreferred
|
|
);
|
|
assert_eq!(
|
|
settings.minecraft_file_source,
|
|
DownloadSourceMode::OfficialPreferred
|
|
);
|
|
assert_eq!(
|
|
settings.modrinth_source,
|
|
DownloadSourceMode::OfficialPreferred
|
|
);
|
|
assert_eq!(
|
|
settings.curseforge_source,
|
|
DownloadSourceMode::OfficialPreferred
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn curseforge_bypass_defaults_on_and_round_trips() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
assert!(settings.bypass_curseforge_download_restrictions);
|
|
|
|
settings.bypass_curseforge_download_restrictions = false;
|
|
settings.update(&pool).await.unwrap();
|
|
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert!(!reloaded.bypass_curseforge_download_restrictions);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unicode_font_defaults_off_after_upgrade_and_round_trips() {
|
|
let migrator = sqlx::migrate!();
|
|
for previous_schema in [false, true] {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
if previous_schema {
|
|
let previous_migrator = sqlx::migrate::Migrator {
|
|
migrations: std::borrow::Cow::Owned(
|
|
migrator
|
|
.iter()
|
|
.filter(|migration| {
|
|
migration.version < 20260919000000
|
|
})
|
|
.cloned()
|
|
.collect(),
|
|
),
|
|
..sqlx::migrate::Migrator::DEFAULT
|
|
};
|
|
previous_migrator.run(&pool).await.unwrap();
|
|
sqlx::query(
|
|
"UPDATE settings SET locale = 'zh-TW' WHERE id = 0",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
migrator.run(&pool).await.unwrap();
|
|
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
assert!(!settings.force_unicode_font);
|
|
if previous_schema {
|
|
assert_eq!(settings.locale, "zh-TW");
|
|
}
|
|
for enabled in [true, false] {
|
|
settings.force_unicode_font = enabled;
|
|
settings.update(&pool).await.unwrap();
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(reloaded.force_unicode_font, enabled);
|
|
}
|
|
|
|
// Older clients and serialized settings omit the newly added field.
|
|
let mut legacy = serde_json::to_value(&settings).unwrap();
|
|
legacy.as_object_mut().unwrap().remove("force_unicode_font");
|
|
let restored: Settings = serde_json::from_value(legacy).unwrap();
|
|
assert!(!restored.force_unicode_font);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn memory_optimization_round_trips_in_a_fresh_database() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
assert!(!settings.memory.optimize_before_launch);
|
|
|
|
settings.memory.optimize_before_launch = true;
|
|
settings.update(&pool).await.unwrap();
|
|
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert!(reloaded.memory.optimize_before_launch);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn lightweight_mode_setting_defaults_off_and_round_trips() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
assert!(!settings.enter_lightweight_mode_on_game_launch);
|
|
|
|
settings.enter_lightweight_mode_on_game_launch = true;
|
|
settings.update(&pool).await.unwrap();
|
|
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert!(reloaded.enter_lightweight_mode_on_game_launch);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn lightweight_mode_migration_upgrades_existing_settings_database() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::query(
|
|
"CREATE TABLE settings (id INTEGER PRIMARY KEY CHECK (id = 0))",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
sqlx::query("INSERT INTO settings (id) VALUES (0)")
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
sqlx::raw_sql(include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/migrations/20260821120001_lightweight-mode.sql"
|
|
)))
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
let enabled: i64 = sqlx::query_scalar(
|
|
"SELECT enter_lightweight_mode_on_game_launch FROM settings WHERE id = 0",
|
|
)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(enabled, 0);
|
|
assert!(
|
|
sqlx::query("PRAGMA foreign_key_check")
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn home_widgets_round_trip_in_a_fresh_database() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
|
|
let expected = serde_json::json!({
|
|
"version": 1,
|
|
"widgets": [
|
|
{
|
|
"id": "calendar-one",
|
|
"kind": "calendar",
|
|
"size": "2x2"
|
|
}
|
|
]
|
|
});
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
settings.home_widgets = Some(expected.clone());
|
|
settings.update(&pool).await.unwrap();
|
|
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(reloaded.home_widgets, Some(expected));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn terracotta_public_nodes_default_and_empty_list_round_trip() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(
|
|
settings.terracotta_public_nodes,
|
|
default_terracotta_public_nodes()
|
|
);
|
|
|
|
settings.terracotta_public_nodes.clear();
|
|
settings.update(&pool).await.unwrap();
|
|
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert!(reloaded.terracotta_public_nodes.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mojang_auth_source_round_trip_in_a_fresh_database() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
|
|
let mut settings = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(settings.mojang_auth_source, DownloadSourceMode::Auto);
|
|
settings.mojang_auth_source = DownloadSourceMode::MirrorPreferred;
|
|
settings.update(&pool).await.unwrap();
|
|
|
|
let reloaded = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(
|
|
reloaded.mojang_auth_source,
|
|
DownloadSourceMode::MirrorPreferred
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_source_detection_distinguishes_mainland_locales() {
|
|
assert!(locale_prefers_mirror("zh-CN"));
|
|
assert!(locale_prefers_mirror("zh_Hans"));
|
|
assert!(locale_prefers_mirror("Asia/Shanghai"));
|
|
assert!(!locale_prefers_mirror("zh-TW"));
|
|
assert!(!locale_prefers_mirror("en-US"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn legacy_mirror_settings_keep_their_previous_intent() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
let settings = Settings::get(&pool).await.unwrap();
|
|
assert!(settings.auto_concurrent_downloads);
|
|
assert!(settings.auto_set_java_high_performance_mode);
|
|
assert!(!settings.auto_hide_downloads_button);
|
|
assert_eq!(settings.home_layout, HomeLayout::Standard);
|
|
assert_eq!(settings.minimal_home_instance_id, None);
|
|
assert_eq!(settings.home_widgets, None);
|
|
assert_eq!(
|
|
settings.minecraft_metadata_source,
|
|
DownloadSourceMode::Auto
|
|
);
|
|
assert_eq!(settings.minecraft_file_source, DownloadSourceMode::Auto);
|
|
assert_eq!(settings.modrinth_source, DownloadSourceMode::Auto);
|
|
assert_eq!(settings.curseforge_source, DownloadSourceMode::Auto);
|
|
let mut legacy = serde_json::to_value(settings).unwrap();
|
|
let object = legacy.as_object_mut().unwrap();
|
|
object.remove("minecraft_metadata_source");
|
|
object.remove("minecraft_file_source");
|
|
object.remove("modrinth_source");
|
|
object.remove("curseforge_source");
|
|
object.insert("use_minecraft_mirror".to_string(), true.into());
|
|
object.insert("use_modrinth_mirror".to_string(), false.into());
|
|
object.insert("use_curseforge_mirror".to_string(), false.into());
|
|
|
|
let mut migrated: Settings = serde_json::from_value(legacy).unwrap();
|
|
migrated.apply_legacy_download_source_settings();
|
|
|
|
assert_eq!(
|
|
migrated.minecraft_metadata_source,
|
|
DownloadSourceMode::MirrorPreferred
|
|
);
|
|
assert_eq!(
|
|
migrated.minecraft_file_source,
|
|
DownloadSourceMode::MirrorPreferred
|
|
);
|
|
assert_eq!(migrated.modrinth_source, DownloadSourceMode::OfficialOnly);
|
|
assert_eq!(
|
|
migrated.curseforge_source,
|
|
DownloadSourceMode::OfficialOnly
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn download_source_reset_migration_sets_all_sources_to_auto() {
|
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&pool).await.unwrap();
|
|
sqlx::query(
|
|
"
|
|
UPDATE settings
|
|
SET
|
|
minecraft_metadata_source = 'official_only',
|
|
minecraft_file_source = 'mirror_preferred',
|
|
modrinth_source = 'official_only',
|
|
curseforge_source = 'mirror_preferred'
|
|
",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
sqlx::query(include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/migrations/20260721120000_reset-download-sources-to-auto.sql"
|
|
)))
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
let settings = Settings::get(&pool).await.unwrap();
|
|
assert_eq!(
|
|
settings.minecraft_metadata_source,
|
|
DownloadSourceMode::Auto
|
|
);
|
|
assert_eq!(settings.minecraft_file_source, DownloadSourceMode::Auto);
|
|
assert_eq!(settings.modrinth_source, DownloadSourceMode::Auto);
|
|
assert_eq!(settings.curseforge_source, DownloadSourceMode::Auto);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn removed_data_collection_schema_is_cleaned_up() {
|
|
let fresh = sqlx::sqlite::SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect("sqlite::memory:")
|
|
.await
|
|
.unwrap();
|
|
sqlx::migrate!().run(&fresh).await.unwrap();
|
|
let telemetry_tables = sqlx::query_scalar::<_, String>(
|
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'telemetry_%'",
|
|
)
|
|
.fetch_all(&fresh)
|
|
.await
|
|
.unwrap();
|
|
assert!(telemetry_tables.is_empty());
|
|
assert!(
|
|
sqlx::query("PRAGMA foreign_key_check")
|
|
.fetch_all(&fresh)
|
|
.await
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
}
|
|
}
|