This will mainly focus on what the codebase is like for any potential contributors. Feel free to ask any questions on Discord.
quantum_launcher- The GUI frontendql_instances- Instance management, authentication and launchingql_servers- A self-hosted server management system (incomplete)ql_mod_manager- Mod management and installationql_packager- Code related to packaging/importing instancesql_core- Core utilities and shared codeql_java_handler- A library to auto-install and provide Java runtimes
The architecture of the launcher is based on the Model-View-Controller pattern (AKA the thing used in iced).
- The
Launcherstruct holds the state of the app. view(&Launcher)renders the app's view based on the current state.update(&mut Launcher)processes messages and updates the state accordingly.- The
state::Stateenum determines which menu is currently open.
So it's a back-and-forth between Messages coming from interaction,
and code to deal with the messages in update().
Use info!(), pt!(), err!() for launcher logs:
[info] Installing something
- Doing step 1...
- Doing step 2...
- Another point message
[error] 404 when downloading library at (https://example.com/downloads/something.jar), skipping...Log only useful messages that aid troubleshooting. When a user sends logs for a broken launcher, the message should help identify the issue.
- Info: Big-picture updates.
- Pt (point): Small, step-by-step details.
- Err Errors. Use Result<T, E> for non-recoverable errors, err!() for recoverable warnings.
There is no warn!() macro, as non-fatal errors are logged with err!(), and fatal ones should be returned directly.
Prefer async code for filesystem and network operations. This can be relaxed occasionally, but it's generally recommended.
Use tokio::fs for filesystem tasks and
ql_core::file_utils::download_file_to_* for networking.
Explore ql_core::file_utils for useful utilities, or check cargo doc.
A common pattern is importing ql_core::file_utils and calling file_utils::* manually.
Return fatal errors as Result<T, E> for those that can’t be ignored.
Create custom error enums for specific tasks, like ForgeInstallError, GameLaunchError, etc.
Avoid Box<dyn Error>, instead convert errors to String using .strerr()
(via ql_core::IntoStringError trait).
icedrequires that all messages be cloneable.Box<dyn Error>can't be cloned butStringcan.
Use thiserror with #[derive(Debug, thiserror::Error)] for your error types.
All errors must implement Debug, thiserror::Error, and Display.
Use #[from] and #[error] as needed:
use thiserror::Error;
const MY_ERR_PREFIX: &str = "while doing my thing\n:";
#[derive(Debug, Error)]
enum MyError {
// Add context for third-party errors
#[error("{MY_ERR_PREFIX}while extracting zip:\n{0}")]
Zip(ZipError),
// But not for QuantumLauncher-defined errors
#[error("{MY_ERR_PREFIX}{0}")]
Io(#[from] IoError),
#[error("{MY_ERR_PREFIX}{0}")]
Request(#[from] RequestError),
#[error("{MY_ERR_PREFIX}no valid forge version found")]
NoForgeVersionFound,
}For user-facing errors, make them clear and friendly, e.g.:
while doing my thing:
while installing forge:
while extracting installer:
Zip file contains invalid data!For common errors (IO, network), offer simple troubleshooting steps and move technical details out of the way (but don't hide them!). Capitalization is flexible, use what feels best.
Here are some handy error handling methods that can be called on Result<T, E>:
Converts std::io::Error into a nicer ql_core::IoError.
tokio::fs::write(&path, &bytes).await.path(path)?;For adding context when parsing JSON strings into structs. Use on serde_json errors.
For use when converting structs into json strings.
Converts any error into Result<T, String>,
useful for "dynamic" or "generic" errors.
Required for async functions called by the GUI.
More docs coming in the future...