helixlauncher_core/
instance.rs

1use std::{
2    fs::{self, File},
3    io::BufReader,
4    path::{Path, PathBuf},
5};
6
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Debug)]
10pub struct Instance {
11    pub name: String,
12    pub components: Vec<Component>,
13    pub launch: InstanceLaunch,
14}
15
16#[derive(Default, Serialize, Deserialize, Debug)]
17pub struct InstanceLaunch {
18    pub args: Vec<String>,
19    pub jvm_args: Vec<String>,
20    pub prelaunch_command: Option<String>,
21    pub postlaunch_command: Option<String>,
22    pub allocation: Option<RamAllocation>,
23    // javaagent: Option<PathBuf>,
24}
25
26type Mebibytes = u32;
27
28#[derive(Serialize, Deserialize, Debug)]
29pub struct RamAllocation {
30    min: Mebibytes,
31    max: Mebibytes,
32}
33
34#[derive(Serialize, Deserialize, Debug)]
35pub struct Component {
36    pub id: String,
37    pub version: String,
38}
39
40const INSTX_CONFIG_NAME: &str = "instance.helix.json";
41const SUBDIR_CONFIG_NAME: &str = "directory.helix.json";
42
43impl Instance {
44    /// Make a new instance.
45    ///
46    /// ```
47    /// let name = "New instance";
48    /// let instances_dir = PathBuf::from(r"/home/user/.launcher/instance/")
49    /// let instance = Instance::new(name, InstanceLaunch::default());
50    /// ```
51    pub fn new(name: String, mc_version: String, launch: InstanceLaunch, instances_dir: &Path) -> Self {
52        let instance = Self { name, components: vec![Component {id: String::from("net.minecraft"), version: mc_version}], launch };
53
54        // make instance folder & skeleton (try to avoid collisions)
55        let mut instance_dir = instances_dir.join(&instance.name);
56        if instance_dir.try_exists().unwrap() {
57            todo!("Resolve folder collision (1)");
58        }
59
60        // make the .minecraft dir & instance dir in one line
61        fs::create_dir_all(instance_dir.join(".minecraft")).unwrap();
62
63        // create instance config
64        let instance_json = File::create(instance_dir.join(INSTX_CONFIG_NAME)).unwrap();
65        serde_json::to_writer_pretty(instance_json, &instance).unwrap();
66        
67        instance
68    }
69
70    /// Fetch instance from its path.
71    ///
72    /// ```
73    /// let path = PathBuf::from(r"/home/user/.launcher/instance/minecraft");
74    /// let instance = Instance::from(path);
75    /// ```
76    pub fn from_path<P: AsRef<Path>>(path: P) -> Self {
77        if !InstanceFolderSearchItems::is_instance(&path) {
78            panic!("put a real option/result here!");
79        }
80        let instance_json = path.as_ref().join(INSTX_CONFIG_NAME);
81        read_json_file(&instance_json)
82    }
83
84    pub fn list_instances<P: AsRef<Path>>(instances_dir: P) -> Vec<Self> {
85        fs::read_dir(instances_dir)
86            .unwrap()
87            .map(|i| Self::from_path(i.unwrap().path()))
88            .collect()
89    }
90}
91
92fn read_json_file<T: DeserializeOwned>(path: &Path) -> T {
93    let file = BufReader::new(File::open(path).unwrap());
94    serde_json::from_reader(file).unwrap()
95}
96
97#[derive(Serialize, Deserialize)]
98pub struct InstanceDirectory {
99    name: String,
100    children: Vec<String>,
101    relative_path: PathBuf,
102}
103impl InstanceDirectory {
104    // silly logic:
105    // there is always one base instancedir, of course.
106    // to add a directory you have to call a method on
107    // the parent directory.
108
109    pub fn base(instances_dir: PathBuf) -> Self {
110        let directory_json = instances_dir.join(SUBDIR_CONFIG_NAME);
111
112        // case: there is no directory.helix.json
113        //     write default, save, return
114
115        if instances_dir.try_exists().unwrap() {
116            read_json_file(&directory_json)
117        } else {
118            // write defaults
119            let inst_dir = Self {
120                name: String::from("Base Directory"),
121                children: vec![],
122                relative_path: PathBuf::from("."),
123            };
124            let directory_json = File::create(directory_json).unwrap();
125            serde_json::to_writer_pretty(directory_json, &inst_dir).unwrap();
126
127            inst_dir
128        }
129    }
130    pub fn new(name: String, parent: &mut Self, instances_dir: PathBuf) -> Self {
131        let inst_dir = Self {
132            name: name.clone(),
133            children: vec![],
134            relative_path: parent.relative_path.join(name),
135        };
136
137        // make this path absolute
138        let instance_dir = instances_dir.join(inst_dir.relative_path.clone());
139
140        fs::create_dir_all(&instance_dir).unwrap();
141
142        // write json
143        let directory_json = instance_dir.join(SUBDIR_CONFIG_NAME);
144        let directory_json = File::create(directory_json).unwrap();
145        serde_json::to_writer_pretty(directory_json, &inst_dir).unwrap();
146
147        // edit parent's config to add child
148        parent.children.push(inst_dir.name.clone());
149        let parent_dir = instances_dir.join(parent.relative_path.clone());
150        let directory_json = parent_dir.join(SUBDIR_CONFIG_NAME);
151        let directory_json = File::create(directory_json).unwrap();
152        serde_json::to_writer_pretty(directory_json, &parent).unwrap();
153
154        inst_dir
155    }
156}
157enum InstanceFolderSearchItems {
158    InstanceDir,
159    DirectoryDir,
160    UnknownDir,
161}
162impl InstanceFolderSearchItems {
163    fn identify_item<P: AsRef<Path>>(path: P) -> Self {
164        // weird edge case here: folder could have both files in. if so, whoops!
165        fs::read_dir(path)
166            .unwrap()
167            .find_map(|file| {
168                let file_name = file.unwrap().file_name();
169                if file_name == INSTX_CONFIG_NAME {
170                    Some(InstanceFolderSearchItems::InstanceDir)
171                } else if file_name == SUBDIR_CONFIG_NAME {
172                    Some(InstanceFolderSearchItems::DirectoryDir)
173                } else {
174                    None
175                }
176            })
177            .unwrap_or(InstanceFolderSearchItems::UnknownDir)
178    }
179    fn is_instance<P: AsRef<Path>>(path: P) -> bool {
180        // weird edge case here: folder could have both files in. if so, whoops!
181        fs::read_dir(path)
182            .unwrap()
183            .any(|file| file.unwrap().file_name() == INSTX_CONFIG_NAME)
184    }
185}
186