Making the source code public

Finally ! here's the source code
This commit is contained in:
KayJann
2026-03-12 13:52:32 +01:00
parent 7542b67070
commit c94db91238
87 changed files with 9307 additions and 1 deletions
+34
View File
@@ -0,0 +1,34 @@
# Rust
/src-tauri/target/
/src-tauri/Cargo.lock
# Node
/node_modules/
/dist/
/npm-debug.log*
/yarn-debug.log*
/yarn-error.log*
/lerna-debug.log*
/.pnpm-debug.log*
# Editor directories and files
.vscode*
.idea*
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# System files
.DS_Store
Thumbs.db
# Build output
/app-icon.png
/src-tauri/icons/*.png
/src-tauri/icons/*.ico
/src-tauri/icons/*.icns
# Log files
*.log
+4 -1
View File
@@ -22,7 +22,9 @@
---
### PROJECT OVERVIEW
Emerald Legacy Launcher started as a solo project to create a better way to play Legacy Console Edition. Over time, other developers joined the effort, bringing expertise from various preservation projects into one place. By combining these efforts into a single unified launcher, we aim to build the best possible experience for Legacy Console Edition on PC.
Emerald Legacy Launcher started as a solo project to create a better way to play Legacy Console Edition. Over time, other developers joined the effort, bringing expertise from various preservation projects into one place.
**Our Philosophy:** For too long, the LCE scene has been fragmented across different, often resource-heavy launchers. Emerald was built to stop this fragmentation and centralize everything into a single, definitive hub. By avoiding bloated frameworks and utilizing a modern Rust/Tauri stack, we deliver a high-performance, cross-platform experience that uses only ~15MB of RAM—leaving all your PC's resources dedicated to the game itself.
---
@@ -67,3 +69,4 @@ Click the heading above to track progress, view active tasks, and see upcoming l
---
*Developed by The Emerald Team and the Legacy Console Edition Community.*
***This project is licensed under the GNU GPL v3 License - see the LICENSE file for details.***
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Emerald Legacy</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+3565
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "emerald-legacy-launcher",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.1",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-opener": "^2.5.3",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.10.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.8",
"tailwindcss": "^3.4.19",
"typescript": "~5.8.3",
"vite": "^7.0.4"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "emerald-legacy-launcher"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
[lib]
name = "emerald_legacy_launcher_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", features = ["stream"] }
tokio = { version = "1", features = ["full"] }
futures-util = "0.3" #
tokio-util = { version = "0.7.18", features = ["rt"] }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
{
"identifier": "opener:allow-open-url",
"allow": [
{ "url": "https://github.com/KayJannOnGit/*" },
{ "url": "https://discord.gg/*" },
{ "url": "https://reddit.com/*" }
]
}
]
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","opener:default",{"identifier":"opener:allow-open-url","allow":[{"url":"https://github.com/KayJannOnGit/*"},{"url":"https://discord.gg/*"},{"url":"https://reddit.com/*"}]}]}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+150
View File
@@ -0,0 +1,150 @@
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use tauri::{AppHandle, Emitter, State, Manager};
use futures_util::StreamExt;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tauri_plugin_opener::OpenerExt;
pub struct DownloadState { pub token: Arc<Mutex<Option<CancellationToken>>> }
fn get_app_dir(app: &AppHandle) -> PathBuf {
app.path().app_local_data_dir().unwrap_or_else(|_| {
std::env::current_dir().unwrap_or_default()
})
}
#[tauri::command]
fn save_config(app: AppHandle, username: String) {
let path = get_app_dir(&app).join("emerald_legacy_config.txt");
let _ = fs::create_dir_all(path.parent().unwrap());
let _ = fs::write(path, &username);
}
#[tauri::command]
fn load_config(app: AppHandle) -> String {
let path = get_app_dir(&app).join("emerald_legacy_config.txt");
fs::read_to_string(path).unwrap_or_default()
}
#[tauri::command]
#[allow(non_snake_case)]
fn check_game_installed(app: AppHandle, instanceId: String) -> bool {
get_app_dir(&app).join("instances").join(&instanceId).join("Minecraft.Client.exe").exists()
}
#[tauri::command]
#[allow(non_snake_case)]
fn open_instance_folder(app: AppHandle, instanceId: String) {
let dir = get_app_dir(&app).join("instances").join(&instanceId);
if dir.exists() {
let _ = app.opener().open_path(dir.to_str().unwrap(), None::<&str>);
}
}
#[tauri::command]
async fn cancel_download(state: State<'_, DownloadState>) -> Result<(), String> {
if let Some(token) = state.token.lock().await.take() { token.cancel(); }
Ok(())
}
#[tauri::command]
#[allow(non_snake_case)]
async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, url: String, instanceId: String) -> Result<String, String> {
let root = get_app_dir(&app);
let instance_dir = root.join("instances").join(&instanceId);
let token = CancellationToken::new();
let child_token = token.clone();
{ *state.token.lock().await = Some(token); }
if instance_dir.exists() { let _ = fs::remove_dir_all(&instance_dir); }
fs::create_dir_all(&instance_dir).map_err(|e| e.to_string())?;
let zip_path = root.join(format!("temp_{}.zip", instanceId));
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
if !response.status().is_success() {
return Err(format!("Download failed: {}", response.status()));
}
let total_size = response.content_length().unwrap_or(0) as f64;
let mut file = fs::File::create(&zip_path).map_err(|e| e.to_string())?;
let mut downloaded = 0.0;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
if child_token.is_cancelled() {
drop(file); let _ = fs::remove_file(&zip_path);
return Err("CANCELLED".into());
}
let chunk = chunk.map_err(|e| e.to_string())?;
file.write_all(&chunk).map_err(|e| e.to_string())?;
downloaded += chunk.len() as f64;
if total_size > 0.0 { let _ = app.emit("download-progress", downloaded / total_size * 100.0); }
}
drop(file);
{ *state.token.lock().await = None; }
let status = Command::new("tar")
.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()])
.status()
.map_err(|e| e.to_string())?;
let _ = fs::remove_file(&zip_path);
if !status.success() {
return Err("Extraction failed".into());
}
if let Ok(entries) = fs::read_dir(&instance_dir) {
let entries_vec: Vec<_> = entries.flatten().collect();
if entries_vec.len() == 1 && entries_vec[0].path().is_dir() {
let inner_dir = entries_vec[0].path();
if let Ok(inner_entries) = fs::read_dir(&inner_dir) {
for inner_entry in inner_entries.flatten() {
let file_name = inner_entry.file_name();
let dest_path = instance_dir.join(file_name);
let _ = fs::rename(inner_entry.path(), dest_path);
}
}
let _ = fs::remove_dir(&inner_dir);
}
}
Ok("Success".into())
}
#[tauri::command]
#[allow(non_snake_case)]
async fn launch_game(app: AppHandle, instanceId: String) -> Result<(), String> {
let root = get_app_dir(&app);
let instance_dir = root.join("instances").join(&instanceId);
let config_path = root.join("emerald_legacy_config.txt");
let username = fs::read_to_string(config_path).unwrap_or_else(|_| "Player".into());
let _ = fs::write(instance_dir.join("username.txt"), &username);
let game_exe = instance_dir.join("Minecraft.Client.exe");
if !game_exe.exists() {
return Err("Game executable not found in instance folder.".into());
}
let _ = Command::new(&game_exe).spawn().map_err(|e| e.to_string())?;
Ok(())
}
pub fn run() {
tauri::Builder::default()
.manage(DownloadState { token: Arc::new(Mutex::new(None)) })
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![launch_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download])
.run(tauri::generate_context!())
.expect("error");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
emerald_legacy_launcher_lib::run()
}
+44
View File
@@ -0,0 +1,44 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Emerald Legacy Launcher",
"version": "0.1.0",
"identifier": "com.emerald.legacy",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Emerald Legacy",
"width": 1000,
"height": 650,
"decorations": true,
"transparent": false,
"titleBarStyle": "Visible",
"devtools": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"shell": {
"open": true
}
}
}
+38
View File
@@ -0,0 +1,38 @@
@font-face { font-family: 'Minecraft'; src: url('/fonts/Mojangles.ttf') format('truetype'); }
@tailwind base; @tailwind components; @tailwind utilities;
* { border-radius: 0 !important; font-family: 'Minecraft', sans-serif !important; image-rendering: pixelated; font-weight: normal !important; }
body { background: #000; margin: 0; }
main { background: url('/images/dirt-background.png') repeat !important; background-size: 2000px !important; position: relative; }
main::before { content: ""; position: absolute; inset: 0; background: rgba(0, 0, 0, 0.7); z-index: 0; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.legacy-btn {
@apply transition-all duration-75 flex items-center justify-center;
background: #bebebe; border: 4px solid #000 !important;
box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
color: #3e3e3e !important; cursor: pointer;
}
.legacy-btn:hover:not(:disabled) { background: #d0d0d0; outline: 4px solid #fff; outline-offset: -8px; }
.legacy-btn:active:not(:disabled) { box-shadow: inset -4px -4px #ffffff, inset 4px 4px #555555 !important; }
.legacy-btn:disabled { opacity: 0.6; cursor: not-allowed; filter: grayscale(1); }
.active-tab { background: #388e3c !important; box-shadow: inset 4px 4px #55ff55, inset -4px -4px #1a4d0a !important; color: #fff !important; }
.reinstall-btn, .confirm-red-btn, .cancel-download-btn { background: #ff5555 !important; box-shadow: inset 4px 4px #ffaaaa, inset -4px -4px #aa0000 !important; color: #fff !important; }
.mc-range { -webkit-appearance: none; background: #000; height: 12px; border: 3px solid #555; width: 100%; }
.mc-range::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 24px; background: #bebebe; border: 3px solid #000; box-shadow: inset 2px 2px #fff, inset -2px -2px #555; cursor: pointer; }
.social-btn { background: transparent; border: 4px solid #000; box-shadow: inset 4px 4px #555, inset -4px -4px #111; width: 64px; height: 64px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.1s; }
.social-btn:hover { transform: scale(1.1); }
.social-btn svg { fill: white; width: 32px; height: 32px; }
.btn-discord { background: #5865F2 !important; box-shadow: inset 4px 4px #8ea1e1, inset -4px -4px #313338 !important; }
.btn-github { background: #333 !important; box-shadow: inset 4px 4px #666, inset -4px -4px #000 !important; }
.btn-reddit { background: #FF4500 !important; box-shadow: inset 4px 4px #ff8b60, inset -4px -4px #942700 !important; }
.splash-text { color: #ffff00; text-shadow: 4px 4px #3f3f00; white-space: nowrap; pointer-events: none; z-index: 20; transform: rotate(-20deg); animation: splash-pulse 0.5s infinite alternate; transform-origin: center; }
@keyframes splash-pulse { from { transform: rotate(-20deg) scale(1); } to { transform: rotate(-20deg) scale(1.05); } }
+330
View File
@@ -0,0 +1,330 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useState, useEffect, useRef } from "react";
import "./App.css";
const Icons = {
Discord: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M6 6h20v4h2v12h-2v4h-4v-4h-8v4H6v-4H4V10h2V6zm4 6v4h4v-4h-4zm8 0v4h4v-4h-4z"/></svg>,
Github: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M12 4h8v4h4v4h4v8h-4v4h-4v4h-8v-4H8v-4H4v-8h4V8h4V4zm2 8v4h4v-4h-4z"/></svg>,
Reddit: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M10 4h12v4h4v4h2v12h-2v4H10v-4H4V12h2V8h4V4zm2 10v4h8v-4h-8z"/></svg>,
Volume: ({ level }: { level: number }) => (
<svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M4 12h8l8-8v24l-8-8H4v-8z"/>{level > 0 && <path d="M24 12h2v8h-2z"/>}{level > 0.5 && <path d="M28 8h2v16h-2z"/>}</svg>
)
};
let audioCtx: AudioContext | null = null;
let sfxGain: GainNode | null = null;
const buffers: Record<string, AudioBuffer> = {};
export default function App() {
const [username, setUsername] = useState("");
const [activeTab, setActiveTab] = useState("home");
const [isFirstRun, setIsFirstRun] = useState(true);
const [isRunning, setIsRunning] = useState(false);
const [installingInstance, setInstallingInstance] = useState<string | null>(null);
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [installedStatus, setInstalledStatus] = useState<Record<string, boolean>>({ vanilla_tu19: false, vanilla_tu24: false });
const [selectedInstance, setSelectedInstance] = useState<string>("vanilla_tu19");
const [reinstallModal, setReinstallModal] = useState<{ id: string, url: string } | null>(null);
const [mcNotif, setMcNotif] = useState<{t: string, m: string} | null>(null);
const [musicVol, setMusicVol] = useState(parseFloat(localStorage.getItem("musicVol") || "0.4"));
const [sfxVol, setSfxVol] = useState(parseFloat(localStorage.getItem("sfxVol") || "0.7"));
const [isMuted, setIsMuted] = useState(localStorage.getItem("isMuted") === "true");
const musicRef = useRef<HTMLAudioElement | null>(null);
const lastTrack = useRef<number>(0);
useEffect(() => {
localStorage.setItem("musicVol", musicVol.toString());
localStorage.setItem("sfxVol", sfxVol.toString());
localStorage.setItem("isMuted", isMuted.toString());
if (musicRef.current) musicRef.current.volume = isMuted ? 0 : musicVol;
}, [musicVol, sfxVol, isMuted]);
const ensureAudio = async () => {
let currentCtx = audioCtx;
if (!currentCtx) {
const AC = (window as any).AudioContext || (window as any).webkitAudioContext;
if (!AC) return;
const newCtx = new AC() as AudioContext;
const newGain = newCtx.createGain();
newGain.connect(newCtx.destination);
audioCtx = newCtx; sfxGain = newGain; currentCtx = newCtx;
const sounds = ['click.wav', 'orb.ogg', 'levelup.ogg', 'back.ogg', 'pop.wav', 'wood click.wav'];
sounds.forEach(s => {
fetch(`/sounds/${s}`).then(r => r.arrayBuffer()).then(b => newCtx.decodeAudioData(b)).then(buf => { if (buf) buffers[s] = buf; });
});
}
if (currentCtx.state === 'suspended') await currentCtx.resume();
};
const playSfx = async (n: string, multiplier: number = 1.0) => {
await ensureAudio();
const currentCtx = audioCtx;
const currentGain = sfxGain;
if (!currentCtx || !currentGain || !buffers[n] || isMuted) return;
const s = currentCtx.createBufferSource();
s.buffer = buffers[n];
const g = currentCtx.createGain();
g.gain.value = sfxVol * multiplier;
s.connect(g); g.connect(currentCtx.destination);
s.start(0);
};
const playRandomMusic = () => {
if (!musicRef.current) return;
let track = Math.floor(Math.random() * 5) + 1;
if (track === lastTrack.current) track = (track % 5) + 1;
lastTrack.current = track;
musicRef.current.src = `/music/music${track}.ogg`;
musicRef.current.volume = isMuted ? 0 : musicVol;
musicRef.current.play().catch(() => {});
};
const fadeAndLaunch = async () => {
playSfx('levelup.ogg', 0.4);
setIsRunning(true);
if (musicRef.current && !isMuted) {
const startVol = musicRef.current.volume;
const steps = 20;
let currentStep = 0;
const fade = setInterval(() => {
currentStep++;
if (musicRef.current) musicRef.current.volume = Math.max(0, startVol * (1 - currentStep / steps));
if (currentStep >= steps) { clearInterval(fade); if (musicRef.current) musicRef.current.pause(); }
}, 50);
}
setTimeout(async () => {
try {
await invoke("launch_game", { instanceId: selectedInstance });
} catch (e) {
alert(`Failed to launch game: ${e}`);
} finally {
setIsRunning(false);
if (musicRef.current) {
musicRef.current.volume = isMuted ? 0 : musicVol;
playRandomMusic();
}
}
}, 1500);
};
const executeInstall = async (id: string, url: string) => {
setInstallingInstance(id); setDownloadProgress(0);
try {
await invoke("download_and_install", { url, instanceId: id });
setMcNotif({ t: "Success!", m: "Ready to play." }); playSfx('orb.ogg');
setTimeout(() => setMcNotif(null), 4000);
updateAllStatus();
} catch (e) {
console.error(e);
alert("Error during installation: " + e);
}
setInstallingInstance(null);
};
const updateAllStatus = async () => {
const v19 = await invoke<boolean>("check_game_installed", { instanceId: "vanilla_tu19" });
const v24 = await invoke<boolean>("check_game_installed", { instanceId: "vanilla_tu24" });
setInstalledStatus({ vanilla_tu19: v19, vanilla_tu24: v24 });
};
useEffect(() => {
invoke("load_config").then((n) => {
const savedName = n as string;
if (savedName && savedName.trim() !== "") {
setUsername(savedName);
setIsFirstRun(false);
setTimeout(playRandomMusic, 1000);
}
});
updateAllStatus();
const unlisten = listen<number>("download-progress", (e) => setDownloadProgress(Math.round(e.payload)));
return () => { unlisten.then(f => f()); };
}, []);
if (isFirstRun) {
return (
<div className="h-screen flex flex-col items-center justify-center bg-black text-white p-12 select-none" onContextMenu={e => e.preventDefault()}>
<img src="/images/MenuTitle.png" className="w-[500px] mb-12" />
<div className="bg-[#2a2a2a] p-10 border-4 border-black w-full max-w-2xl text-center shadow-[inset_4px_4px_#555,inset_-4px_-4px_#111]">
<h2 className="text-4xl text-emerald-400 mb-4">Welcome to Emerald Legacy!</h2>
<input type="text" value={username} onChange={e => setUsername(e.target.value)} className="w-full bg-black border-4 border-emerald-900 p-4 text-3xl text-center mb-8 outline-none" placeholder="Username..." />
<button onClick={() => { ensureAudio(); playSfx('click.wav'); invoke("save_config", { username }); setIsFirstRun(false); setTimeout(playRandomMusic, 500); }} disabled={!username.trim()} className="legacy-btn py-4 px-12 text-3xl w-full">Start Setup</button>
</div>
</div>
);
}
return (
<div className="h-screen flex select-none overflow-hidden bg-black text-white" onContextMenu={e => e.preventDefault()}>
<audio ref={musicRef} onEnded={playRandomMusic} />
<aside className="w-64 bg-[#2a2a2a] border-r-4 border-black p-6 flex flex-col gap-2 z-20 shadow-[inset_-4px_0_#555]">
<div className="mb-10 px-2">
<h2 className="text-emerald-500 text-3xl italic tracking-tighter">EMERALD LEGACY</h2>
<p className="text-[10px] text-slate-500 uppercase tracking-widest">Launcher</p>
</div>
<nav className="flex flex-col gap-3">
<button onClick={() => { playSfx('click.wav'); setActiveTab("home"); updateAllStatus(); }} className={`p-4 legacy-btn justify-start ${activeTab === "home" ? "active-tab" : ""}`}>HOME</button>
<button onClick={() => { playSfx('click.wav'); setActiveTab("versions"); updateAllStatus(); }} className={`p-4 legacy-btn justify-start ${activeTab === "versions" ? "active-tab" : ""}`}>VERSIONS</button>
<button onClick={() => { playSfx('click.wav'); setActiveTab("settings"); }} className={`p-4 legacy-btn justify-start ${activeTab === "settings" ? "active-tab" : ""}`}>SETTINGS</button>
</nav>
<div onClick={() => { playSfx('click.wav'); openUrl("https://github.com/KayJannOnGit"); }} className="mt-auto pt-6 flex flex-col items-center border-t-4 border-black/30 cursor-pointer group">
<span className="text-slate-500 text-[10px] uppercase">Developed by</span>
<span className="text-emerald-500 text-sm font-bold group-hover:underline">KayJann</span>
</div>
</aside>
<main className="flex-1 relative h-full">
<div className="h-full flex flex-col items-center justify-center p-12 relative z-10">
{activeTab === "home" && (
<div className="flex flex-col items-center text-center animate-in fade-in">
<div className="relative mb-12 flex flex-col items-center">
<img src="/images/MenuTitle.png" className="w-[550px]" />
<div className="splash-text absolute bottom-2 -right-12 text-3xl">Welcome, {username}!</div>
</div>
<div className="bg-black/80 p-8 border-4 border-black w-[550px] flex flex-col gap-6 mt-12">
{installedStatus.vanilla_tu19 || installedStatus.vanilla_tu24 ? (
<>
<select value={selectedInstance} onChange={e => { playSfx('click.wav'); setSelectedInstance(e.target.value); }} className="w-full bg-[#2a2a2a] border-4 border-black p-3 text-2xl text-white outline-none">
{installedStatus.vanilla_tu19 && <option value="vanilla_tu19">Vanilla Nightly (TU19)</option>}
{installedStatus.vanilla_tu24 && <option value="vanilla_tu24">Vanilla TU24</option>}
</select>
<button onClick={fadeAndLaunch} disabled={isRunning || !!installingInstance} className="legacy-btn py-4 text-6xl w-full">{installingInstance ? "DOWNLOADING..." : isRunning ? "RUNNING..." : "PLAY"}</button>
</>
) : (
<div className="text-center">
<p className="text-2xl text-red-400 mb-6 font-bold uppercase">Game not installed</p>
<button onClick={() => { playSfx('click.wav'); setActiveTab("versions"); }} className="legacy-btn py-4 px-8 text-3xl w-full">Go to Versions</button>
</div>
)}
</div>
</div>
)}
{activeTab === "versions" && (
<div className="w-full max-w-3xl bg-black/80 p-12 border-4 border-black h-full overflow-y-auto no-scrollbar animate-in fade-in">
<h2 className="text-5xl mb-8 border-b-4 border-white/20 pb-4">Instances</h2>
<div className="flex flex-col gap-6">
{/* TU19 */}
<div className="flex justify-between items-center bg-[#2a2a2a] border-4 border-black p-6">
<div><h3 className="text-2xl font-bold">Vanilla Nightly (TU19)</h3><p className="text-slate-400 text-sm">Leaked 4J Studios build.</p></div>
<div className="flex gap-2">
{installedStatus.vanilla_tu19 ? (
<>
<button onClick={() => { playSfx('pop.wav'); invoke("open_instance_folder", { instanceId: "vanilla_tu19" }); }} className="legacy-btn px-4 py-2 text-xl">Folder</button>
<button onClick={() => { playSfx('click.wav'); setReinstallModal({ id: "vanilla_tu19", url: "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu19_vanilla.zip" }); }} disabled={!!installingInstance} className="legacy-btn px-4 py-2 text-xl reinstall-btn">Reinstall</button>
</>
) : (
<button onClick={() => { playSfx('click.wav'); executeInstall("vanilla_tu19", "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu19_vanilla.zip"); }} disabled={!!installingInstance} className="legacy-btn px-6 py-2 text-xl">INSTALL</button>
)}
</div>
</div>
{/* TU24 */}
<div className="flex justify-between items-center bg-[#2a2a2a] border-4 border-black p-6">
<div><h3 className="text-2xl font-bold">Vanilla TU24</h3><p className="text-slate-400 text-sm">Horses and Wither update.</p></div>
<div className="flex gap-2">
{installedStatus.vanilla_tu24 ? (
<>
<button onClick={() => { playSfx('pop.wav'); invoke("open_instance_folder", { instanceId: "vanilla_tu24" }); }} className="legacy-btn px-4 py-2 text-xl">Folder</button>
<button onClick={() => { playSfx('click.wav'); setReinstallModal({ id: "vanilla_tu24", url: "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu24_vanilla.zip" }); }} disabled={!!installingInstance} className="legacy-btn px-4 py-2 text-xl reinstall-btn">Reinstall</button>
</>
) : (
<button onClick={() => { playSfx('click.wav'); executeInstall("vanilla_tu24", "https://huggingface.co/datasets/KayJann/emerald-legacy-assets/resolve/main/emerald_tu24_vanilla.zip"); }} disabled={!!installingInstance} className="legacy-btn px-6 py-2 text-xl">INSTALL</button>
)}
</div>
</div>
{['TU75', 'TU9', 'Modded Pack'].map(v => (
<div key={v} className="flex justify-between items-center bg-[#1a1a1a] border-4 border-black p-6 opacity-50 grayscale">
<div><h3 className="text-2xl font-bold text-slate-500">Vanilla {v}</h3><p className="text-slate-600 text-sm">Legacy version.</p></div>
<span className="text-[#ffff55] text-2xl font-bold italic">SOON</span>
</div>
))}
</div>
{installingInstance && (
<div className="mt-8 p-6 border-4 border-emerald-500 bg-[#1a2a1a] relative">
<div className="flex justify-between mb-3 text-emerald-400 font-bold text-xl uppercase">
<span>Downloading {downloadProgress}%</span>
<button onClick={() => { playSfx('back.ogg'); invoke("cancel_download"); }} className="legacy-btn px-3 py-1 text-sm cancel-download-btn">Cancel</button>
</div>
<div className="w-full bg-black border-4 border-emerald-900 h-8">
<div className="bg-emerald-500 h-full transition-all duration-300" style={{ width: `${downloadProgress}%` }}></div>
</div>
</div>
)}
</div>
)}
{activeTab === "settings" && (
<div className="w-full max-w-3xl bg-black/80 p-12 border-4 border-black h-full overflow-y-auto no-scrollbar animate-in fade-in">
<h2 className="text-5xl mb-8 border-b-4 border-white/20 pb-4">Settings</h2>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-4">
<label className="text-xl text-slate-400 italic">In-game Username</label>
<div className="flex gap-4">
<input type="text" value={username} onChange={e => setUsername(e.target.value)} className="flex-1 bg-black border-4 border-slate-700 p-4 text-3xl outline-none focus:border-emerald-500" />
<button onClick={() => { playSfx('wood click.wav'); invoke("save_config", { username }); }} className="legacy-btn px-8 text-2xl relative">Save</button>
</div>
</div>
<div className="flex flex-col gap-4 bg-[#2a2a2a] p-6 border-4 border-black shadow-[inset_4px_4px_#555]">
<label className="text-xl flex items-center gap-4"><Icons.Volume level={musicVol}/> Audio Controls</label>
<div className="grid grid-cols-2 gap-8">
<div className="flex flex-col gap-2">
<span className="text-sm uppercase opacity-50">Music {Math.round(musicVol*100)}%</span>
<input type="range" min="0" max="1" step="0.01" value={musicVol} onChange={e => setMusicVol(parseFloat(e.target.value))} className="mc-range" />
</div>
<div className="flex flex-col gap-2">
<span className="text-sm uppercase opacity-50">SFX {Math.round(sfxVol*100)}%</span>
<input type="range" min="0" max="1" step="0.01" value={sfxVol} onChange={e => setSfxVol(parseFloat(e.target.value))} className="mc-range" />
</div>
</div>
<button onClick={() => { setIsMuted(!isMuted); playSfx('pop.wav'); }} className="legacy-btn mt-4 py-2">{isMuted ? "UNMUTE ALL" : "MUTE ALL"}</button>
</div>
<div className="about-section border-4 border-black bg-[#2a2a2a] p-6 shadow-[inset_4px_4px_#555]">
<h3 className="text-2xl text-[#ffff55] mb-2 uppercase tracking-wide">About the project</h3>
<p className="text-xl text-white leading-relaxed mb-6 opacity-90">
I'm <span className="text-emerald-400">KayJann</span>, and I absolutely love this project! It's my very first one,
and my goal is to create a central hub for the LCE community to bring us all together.
</p>
<h3 className="text-sm text-slate-500 mb-4 uppercase tracking-widest">Social Links</h3>
<div className="flex gap-6">
<button onClick={() => openUrl("https://discord.gg/nzbxB8Hxjh")} className="social-btn btn-discord" title="Discord"><Icons.Discord /></button>
<button onClick={() => openUrl("https://github.com/KayJannOnGit")} className="social-btn btn-github" title="GitHub"><Icons.Github /></button>
<button onClick={() => openUrl("https://reddit.com/user/KayJann")} className="social-btn btn-reddit" title="Reddit"><Icons.Reddit /></button>
</div> </div>
</div>
</div>
)}
</div>
{reinstallModal && (
<div className="absolute inset-0 bg-black/80 z-[200] flex items-center justify-center animate-in fade-in">
<div className="bg-[#2a2a2a] border-4 border-black p-8 w-[600px] text-center shadow-[inset_4px_4px_#555,inset_-4px_-4px_#111]">
<h3 className="text-4xl text-[#ff5555] mb-6 font-bold uppercase tracking-widest">Warning</h3>
<p className="text-2xl mb-10 leading-relaxed text-white">Reinstalling will delete all data. Continue?</p>
<div className="flex gap-6">
<button onClick={() => { playSfx('back.ogg'); setReinstallModal(null); }} className="legacy-btn px-8 py-4 text-3xl w-1/2">Cancel</button>
<button onClick={() => { playSfx('click.wav'); executeInstall(reinstallModal.id, reinstallModal.url); setReinstallModal(null); }} className="legacy-btn px-8 py-4 text-3xl w-1/2 confirm-red-btn">Confirm</button>
</div>
</div>
</div>
)}
{mcNotif && (
<div className="absolute top-6 right-6 bg-[#202020] border-2 border-black p-4 flex items-center gap-4 shadow-[5px_5px_15px_rgba(0,0,0,0.5)] z-[100] animate-in slide-in-from-right-10">
<div className="w-12 h-12 bg-emerald-500 border-2 border-black flex items-center justify-center text-3xl font-bold"></div>
<div className="flex flex-col"><span className="text-[#ffff55] text-2xl font-bold">{mcNotif.t}</span><span className="text-white text-xl">{mcNotif.m}</span></div>
</div>
)}
</main>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+16
View File
@@ -0,0 +1,16 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import './App.css';
const rootElement = document.getElementById("root");
if (rootElement) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
} else {
console.error("ERREUR FATALE : La div avec l'id 'root' est introuvable dans index.html");
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+10
View File
@@ -0,0 +1,10 @@
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
clearScreen: false,
optimizeDeps: {
entries: ['index.html'],
exclude: ['bin', 'game', 'src-tauri']
},
server: {
port: 1420,
strictPort: true,
watch: {
ignored: ["**/src-tauri/**", "**/bin/**", "**/game/**", "**/*.app/**"],
}
},
});