chore: Start work on JRE downloading
This commit is contained in:
parent
71c98255a1
commit
087e87963e
14 changed files with 199 additions and 246 deletions
|
|
@ -5,6 +5,8 @@ use tauri::{AppHandle, Manager, Runtime};
|
|||
use tauri_plugin_http::reqwest::Client;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
use crate::{constants::CLIENT_ID, utils::http::create_client};
|
||||
|
||||
use super::{Account, AuthenticationMethod};
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -17,8 +19,6 @@ pub async fn login_msa<R: tauri::Runtime>(app: tauri::AppHandle<R>) -> Result<Ac
|
|||
}
|
||||
}
|
||||
|
||||
const CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56";
|
||||
|
||||
struct MicrosoftAuthenticationMethod;
|
||||
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
||||
async fn auth<R: Runtime, F>(handle: &AppHandle<R>, stage: F) -> Result<Account, Box<dyn Error>>
|
||||
|
|
@ -47,7 +47,7 @@ impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
|||
}
|
||||
|
||||
async fn auth_minecraft(xsts_token: String, user_hash: String) -> Result<String, Box<dyn Error>> {
|
||||
let response = Client::new()
|
||||
let response = create_client()
|
||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.json(&json!({
|
||||
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
|
||||
|
|
@ -65,7 +65,7 @@ async fn auth_minecraft(xsts_token: String, user_hash: String) -> Result<String,
|
|||
}
|
||||
|
||||
async fn auth_xsts(token: String) -> Result<String, Box<dyn Error>> {
|
||||
let response = Client::new()
|
||||
let response = create_client()
|
||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
|
|
@ -102,7 +102,7 @@ async fn auth_xsts(token: String) -> Result<String, Box<dyn Error>> {
|
|||
}
|
||||
|
||||
async fn auth_xbl(code: String) -> Result<(String, String), Box<dyn Error>> {
|
||||
let response = Client::new()
|
||||
let response = create_client()
|
||||
.post("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
|
|
@ -131,7 +131,7 @@ async fn auth_xbl(code: String) -> Result<(String, String), Box<dyn Error>> {
|
|||
}
|
||||
|
||||
async fn msa_code_to_token(code: String) -> Result<String, Box<dyn Error>> {
|
||||
let token = Client::new()
|
||||
let token = create_client()
|
||||
.post("https://login.live.com/oauth20_token.srf")
|
||||
.form(&[
|
||||
("client_id", CLIENT_ID),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub struct AccountSkin {
|
|||
pub variant: String,
|
||||
}
|
||||
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait AuthenticationMethod {
|
||||
/// Authenticate with a given method. Stage is a function that takes a string and a u8.
|
||||
/// The string is the message to display to the user, and the u8 is the progress of the authentication.
|
||||
|
|
|
|||
5
src-tauri/src/constants.rs
Normal file
5
src-tauri/src/constants.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub const NAME: &str = "OneLauncher";
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
|
||||
pub const USER_AGENT: &str = "OneLauncher/1.0.0 (polyfrost.org)";
|
||||
pub const CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56";
|
||||
106
src-tauri/src/game/client.rs
Normal file
106
src-tauri/src/game/client.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use std::{env, error::Error, fmt::Display, fs::{self, File}, path::PathBuf};
|
||||
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_http::reqwest::Client;
|
||||
|
||||
use crate::utils::http;
|
||||
|
||||
use super::{manifest::Manifest, JavaVersion};
|
||||
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait GameClient {
|
||||
fn from_manifest(manifest: Manifest) -> Self;
|
||||
|
||||
fn get_handle(&self) -> &AppHandle;
|
||||
fn get_manifest(&self) -> &Manifest;
|
||||
async fn launch(&self) -> Result<(), Box<dyn Error>>;
|
||||
async fn install(&self) -> Result<(), Box<dyn Error>>;
|
||||
async fn download_java(&self) -> Result<PathBuf, JavaDownloadError> {
|
||||
download_java(self.get_handle(), self.get_manifest().java_version.to_owned()).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_java<R: Runtime>(handle: &AppHandle<R>, java_version: JavaVersion) -> Result<PathBuf, JavaDownloadError> {
|
||||
let config_dir = match handle.path().app_config_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => return Err(JavaDownloadError::UnsupportedOS)
|
||||
};
|
||||
|
||||
let java_dir = config_dir.join("java");
|
||||
|
||||
if !java_dir.exists() {
|
||||
match fs::create_dir_all(&java_dir) {
|
||||
Ok(_) => (),
|
||||
Err(_) => return Err(JavaDownloadError::PermissionDenied)
|
||||
};
|
||||
}
|
||||
|
||||
// let java_version = &self.get_manifest().java_version;
|
||||
let os = env::consts::OS;
|
||||
let archive_type = match os {
|
||||
"windows" => "zip",
|
||||
"macos" => "tar.gz",
|
||||
"linux" => "tar.gz",
|
||||
_ => return Err(JavaDownloadError::UnsupportedOS)
|
||||
};
|
||||
|
||||
let url = format!("
|
||||
https://api.azul.com/metadata/v1/zulu/packages/\
|
||||
?java_version={}\
|
||||
&os={}\
|
||||
&arch={}\
|
||||
&archive_type={}\
|
||||
&java_package_type=jre&javafx_bundled=true&release_status=ga&latest=true",
|
||||
java_version.to_string(),
|
||||
os,
|
||||
get_arch(),
|
||||
archive_type
|
||||
);
|
||||
|
||||
let response = match http::create_client().get(&url).send().await {
|
||||
Ok(response) => match response.json::<serde_json::Value>().await {
|
||||
Ok(json) => json,
|
||||
Err(_) => return Err(JavaDownloadError::NoJavaVersionFound)
|
||||
},
|
||||
Err(_) => return Err(JavaDownloadError::NoJavaVersionFound)
|
||||
};
|
||||
|
||||
if !response.is_array() || response.as_array().unwrap().is_empty() {
|
||||
return Err(JavaDownloadError::NoJavaVersionFound);
|
||||
}
|
||||
|
||||
let latest = response.as_array().unwrap().first().unwrap();
|
||||
let download_url = latest.get("download_url").unwrap().as_str().unwrap();
|
||||
let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type);
|
||||
let tmp_archive = java_dir.join(&archive_name);
|
||||
|
||||
if let Err(err) = http::download_file(download_url, tmp_archive.as_path()).await {
|
||||
eprintln!("{}", err);
|
||||
return Err(JavaDownloadError::DownloadError);
|
||||
};
|
||||
|
||||
// TODO: Extract the archive and delete the archive after a SUCCESSFUL extraction.
|
||||
// Name the folder containing the JRE `zulu-{version}-{arch}`
|
||||
|
||||
Ok(tmp_archive)
|
||||
}
|
||||
|
||||
pub fn get_arch() -> String {
|
||||
let arch = env::consts::ARCH;
|
||||
match arch {
|
||||
"x86" => "x86",
|
||||
"x86_64" => "x64",
|
||||
"arm" => "aarch32",
|
||||
"aarch64" => "aarch64",
|
||||
_ => "unsupported"
|
||||
}.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JavaDownloadError {
|
||||
NoJavaVersionFound,
|
||||
PermissionDenied,
|
||||
UnsupportedOS,
|
||||
UnsupportedArch,
|
||||
DownloadError,
|
||||
}
|
||||
11
src-tauri/src/game/manifest.rs
Normal file
11
src-tauri/src/game/manifest.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::JavaVersion;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub main_class: String,
|
||||
pub java_version: JavaVersion,
|
||||
}
|
||||
|
|
@ -1 +1,21 @@
|
|||
pub mod version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod client;
|
||||
pub mod manifest;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum JavaVersion {
|
||||
V8,
|
||||
V16,
|
||||
V17
|
||||
}
|
||||
|
||||
impl ToString for JavaVersion {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
JavaVersion::V8 => "8".to_string(),
|
||||
JavaVersion::V16 => "16".to_string(),
|
||||
JavaVersion::V17 => "17".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetIndex {
|
||||
pub id: String,
|
||||
pub sha1: String,
|
||||
pub size: u64,
|
||||
pub total_size: u64,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VersionDownload {
|
||||
pub sha1: String,
|
||||
pub size: u64,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VersionDownloads {
|
||||
pub client: VersionDownload,
|
||||
pub server: VersionDownload,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JavaVersion {
|
||||
pub major_version: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuleOS {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Rule {
|
||||
pub action: String,
|
||||
pub os: Option<RuleOS>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LibraryArtifact {
|
||||
pub path: String,
|
||||
pub sha1: String,
|
||||
pub size: u64,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LibraryDownloads {
|
||||
pub artifact: LibraryArtifact,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub classifiers: Option<HashMap<String, LibraryArtifact>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LibraryNatives {
|
||||
pub linux: Option<String>,
|
||||
pub osx: Option<String>,
|
||||
pub windows: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Library {
|
||||
pub downloads: LibraryDownloads,
|
||||
pub name: String,
|
||||
pub natives: Option<LibraryNatives>,
|
||||
pub rules: Option<Vec<Rule>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArgumentRule {
|
||||
pub rules: Vec<Rule>,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(untagged)]
|
||||
pub enum Argument {
|
||||
String(String),
|
||||
Rule(ArgumentRule),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Arguments {
|
||||
pub game: Vec<Argument>,
|
||||
pub jvm: Vec<Argument>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModernVersionManifest {
|
||||
pub asset_index: AssetIndex,
|
||||
pub downloads: VersionDownloads,
|
||||
pub id: String,
|
||||
pub java_version: JavaVersion,
|
||||
pub libraries: Vec<Library>,
|
||||
pub main_class: String,
|
||||
pub minecraft_arguments: String,
|
||||
pub release_time: String,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub release_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LegacyVersionManifest {
|
||||
pub asset_index: AssetIndex,
|
||||
pub downloads: VersionDownloads,
|
||||
pub id: String,
|
||||
pub java_version: JavaVersion,
|
||||
pub libraries: Vec<Library>,
|
||||
pub main_class: String,
|
||||
pub minecraft_arguments: String,
|
||||
pub release_time: String,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub release_type: String,
|
||||
|
||||
// TODO
|
||||
// pub logging: Option<Logging>,
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#[allow(async_fn_in_trait)]
|
||||
pub mod auth;
|
||||
pub mod game;
|
||||
pub mod utils;
|
||||
pub mod constants;
|
||||
|
|
@ -3,13 +3,20 @@
|
|||
|
||||
use std::error::Error;
|
||||
|
||||
use polyfrost_launcher::auth;
|
||||
use tauri::{menu::Menu, App};
|
||||
use polyfrost_launcher::{auth, game::{client::{download_java, GameClient}, JavaVersion}};
|
||||
use tauri::{generate_handler, menu::Menu, App, Runtime};
|
||||
|
||||
#[tauri::command]
|
||||
async fn download_java_test<R: Runtime>(app: tauri::AppHandle<R>, window: tauri::Window<R>) {
|
||||
let res = download_java(&app, JavaVersion::V8).await;
|
||||
println!("{:#?}", res);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(auth::init())
|
||||
.invoke_handler(generate_handler![download_java_test])
|
||||
.menu(|handle| Menu::new(handle))
|
||||
.setup(setup)
|
||||
.run(tauri::generate_context!())
|
||||
|
|
|
|||
19
src-tauri/src/utils/http.rs
Normal file
19
src-tauri/src/utils/http.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use std::{error::Error, fs::File, io::{self, Cursor}, path::Path};
|
||||
|
||||
use tauri_plugin_http::reqwest::{Client, ClientBuilder};
|
||||
|
||||
use crate::constants::USER_AGENT;
|
||||
|
||||
pub fn create_client() -> Client {
|
||||
ClientBuilder::new()
|
||||
.user_agent(USER_AGENT)
|
||||
.build().expect("Failed to create HTTP client. Please report this issue on GitHub.")
|
||||
}
|
||||
|
||||
pub async fn download_file(url: &str, path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let response = create_client().get(url).send().await?;
|
||||
let mut file = File::create(path)?;
|
||||
let mut content = Cursor::new(response.bytes().await?);
|
||||
io::copy(&mut content, &mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
1
src-tauri/src/utils/mod.rs
Normal file
1
src-tauri/src/utils/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod http;
|
||||
2
src/types/game.d.ts
vendored
2
src/types/game.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
declare namespace game {
|
||||
export type Client = 'fabric' | 'forge' | 'quilt' | 'vanilla' | 'other';
|
||||
export type Client = 'Fabric' | 'Forge' | 'Quilt' | 'Vanilla' | 'Other';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,15 @@
|
|||
import type { DragEventHandler, Id } from '@thisbeyond/solid-dnd';
|
||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, createSortable, useDragDropContext } from '@thisbeyond/solid-dnd';
|
||||
import type { JSX } from 'solid-js';
|
||||
import { For, createSignal } from 'solid-js';
|
||||
|
||||
declare module 'solid-js' {
|
||||
// eslint-disable-next-line ts/no-namespace
|
||||
namespace JSX {
|
||||
interface Directives {
|
||||
sortable: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
item: boolean | Id | Node | JSX.ArrayElement | null | undefined;
|
||||
};
|
||||
|
||||
function Item(props: ItemProps) {
|
||||
const id = () => props.item as Id;
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const sortable = createSortable(id());
|
||||
|
||||
// @ts-expect-error - `state` is not defined in the type definition
|
||||
const [state] = useDragDropContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
use:sortable
|
||||
class="sortable"
|
||||
classList={{
|
||||
'opacity-25': sortable.isActiveDraggable,
|
||||
'transition-transform': !!state.active.draggable,
|
||||
}}
|
||||
>
|
||||
{props.item}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function List() {
|
||||
const [items, setItems] = createSignal([1, 2, 3]);
|
||||
const [activeItem, setActiveItem] = createSignal<number | null>(null);
|
||||
const ids = () => items();
|
||||
|
||||
const onDragStart: DragEventHandler = ({ draggable }) => setActiveItem(draggable.id as number);
|
||||
|
||||
const onDragEnd: DragEventHandler = ({ draggable, droppable }) => {
|
||||
if (draggable && droppable) {
|
||||
const currentItems = ids();
|
||||
const fromIndex = currentItems.indexOf(draggable.id as number);
|
||||
const toIndex = currentItems.indexOf(droppable.id as number);
|
||||
if (fromIndex !== toIndex) {
|
||||
const updatedItems = currentItems.slice();
|
||||
updatedItems.splice(toIndex, 0, ...updatedItems.splice(fromIndex, 1));
|
||||
setItems(updatedItems);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropProvider
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<div class="column self-stretch">
|
||||
<SortableProvider ids={ids()}>
|
||||
<For each={items()}>{item => <Item item={item} />}</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<div class="sortable">{activeItem()}</div>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
);
|
||||
}
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import Button from '../components/base/Button';
|
||||
|
||||
function BrowserPage() {
|
||||
function downloadJava() {
|
||||
invoke('download_java_test');
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<h1>Browser</h1>
|
||||
<List />
|
||||
<Button onClick={() => downloadJava()}>Download java</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
SearchMdIcon,
|
||||
} from '@untitled-theme/icons-solid';
|
||||
import { For, createSignal } from 'solid-js';
|
||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, createSortable, useDragDropContext } from '@thisbeyond/solid-dnd';
|
||||
import { DragDropProvider, DragDropSensors, SortableProvider, closestCenter, createSortable } from '@thisbeyond/solid-dnd';
|
||||
import type { DragEventHandler, Id } from '@thisbeyond/solid-dnd';
|
||||
import image from '../../assets/images/header.png';
|
||||
import Button from '../components/base/Button';
|
||||
|
|
@ -100,7 +100,7 @@ function InstanceCard(props: InstanceCardProps) {
|
|||
<div class="flex flex-col gap-1.5">
|
||||
<p class="h-4 font-semibold">{props.name}</p>
|
||||
<p class="h-4 text-xs">
|
||||
{upperFirst(props.client)}
|
||||
{props.client}
|
||||
{' '}
|
||||
{props.version}
|
||||
{' '}
|
||||
|
|
@ -167,63 +167,56 @@ function HomePage() {
|
|||
const myInstances: InstanceCardProps[] = [
|
||||
{
|
||||
name: 'Hypixel',
|
||||
client: 'vanilla',
|
||||
client: 'Vanilla',
|
||||
version: '1.8.9',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: 'OneConfig ??',
|
||||
client: 'forge',
|
||||
client: 'Forge',
|
||||
version: '1.8.9',
|
||||
mods: 12,
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
name: 'Fabulously Optimised',
|
||||
client: 'fabric',
|
||||
client: 'Fabric',
|
||||
version: '1.20.4',
|
||||
mods: 48,
|
||||
id: 3,
|
||||
},
|
||||
{
|
||||
name: 'sus client',
|
||||
client: 'other',
|
||||
client: 'Other',
|
||||
version: '1.8.9',
|
||||
id: 4,
|
||||
},
|
||||
{
|
||||
name: 'Hypixel',
|
||||
client: 'vanilla',
|
||||
client: 'Vanilla',
|
||||
version: '1.8.9',
|
||||
id: 5,
|
||||
},
|
||||
{
|
||||
name: 'OneConfig ??',
|
||||
client: 'forge',
|
||||
client: 'Forge',
|
||||
version: '1.8.9',
|
||||
mods: 12,
|
||||
id: 6,
|
||||
},
|
||||
{
|
||||
name: 'Fabulously Optimised',
|
||||
client: 'fabric',
|
||||
client: 'Fabric',
|
||||
version: '1.20.4',
|
||||
mods: 48,
|
||||
id: 7,
|
||||
},
|
||||
{
|
||||
name: 'sus client',
|
||||
client: 'other',
|
||||
client: 'Other',
|
||||
version: '1.8.9',
|
||||
id: 8,
|
||||
},
|
||||
// {
|
||||
// name: 'OneConfig ??',
|
||||
// client: 'forge',
|
||||
// version: '1.8.9',
|
||||
// mods: 12,
|
||||
// id: 5,
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue