diff --git a/apps/app-frontend/src/components/instance/HostedGameDirModal.vue b/apps/app-frontend/src/components/instance/HostedGameDirModal.vue new file mode 100644 index 0000000..2c9586a --- /dev/null +++ b/apps/app-frontend/src/components/instance/HostedGameDirModal.vue @@ -0,0 +1,161 @@ + + + \ No newline at end of file diff --git a/apps/app-frontend/src/composables/useHostedCreation.ts b/apps/app-frontend/src/composables/useHostedCreation.ts index 2798f03..df2534e 100644 --- a/apps/app-frontend/src/composables/useHostedCreation.ts +++ b/apps/app-frontend/src/composables/useHostedCreation.ts @@ -23,7 +23,7 @@ export function useHostedCreation() { completed.value = false } } - async function install() { + async function install(gameDirRoot?: string | null) { if (installing.value) return installing.value = true installError.value = '' @@ -40,7 +40,7 @@ export function useHostedCreation() { } } if (completed.value) return createdInstance.value - createdInstance.value ??= await hostedCreate() + createdInstance.value ??= await hostedCreate(gameDirRoot) const instanceId = createdInstance.value await hostedSync(instanceId) if (attempt !== generation) return diff --git a/apps/app-frontend/src/helpers/hosted-packs.ts b/apps/app-frontend/src/helpers/hosted-packs.ts index ebc40c7..5589cd4 100644 --- a/apps/app-frontend/src/helpers/hosted-packs.ts +++ b/apps/app-frontend/src/helpers/hosted-packs.ts @@ -99,7 +99,10 @@ export interface HostedSyncResult { } export const hostedDefault = () => invokeWithSession('plugin:install|hosted_default') -export const hostedCreate = () => invokeWithSession('plugin:install|hosted_create') +export const hostedCreate = (gameDirRoot?: string | null) => + invokeWithSession('plugin:install|hosted_create', { + gameDirRoot: gameDirRoot ?? null, + }) export const hostedBinding = (instanceId: string) => invokeHosted('plugin:install|hosted_binding', { instanceId }) export const hostedSync = (instanceId: string) => { diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index adf8c8d..a9b6178 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -29,6 +29,14 @@ "app.hosted-packs.auto-installing": { "message": "Downloading and installing the server modpack…" }, "app.hosted-packs.auto-description": { "message": "Download and automatically install the modpack from StarLight to play on the StarLight server with one click." }, "app.hosted-packs.auto-install": { "message": "Install the StarLight modpack" }, + "app.hosted-install.game-dir.header": { "message": "Choose game directory" }, + "app.hosted-install.game-dir.description": { "message": "StarLight instance data (mods, saves, configs, resource packs) is stored in an external game directory. Pick a root folder — the modpack gets its own subfolder inside it." }, + "app.hosted-install.game-dir.label": { "message": "Game directory root" }, + "app.hosted-install.game-dir.browse": { "message": "Browse" }, + "app.hosted-install.game-dir.no-selection": { "message": "No folder selected" }, + "app.hosted-install.game-dir.reset-default": { "message": "Use default location" }, + "app.hosted-install.game-dir.preview": { "message": "Game files will be installed to: {path}" }, + "app.hosted-install.game-dir.confirm": { "message": "Install" }, "app.onboarding.instance-mode.title": { "message": "Choose your instance type" }, "app.onboarding.instance-mode.description": { "message": "StarLight automatically installs the administrator-selected modpack and versions, then checks and completes updates before every launch. A StarLight login is required. Choose Local to select your own versions and modpacks for other servers or single-player." }, "app.instance-mode.title": { "message": "Instance type" }, diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index a7125cfd..d5797f1 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -29,6 +29,14 @@ "app.hosted-packs.auto-installing": { "message": "正在下载并安装服务器整合包…" }, "app.hosted-packs.auto-description": { "message": "从StarLight服务器获取整合包并自动安装,可一键游玩StarLight服务器" }, "app.hosted-packs.auto-install": { "message": "安装 StarLight 官方整合包" }, + "app.hosted-install.game-dir.header": { "message": "选择游戏目录" }, + "app.hosted-install.game-dir.description": { "message": "StarLight 实例的游戏数据(mods、存档、配置、资源包)会存放在外部游戏目录中。请选择一个根目录,整合包会在其中单独建一个子文件夹。" }, + "app.hosted-install.game-dir.label": { "message": "游戏目录根路径" }, + "app.hosted-install.game-dir.browse": { "message": "浏览" }, + "app.hosted-install.game-dir.no-selection": { "message": "尚未选择文件夹" }, + "app.hosted-install.game-dir.reset-default": { "message": "使用默认位置" }, + "app.hosted-install.game-dir.preview": { "message": "游戏文件将安装到:{path}" }, + "app.hosted-install.game-dir.confirm": { "message": "安装" }, "app.onboarding.instance-mode.title": { "message": "选择实例类型" }, "app.onboarding.instance-mode.description": { "message": "StarLight 实例自动安装管理员指定的整合包和版本,每次启动先检查并完成更新,需要登录 StarLight 账号。本地实例可自选版本和整合包,适合第三方服务器与单人游玩。" }, "app.instance-mode.title": { "message": "实例类型" }, diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json index 9586816..63ec9d3 100644 --- a/apps/app-frontend/src/locales/zh-TW/index.json +++ b/apps/app-frontend/src/locales/zh-TW/index.json @@ -12,6 +12,14 @@ "app.hosted-packs.auto-installing": { "message": "正在下載並安裝伺服器整合包…" }, "app.hosted-packs.auto-description": { "message": "從StarLight伺服器取得整合包並自動安裝,可一鍵遊玩StarLight伺服器" }, "app.hosted-packs.auto-install": { "message": "安裝 StarLight 官方整合包" }, + "app.hosted-install.game-dir.header": { "message": "選擇遊戲目錄" }, + "app.hosted-install.game-dir.description": { "message": "StarLight 實例的遊戲資料(mods、存檔、設定、資源包)會存放在外部遊戲目錄中。請選擇一個根目錄,整合包會在其中另外建立一個子資料夾。" }, + "app.hosted-install.game-dir.label": { "message": "遊戲目錄根路徑" }, + "app.hosted-install.game-dir.browse": { "message": "瀏覽" }, + "app.hosted-install.game-dir.no-selection": { "message": "尚未選擇資料夾" }, + "app.hosted-install.game-dir.reset-default": { "message": "使用預設位置" }, + "app.hosted-install.game-dir.preview": { "message": "遊戲檔案將安裝到:{path}" }, + "app.hosted-install.game-dir.confirm": { "message": "安裝" }, "app.onboarding.instance-mode.title": { "message": "選擇實例類型" }, "app.onboarding.instance-mode.description": { "message": "StarLight 實例自動安裝管理員指定的整合包和版本,每次啟動先檢查並完成更新,需要登入 StarLight 帳號。本地實例可自行選擇版本和整合包,適合第三方伺服器與單人遊玩。" }, "app.instance-mode.title": { "message": "實例類型" }, diff --git a/apps/app-frontend/src/pages/Create.vue b/apps/app-frontend/src/pages/Create.vue index 0cce8f6..4a0c03e 100644 --- a/apps/app-frontend/src/pages/Create.vue +++ b/apps/app-frontend/src/pages/Create.vue @@ -4,6 +4,7 @@ import { BigOptionButton, Button, defineMessages, useVIntl } from '@modrinth/ui' import { inject, ref, watch } from 'vue' import { useRouter } from 'vue-router' import InstanceModeOptions from '@/components/instance/InstanceModeOptions.vue' +import HostedGameDirModal from '@/components/instance/HostedGameDirModal.vue' import HostedPackProgress from '@/components/instance/HostedPackProgress.vue' import type { InstanceMode } from '@/helpers/hosted-packs' import { useHostedCreation } from '@/composables/useHostedCreation' @@ -15,6 +16,18 @@ const { installing, installError, createdInstance, completed, acknowledge, insta const instanceMode = ref( installing.value || createdInstance.value ? 'starlight' : 'local', ) + +const hostedGameDirModal = ref>() + +// One-click StarLight installs reuse the same external game-directory choice +// as the custom creation flow: the pack gets its own folder under ``. +function promptHostedGameDir() { + hostedGameDirModal.value?.show() +} + +async function installHostedWithGameDir(gameDirRoot: string) { + await install(gameDirRoot) +} async function openCompleted(instanceId: string) { try { const failure = await router.push(`/instance/${encodeURIComponent(instanceId)}/`) @@ -103,7 +116,12 @@ async function handleStartFresh() { await openCompleted(createdInstance.value) return } - await install() + if (createdInstance.value) { + // A previous attempt already created the instance; retry in place. + await install() + return + } + promptHostedGameDir() return } showModal?.({ @@ -179,6 +197,10 @@ function handleImportExisting() { /> +

diff --git a/apps/app/src/api/install.rs b/apps/app/src/api/install.rs index 56a97fb..919a30d 100644 --- a/apps/app/src/api/install.rs +++ b/apps/app/src/api/install.rs @@ -66,8 +66,10 @@ pub async fn hosted_default() -> Result { } #[tauri::command] -pub async fn hosted_create() -> Result { - Ok(theseus::pack::hosted::create().await?) +pub async fn hosted_create( + game_dir_root: Option, +) -> Result { + Ok(theseus::pack::hosted::create(game_dir_root).await?) } #[tauri::command] diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 6c0254a..9d11770 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -232,6 +232,21 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { Ok(()) } +/// Directory that contains the launcher executable. Used as the default +/// external game-directory root for the one-click StarLight install flow. +#[tauri::command] +fn get_launcher_root_dir() -> api::Result { + let exe = std::env::current_exe().map_err(|error| { + theseus::Error::from(theseus::ErrorKind::FSError(error.to_string())) + })?; + let dir = exe.parent().ok_or_else(|| { + theseus::Error::from(theseus::ErrorKind::FSError( + "Launcher executable has no parent directory".to_string(), + )) + })?; + Ok(dir.to_string_lossy().into_owned()) +} + #[tauri::command] fn get_update_channel(app: tauri::AppHandle) -> api::Result { let channel = read_update_channel_state(&app)? @@ -838,6 +853,7 @@ fn main() { .manage(PendingUpdateData::default()) .invoke_handler(tauri::generate_handler![ initialize_state, + get_launcher_root_dir, get_update_channel, get_current_app_database_path, set_update_channel, diff --git a/packages/app-lib/src/api/pack/hosted.rs b/packages/app-lib/src/api/pack/hosted.rs index c03d019..ad1cb13 100644 --- a/packages/app-lib/src/api/pack/hosted.rs +++ b/packages/app-lib/src/api/pack/hosted.rs @@ -426,10 +426,26 @@ pub async fn default_publication() -> crate::Result { }) } -pub async fn create() -> crate::Result { +pub async fn create( + game_dir_root: Option, +) -> crate::Result { let publication = default_publication().await?; let runtime = &publication.manifest.runtime; let state = State::get().await?; + // The pack's game files live in their own folder under the chosen root, + // e.g. `/`. Avoid a `versions/` layout: that shape + // is reserved for externally linked launcher instances and would make the + // launcher expect a Minecraft version JSON beside the pack. + let game_dir_override = game_dir_root + .as_deref() + .map(str::trim) + .filter(|root| !root.is_empty()) + .map(|root| { + Path::new(root) + .join(&publication.manifest.name) + .to_string_lossy() + .into_owned() + }); let instance = crate::state::create_instance( crate::state::CreateInstance { name: publication.manifest.name.clone(), @@ -440,7 +456,7 @@ pub async fn create() -> crate::Result { icon_path: None, link: crate::state::InstanceLink::Unmanaged, symlink_target: None, - game_dir_override: None, + game_dir_override, }, &state, )