physis/
existing_dirs.rs

1// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4// Rust deprecating this is stupid, I don't want to use a crate here
5#[allow(deprecated)]
6use std::env::home_dir;
7
8use std::fs;
9use std::fs::read_dir;
10use std::path::PathBuf;
11
12/// Where the existing installation came from
13#[derive(Clone, Copy)]
14#[repr(C)]
15pub enum ExistingInstallType {
16    /// Installed via the official launcher
17    OfficialLauncher,
18    /// Installed via XIVQuickLauncher
19    XIVQuickLauncher,
20    /// Installed via XIVLauncherCore
21    XIVLauncherCore,
22    /// Installed via XIVOnMac
23    XIVOnMac,
24    /// Installed via Astra
25    Astra,
26}
27
28/// An existing install location on disk
29pub struct ExistingGameDirectory {
30    /// The application where this installation was from
31    pub install_type: ExistingInstallType,
32    /// The path to the "main folder" where "game" and "boot" sits
33    pub path: String,
34}
35
36/// Finds existing installations on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
37pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
38    let mut install_dirs = Vec::new();
39
40    match std::env::consts::OS {
41        "linux" => {
42            // Official install (Wine)
43            install_dirs.push(ExistingGameDirectory {
44                install_type: ExistingInstallType::OfficialLauncher,
45                path: from_home_dir(".wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn")
46            });
47
48            // Official install (Steam)
49            install_dirs.push(ExistingGameDirectory {
50                install_type: ExistingInstallType::OfficialLauncher,
51                path: from_home_dir(
52                    ".steam/steam/steamapps/common/FINAL FANTASY XIV - A Realm Reborn",
53                ),
54            });
55
56            // XIVLauncherCore location
57            install_dirs.push(ExistingGameDirectory {
58                install_type: ExistingInstallType::XIVLauncherCore,
59                path: from_home_dir(".xlcore/ffxiv"),
60            });
61
62            // Astra location. But we have to iterate through each UUID.
63            if let Ok(entries) = read_dir(from_home_dir(".local/share/astra/game/")) {
64                entries
65                    .flatten()
66                    .flat_map(|entry| {
67                        let Ok(meta) = entry.metadata() else {
68                            return vec![];
69                        };
70                        if meta.is_dir() {
71                            return vec![entry.path()];
72                        }
73                        vec![]
74                    })
75                    .for_each(|path| {
76                        install_dirs.push(ExistingGameDirectory {
77                            install_type: ExistingInstallType::Astra,
78                            path: path.into_os_string().into_string().unwrap(),
79                        })
80                    });
81            }
82        }
83        "macos" => {
84            // Official Launcher (macOS)
85            install_dirs.push(ExistingGameDirectory {
86                install_type: ExistingInstallType::OfficialLauncher,
87                path: from_home_dir("Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn")
88            });
89
90            // TODO: add XIV on Mac
91        }
92        "windows" => {
93            // Official install (Wine)
94            install_dirs.push(ExistingGameDirectory {
95                install_type: ExistingInstallType::OfficialLauncher,
96                path: "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"
97                    .parse()
98                    .unwrap(),
99            });
100
101            // TODO: Add Astra
102        }
103        &_ => {}
104    }
105
106    install_dirs
107        .into_iter()
108        .filter(|dir| is_valid_game_dir(&dir.path))
109        .collect()
110}
111
112/// An existing user directory
113pub struct ExistingUserDirectory {
114    /// The application where this directory was from
115    pub install_type: ExistingInstallType,
116    /// The path to the user folder
117    pub path: String,
118}
119
120/// Finds existing user folders on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
121pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
122    let mut user_dirs = Vec::new();
123    #[allow(deprecated)] // We still want std::env::home_dir
124    let Some(_) = home_dir() else {
125        return user_dirs;
126    };
127
128    match std::env::consts::OS {
129        "linux" => {
130            // Official install (Wine)
131            user_dirs.push(ExistingUserDirectory {
132                install_type: ExistingInstallType::OfficialLauncher,
133                path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
134            });
135
136            // XIVLauncherCore location
137            user_dirs.push(ExistingUserDirectory {
138                install_type: ExistingInstallType::XIVLauncherCore,
139                path: from_home_dir(".xlcore/ffxivConfig"),
140            });
141
142            // Astra location. But we have to iterate through each UUID.
143            if let Ok(entries) = read_dir(from_home_dir(".local/share/astra/user/")) {
144                entries
145                    .flatten()
146                    .flat_map(|entry| {
147                        let Ok(meta) = entry.metadata() else {
148                            return vec![];
149                        };
150                        if meta.is_dir() {
151                            return vec![entry.path()];
152                        }
153                        vec![]
154                    })
155                    .for_each(|path| {
156                        user_dirs.push(ExistingUserDirectory {
157                            install_type: ExistingInstallType::Astra,
158                            path: path.into_os_string().into_string().unwrap(),
159                        })
160                    });
161            }
162        }
163        "macos" => {
164            // Official install (Wine)
165            user_dirs.push(ExistingUserDirectory {
166                install_type: ExistingInstallType::OfficialLauncher,
167                path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
168            })
169
170            // TODO: Add XIV on Mac?
171        }
172        "windows" => {
173            // Official install
174            user_dirs.push(ExistingUserDirectory {
175                install_type: ExistingInstallType::OfficialLauncher,
176                path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
177            })
178
179            // TODO: Add Astra
180        }
181        &_ => {}
182    }
183
184    user_dirs
185        .into_iter()
186        .filter(|dir| is_valid_user_dir(&dir.path))
187        .collect()
188}
189
190fn from_home_dir(path: &'static str) -> String {
191    #[allow(deprecated)] // We still want std::env::home_dir
192    let mut new_path = home_dir().unwrap();
193    new_path.push(path);
194    new_path.into_os_string().into_string().unwrap()
195}
196
197fn is_valid_game_dir(path: &String) -> bool {
198    let mut d = PathBuf::from(path);
199
200    // Check for the dir itself
201    if fs::metadata(d.as_path()).is_err() {
202        return false;
203    }
204
205    // Check for "game"
206    d.push("game");
207
208    if fs::metadata(d.as_path()).is_err() {
209        return false;
210    }
211
212    // Check for "boot"
213    d.pop();
214    d.push("boot");
215
216    if fs::metadata(d.as_path()).is_err() {
217        return false;
218    }
219
220    true
221}
222
223fn is_valid_user_dir(path: &String) -> bool {
224    let mut d = PathBuf::from(path);
225
226    // Check for the dir itself
227    if fs::metadata(d.as_path()).is_err() {
228        return false;
229    }
230
231    // Check for "FFXIV.cfg"
232    d.push("FFXIV.cfg");
233
234    if fs::metadata(d.as_path()).is_err() {
235        return false;
236    }
237
238    true
239}