@thomasfarineau/anvil 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -193
- package/dist/cli.cjs +554 -0
- package/package.json +19 -6
- package/src/client/config.schema.json +24 -1
- package/src/client/index.d.ts +36 -0
- package/src/rust/src/lib.rs +320 -17
- package/src/template/_gitignore +1 -0
- package/src/template/capabilities/default.json +7 -1
- package/src/template/config.json +2 -2
- package/src/template/frontends/react-js/src/App.jsx +183 -0
- package/src/template/frontends/react-js/src/index.html +12 -0
- package/src/template/frontends/react-js/src/main.jsx +5 -0
- package/src/template/frontends/react-js/vite.config.js +10 -0
- package/src/template/frontends/react-ts/src/App.tsx +190 -0
- package/src/template/frontends/react-ts/src/index.html +12 -0
- package/src/template/frontends/react-ts/src/main.tsx +5 -0
- package/src/template/frontends/react-ts/tsconfig.json +14 -0
- package/src/template/frontends/react-ts/vite.config.ts +10 -0
- package/src/template/frontends/solid-js/src/App.jsx +190 -0
- package/src/template/frontends/solid-js/src/index.html +12 -0
- package/src/template/frontends/solid-js/src/main.jsx +5 -0
- package/src/template/frontends/solid-js/vite.config.js +10 -0
- package/src/template/frontends/solid-ts/src/App.tsx +193 -0
- package/src/template/frontends/solid-ts/src/index.html +12 -0
- package/src/template/frontends/solid-ts/src/main.tsx +5 -0
- package/src/template/frontends/solid-ts/tsconfig.json +15 -0
- package/src/template/frontends/solid-ts/vite.config.ts +10 -0
- package/src/template/{src → frontends/vanilla-js/src}/index.html +110 -178
- package/src/template/frontends/vanilla-ts/src/index.html +51 -0
- package/src/template/frontends/vanilla-ts/src/main.ts +193 -0
- package/src/template/frontends/vanilla-ts/tsconfig.json +13 -0
- package/src/template/frontends/vanilla-ts/vite.config.ts +8 -0
- package/src/template/frontends/vue-js/src/App.vue +155 -0
- package/src/template/frontends/vue-js/src/index.html +12 -0
- package/src/template/frontends/vue-js/src/main.js +5 -0
- package/src/template/frontends/vue-js/vite.config.js +10 -0
- package/src/template/frontends/vue-ts/src/App.vue +158 -0
- package/src/template/frontends/vue-ts/src/index.html +12 -0
- package/src/template/frontends/vue-ts/src/main.ts +5 -0
- package/src/template/frontends/vue-ts/tsconfig.json +13 -0
- package/src/template/frontends/vue-ts/vite.config.ts +10 -0
- package/src/template/{src → shared}/api.js +27 -1
- package/src/template/shared/logo.svg +6 -0
- package/src/template/shared/style.css +226 -0
- package/src/cli.cjs +0 -352
package/src/client/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export interface ModConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
file_name?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export interface InstanceConfig {
|
|
2
8
|
id: string;
|
|
3
9
|
name: string;
|
|
@@ -6,6 +12,7 @@ export interface InstanceConfig {
|
|
|
6
12
|
loader_version: string;
|
|
7
13
|
server_ip: string;
|
|
8
14
|
server_port: number;
|
|
15
|
+
mods: ModConfig[];
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export interface LauncherConfig {
|
|
@@ -38,6 +45,7 @@ export interface InstanceStatus {
|
|
|
38
45
|
id: string;
|
|
39
46
|
name: string;
|
|
40
47
|
installed: boolean;
|
|
48
|
+
running: boolean;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export interface InitStatus {
|
|
@@ -46,6 +54,14 @@ export interface InitStatus {
|
|
|
46
54
|
instances: InstanceStatus[];
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
export interface ModInfo {
|
|
58
|
+
file_name: string;
|
|
59
|
+
name: string;
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
size: number;
|
|
62
|
+
managed: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export interface SetupProgress {
|
|
50
66
|
step: string;
|
|
51
67
|
current: number;
|
|
@@ -76,15 +92,35 @@ export declare const MC: {
|
|
|
76
92
|
getSettings(): Promise<Settings>;
|
|
77
93
|
saveSettings(s: Settings): Promise<void>;
|
|
78
94
|
getDefaultDir(): Promise<string>;
|
|
95
|
+
getVersion(): Promise<string>;
|
|
79
96
|
getInitStatus(): Promise<InitStatus>;
|
|
80
97
|
runSetup(): Promise<void>;
|
|
81
98
|
verify(instanceId: string): Promise<void>;
|
|
82
99
|
play(instanceId: string): Promise<void>;
|
|
100
|
+
stop(instanceId: string): Promise<void>;
|
|
101
|
+
getRunning(): Promise<string[]>;
|
|
102
|
+
isRunning(instanceId: string): Promise<boolean>;
|
|
103
|
+
mods: {
|
|
104
|
+
list(instanceId: string): Promise<ModInfo[]>;
|
|
105
|
+
add(
|
|
106
|
+
instanceId: string,
|
|
107
|
+
url: string,
|
|
108
|
+
fileName?: string | null,
|
|
109
|
+
): Promise<ModInfo>;
|
|
110
|
+
remove(instanceId: string, fileName: string): Promise<void>;
|
|
111
|
+
enable(instanceId: string, fileName: string): Promise<void>;
|
|
112
|
+
disable(instanceId: string, fileName: string): Promise<void>;
|
|
113
|
+
openFolder(instanceId: string): Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
openInstanceFolder(instanceId: string): Promise<void>;
|
|
83
116
|
checkUpdate(): Promise<UpdateInfo | null>;
|
|
84
117
|
doUpdate(url: string): Promise<void>;
|
|
85
118
|
setSession(session: CustomSession): Promise<void>;
|
|
86
119
|
clearSession(): Promise<void>;
|
|
87
120
|
close(): Promise<void>;
|
|
121
|
+
minimize(): Promise<void>;
|
|
122
|
+
toggleMaximize(): Promise<void>;
|
|
123
|
+
startDrag(): Promise<void>;
|
|
88
124
|
on: {
|
|
89
125
|
setupProgress(cb: (p: SetupProgress) => void): Promise<() => void>;
|
|
90
126
|
setupDone(cb: () => void): Promise<() => void>;
|
package/src/rust/src/lib.rs
CHANGED
|
@@ -15,6 +15,13 @@ const RESOURCES_URL: &str = "https://resources.download.minecraft.net/";
|
|
|
15
15
|
|
|
16
16
|
// ── Config (config.json) ──────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
+
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
19
|
+
pub struct ModConfig {
|
|
20
|
+
pub url: String,
|
|
21
|
+
#[serde(default)] pub name: String,
|
|
22
|
+
#[serde(default)] pub file_name: String,
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
19
26
|
pub struct InstanceConfig {
|
|
20
27
|
pub id: String,
|
|
@@ -24,6 +31,7 @@ pub struct InstanceConfig {
|
|
|
24
31
|
#[serde(default)] pub loader_version: String,
|
|
25
32
|
#[serde(default)] pub server_ip: String,
|
|
26
33
|
#[serde(default = "default_port")] pub server_port: u16,
|
|
34
|
+
#[serde(default)] pub mods: Vec<ModConfig>,
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
@@ -97,6 +105,7 @@ pub struct AppState {
|
|
|
97
105
|
pub settings: Mutex<Settings>,
|
|
98
106
|
pub data_dir: PathBuf,
|
|
99
107
|
pub custom_session: Mutex<Option<CustomSession>>,
|
|
108
|
+
pub running: Mutex<HashMap<String, std::sync::Arc<Mutex<std::process::Child>>>>,
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
// ── Types Mojang (internes) ───────────────────────────────────────────────────
|
|
@@ -193,6 +202,16 @@ pub struct InstanceStatus {
|
|
|
193
202
|
pub id: String,
|
|
194
203
|
pub name: String,
|
|
195
204
|
pub installed: bool,
|
|
205
|
+
pub running: bool,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[derive(Serialize, Clone)]
|
|
209
|
+
pub struct ModInfo {
|
|
210
|
+
pub file_name: String,
|
|
211
|
+
pub name: String,
|
|
212
|
+
pub enabled: bool,
|
|
213
|
+
pub size: u64,
|
|
214
|
+
pub managed: bool,
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
#[derive(Serialize, Clone)]
|
|
@@ -241,6 +260,35 @@ fn instance_data_dir(launcher_dir: &Path, instance: &InstanceConfig) -> PathBuf
|
|
|
241
260
|
|
|
242
261
|
fn log_dir(launcher_dir: &Path) -> PathBuf { launcher_dir.join("logs") }
|
|
243
262
|
|
|
263
|
+
/// Dossier mods/ de l'instance (lu par Fabric/Forge/Quilt depuis le game dir)
|
|
264
|
+
fn mods_dir(launcher_dir: &Path, inst: &InstanceConfig) -> PathBuf {
|
|
265
|
+
instance_data_dir(launcher_dir, inst).join("mods")
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn mod_file_name(m: &ModConfig) -> String {
|
|
269
|
+
if !m.file_name.is_empty() { return m.file_name.clone(); }
|
|
270
|
+
let base = m.url.split(['?', '#']).next().unwrap_or(&m.url);
|
|
271
|
+
let name = base.rsplit('/').next().filter(|n| !n.is_empty()).unwrap_or("mod.jar");
|
|
272
|
+
if name.ends_with(".jar") { name.to_string() } else { format!("{name}.jar") }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn check_file_name(name: &str) -> Result<(), String> {
|
|
276
|
+
if name.is_empty() || name.contains('/') || name.contains('\\') || name.contains("..") {
|
|
277
|
+
return Err(format!("Nom de fichier invalide : '{name}'"));
|
|
278
|
+
}
|
|
279
|
+
Ok(())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn open_in_file_manager(path: &Path) -> Result<(), String> {
|
|
283
|
+
std::fs::create_dir_all(path).map_err(|e| e.to_string())?;
|
|
284
|
+
let cmd = if cfg!(windows) { "explorer" }
|
|
285
|
+
else if cfg!(target_os = "macos") { "open" }
|
|
286
|
+
else { "xdg-open" };
|
|
287
|
+
std::process::Command::new(cmd).arg(path).spawn()
|
|
288
|
+
.map(|_| ())
|
|
289
|
+
.map_err(|e| format!("Impossible d'ouvrir le dossier : {e}"))
|
|
290
|
+
}
|
|
291
|
+
|
|
244
292
|
fn find_bundled_java(launcher_dir: &Path) -> Option<PathBuf> {
|
|
245
293
|
let java_dir = launcher_dir.join("java");
|
|
246
294
|
let bin = if cfg!(windows) { "javaw.exe" } else { "java" };
|
|
@@ -341,6 +389,37 @@ fn maven_path(name: &str) -> String {
|
|
|
341
389
|
format!("{}/{}/{}/{}-{}.jar", p[0].replace('.', "/"), p[1], p[2], p[1], p[2])
|
|
342
390
|
}
|
|
343
391
|
|
|
392
|
+
/// Maven coordinate without its version: "group:artifact[:classifier]".
|
|
393
|
+
/// Used to dedupe libraries so a single ASM/Guava/etc. survives on the
|
|
394
|
+
/// classpath — Fabric aborts if two versions of the same jar are present.
|
|
395
|
+
fn lib_key(name: &str) -> String {
|
|
396
|
+
let p: Vec<&str> = name.split(':').collect();
|
|
397
|
+
if p.len() < 3 { return name.to_string(); }
|
|
398
|
+
// Keep group:artifact plus any classifier (index 3+), drop the version.
|
|
399
|
+
let mut key = format!("{}:{}", p[0], p[1]);
|
|
400
|
+
for extra in &p[3..] { key.push(':'); key.push_str(extra); }
|
|
401
|
+
key
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// Collapse a merged (vanilla + loader) library list so each coordinate
|
|
405
|
+
/// appears once. The later entry wins — loader libraries follow the parent's
|
|
406
|
+
/// in the merge, so Fabric's versions override vanilla's.
|
|
407
|
+
fn dedup_libraries(libs: Vec<LibEntry>) -> Vec<LibEntry> {
|
|
408
|
+
let mut seen: HashMap<String, usize> = HashMap::new();
|
|
409
|
+
let mut out: Vec<LibEntry> = Vec::new();
|
|
410
|
+
for lib in libs {
|
|
411
|
+
let key = lib_key(&lib.name);
|
|
412
|
+
match seen.get(&key) {
|
|
413
|
+
Some(&i) => out[i] = lib,
|
|
414
|
+
None => {
|
|
415
|
+
seen.insert(key, out.len());
|
|
416
|
+
out.push(lib);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
out
|
|
421
|
+
}
|
|
422
|
+
|
|
344
423
|
async fn http_bytes(client: &reqwest::Client, url: &str) -> Result<Vec<u8>, String> {
|
|
345
424
|
client.get(url).send().await.map_err(|e| format!("GET {url}: {e}"))?
|
|
346
425
|
.bytes().await.map_err(|e| e.to_string()).map(|b| b.to_vec())
|
|
@@ -422,10 +501,12 @@ fn get_default_launcher_dir(state: State<AppState>) -> String {
|
|
|
422
501
|
fn get_init_status(state: State<AppState>) -> InitStatus {
|
|
423
502
|
let settings = state.settings.lock().unwrap().clone();
|
|
424
503
|
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
504
|
+
let running = state.running.lock().unwrap();
|
|
425
505
|
let instances = state.config.instances.iter().map(|inst| InstanceStatus {
|
|
426
506
|
id: inst.id.clone(),
|
|
427
507
|
name: inst.name.clone(),
|
|
428
508
|
installed: is_instance_installed(&launcher_dir, inst),
|
|
509
|
+
running: running.contains_key(&inst.id),
|
|
429
510
|
}).collect();
|
|
430
511
|
InitStatus {
|
|
431
512
|
launcher_dir: launcher_dir.to_string_lossy().into(),
|
|
@@ -641,6 +722,166 @@ async fn install_fabric(
|
|
|
641
722
|
Ok(())
|
|
642
723
|
}
|
|
643
724
|
|
|
725
|
+
// ── Mods ──────────────────────────────────────────────────────────────────────
|
|
726
|
+
|
|
727
|
+
/// Télécharge les mods déclarés dans config.json qui manquent dans mods/.
|
|
728
|
+
/// Un mod désactivé (.jar.disabled) n'est pas re-téléchargé.
|
|
729
|
+
async fn sync_mods(
|
|
730
|
+
app: &AppHandle,
|
|
731
|
+
client: &reqwest::Client,
|
|
732
|
+
launcher_dir: &Path,
|
|
733
|
+
inst: &InstanceConfig,
|
|
734
|
+
) -> Result<(), String> {
|
|
735
|
+
if inst.mods.is_empty() { return Ok(()); }
|
|
736
|
+
let dir = mods_dir(launcher_dir, inst);
|
|
737
|
+
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
|
738
|
+
|
|
739
|
+
let total = inst.mods.len();
|
|
740
|
+
for (i, m) in inst.mods.iter().enumerate() {
|
|
741
|
+
let file = mod_file_name(m);
|
|
742
|
+
check_file_name(&file)?;
|
|
743
|
+
if dir.join(&file).exists() || dir.join(format!("{file}.disabled")).exists() { continue; }
|
|
744
|
+
let label = if m.name.is_empty() { file.clone() } else { m.name.clone() };
|
|
745
|
+
let _ = app.emit("setup:progress", SetupProgress {
|
|
746
|
+
step: inst.id.clone(),
|
|
747
|
+
current: 100 * i / total.max(1),
|
|
748
|
+
total: 100,
|
|
749
|
+
label: format!("Mod {label} ({}/{total})", i + 1),
|
|
750
|
+
error: false,
|
|
751
|
+
});
|
|
752
|
+
save_if_missing(client, &m.url, &dir.join(&file)).await
|
|
753
|
+
.map_err(|e| format!("Mod '{label}': {e}"))?;
|
|
754
|
+
}
|
|
755
|
+
Ok(())
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fn find_instance<'a>(cfg: &'a LauncherConfig, instance_id: &str) -> Result<&'a InstanceConfig, String> {
|
|
759
|
+
cfg.instances.iter().find(|i| i.id == instance_id)
|
|
760
|
+
.ok_or_else(|| format!("Instance '{instance_id}' introuvable dans config.json"))
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
#[tauri::command]
|
|
764
|
+
fn get_mods(state: State<AppState>, instance_id: String) -> Result<Vec<ModInfo>, String> {
|
|
765
|
+
let settings = state.settings.lock().unwrap().clone();
|
|
766
|
+
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
767
|
+
let inst = find_instance(&state.config, &instance_id)?;
|
|
768
|
+
let dir = mods_dir(&launcher_dir, inst);
|
|
769
|
+
|
|
770
|
+
let managed: HashMap<String, &ModConfig> = inst.mods.iter()
|
|
771
|
+
.map(|m| (mod_file_name(m), m)).collect();
|
|
772
|
+
|
|
773
|
+
let mut mods = Vec::new();
|
|
774
|
+
if let Ok(entries) = std::fs::read_dir(&dir) {
|
|
775
|
+
for e in entries.flatten() {
|
|
776
|
+
let raw = e.file_name().to_string_lossy().to_string();
|
|
777
|
+
let (file, enabled) = match raw.strip_suffix(".disabled") {
|
|
778
|
+
Some(base) => (base.to_string(), false),
|
|
779
|
+
None => (raw.clone(), true),
|
|
780
|
+
};
|
|
781
|
+
if !file.ends_with(".jar") { continue; }
|
|
782
|
+
let decl = managed.get(&file);
|
|
783
|
+
mods.push(ModInfo {
|
|
784
|
+
name: decl.map(|m| m.name.clone()).filter(|n| !n.is_empty())
|
|
785
|
+
.unwrap_or_else(|| file.trim_end_matches(".jar").to_string()),
|
|
786
|
+
file_name: file,
|
|
787
|
+
enabled,
|
|
788
|
+
size: e.metadata().map(|m| m.len()).unwrap_or(0),
|
|
789
|
+
managed: decl.is_some(),
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
mods.sort_by(|a, b| a.file_name.cmp(&b.file_name));
|
|
794
|
+
Ok(mods)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
#[tauri::command]
|
|
798
|
+
async fn add_mod(
|
|
799
|
+
state: State<'_, AppState>,
|
|
800
|
+
instance_id: String,
|
|
801
|
+
url: String,
|
|
802
|
+
file_name: Option<String>,
|
|
803
|
+
) -> Result<ModInfo, String> {
|
|
804
|
+
let settings = state.settings.lock().unwrap().clone();
|
|
805
|
+
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
806
|
+
let inst = find_instance(&state.config, &instance_id)?;
|
|
807
|
+
|
|
808
|
+
let m = ModConfig { url, name: String::new(), file_name: file_name.unwrap_or_default() };
|
|
809
|
+
let file = mod_file_name(&m);
|
|
810
|
+
check_file_name(&file)?;
|
|
811
|
+
|
|
812
|
+
let dir = mods_dir(&launcher_dir, inst);
|
|
813
|
+
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
|
814
|
+
let dest = dir.join(&file);
|
|
815
|
+
if dest.exists() { return Err(format!("Le mod '{file}' existe déjà.")); }
|
|
816
|
+
|
|
817
|
+
let client = reqwest::Client::builder().user_agent("HomeLauncher/1.0").build()
|
|
818
|
+
.map_err(|e| e.to_string())?;
|
|
819
|
+
let bytes = http_bytes(&client, &m.url).await?;
|
|
820
|
+
std::fs::write(&dest, &bytes).map_err(|e| e.to_string())?;
|
|
821
|
+
|
|
822
|
+
Ok(ModInfo {
|
|
823
|
+
name: file.trim_end_matches(".jar").to_string(),
|
|
824
|
+
file_name: file,
|
|
825
|
+
enabled: true,
|
|
826
|
+
size: bytes.len() as u64,
|
|
827
|
+
managed: false,
|
|
828
|
+
})
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
#[tauri::command]
|
|
832
|
+
fn remove_mod(state: State<AppState>, instance_id: String, file_name: String) -> Result<(), String> {
|
|
833
|
+
check_file_name(&file_name)?;
|
|
834
|
+
let settings = state.settings.lock().unwrap().clone();
|
|
835
|
+
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
836
|
+
let inst = find_instance(&state.config, &instance_id)?;
|
|
837
|
+
let dir = mods_dir(&launcher_dir, inst);
|
|
838
|
+
for candidate in [dir.join(&file_name), dir.join(format!("{file_name}.disabled"))] {
|
|
839
|
+
if candidate.exists() {
|
|
840
|
+
return std::fs::remove_file(&candidate).map_err(|e| e.to_string());
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
Err(format!("Mod '{file_name}' introuvable."))
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
#[tauri::command]
|
|
847
|
+
fn set_mod_enabled(
|
|
848
|
+
state: State<AppState>,
|
|
849
|
+
instance_id: String,
|
|
850
|
+
file_name: String,
|
|
851
|
+
enabled: bool,
|
|
852
|
+
) -> Result<(), String> {
|
|
853
|
+
check_file_name(&file_name)?;
|
|
854
|
+
let settings = state.settings.lock().unwrap().clone();
|
|
855
|
+
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
856
|
+
let inst = find_instance(&state.config, &instance_id)?;
|
|
857
|
+
let dir = mods_dir(&launcher_dir, inst);
|
|
858
|
+
let active = dir.join(&file_name);
|
|
859
|
+
let disabled = dir.join(format!("{file_name}.disabled"));
|
|
860
|
+
match (enabled, active.exists(), disabled.exists()) {
|
|
861
|
+
(true, true, _) => Ok(()),
|
|
862
|
+
(true, false, true) => std::fs::rename(&disabled, &active).map_err(|e| e.to_string()),
|
|
863
|
+
(false, true, _) => std::fs::rename(&active, &disabled).map_err(|e| e.to_string()),
|
|
864
|
+
(false, false, true) => Ok(()),
|
|
865
|
+
_ => Err(format!("Mod '{file_name}' introuvable.")),
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
#[tauri::command]
|
|
870
|
+
fn open_mods_folder(state: State<AppState>, instance_id: String) -> Result<(), String> {
|
|
871
|
+
let settings = state.settings.lock().unwrap().clone();
|
|
872
|
+
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
873
|
+
let inst = find_instance(&state.config, &instance_id)?;
|
|
874
|
+
open_in_file_manager(&mods_dir(&launcher_dir, inst))
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
#[tauri::command]
|
|
878
|
+
fn open_instance_folder(state: State<AppState>, instance_id: String) -> Result<(), String> {
|
|
879
|
+
let settings = state.settings.lock().unwrap().clone();
|
|
880
|
+
let launcher_dir = get_launcher_dir(&settings, &state.config);
|
|
881
|
+
let inst = find_instance(&state.config, &instance_id)?;
|
|
882
|
+
open_in_file_manager(&instance_data_dir(&launcher_dir, inst))
|
|
883
|
+
}
|
|
884
|
+
|
|
644
885
|
#[tauri::command]
|
|
645
886
|
async fn run_setup(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
|
646
887
|
let settings = state.settings.lock().unwrap().clone();
|
|
@@ -668,19 +909,22 @@ async fn run_setup(app: AppHandle, state: State<'_, AppState>) -> Result<(), Str
|
|
|
668
909
|
.map_err(|e| e.to_string())?;
|
|
669
910
|
|
|
670
911
|
for inst in &cfg.instances {
|
|
671
|
-
if is_instance_installed(&launcher_dir, inst) {
|
|
912
|
+
let result = if is_instance_installed(&launcher_dir, inst) {
|
|
672
913
|
launcher_log(&logs, &format!("Instance '{}' déjà installée", inst.id));
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
});
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
launcher_log(&logs, &format!("Installation instance '{}' ({})", inst.id, inst.mc_version));
|
|
680
|
-
let result = if inst.loader == "fabric" {
|
|
681
|
-
install_fabric(&app, &client, &game_dir, inst).await
|
|
914
|
+
// On synchronise quand même les mods : un ajout dans config.json
|
|
915
|
+
// doit être téléchargé sans réinstaller l'instance.
|
|
916
|
+
sync_mods(&app, &client, &launcher_dir, inst).await
|
|
682
917
|
} else {
|
|
683
|
-
|
|
918
|
+
launcher_log(&logs, &format!("Installation instance '{}' ({})", inst.id, inst.mc_version));
|
|
919
|
+
let r = if inst.loader == "fabric" {
|
|
920
|
+
install_fabric(&app, &client, &game_dir, inst).await
|
|
921
|
+
} else {
|
|
922
|
+
install_vanilla(&app, &client, &game_dir, inst).await
|
|
923
|
+
};
|
|
924
|
+
match r {
|
|
925
|
+
Ok(_) => sync_mods(&app, &client, &launcher_dir, inst).await,
|
|
926
|
+
Err(e) => Err(e),
|
|
927
|
+
}
|
|
684
928
|
};
|
|
685
929
|
match result {
|
|
686
930
|
Ok(_) => {
|
|
@@ -688,7 +932,7 @@ async fn run_setup(app: AppHandle, state: State<'_, AppState>) -> Result<(), Str
|
|
|
688
932
|
step: inst.id.clone(), current: 100, total: 100,
|
|
689
933
|
label: format!("{} installé.", inst.name), error: false,
|
|
690
934
|
});
|
|
691
|
-
launcher_log(&logs, &format!("Instance '{}'
|
|
935
|
+
launcher_log(&logs, &format!("Instance '{}' prête", inst.id));
|
|
692
936
|
}
|
|
693
937
|
Err(e) => {
|
|
694
938
|
launcher_log(&logs, &format!("Erreur instance '{}': {e}", inst.id));
|
|
@@ -727,7 +971,11 @@ fn read_version_chain(game_dir: &Path, version_id: &str) -> Result<VersionJson,
|
|
|
727
971
|
(None, Some(pa)) => ver.arguments = Some(pa),
|
|
728
972
|
_ => {}
|
|
729
973
|
}
|
|
730
|
-
ver.libraries = {
|
|
974
|
+
ver.libraries = {
|
|
975
|
+
let mut m = parent.libraries;
|
|
976
|
+
m.extend(std::mem::take(&mut ver.libraries));
|
|
977
|
+
dedup_libraries(m)
|
|
978
|
+
};
|
|
731
979
|
}
|
|
732
980
|
Ok(ver)
|
|
733
981
|
}
|
|
@@ -763,6 +1011,14 @@ fn verify_instance(launcher_dir: &Path, inst: &InstanceConfig) -> Result<(), Str
|
|
|
763
1011
|
if !path.exists() { missing.push(format!("lib : {}", lib.name)); }
|
|
764
1012
|
}
|
|
765
1013
|
|
|
1014
|
+
let m_dir = mods_dir(launcher_dir, inst);
|
|
1015
|
+
for m in &inst.mods {
|
|
1016
|
+
let file = mod_file_name(m);
|
|
1017
|
+
if !m_dir.join(&file).exists() && !m_dir.join(format!("{file}.disabled")).exists() {
|
|
1018
|
+
missing.push(format!("mod : {file}"));
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
766
1022
|
if missing.is_empty() { Ok(()) } else {
|
|
767
1023
|
Err(format!(
|
|
768
1024
|
"{} fichier(s) manquant(s) dans '{}' :\n{}\n\nRelancez la configuration initiale.",
|
|
@@ -840,6 +1096,10 @@ async fn launch_game(
|
|
|
840
1096
|
.ok_or_else(|| format!("Instance '{instance_id}' introuvable"))?
|
|
841
1097
|
.clone();
|
|
842
1098
|
|
|
1099
|
+
if state.running.lock().unwrap().contains_key(&instance_id) {
|
|
1100
|
+
return Err(format!("L'instance '{}' est déjà en cours d'exécution.", inst.name));
|
|
1101
|
+
}
|
|
1102
|
+
|
|
843
1103
|
verify_instance(&launcher_dir, &inst)?;
|
|
844
1104
|
|
|
845
1105
|
let java = find_bundled_java(&launcher_dir).unwrap();
|
|
@@ -1003,17 +1263,50 @@ async fn launch_game(
|
|
|
1003
1263
|
}
|
|
1004
1264
|
});
|
|
1005
1265
|
}
|
|
1266
|
+
// Enregistre le process pour stop_game / get_running_instances.
|
|
1267
|
+
// Le thread de fin utilise try_wait en boucle pour ne pas garder le lock.
|
|
1268
|
+
let child = std::sync::Arc::new(Mutex::new(child));
|
|
1269
|
+
state.running.lock().unwrap().insert(instance_id.clone(), child.clone());
|
|
1270
|
+
|
|
1006
1271
|
let iid_exit = instance_id.clone();
|
|
1007
1272
|
let logs2 = logs.clone();
|
|
1273
|
+
let app_exit = app.clone();
|
|
1008
1274
|
std::thread::spawn(move || {
|
|
1009
|
-
let code =
|
|
1275
|
+
let code = loop {
|
|
1276
|
+
match child.lock().unwrap().try_wait() {
|
|
1277
|
+
Ok(Some(status)) => break status.code().unwrap_or(-1),
|
|
1278
|
+
Ok(None) => {}
|
|
1279
|
+
Err(_) => break -1,
|
|
1280
|
+
}
|
|
1281
|
+
std::thread::sleep(std::time::Duration::from_millis(400));
|
|
1282
|
+
};
|
|
1283
|
+
let state = app_exit.state::<AppState>();
|
|
1284
|
+
state.running.lock().unwrap().remove(&iid_exit);
|
|
1010
1285
|
launcher_log(&logs2, &format!("Instance '{}' terminée (code {code})", iid_exit));
|
|
1011
|
-
let _ =
|
|
1286
|
+
let _ = app_exit.emit("game:exit", serde_json::json!({ "instance_id": iid_exit, "code": code }));
|
|
1012
1287
|
});
|
|
1013
1288
|
|
|
1014
1289
|
Ok(())
|
|
1015
1290
|
}
|
|
1016
1291
|
|
|
1292
|
+
#[tauri::command]
|
|
1293
|
+
fn stop_game(state: State<AppState>, instance_id: String) -> Result<(), String> {
|
|
1294
|
+
let child = state.running.lock().unwrap().get(&instance_id).cloned()
|
|
1295
|
+
.ok_or_else(|| format!("Instance '{instance_id}' non lancée."))?;
|
|
1296
|
+
let result = child.lock().unwrap().kill().map_err(|e| format!("Impossible d'arrêter le jeu : {e}"));
|
|
1297
|
+
result
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
#[tauri::command]
|
|
1301
|
+
fn get_running_instances(state: State<AppState>) -> Vec<String> {
|
|
1302
|
+
state.running.lock().unwrap().keys().cloned().collect()
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
#[tauri::command]
|
|
1306
|
+
fn get_launcher_version() -> String {
|
|
1307
|
+
env!("CARGO_PKG_VERSION").into()
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1017
1310
|
// ── Mise à jour ───────────────────────────────────────────────────────────────
|
|
1018
1311
|
|
|
1019
1312
|
#[tauri::command]
|
|
@@ -1055,13 +1348,13 @@ async fn do_update(app: AppHandle, url: String) -> Result<(), String> {
|
|
|
1055
1348
|
|
|
1056
1349
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
1057
1350
|
|
|
1058
|
-
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
1059
1351
|
#[tauri::command]
|
|
1060
1352
|
fn set_custom_session(state: State<'_, AppState>, session: Option<CustomSession>) -> Result<(), String> {
|
|
1061
1353
|
*state.custom_session.lock().unwrap() = session;
|
|
1062
1354
|
Ok(())
|
|
1063
1355
|
}
|
|
1064
1356
|
|
|
1357
|
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
1065
1358
|
pub fn run() {
|
|
1066
1359
|
tauri::Builder::default()
|
|
1067
1360
|
.setup(|app| {
|
|
@@ -1083,7 +1376,13 @@ pub fn run() {
|
|
|
1083
1376
|
let _ = win.set_resizable(config.window_resizable);
|
|
1084
1377
|
}
|
|
1085
1378
|
|
|
1086
|
-
app.manage(AppState {
|
|
1379
|
+
app.manage(AppState {
|
|
1380
|
+
config,
|
|
1381
|
+
settings: Mutex::new(settings),
|
|
1382
|
+
data_dir,
|
|
1383
|
+
custom_session: Mutex::new(None),
|
|
1384
|
+
running: Mutex::new(HashMap::new()),
|
|
1385
|
+
});
|
|
1087
1386
|
if cfg!(debug_assertions) {
|
|
1088
1387
|
app.handle().plugin(
|
|
1089
1388
|
tauri_plugin_log::Builder::default().level(log::LevelFilter::Info).build()
|
|
@@ -1096,8 +1395,12 @@ pub fn run() {
|
|
|
1096
1395
|
get_settings, save_settings,
|
|
1097
1396
|
get_default_launcher_dir, get_init_status,
|
|
1098
1397
|
run_setup, verify_game, launch_game,
|
|
1398
|
+
stop_game, get_running_instances,
|
|
1399
|
+
get_mods, add_mod, remove_mod, set_mod_enabled,
|
|
1400
|
+
open_mods_folder, open_instance_folder,
|
|
1099
1401
|
check_update, do_update,
|
|
1100
1402
|
set_custom_session,
|
|
1403
|
+
get_launcher_version,
|
|
1101
1404
|
])
|
|
1102
1405
|
.run(tauri::generate_context!())
|
|
1103
1406
|
.expect("error while running tauri application");
|
package/src/template/_gitignore
CHANGED
|
@@ -3,5 +3,11 @@
|
|
|
3
3
|
"identifier": "default",
|
|
4
4
|
"description": "enables the default permissions",
|
|
5
5
|
"windows": ["main"],
|
|
6
|
-
"permissions": [
|
|
6
|
+
"permissions": [
|
|
7
|
+
"core:default",
|
|
8
|
+
"core:window:allow-close",
|
|
9
|
+
"core:window:allow-minimize",
|
|
10
|
+
"core:window:allow-toggle-maximize",
|
|
11
|
+
"core:window:allow-start-dragging"
|
|
12
|
+
]
|
|
7
13
|
}
|
package/src/template/config.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "node_modules/anvil/src/client/config.schema.json",
|
|
2
|
+
"$schema": "node_modules/@thomasfarineau/anvil/src/client/config.schema.json",
|
|
3
3
|
"identifier": "",
|
|
4
4
|
"target": "",
|
|
5
5
|
"app_name": "My Launcher",
|
|
6
6
|
"data_folder": ".my-launcher",
|
|
7
7
|
"java_version": 21,
|
|
8
8
|
"update_url": "",
|
|
9
|
-
"logo": "",
|
|
9
|
+
"logo": "logo.svg",
|
|
10
10
|
"session": "none",
|
|
11
11
|
"window_decorations": true,
|
|
12
12
|
"window_resizable": false,
|