diff --git a/core/Cargo.toml b/core/Cargo.toml index 6a389ad2..1aa178ab 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,3 +25,5 @@ chrono = { workspace = true } reqwest = { workspace = true } dirs = { workspace = true } open = { workspace = true } + +sha1 = "0.10.6" diff --git a/core/src/game/client.rs b/core/src/game/client.rs index 6e5f61ac..c5013be5 100644 --- a/core/src/game/client.rs +++ b/core/src/game/client.rs @@ -22,13 +22,14 @@ pub struct Version { release_time: chrono::DateTime } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Manifest { pub id: Uuid, pub manifest: MinecraftManifest } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Instance { pub id: Uuid, pub created_at: DateTime, @@ -66,7 +67,7 @@ pub trait ClientTrait<'a>: Send + Sync { async fn launch(&self) -> PolyResult<()>; async fn setup(&self) -> PolyResult<()>; - async fn install_game(&self) -> PolyResult<()>; + async fn install_game(&self) -> PolyResult; async fn install_libraries(&self) -> PolyResult; async fn install_natives(&self) -> PolyResult<()>; async fn install_assets(&self) -> PolyResult<()>; @@ -236,8 +237,21 @@ fn load_and_serialize(dir: &PathBuf) -> PolyResult> where T: }; let path = file.path(); - let content = fs::read_to_string(&path)?; - let parsed = serde_json::from_str::(&content)?; + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(_) => { + eprintln!("Couldn't read file: '{:?}'", path); + continue; + } + }; + + let parsed = match serde_json::from_str::(&content) { + Ok(parsed) => parsed, + Err(_) => { + eprintln!("Couldn't parse file content as JSON: '{:?}'", content); + continue; + } + }; result.insert(uuid, parsed); } diff --git a/core/src/game/vanilla.rs b/core/src/game/vanilla.rs index 9a0a3f40..a7c45267 100644 --- a/core/src/game/vanilla.rs +++ b/core/src/game/vanilla.rs @@ -2,7 +2,7 @@ use std::{fs, path::PathBuf}; use anyhow::anyhow; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use crate::{constants::MINECRAFT_VERSIONS_MANIFEST, create_game_client, impl_game_client, utils::http, PolyResult}; +use crate::{constants::MINECRAFT_VERSIONS_MANIFEST, create_game_client, impl_game_client, utils::{dirs, file, http}, PolyResult}; use super::{client::{ClientTrait, Instance, Manifest, Version}, minecraft::MinecraftManifest}; create_game_client! { @@ -30,10 +30,24 @@ impl<'a> ClientTrait<'a> for VanillaClient<'a> { Ok(()) } - async fn install_game(&self) -> PolyResult<()> { - let artifact = &self.manifest.manifest.downloads.client; + async fn install_game(&self) -> PolyResult { + let manifest = &self.manifest.manifest; + let file = dirs::clients_dir()?.join(format!("{}.jar", manifest.version)); + + if !file.exists() { + fs::create_dir_all(&file)?; + let artifact = &manifest.downloads.client; + + http::download_file(&artifact.url, &file).await?; + let file_hash = file::file_sha1(&file)?; + + println!("Downloaded: '{}' | '{}'", file_hash, artifact.sha1); + if file_hash != artifact.sha1 { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Hashes do not match").into()); + } + } - Ok(()) + Ok(file) } async fn install_libraries(&self) -> PolyResult { diff --git a/core/src/utils/dirs.rs b/core/src/utils/dirs.rs index 3a4efbc2..2e8987a1 100644 --- a/core/src/utils/dirs.rs +++ b/core/src/utils/dirs.rs @@ -6,8 +6,6 @@ use crate::{constants, PolyResult}; #[derive(Debug, Error)] pub enum DirectoryError { - #[error("failed to get the user's home directory")] - HomeDir, #[error("failed to get the user's config directory")] ConfigDir, } @@ -32,4 +30,8 @@ pub fn instances_dir() -> PolyResult { pub fn manifests_dir() -> PolyResult { Ok(app_config_dir()?.join("manifests")) -} \ No newline at end of file +} + +pub fn clients_dir() -> PolyResult { + Ok(app_config_dir()?.join("clients")) +} diff --git a/core/src/utils/file.rs b/core/src/utils/file.rs index df9b5668..1d91f6a9 100644 --- a/core/src/utils/file.rs +++ b/core/src/utils/file.rs @@ -1,8 +1,20 @@ use anyhow::anyhow; -use std::{fs::File, path::Path}; +use sha1::{Digest, Sha1}; +use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; use crate::{PolyError, PolyResult}; +pub fn file_sha1(file: &PathBuf) -> PolyResult { + let file = File::open(file)?; + let reader = BufReader::new(file); + + let mut hasher = Sha1::new(); + hasher.update(&reader.buffer()); + let hash = hasher.finalize(); + + Ok(format!("{:x}", hash)) +} + pub fn extract_archive(archive: &Path, dest: &Path) -> PolyResult<()> { let ext = match archive.extension() { Some(ext) => ext.to_str().unwrap(), diff --git a/eslint.config.js b/eslint.config.js index 2dde76c5..0ebca22c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,4 +23,9 @@ export default petal({ 'pnpm-lock.yaml', './src-tauri/prisma/migrations/migration_lock.toml', ], -}, { rules: { 'petal/consistent-list-newline': 'off' } }); +}, { + rules: { + 'petal/consistent-list-newline': 'off', + 'no-console': 'off', + }, +}); diff --git a/src-tauri/build.rs b/src-tauri/build.rs index ec058383..12b1a1d4 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -7,9 +7,11 @@ fn main() { "launcher-core", InlinedPlugin::new().commands(&[ "login_msa", + "refresh_client_manager", "create_instance", "get_instances", "get_instance", + "get_manifest" ]), ), ); diff --git a/src-tauri/permissions/launcher-core/default.toml b/src-tauri/permissions/launcher-core/default.toml index c5f907f4..af03ad78 100644 --- a/src-tauri/permissions/launcher-core/default.toml +++ b/src-tauri/permissions/launcher-core/default.toml @@ -1,8 +1,10 @@ [default] description = "Default permissions for the plugin" permissions = [ - "allow-get-instance", - "allow-get-instances", + "allow-refresh-client-manager", "allow-create-instance", - "allow-login-msa" + "allow-get-instances", + "allow-get-instance", + "allow-get-manifest", + "allow-login-msa", ] diff --git a/src-tauri/src/launcher_core/game.rs b/src-tauri/src/launcher_core/game.rs index 601ea2b8..5696adf4 100644 --- a/src-tauri/src/launcher_core/game.rs +++ b/src-tauri/src/launcher_core/game.rs @@ -1,4 +1,4 @@ -use launcher_core::game::client::{ClientType, Instance}; +use launcher_core::game::client::{ClientType, Instance, Manifest}; use tauri::State; use tokio::sync::Mutex; use uuid::Uuid; @@ -35,4 +35,14 @@ pub async fn get_instance( let manager = &mut state.lock().await.client_manager; let instance = manager.get_instance(uuid)?; Ok(instance.clone()) +} + +#[tauri::command] +pub async fn get_manifest( + state: State<'_, Mutex>, + uuid: Uuid +) -> Result { + let manager = &mut state.lock().await.client_manager; + let manifest = manager.get_manifest(uuid)?; + Ok(manifest.clone()) } \ No newline at end of file diff --git a/src-tauri/src/launcher_core/mod.rs b/src-tauri/src/launcher_core/mod.rs index c5f2b6f4..5a8a09aa 100644 --- a/src-tauri/src/launcher_core/mod.rs +++ b/src-tauri/src/launcher_core/mod.rs @@ -1,5 +1,5 @@ use launcher_core::game::client::ClientManager; -use tauri::{plugin::TauriPlugin, Manager}; +use tauri::{plugin::TauriPlugin, Manager, State}; use tokio::sync::Mutex; mod auth; @@ -26,6 +26,17 @@ pub fn init() -> TauriPlugin { game::create_instance, game::get_instances, game::get_instance, + game::get_manifest, + refresh_client_manager, ]) .build() } + +#[tauri::command] +async fn refresh_client_manager( + state: State<'_, Mutex> +) -> Result<(), String> { + let mut state = state.lock().await; + state.client_manager = ClientManager::new()?; + Ok(()) +} \ No newline at end of file diff --git a/src/bridge/auth.ts b/src/bridge/auth.ts index f90f8cb1..d7a25ac8 100644 --- a/src/bridge/auth.ts +++ b/src/bridge/auth.ts @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; type MicrosoftLoginCallback = (msg: string, stage: number) => never; -export async function loginMicrosoft(cb?: MicrosoftLoginCallback): Promise { +export async function loginMicrosoft(cb?: MicrosoftLoginCallback): Promise { if (cb) { const unlisten = await listen<[string, number, boolean]>('msa:status', (event) => { cb(event.payload[0], event.payload[1]); diff --git a/src/bridge/game.ts b/src/bridge/game.ts new file mode 100644 index 00000000..f71a576f --- /dev/null +++ b/src/bridge/game.ts @@ -0,0 +1,30 @@ +import { invoke } from '@tauri-apps/api/core'; + +export async function getInstance(uuid: string): Promise { + return await invoke('plugin:launcher-core|get_instance', { + uuid, + }); +} + +export async function getManifest(uuid: string): Promise { + return await invoke('plugin:launcher-core|get_manifest', { + uuid, + }); +} + +export async function getInstancesWithManifests(): Promise { + const instances = await getInstances(); + + return Promise.all(instances.map(async instance => ({ + instance, + manifest: await getManifest(instance.id), + }))); +} + +export async function getInstances(): Promise { + return await invoke('plugin:launcher-core|get_instances'); +} + +export async function refreshClientManager(): Promise { + return await invoke('plugin:launcher-core|refresh_client_manager'); +} diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts index bf8d6385..f8682764 100644 --- a/src/types/auth.d.ts +++ b/src/types/auth.d.ts @@ -1,4 +1,4 @@ -declare namespace auth { +declare namespace Core { export interface Account { username: string; uuid: string; diff --git a/src/types/game.d.ts b/src/types/game.d.ts index e2c5713f..7c3ca779 100644 --- a/src/types/game.d.ts +++ b/src/types/game.d.ts @@ -1,22 +1,35 @@ -declare namespace game { - export interface GameClientDetails { - uuid: Uuid; - name: string; - version: string; - main_class: string; - java_version: JavaVersion; - startup_args: Vec; - client_type: { - type: keyof GameClientType; - manifest: GameClientType[keyof GameClientType]; - }; - } +declare namespace Core { + export interface InstanceWithManifest { + instance: Instance, + manifest: Manifest, + } - export interface GameClientType { - Vanilla: VanillaManifest; - } + export interface Manifest { + id: string, + manifest: MinecraftManifest + } - export type ClientType = keyof GameClientType; + export interface MinecraftManifest { + id: string, + javaVersion: { + majorVersion: number; + }, + } - export interface VanillaManifest {} + export interface Instance { + id: string, + createdAt: number, + name: string, + cover: string | null, + group: string | null, + client: ClientType[T], + } + + export interface ClientType { + Vanilla: VanillaProps, + } + + export interface VanillaProps { + type: "Vanilla", + } } diff --git a/src/ui/hooks/useAccount.ts b/src/ui/hooks/useAccount.ts index d053b33a..28190f57 100644 --- a/src/ui/hooks/useAccount.ts +++ b/src/ui/hooks/useAccount.ts @@ -1,4 +1,4 @@ -function useAccount(): auth.Account { +function useAccount(): Core.Account { // TODO return the currently logged in account return { diff --git a/src/ui/pages/Browser.tsx b/src/ui/pages/Browser.tsx index 33747cfe..a7766200 100644 --- a/src/ui/pages/Browser.tsx +++ b/src/ui/pages/Browser.tsx @@ -1,47 +1,8 @@ -import { invoke } from '@tauri-apps/api/core'; -import Button from '../components/base/Button'; - function BrowserPage() { return (

Browser

- - - -
); } diff --git a/src/ui/pages/Home.tsx b/src/ui/pages/Home.tsx index 8a648e1b..231849ad 100644 --- a/src/ui/pages/Home.tsx +++ b/src/ui/pages/Home.tsx @@ -5,20 +5,21 @@ import { EyeIcon, PlayIcon, PlusIcon, + RefreshCw01Icon, SearchMdIcon, } from '@untitled-theme/icons-solid'; import type { Accessor, Setter } from 'solid-js'; -import { For, createSignal } from 'solid-js'; +import { For, Suspense, createSignal, lazy } from 'solid-js'; import { DragDropProvider, DragDropSensors, SortableProvider, closestCenter, createSortable } from '@thisbeyond/solid-dnd'; import type { DragEventHandler, Id } from '@thisbeyond/solid-dnd'; import { useNavigate } from '@solidjs/router'; -import { v4 } from 'uuid'; import image from '../../assets/images/header.png'; import Button from '../components/base/Button'; import Tag from '../components/base/Tag'; import TextField from '../components/base/TextField'; import defaultCover from '../../assets/images/default_instance_cover.jpg'; import ContextMenu from '../components/overlay/ContextMenu'; +import { getInstancesWithManifests, refreshClientManager } from '../../bridge/game'; // TODO: Replace this into it's own component function OneConfigLogo() { @@ -63,26 +64,6 @@ function Banner() { ); } -interface InstanceCardProps { - elementId: number; - id: string; - name: string; - version: string; - clientType: game.ClientType; - mods?: number; - cover?: string; - lastPlayed?: number; -} - -declare module 'solid-js' { - // eslint-disable-next-line ts/no-namespace - namespace JSX { - interface Directives { - sortable: any; - } - } -} - interface InstanceCardContextMenuProps { pos: Accessor<{ x: number; y: number }>; contextMenuVisible: Accessor; @@ -117,12 +98,27 @@ function InstanceCardContextMenu(props: InstanceCardContextMenuProps) { ); } +interface InstanceCardProps { + id: number; + instance: Core.Instance; + manifest: Core.Manifest; +} + +declare module 'solid-js' { + // eslint-disable-next-line ts/no-namespace + namespace JSX { + interface Directives { + sortable: any; + } + } +} + function InstanceCard(props: InstanceCardProps) { const navigate = useNavigate(); const [pos, setPos] = createSignal({ x: 0, y: 0 }); const [dragged, setDragged] = createSignal(false); const [contextMenuVisible, setContextMenuVisible] = createSignal(false); - const id = () => props.elementId as Id; + const id = () => props.id as Id; let ref!: HTMLDivElement; const sortable = createSortable(id()); @@ -141,7 +137,7 @@ function InstanceCard(props: InstanceCardProps) { if (didDrag()) return; - navigate(`/instances/?id=${props.id}`); + navigate(`/instances/?id=${props.instance.id}`); } function openContextMenu(e: MouseEvent) { @@ -182,20 +178,20 @@ function InstanceCard(props: InstanceCardProps) { style={{ '-webkit-transform': 'translateZ(0)' }} >
-

{props.name}

+

{props.instance.name}

- {props.clientType} + {props.instance.client.type} {' '} - {props.version} - {' '} - {props.mods && `• ${props.mods} mods`} + {props.manifest.manifest.id} + {/* {' '} + {props.instance.mods && `• ${props.mods} mods`} */}

+
- - + + + ); }