├── assets ├── .gitkeep ├── images │ ├── missing.png │ ├── default_head.png │ ├── default_mod.png │ └── default_world.png ├── fonts │ ├── inter │ │ ├── Inter-Regular.ttf │ │ └── LICENSE.txt │ └── roboto-mono │ │ ├── RobotoMono-Regular.ttf │ │ └── OFL.txt ├── icons │ ├── dash.svg │ ├── minus.svg │ ├── check.svg │ ├── chevron-up.svg │ ├── plus.svg │ ├── close.svg │ ├── moon.svg │ ├── chevron-down.svg │ ├── arrow-up.svg │ ├── chevron-left.svg │ ├── chevron-right.svg │ ├── arrow-down.svg │ ├── mountain.svg │ ├── loader-circle.svg │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── circle-check.svg │ ├── info.svg │ ├── circle-x.svg │ ├── search.svg │ ├── panel-left.svg │ ├── panel-right.svg │ ├── asterisk.svg │ ├── chevrons-up-down.svg │ ├── panel-bottom.svg │ ├── bell.svg │ ├── ellipsis.svg │ ├── type.svg │ ├── play.svg │ ├── book.svg │ ├── ellipsis-vertical.svg │ ├── grid-2x2.svg │ ├── menu.svg │ ├── calendar.svg │ ├── copy.svg │ ├── panel-left-open.svg │ ├── star.svg │ ├── download.svg │ ├── panel-bottom-open.svg │ ├── panel-right-open.svg │ ├── globe.svg │ ├── a-large-small.svg │ ├── briefcase.svg │ ├── dollar-sign.svg │ ├── panels-top-left.svg │ ├── delete.svg │ ├── archive.svg │ ├── heart.svg │ ├── compass.svg │ ├── eye.svg │ ├── maximize.svg │ ├── computer.svg │ ├── diamond.svg │ ├── headphones.svg │ ├── triangle-alert.svg │ ├── ice-cream-cone.svg │ ├── scroll.svg │ ├── star-off.svg │ ├── award.svg │ ├── inbox.svg │ ├── lightbulb.svg │ ├── message-circle.svg │ ├── users.svg │ ├── zap.svg │ ├── bath.svg │ ├── case-sensitive.svg │ ├── flip-horizontal-2.svg │ ├── house.svg │ ├── trash-2.svg │ ├── feather.svg │ ├── box.svg │ ├── brush.svg │ ├── minimize.svg │ ├── thumbs-down.svg │ ├── thumbs-up.svg │ ├── film.svg │ ├── server.svg │ ├── sort-ascending.svg │ ├── sort-descending.svg │ ├── wand.svg │ ├── router.svg │ ├── tree-pine.svg │ ├── sliders-vertical.svg │ ├── network.svg │ ├── sun.svg │ ├── camera.svg │ ├── heart-off.svg │ ├── panel-left-close.svg │ ├── panel-right-close.svg │ ├── chart-no-axes-combined.svg │ ├── file-question-mark.svg │ ├── file.svg │ ├── user.svg │ ├── waypoints.svg │ ├── external-link.svg │ ├── anvil.svg │ ├── truck.svg │ ├── circle-user.svg │ ├── settings-2.svg │ ├── hard-drive.svg │ ├── loader.svg │ ├── bot.svg │ ├── folder.svg │ ├── chart-pie.svg │ ├── cloud-sun-rain.svg │ ├── book-open.svg │ ├── gallery-vertical-end.svg │ ├── carrot.svg │ ├── layers.svg │ ├── square-terminal.svg │ ├── eye-off.svg │ ├── folder-closed.svg │ ├── cat.svg │ ├── wand-sparkles.svg │ ├── cpu.svg │ ├── folder-open.svg │ ├── swords.svg │ ├── palette.svg │ ├── bug.svg │ ├── frame.svg │ ├── replace.svg │ ├── building-2.svg │ ├── map.svg │ ├── layout-dashboard.svg │ ├── resize-corner.svg │ ├── github.svg │ ├── inspector.svg │ ├── settings.svg │ ├── window-minimize.svg │ ├── window-close.svg │ ├── window-maximize.svg │ ├── window-restore.svg │ └── pandora.svg └── OFL.txt ├── crates ├── frontend │ ├── src │ │ ├── pages │ │ │ ├── instance_main_subpage.rs │ │ │ ├── mod.rs │ │ │ └── instance │ │ │ │ └── mod.rs │ │ ├── modals │ │ │ ├── mod.rs │ │ │ └── delete_instance.rs │ │ ├── component │ │ │ ├── mod.rs │ │ │ ├── named_dropdown.rs │ │ │ ├── error_alert.rs │ │ │ ├── instance_dropdown.rs │ │ │ ├── progress_bar.rs │ │ │ └── search_helper.rs │ │ ├── entity │ │ │ ├── mod.rs │ │ │ ├── account.rs │ │ │ └── metadata.rs │ │ └── png_render_cache.rs │ ├── Cargo.toml │ └── locales │ │ └── locales.yml ├── backend │ ├── src │ │ ├── metadata │ │ │ └── mod.rs │ │ ├── arcfactory.rs │ │ ├── launch_wrapper.rs │ │ ├── id_slab.rs │ │ ├── directories.rs │ │ ├── account.rs │ │ └── lib.rs │ └── Cargo.toml ├── nbt │ ├── src │ │ ├── stringified │ │ │ ├── mod.rs │ │ │ └── to_snbt.rs │ │ └── encode.rs │ └── Cargo.toml ├── auth │ ├── README.txt │ ├── src │ │ ├── lib.rs │ │ ├── constants.rs │ │ ├── auth_page.html │ │ ├── credentials.rs │ │ └── models.rs │ ├── Cargo.toml │ └── LICENSE ├── bridge │ ├── src │ │ ├── game_output.rs │ │ ├── account.rs │ │ ├── lib.rs │ │ ├── meta.rs │ │ ├── install.rs │ │ ├── keep_alive.rs │ │ ├── notify_signal.rs │ │ ├── serial.rs │ │ ├── instance.rs │ │ └── message.rs │ └── Cargo.toml ├── schema │ ├── src │ │ ├── content.rs │ │ ├── lib.rs │ │ ├── modification.rs │ │ ├── fabric_loader_manifest.rs │ │ ├── assets_index.rs │ │ ├── version_manifest.rs │ │ ├── java_runtime_component.rs │ │ ├── java_runtimes.rs │ │ ├── loader.rs │ │ └── fabric_launch.rs │ └── Cargo.toml ├── pandora_launcher │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── panic.rs ├── ftree │ ├── Cargo.toml │ ├── LICENSE.md │ ├── CHANGELOG.md │ └── README.md └── reqwest_client │ ├── Cargo.toml │ └── src │ └── http_client_tls.rs ├── .gitignore ├── flamegraph.sh ├── screenshots └── instance.png ├── wrapper ├── LaunchWrapper.jar ├── com │ └── moulberry │ │ └── pandora │ │ ├── LaunchWrapper.class │ │ └── LaunchWrapper.java └── build.sh ├── rustfmt.toml ├── LICENSE ├── README.md ├── .github └── workflows │ └── cd.yml └── Cargo.toml /assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/frontend/src/pages/instance_main_subpage.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/backend/src/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod manager; 2 | pub mod items; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | perf.data* 3 | client_id.txt 4 | flamegraph.svg 5 | heaptrack* 6 | -------------------------------------------------------------------------------- /assets/images/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/assets/images/missing.png -------------------------------------------------------------------------------- /crates/frontend/src/modals/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generic; 2 | pub mod modrinth_install; 3 | pub mod delete_instance; 4 | -------------------------------------------------------------------------------- /crates/frontend/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod instance; 2 | pub mod instances_page; 3 | pub mod modrinth_page; 4 | -------------------------------------------------------------------------------- /flamegraph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | RUSTFLAGS="-C force-frame-pointers=yes" PATH=~/.cargo/bin/:$PATH cargo flamegraph 3 | -------------------------------------------------------------------------------- /screenshots/instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/screenshots/instance.png -------------------------------------------------------------------------------- /wrapper/LaunchWrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/wrapper/LaunchWrapper.jar -------------------------------------------------------------------------------- /assets/images/default_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/assets/images/default_head.png -------------------------------------------------------------------------------- /assets/images/default_mod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/assets/images/default_mod.png -------------------------------------------------------------------------------- /assets/images/default_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/assets/images/default_world.png -------------------------------------------------------------------------------- /crates/nbt/src/stringified/mod.rs: -------------------------------------------------------------------------------- 1 | mod to_snbt; 2 | pub use to_snbt::*; 3 | 4 | mod from_snbt; 5 | pub use from_snbt::*; 6 | -------------------------------------------------------------------------------- /assets/fonts/inter/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/assets/fonts/inter/Inter-Regular.ttf -------------------------------------------------------------------------------- /crates/auth/README.txt: -------------------------------------------------------------------------------- 1 | The code in this crate is based off https://github.com/OpenXbox/xal-rs/, adapted to suit the needs of the launcher 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | fn_call_width = 90 3 | chain_width = 90 4 | struct_variant_width = 20 5 | match_block_trailing_comma = true 6 | -------------------------------------------------------------------------------- /assets/fonts/roboto-mono/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/assets/fonts/roboto-mono/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /wrapper/com/moulberry/pandora/LaunchWrapper.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moulberry/PandoraLauncher/HEAD/wrapper/com/moulberry/pandora/LaunchWrapper.class -------------------------------------------------------------------------------- /crates/auth/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod authenticator; 2 | pub mod constants; 3 | pub mod credentials; 4 | pub mod models; 5 | pub mod secret; 6 | pub mod serve_redirect; 7 | -------------------------------------------------------------------------------- /crates/frontend/src/pages/instance/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod instance_page; 2 | pub mod logs_subpage; 3 | pub mod mods_subpage; 4 | pub mod quickplay_subpage; 5 | pub mod settings_subpage; 6 | -------------------------------------------------------------------------------- /crates/frontend/src/component/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error_alert; 2 | pub mod instance_dropdown; 3 | pub mod instance_list; 4 | pub mod named_dropdown; 5 | pub mod progress_bar; 6 | pub mod readonly_text_field; 7 | pub mod search_helper; 8 | -------------------------------------------------------------------------------- /wrapper/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd "$(dirname "$0")" 4 | rm com/moulberry/pandora/LaunchWrapper.class 5 | javac com/moulberry/pandora/LaunchWrapper.java 6 | jar cvf LaunchWrapper.jar com/moulberry/pandora/LaunchWrapper.class 7 | -------------------------------------------------------------------------------- /crates/bridge/src/game_output.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 2 | pub enum GameOutputLogLevel { 3 | Fatal, 4 | Error, 5 | Warn, 6 | Info, 7 | Debug, 8 | Trace, 9 | Other, 10 | } 11 | -------------------------------------------------------------------------------- /crates/bridge/src/account.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use uuid::Uuid; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Account { 7 | pub uuid: Uuid, 8 | pub username: Arc, 9 | pub head: Option>, 10 | } 11 | -------------------------------------------------------------------------------- /crates/schema/src/content.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 4 | #[serde(rename_all = "lowercase")] 5 | pub enum ContentSource { 6 | Manual, 7 | Modrinth, 8 | } 9 | -------------------------------------------------------------------------------- /assets/icons/dash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/bridge/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod game_output; 3 | pub mod handle; 4 | pub mod install; 5 | pub mod instance; 6 | pub mod keep_alive; 7 | pub mod message; 8 | pub mod meta; 9 | pub mod modal_action; 10 | pub mod serial; 11 | pub mod notify_signal; 12 | -------------------------------------------------------------------------------- /assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/mountain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/loader-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/circle-check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/circle-x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/panel-left.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-right.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/asterisk.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/chevrons-up-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/panel-bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/schema/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod assets_index; 2 | pub mod content; 3 | pub mod fabric_launch; 4 | pub mod fabric_loader_manifest; 5 | pub mod java_runtime_component; 6 | pub mod java_runtimes; 7 | pub mod loader; 8 | pub mod modification; 9 | pub mod modrinth; 10 | pub mod version; 11 | pub mod version_manifest; 12 | -------------------------------------------------------------------------------- /crates/nbt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nbt" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | slab.workspace = true 8 | anyhow.workspace = true 9 | byteorder.workspace = true 10 | bytes.workspace = true 11 | paste.workspace = true 12 | num.workspace = true 13 | cesu8.workspace = true 14 | -------------------------------------------------------------------------------- /assets/icons/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/ellipsis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/type.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/book.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/ellipsis-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/grid-2x2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/bridge/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bridge" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | atomic-time.workspace = true 8 | atomic_enum.workspace = true 9 | schema.workspace = true 10 | ustr.workspace = true 11 | tokio.workspace = true 12 | uuid.workspace = true 13 | tokio-util.workspace = true 14 | -------------------------------------------------------------------------------- /assets/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/panel-left-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "schema" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | chrono.workspace = true 8 | indexmap.workspace = true 9 | serde.workspace = true 10 | serde-untagged.workspace = true 11 | ustr.workspace = true 12 | thiserror.workspace = true 13 | enumset.workspace = true 14 | -------------------------------------------------------------------------------- /assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/panel-bottom-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-right-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/a-large-small.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/briefcase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dollar-sign.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/panels-top-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/pandora_launcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pandora_launcher" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | frontend.workspace = true 8 | backend.workspace = true 9 | tracing-subscriber.workspace = true 10 | tokio.workspace = true 11 | bridge.workspace = true 12 | backtrace.workspace = true 13 | parking_lot.workspace = true 14 | -------------------------------------------------------------------------------- /assets/icons/archive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/compass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/backend/src/arcfactory.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Default)] 4 | pub struct ArcStrFactory { 5 | last: Arc, 6 | } 7 | 8 | impl ArcStrFactory { 9 | pub fn create(&mut self, string: &str) -> Arc { 10 | if &*self.last != string { 11 | self.last = string.into(); 12 | } 13 | self.last.clone() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/icons/computer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/diamond.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/headphones.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/triangle-alert.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/ice-cream-cone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/scroll.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/star-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/award.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/inbox.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/lightbulb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/message-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/users.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/zap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/bath.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/case-sensitive.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/flip-horizontal-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/house.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/trash-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/feather.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/box.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/brush.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/thumbs-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/film.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/sort-ascending.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/sort-descending.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/wand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/router.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/tree-pine.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/sliders-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/network.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/heart-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-left-close.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/panel-right-close.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/chart-no-axes-combined.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/file-question-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/file.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /assets/icons/waypoints.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/anvil.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/truck.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/circle-user.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/settings-2.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /assets/icons/hard-drive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/bot.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/chart-pie.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/cloud-sun-rain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/book-open.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/gallery-vertical-end.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /assets/icons/carrot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/layers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/square-terminal.svg: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /assets/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/folder-closed.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /crates/frontend/src/entity/mod.rs: -------------------------------------------------------------------------------- 1 | use bridge::handle::BackendHandle; 2 | use gpui::Entity; 3 | 4 | use crate::entity::{ 5 | account::AccountEntries, instance::InstanceEntries, metadata::FrontendMetadata 6 | }; 7 | 8 | pub mod account; 9 | pub mod instance; 10 | pub mod metadata; 11 | 12 | #[derive(Clone)] 13 | pub struct DataEntities { 14 | pub instances: Entity, 15 | pub metadata: Entity, 16 | pub accounts: Entity, 17 | pub backend_handle: BackendHandle, 18 | } 19 | -------------------------------------------------------------------------------- /assets/icons/cat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/wand-sparkles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/cpu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/folder-open.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/swords.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/palette.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/frame.svg: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /crates/schema/src/modification.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::modrinth::{ModrinthHashes, ModrinthSideRequirement}; 6 | 7 | #[derive(Debug, Clone, Deserialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct ModrinthModpackFileDownload { 10 | pub path: Arc, 11 | pub hashes: ModrinthHashes, 12 | pub env: Option, 13 | pub downloads: Arc<[Arc]>, 14 | pub file_size: usize, 15 | } 16 | 17 | #[derive(Debug, Clone, Copy, Deserialize)] 18 | pub struct ModrinthEnv { 19 | pub client: ModrinthSideRequirement, 20 | } 21 | -------------------------------------------------------------------------------- /crates/ftree/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ftree" 3 | version = "1.2.0" 4 | edition = "2021" 5 | documentation = "https://docs.rs/ftree/" 6 | repository = "https://github.com/brurucy/ftree" 7 | license = "Apache-2.0 OR MIT" 8 | description = "A very fast fenwick tree implementation" 9 | keywords = ["sum", "prefix", "fenwick", "tree", "nostd"] 10 | categories = ["data-structures"] 11 | readme = "README.md" 12 | 13 | 14 | [dependencies] 15 | serde = { version = "1.0.151", optional = true, features = ["derive"] } 16 | 17 | [dev-dependencies] 18 | ntest = "0.9" 19 | 20 | [features] 21 | serde = ["dep:serde"] 22 | -------------------------------------------------------------------------------- /assets/icons/replace.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /crates/reqwest_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reqwest_client" 3 | version = "0.4.0-preview0" 4 | publish = false 5 | license = "Apache-2.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | path = "src/reqwest_client.rs" 10 | 11 | [dependencies] 12 | gpui.workspace = true 13 | anyhow.workspace = true 14 | 15 | reqwest = { version = "0.12.15-zed", package = "zed-reqwest" } 16 | rustls = { version = "0.23.26" } 17 | rustls-platform-verifier = "0.5.0" 18 | bytes = "1.0" 19 | futures = "0.3" 20 | tokio = { version = "1", features = ["rt", "rt-multi-thread"] } 21 | regex = "1.5" 22 | 23 | [dev-dependencies] 24 | gpui.workspace = true 25 | -------------------------------------------------------------------------------- /assets/icons/building-2.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /assets/icons/map.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /crates/schema/src/fabric_loader_manifest.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use ustr::Ustr; 3 | 4 | pub const FABRIC_LOADER_MANIFEST_URL: &str = "https://meta.fabricmc.net/v2/versions/loader?limit=10"; 5 | 6 | #[derive(Deserialize, Debug)] 7 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 8 | pub struct FabricLoaderManifest(pub Vec); 9 | 10 | #[derive(Deserialize, Debug)] 11 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 12 | pub struct FabricLoaderVersion { 13 | pub separator: Ustr, 14 | pub build: usize, 15 | pub maven: Ustr, 16 | pub version: Ustr, 17 | pub stable: bool, 18 | } 19 | -------------------------------------------------------------------------------- /crates/backend/src/launch_wrapper.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use sha1::{Digest, Sha1}; 4 | 5 | const LAUNCH_WRAPPER: &[u8] = include_bytes!("../../../wrapper/LaunchWrapper.jar"); 6 | 7 | pub fn create_wrapper(temp_dir: &Path) -> PathBuf { 8 | let mut hasher = Sha1::new(); 9 | hasher.update(LAUNCH_WRAPPER); 10 | let hash = hasher.finalize(); 11 | 12 | let hash = hex::encode(hash); 13 | let launch_wrapper = temp_dir.join(format!("LaunchWrapper-{}.jar", hash)); 14 | 15 | if !launch_wrapper.exists() { 16 | let _ = std::fs::write(&launch_wrapper, LAUNCH_WRAPPER); 17 | } 18 | 19 | launch_wrapper 20 | } 21 | -------------------------------------------------------------------------------- /assets/icons/layout-dashboard.svg: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /crates/reqwest_client/src/http_client_tls.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use rustls::ClientConfig; 4 | use rustls_platform_verifier::ConfigVerifierExt; 5 | 6 | static TLS_CONFIG: OnceLock = OnceLock::new(); 7 | 8 | pub fn tls_config() -> ClientConfig { 9 | TLS_CONFIG 10 | .get_or_init(|| { 11 | // rustls uses the `aws_lc_rs` provider by default 12 | // This only errors if the default provider has already 13 | // been installed. We can ignore this `Result`. 14 | rustls::crypto::aws_lc_rs::default_provider().install_default().ok(); 15 | 16 | ClientConfig::with_platform_verifier() 17 | }) 18 | .clone() 19 | } 20 | -------------------------------------------------------------------------------- /crates/bridge/src/meta.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use schema::{modrinth::{ModrinthProjectVersionsRequest, ModrinthProjectVersionsResult, ModrinthSearchRequest, ModrinthSearchResult}, version_manifest::MinecraftVersionManifest}; 4 | 5 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 6 | pub enum MetadataRequest { 7 | MinecraftVersionManifest, 8 | ModrinthSearch(ModrinthSearchRequest), 9 | ModrinthProjectVersions(ModrinthProjectVersionsRequest), 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum MetadataResult { 14 | MinecraftVersionManifest(Arc), 15 | ModrinthSearchResult(Arc), 16 | ModrinthProjectVersionsResult(Arc), 17 | } 18 | -------------------------------------------------------------------------------- /assets/icons/resize-corner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/inspector.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /crates/schema/src/assets_index.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::Deserialize; 3 | use ustr::Ustr; 4 | 5 | #[derive(Deserialize, Clone, Debug)] 6 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 7 | pub struct AssetsIndex { 8 | pub objects: IndexMap, 9 | // Used for 1.7 and below, indicates that the objects should be stored 10 | // in assets/virtual/{assets_id}/ instead 11 | pub r#virtual: Option, 12 | // Used for 1.5 and below, indicates that the objects should be stored 13 | // in .minecraft/resources instead 14 | pub map_to_resources: Option, 15 | } 16 | 17 | #[derive(Deserialize, Clone, Debug)] 18 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 19 | pub struct AssetObject { 20 | pub hash: Ustr, 21 | pub size: u32, 22 | } 23 | -------------------------------------------------------------------------------- /crates/auth/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const CLIENT_ID: &str = "e5226706-5096-431d-9516-ae48fe263401"; 2 | pub const AUTH_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; 3 | pub const TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; 4 | pub const REDIRECT_URL_BASE: &str = "http://localhost:3160"; 5 | pub const REDIRECT_URL: &str = "http://localhost:3160/auth"; 6 | pub const SERVER_ADDRESS: &str = "0.0.0.0:3160"; 7 | pub const XBOX_AUTHENTICATE_URL: &str = "https://user.auth.xboxlive.com/user/authenticate"; 8 | pub const XSTS_AUTHORIZE_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize"; 9 | pub const MINECRAFT_LOGIN_WITH_XBOX_URL: &str = "https://api.minecraftservices.com/authentication/login_with_xbox"; 10 | pub const MINECRAFT_PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile"; 11 | -------------------------------------------------------------------------------- /crates/auth/src/auth_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pandora Launcher 6 | 10 | 11 | 24 |
25 |

{}

26 |

{}

27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /crates/bridge/src/install.rs: -------------------------------------------------------------------------------- 1 | use std::{path::{Path, PathBuf}, sync::Arc}; 2 | 3 | use schema::content::ContentSource; 4 | 5 | use crate::instance::InstanceID; 6 | 7 | #[derive(Debug, Clone, Copy)] 8 | pub enum InstallTarget { 9 | Instance(InstanceID), 10 | Library, 11 | NewInstance, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct ContentInstall { 16 | pub target: InstallTarget, 17 | pub files: Arc<[ContentInstallFile]>, 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct ContentInstallFile { 22 | pub replace_old: Option>, 23 | pub path: Arc, 24 | pub download: ContentDownload, 25 | pub content_source: ContentSource, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub enum ContentDownload { 30 | Url { 31 | url: Arc, 32 | sha1: Arc, 33 | size: usize, 34 | }, 35 | File { 36 | path: PathBuf, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /wrapper/com/moulberry/pandora/LaunchWrapper.java: -------------------------------------------------------------------------------- 1 | package com.moulberry.pandora; 2 | 3 | import java.util.Scanner; 4 | import java.util.List; 5 | import java.util.ArrayList; 6 | 7 | public class LaunchWrapper { 8 | public static void main(String[] args) throws Throwable { 9 | Scanner scanner = new Scanner(System.in); 10 | List arguments = new ArrayList<>(); 11 | while (true) { 12 | String command = scanner.nextLine(); 13 | String value = scanner.nextLine(); 14 | 15 | if (command.equals("arg")) { 16 | arguments.add(value); 17 | } else if (command.equals("launch")) { 18 | String[] argumentsArray = arguments.toArray(new String[0]); 19 | Class.forName(value).getDeclaredMethod("main", String[].class).invoke(null, (Object) argumentsArray); 20 | return; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /crates/frontend/src/entity/account.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use bridge::account::Account; 4 | use gpui::{AppContext, Entity}; 5 | use uuid::Uuid; 6 | 7 | #[derive(Default)] 8 | pub struct AccountEntries { 9 | pub accounts: Arc<[Account]>, 10 | pub selected_account_uuid: Option, 11 | pub selected_account: Option, 12 | } 13 | 14 | impl AccountEntries { 15 | pub fn set( 16 | entity: &Entity, 17 | accounts: Arc<[Account]>, 18 | selected_account: Option, 19 | cx: &mut C, 20 | ) { 21 | entity.update(cx, |entries, cx| { 22 | entries.selected_account = 23 | selected_account.and_then(|uuid| accounts.iter().find(|acc| acc.uuid == uuid).cloned()); 24 | entries.accounts = accounts; 25 | entries.selected_account_uuid = selected_account; 26 | cx.notify(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | backtrace.workspace = true 8 | chrono.workspace = true 9 | ftree.workspace = true 10 | gpui.workspace = true 11 | gpui-component.workspace = true 12 | image.workspace = true 13 | lexical-sort.workspace = true 14 | lru.workspace = true 15 | rust-embed.workspace = true 16 | rustc-hash.workspace = true 17 | sanitize-filename.workspace = true 18 | anyhow.workspace = true 19 | bridge.workspace = true 20 | schema.workspace = true 21 | tokio.workspace = true 22 | once_cell.workspace = true 23 | ustr.workspace = true 24 | indexmap.workspace = true 25 | heapless.workspace = true 26 | slab.workspace = true 27 | intrusive-collections.workspace = true 28 | atomic-time.workspace = true 29 | reqwest_client.workspace = true 30 | uuid.workspace = true 31 | enumset.workspace = true 32 | hex.workspace = true 33 | 34 | rust-i18n = "3.1.5" 35 | -------------------------------------------------------------------------------- /crates/auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auth" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | reqwest.workspace = true 8 | cvlib.workspace = true 9 | oauth2.workspace = true 10 | p256.workspace = true 11 | base64ct.workspace = true 12 | chrono.workspace = true 13 | serde.workspace = true 14 | sha2.workspace = true 15 | rand.workspace = true 16 | thiserror.workspace = true 17 | serde_json.workspace = true 18 | url.workspace = true 19 | nt-time.workspace = true 20 | anyhow.workspace = true 21 | uuid.workspace = true 22 | keyring.workspace = true 23 | obfstr.workspace = true 24 | tokio.workspace = true 25 | tokio-util.workspace = true 26 | httparse.workspace = true 27 | 28 | [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] 29 | oo7 = { version = "0.5.0", default-features = false, features = ["native_crypto", "async-std"] } 30 | 31 | [target.'cfg(target_os = "windows")'.dependencies] 32 | windows = { version = "0.62.2", features = ["Win32_Security_Credentials"] } 33 | -------------------------------------------------------------------------------- /crates/bridge/src/keep_alive.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | Arc, 3 | atomic::{AtomicBool, Ordering}, 4 | }; 5 | 6 | #[derive(Debug)] 7 | pub struct KeepAlive { 8 | alive: Arc, 9 | } 10 | 11 | impl Default for KeepAlive { 12 | fn default() -> Self { 13 | Self::new() 14 | } 15 | } 16 | 17 | impl KeepAlive { 18 | pub fn new() -> Self { 19 | Self { 20 | alive: Arc::new(AtomicBool::new(true)), 21 | } 22 | } 23 | 24 | pub fn create_handle(&self) -> KeepAliveHandle { 25 | KeepAliveHandle { 26 | alive: Arc::clone(&self.alive), 27 | } 28 | } 29 | } 30 | 31 | impl Drop for KeepAlive { 32 | fn drop(&mut self) { 33 | self.alive.store(false, Ordering::SeqCst); 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct KeepAliveHandle { 39 | alive: Arc, 40 | } 41 | 42 | impl KeepAliveHandle { 43 | pub fn is_alive(&self) -> bool { 44 | self.alive.load(Ordering::SeqCst) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Moulberry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/auth/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Team OpenXbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/ftree/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bruno Rucy Carneiro Alves de Lima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | auth.workspace = true 8 | tokio.workspace = true 9 | serde.workspace = true 10 | sha1.workspace = true 11 | ustr.workspace = true 12 | schema.workspace = true 13 | nbt.workspace = true 14 | quick-xml.workspace = true 15 | bridge.workspace = true 16 | chrono.workspace = true 17 | hex.workspace = true 18 | futures.workspace = true 19 | regex.workspace = true 20 | reqwest.workspace = true 21 | thiserror.workspace = true 22 | zip.workspace = true 23 | os_info.workspace = true 24 | anyhow.workspace = true 25 | serde_json.workspace = true 26 | flate2.workspace = true 27 | slab.workspace = true 28 | notify.workspace = true 29 | sanitize-filename.workspace = true 30 | notify-debouncer-full.workspace = true 31 | directories.workspace = true 32 | base64.workspace = true 33 | uuid.workspace = true 34 | xmlparser.workspace = true 35 | image.workspace = true 36 | mini-moka.workspace = true 37 | rustc-hash.workspace = true 38 | lexical-sort.workspace = true 39 | indexmap.workspace = true 40 | serde_with.workspace = true 41 | once_cell.workspace = true 42 | tokio-util.workspace = true 43 | typed-path.workspace = true 44 | parking_lot.workspace = true 45 | lzma-rs.workspace = true 46 | -------------------------------------------------------------------------------- /crates/schema/src/version_manifest.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::Deserialize; 3 | use ustr::Ustr; 4 | 5 | pub const MOJANG_VERSION_MANIFEST_URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; 6 | 7 | #[derive(Deserialize, Clone, Debug)] 8 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 9 | pub struct MinecraftVersionManifest { 10 | pub latest: LatestMinecraftVersions, 11 | pub versions: Vec, 12 | } 13 | 14 | #[derive(Deserialize, Clone, Debug)] 15 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 16 | pub struct LatestMinecraftVersions { 17 | pub release: Ustr, 18 | pub snapshot: Ustr, 19 | } 20 | 21 | #[derive(Deserialize, Clone, Debug)] 22 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct MinecraftVersionLink { 25 | pub id: Ustr, 26 | pub r#type: MinecraftVersionType, 27 | pub url: Ustr, 28 | pub time: DateTime, 29 | pub release_time: DateTime, 30 | pub sha1: Ustr, 31 | pub compliance_level: u32, 32 | } 33 | 34 | #[derive(Deserialize, Clone, Debug)] 35 | #[serde(rename_all = "snake_case")] 36 | pub enum MinecraftVersionType { 37 | Release, 38 | Snapshot, 39 | OldBeta, 40 | OldAlpha, 41 | } 42 | -------------------------------------------------------------------------------- /crates/schema/src/java_runtime_component.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, sync::Arc}; 2 | 3 | use indexmap::IndexMap; 4 | use serde::Deserialize; 5 | use ustr::Ustr; 6 | 7 | #[derive(Deserialize, Clone, Debug)] 8 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 9 | pub struct JavaRuntimeComponentManifest { 10 | pub files: IndexMap, JavaRuntimeComponentFile>, 11 | } 12 | 13 | #[derive(Deserialize, Clone, Debug)] 14 | #[serde(tag = "type")] 15 | #[serde(rename_all = "lowercase")] 16 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 17 | pub enum JavaRuntimeComponentFile { 18 | Directory, 19 | File { 20 | executable: bool, 21 | downloads: JavaRuntimeComponentFileDownloads, 22 | }, 23 | Link { 24 | target: Arc, 25 | }, 26 | } 27 | 28 | #[derive(Deserialize, Clone, Debug)] 29 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 30 | pub struct JavaRuntimeComponentFileDownloads { 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub lzma: Option, 33 | pub raw: JavaRuntimeComponentFileDownload, 34 | } 35 | 36 | #[derive(Deserialize, Clone, Debug)] 37 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 38 | pub struct JavaRuntimeComponentFileDownload { 39 | pub sha1: Ustr, 40 | pub size: u32, 41 | pub url: Ustr, 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pandora Launcher 2 | 3 | Work in progress 4 | 5 | ## Features 6 | - Instance management 7 | - Mod deduplication when installed through launcher (using hard links) 8 | - Secure account credential management using platform keyrings 9 | - Custom game output window 10 | - Mod browser using Modrinth's API 11 | - Automatic redaction of sensitive information (i.e. access tokens) in logs 12 | 13 | ## Missing Features (WIP) 14 | - NeoForge/Forge support 15 | - Modpack support 16 | - Account switching 17 | 18 | ## FAQ 19 | 20 | ### Where can I suggest a feature/report a bug? 21 | 22 | Please use GitHub issues. 23 | 24 | ### Why should I use Pandora over other launchers? 25 | 26 | At this point, you probably shouldn't since it doesn't have feature parity with other launchers. 27 | 28 | ### Will Pandora be monetized? 29 | 30 | Unlikely, for a few reasons: 31 | - I believe that it is wrong for launchers to be monetized without distributing revenue back to mod creators that give the launcher value in the first place. Since I don't have the infrastructure to be able to redistribute revenue to mod creators, this is a big barrier. 32 | - Dealing with monetization takes a lot of (ongoing) work, probably more work than creating the launcher itself. 33 | - I personally dislike advertisements. 34 | 35 | ## Instance Page 36 | ![Instance Page](https://raw.githubusercontent.com/Moulberry/PandoraLauncher/refs/heads/master/screenshots/instance.png) 37 | -------------------------------------------------------------------------------- /crates/ftree/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Changed 9 | 10 | ## [1.2.0] - 2024-11-09 11 | ### Added 12 | - `push` and `pop` methods. Approach them with skepticism. 13 | 14 | ## [1.1.0] - 2024-05-24 15 | ### Added 16 | - `PartialEq`, `Eq`, `Ord`, `PartialOrd`, `Hash` trait derivations 17 | 18 | ## [1.0.1] - 2024-02-17 19 | ### Changed 20 | - Fixed a bug that caused the ftree to loop forever. Thanks to @Cydhra 21 | 22 | ## [1.0.0] - 2023-07-14 23 | ### Changed 24 | - all APIs 25 | - refactored all functions thanks to @matthieum 26 | 27 | ## [0.1.1] - 2023-07-13 28 | ### Changed 29 | - implementation of msb and lsb 30 | 31 | ## [0.1.0] - 2023-07-12 32 | ### Added 33 | - `FenwickTree` 34 | 35 | 36 | [Unreleased]: https://github.com/brurucy/indexset/compare/v1.2.0...HEAD 37 | [1.2.0]: https://github.com/brurucy/indexset/releases/tag/v1.2.0 38 | [1.1.0]: https://github.com/brurucy/indexset/releases/tag/v1.1.0 39 | [1.0.1]: https://github.com/brurucy/indexset/releases/tag/v1.0.1 40 | [1.0.0]: https://github.com/brurucy/indexset/releases/tag/v1.0.0 41 | [0.1.1]: https://github.com/brurucy/indexset/releases/tag/v0.1.1 42 | [0.1.0]: https://github.com/brurucy/indexset/releases/tag/v0.1.0 43 | -------------------------------------------------------------------------------- /assets/icons/window-minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /crates/schema/src/java_runtimes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use serde::Deserialize; 5 | use ustr::Ustr; 6 | 7 | pub const JAVA_RUNTIMES_URL: &str = 8 | "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"; 9 | 10 | #[derive(Deserialize, Debug)] 11 | pub struct JavaRuntimes { 12 | #[serde(flatten)] 13 | pub platforms: HashMap, 14 | } 15 | 16 | #[derive(Deserialize, Debug)] 17 | pub struct JavaRuntimePlatform { 18 | #[serde(flatten)] 19 | pub components: HashMap>, 20 | } 21 | 22 | #[derive(Deserialize, Debug)] 23 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 24 | pub struct JavaRuntimeComponent { 25 | pub availability: JavaRuntimeComponentAvailability, 26 | pub manifest: JavaRuntimeComponentManifestLink, 27 | pub version: JavaRuntimeComponentVersion, 28 | } 29 | 30 | #[derive(Deserialize, Debug)] 31 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 32 | pub struct JavaRuntimeComponentManifestLink { 33 | pub sha1: Ustr, 34 | pub size: u32, 35 | pub url: Ustr, 36 | } 37 | 38 | #[derive(Deserialize, Debug)] 39 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 40 | pub struct JavaRuntimeComponentVersion { 41 | pub name: Ustr, 42 | pub released: DateTime, 43 | } 44 | 45 | #[derive(Deserialize, Debug)] 46 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 47 | pub struct JavaRuntimeComponentAvailability { 48 | pub group: u32, 49 | pub progress: u32, 50 | } 51 | -------------------------------------------------------------------------------- /crates/schema/src/loader.rs: -------------------------------------------------------------------------------- 1 | use enumset::EnumSetType; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::modrinth::ModrinthLoader; 5 | 6 | #[derive(EnumSetType, Serialize, Deserialize, Debug)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum Loader { 9 | #[serde(alias = "Vanilla")] 10 | Vanilla, 11 | #[serde(alias = "Fabric")] 12 | Fabric, 13 | #[serde(alias = "Forge")] 14 | Forge, 15 | #[serde(alias = "NeoForge")] 16 | NeoForge, 17 | #[serde(other)] 18 | Unknown, 19 | } 20 | 21 | impl Loader { 22 | pub fn name(self) -> &'static str { 23 | match self { 24 | Loader::Vanilla => "Vanilla", 25 | Loader::Fabric => "Fabric", 26 | Loader::Forge => "Forge", 27 | Loader::NeoForge => "NeoForge", 28 | Loader::Unknown => "Unknown", 29 | } 30 | } 31 | 32 | pub fn from_name(str: &str) -> Self { 33 | match str { 34 | "Vanilla" | "vanilla" => Self::Vanilla, 35 | "Fabric" | "fabric" => Self::Fabric, 36 | "Forge" | "forge" => Self::Forge, 37 | "NeoForge" | "neoforge" => Self::NeoForge, 38 | _ => Self::Unknown, 39 | } 40 | } 41 | 42 | pub fn as_modrinth_loader(self) -> ModrinthLoader { 43 | match self { 44 | Loader::Vanilla => ModrinthLoader::Unknown, 45 | Loader::Fabric => ModrinthLoader::Fabric, 46 | Loader::Forge => ModrinthLoader::Forge, 47 | Loader::NeoForge => ModrinthLoader::NeoForge, 48 | Loader::Unknown => ModrinthLoader::Unknown, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/backend/src/id_slab.rs: -------------------------------------------------------------------------------- 1 | use slab::Slab; 2 | 3 | #[derive(Debug)] 4 | pub struct IdSlab { 5 | slab: Slab, 6 | } 7 | 8 | impl Default for IdSlab { 9 | fn default() -> Self { 10 | Self { slab: Default::default() } 11 | } 12 | } 13 | 14 | pub trait Id: PartialEq { 15 | fn get_index(&self) -> usize; 16 | } 17 | 18 | pub trait GetId { 19 | type Id: Id; 20 | 21 | fn get_id(&self) -> Self::Id; 22 | } 23 | 24 | impl IdSlab { 25 | pub fn get(&self, id: T::Id) -> Option<&T> { 26 | let v = self.slab.get(id.get_index())?; 27 | if v.get_id() != id { 28 | return None; 29 | } 30 | return Some(v); 31 | } 32 | 33 | pub fn get_mut(&mut self, id: T::Id) -> Option<&mut T> { 34 | let v = self.slab.get_mut(id.get_index())?; 35 | if v.get_id() != id { 36 | return None; 37 | } 38 | return Some(v); 39 | } 40 | 41 | pub fn remove(&mut self, id: T::Id) -> Option { 42 | let v = self.slab.get(id.get_index())?; 43 | if v.get_id() != id { 44 | return None; 45 | } 46 | return self.slab.try_remove(id.get_index()); 47 | } 48 | 49 | pub fn iter(&self) -> impl Iterator { 50 | self.slab.iter().map(|(_, v)| v) 51 | } 52 | 53 | pub fn iter_mut(&mut self) -> impl Iterator { 54 | self.slab.iter_mut().map(|(_, v)| v) 55 | } 56 | 57 | pub fn insert(&mut self, f: impl FnOnce(usize) -> T) -> &mut T { 58 | let vacant = self.slab.vacant_entry(); 59 | let v = (f)(vacant.key()); 60 | assert_eq!(v.get_id().get_index(), vacant.key()); 61 | vacant.insert(v) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/ftree/README.md: -------------------------------------------------------------------------------- 1 | # ftree 2 | 3 | [![crates.io](https://img.shields.io/crates/v/ftree.svg)](https://crates.io/crates/ftree) 4 | [![docs](https://docs.rs/ftree/badge.svg)](https://docs.rs/ftree) 5 | 6 | A pure-rust(with zero dependencies, no-std) fenwick tree, for the efficient computation of dynamic prefix sums. 7 | 8 | # Background 9 | 10 | Did you ever have to keep track of a sum, and update it at the same time? 11 | 12 | Let's say that you have an array that represents the lengths of some other containers: 13 | [1, 6, 3, 9, 2] 14 | 15 | What if you want to get the sum up until the n-th element? In the worst-case, this will take O(n) time. Updating on the other hand, is simply 16 | a matter of incrementing at the specified index, at O(1). 17 | 18 | A fenwick tree allows you to both get the sum and do updates in O(log n) time. 19 | 20 | Moreover, let's assume that you want to get the index of the first value such that <= sum. 21 | 22 | Without using a Fenwick tree, this would take (n * log n) time (a binary search with the sum being computed during each iteration). Using 23 | one however, only takes O(log n) time. This might seem like a very niche need, and it is. It is utilized in the [indexset](https://crates.io/crates/indexset) 24 | crate, a two-level B-Tree, to very efficiently support vector-like indexing by position. 25 | 26 | # Performance 27 | 28 | It's very performant. I have searched all over codeforces for all competitive programming fenwick tree performance tricks that there are, and put them 29 | all in this crate. 30 | 31 | # Naming 32 | 33 | This library is called `ftree`, because the base data structure is `FenwickTree`. 34 | 35 | # Changelog 36 | 37 | See [CHANGELOG.md](https://github.com/brurucy/ftree/blob/master/CHANGELOG.md). 38 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | release: 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | platform: 17 | - os_name: Linux-x86_64 18 | os: ubuntu-latest 19 | target: x86_64-unknown-linux-gnu 20 | binext: "" 21 | - os_name: Windows-x86_64 22 | os: windows-latest 23 | target: x86_64-pc-windows-msvc 24 | binext: .exe 25 | 26 | runs-on: ${{ matrix.platform.os }} 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Install toolchain 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | targets: ${{ matrix.platform.target }} 35 | 36 | - uses: Swatinem/rust-cache@v2 37 | 38 | - name: Install needed packages on Linux 39 | run: sudo apt-get update --yes && sudo apt-get install --yes libssl-dev libdbus-1-dev libx11-xcb1 libxkbcommon-x11-dev pkg-config 40 | if: contains(matrix.platform.os, 'ubuntu') 41 | 42 | - name: Install mold linker 43 | uses: rui314/setup-mold@v1 44 | 45 | - name: Run build command 46 | run: cargo build --release --target ${{ matrix.platform.target }} 47 | 48 | - name: Strip debug information 49 | run: strip target/${{ matrix.platform.target }}/release/pandora_launcher${{ matrix.platform.binext }} 50 | 51 | - name: Create release 52 | uses: softprops/action-gh-release@v2 53 | with: 54 | files: target/${{ matrix.platform.target }}/release/pandora_launcher${{ matrix.platform.binext }} 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /crates/schema/src/fabric_launch.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serde::Deserialize; 4 | use ustr::Ustr; 5 | 6 | use crate::fabric_loader_manifest::FabricLoaderVersion; 7 | 8 | #[derive(Deserialize, Debug)] 9 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 10 | pub struct FabricLaunch { 11 | pub loader: Option, 12 | pub intermediary: Option, 13 | #[serde(rename = "launcherMeta")] 14 | pub launcher_meta: FabricLaunchLauncherMeta, 15 | } 16 | 17 | #[derive(Deserialize, Debug)] 18 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 19 | pub struct FabricIntermediaryVersion { 20 | pub maven: Ustr, 21 | pub version: Ustr, 22 | pub stable: bool, 23 | } 24 | 25 | #[derive(Deserialize, Debug)] 26 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 27 | pub struct FabricLaunchLauncherMeta { 28 | pub version: u32, 29 | pub min_java_version: u32, 30 | pub libraries: FabricLaunchLibraries, 31 | #[serde(rename = "mainClass")] 32 | pub main_class: FabricLaunchMainClasses, 33 | } 34 | 35 | #[derive(Deserialize, Debug)] 36 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 37 | pub struct FabricLaunchLibraries { 38 | pub client: Arc<[FabricLaunchLibrary]>, 39 | pub common: Arc<[FabricLaunchLibrary]>, 40 | pub server: Arc<[FabricLaunchLibrary]>, 41 | pub development: Arc<[FabricLaunchLibrary]>, 42 | } 43 | 44 | #[derive(Deserialize, Debug)] 45 | pub struct FabricLaunchLibrary { 46 | pub name: Ustr, 47 | pub url: Ustr, 48 | pub sha1: Ustr, 49 | pub size: u32, 50 | } 51 | 52 | #[derive(Deserialize, Debug)] 53 | #[cfg_attr(debug_assertions, serde(deny_unknown_fields))] 54 | pub struct FabricLaunchMainClasses { 55 | pub client: Ustr, 56 | pub server: Ustr, 57 | } 58 | -------------------------------------------------------------------------------- /assets/icons/window-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /crates/bridge/src/notify_signal.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::{AtomicBool, Ordering}, Arc}; 2 | 3 | use tokio::sync::Semaphore; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct NotifySignal(Arc); 7 | 8 | #[derive(Debug)] 9 | struct NotifySignalInner { 10 | value: AtomicBool, 11 | notify: Semaphore, 12 | } 13 | 14 | impl NotifySignal { 15 | pub fn new() -> Self { 16 | Self(Arc::new(NotifySignalInner { 17 | value: AtomicBool::new(false), 18 | notify: Semaphore::new(0), 19 | })) 20 | } 21 | 22 | pub fn notify(&self) { 23 | if !self.0.value.swap(true, Ordering::AcqRel) { 24 | self.0.notify.add_permits(Semaphore::MAX_PERMITS); 25 | } 26 | } 27 | 28 | pub fn is_notified(&self) -> bool { 29 | self.0.value.load(Ordering::Acquire) 30 | } 31 | 32 | pub async fn await_notification(&self) { 33 | if self.is_notified() { 34 | return; 35 | } 36 | let _ = self.0.notify.acquire().await; 37 | } 38 | } 39 | 40 | #[derive(Debug)] 41 | pub struct KeepAliveNotifySignal(NotifySignal); 42 | 43 | impl KeepAliveNotifySignal { 44 | pub fn new() -> Self { 45 | Self(NotifySignal::new()) 46 | } 47 | 48 | pub fn notify(self) { 49 | std::mem::drop(self); 50 | } 51 | 52 | pub fn create_handle(&self) -> KeepAliveNotifySignalHandle { 53 | KeepAliveNotifySignalHandle(self.0.clone()) 54 | } 55 | } 56 | 57 | impl Drop for KeepAliveNotifySignal { 58 | fn drop(&mut self) { 59 | self.0.notify(); 60 | } 61 | } 62 | 63 | #[derive(Debug, Clone)] 64 | pub struct KeepAliveNotifySignalHandle(NotifySignal); 65 | 66 | impl KeepAliveNotifySignalHandle { 67 | pub async fn await_notification(&self) { 68 | self.0.await_notification().await 69 | } 70 | 71 | pub fn is_notified(&self) -> bool { 72 | self.0.is_notified() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/bridge/src/serial.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, sync::{atomic::{AtomicBool, AtomicUsize}, Arc}}; 2 | 3 | #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] 4 | pub struct Serial(usize); 5 | 6 | impl Serial { 7 | pub fn increment(&mut self) { 8 | self.0 = self.0.wrapping_add(1); 9 | } 10 | } 11 | 12 | impl PartialOrd for Serial { 13 | fn partial_cmp(&self, other: &Self) -> Option { 14 | let distance = self.0.abs_diff(other.0); 15 | if distance < usize::MAX / 2 { 16 | self.0.partial_cmp(&other.0) 17 | } else { 18 | other.0.partial_cmp(&self.0) 19 | } 20 | } 21 | } 22 | 23 | #[derive(Default, Debug, Clone)] 24 | pub struct AtomicSetSerial(pub(crate) Arc); 25 | 26 | impl AtomicSetSerial { 27 | pub fn set(&self, serial: Serial) { 28 | self.0.store(serial.0, std::sync::atomic::Ordering::Relaxed); 29 | } 30 | 31 | pub fn get(&self) -> Serial { 32 | Serial(self.0.load(std::sync::atomic::Ordering::Relaxed)) 33 | } 34 | } 35 | 36 | #[derive(Default, Debug, Clone)] 37 | pub struct AtomicSerialProvider(Arc); 38 | 39 | impl AtomicSerialProvider { 40 | pub fn next(&self) -> Serial { 41 | Serial(self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed).wrapping_add(1)) 42 | } 43 | } 44 | 45 | #[derive(Default, Debug, Clone)] 46 | pub struct AtomicOptionSerial(Arc<(AtomicUsize, AtomicBool)>); 47 | 48 | impl AtomicOptionSerial { 49 | pub(crate) fn set(&self, serial: Serial) { 50 | self.0.0.store(serial.0, std::sync::atomic::Ordering::SeqCst); 51 | self.0.1.store(true, std::sync::atomic::Ordering::SeqCst); 52 | } 53 | 54 | pub(crate) fn get(&self) -> Option { 55 | if self.0.1.load(std::sync::atomic::Ordering::SeqCst) { 56 | return Some(Serial(self.0.0.load(std::sync::atomic::Ordering::SeqCst))); 57 | } else { 58 | return None; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/frontend/src/component/named_dropdown.rs: -------------------------------------------------------------------------------- 1 | use gpui::{prelude::*, *}; 2 | use gpui_component::{ 3 | IndexPath, 4 | select::{SelectDelegate, SelectItem, SelectState}, 5 | }; 6 | 7 | #[derive(Clone)] 8 | pub struct NamedDropdownItem { 9 | pub name: SharedString, 10 | pub item: T 11 | } 12 | 13 | impl SelectItem for NamedDropdownItem { 14 | type Value = Self; 15 | 16 | fn title(&self) -> SharedString { 17 | self.name.clone() 18 | } 19 | 20 | fn value(&self) -> &Self::Value { 21 | self 22 | } 23 | } 24 | 25 | pub struct NamedDropdown { 26 | items: Vec>, 27 | } 28 | 29 | impl NamedDropdown { 30 | pub fn create(items: Vec>, window: &mut Window, cx: &mut App) -> Entity> { 31 | cx.new(|cx| { 32 | let instance_list = Self { 33 | items, 34 | }; 35 | SelectState::new(instance_list, None, window, cx) 36 | }) 37 | } 38 | } 39 | 40 | impl SelectDelegate for NamedDropdown { 41 | type Item = NamedDropdownItem; 42 | 43 | fn items_count(&self, _section: usize) -> usize { 44 | self.items.len() 45 | } 46 | 47 | fn item(&self, ix: gpui_component::IndexPath) -> Option<&Self::Item> { 48 | self.items.get(ix.row) 49 | } 50 | 51 | fn position(&self, value: &V) -> Option 52 | where 53 | Self::Item: gpui_component::select::SelectItem, 54 | V: PartialEq, 55 | { 56 | for (ix, item) in self.items.iter().enumerate() { 57 | if item.value() == value { 58 | return Some(IndexPath::default().row(ix)); 59 | } 60 | } 61 | 62 | None 63 | } 64 | 65 | fn perform_search( 66 | &mut self, 67 | query: &str, 68 | _window: &mut Window, 69 | _: &mut Context>, 70 | ) -> Task<()> { 71 | Task::ready(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/frontend/src/component/error_alert.rs: -------------------------------------------------------------------------------- 1 | use gpui::{prelude::*, *}; 2 | use gpui_component::{ActiveTheme as _, IconName, StyledExt, h_flex, v_flex}; 3 | 4 | #[derive(IntoElement)] 5 | pub struct ErrorAlert { 6 | id: ElementId, 7 | title: SharedString, 8 | message: SharedString, 9 | } 10 | 11 | impl ErrorAlert { 12 | pub fn new(id: impl Into, title: SharedString, message: SharedString) -> Self { 13 | Self { 14 | id: id.into(), 15 | title, 16 | message, 17 | } 18 | } 19 | } 20 | 21 | impl RenderOnce for ErrorAlert { 22 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 23 | let radius = cx.theme().radius; 24 | let padding_x = px(16.0); 25 | let padding_y = px(10.0); 26 | let gap = px(12.0); 27 | 28 | let danger = cx.theme().danger; 29 | let bg = danger.opacity(0.08); 30 | let fg = cx.theme().red; 31 | let border_color = danger; 32 | 33 | h_flex() 34 | .id(self.id) 35 | .w_full() 36 | .text_color(fg) 37 | .bg(bg) 38 | .px(padding_x) 39 | .py(padding_y) 40 | .gap(gap) 41 | .justify_between() 42 | .text_sm() 43 | .border_1() 44 | .border_color(border_color) 45 | .rounded(radius) 46 | .items_start() 47 | .child( 48 | div() 49 | .flex() 50 | .flex_1() 51 | .overflow_hidden() 52 | .gap(gap) 53 | .child(div().mt(px(6.0)).child(IconName::CircleX)) 54 | .child( 55 | v_flex() 56 | .overflow_hidden() 57 | .child(div().w_full().text_base().truncate().font_semibold().child(self.title)) 58 | .child(self.message), 59 | ), 60 | ) 61 | .into_any_element() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/pandora_launcher/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_must_use)] 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use std::sync::{Arc, RwLock}; 5 | use std::fmt::Write; 6 | 7 | pub mod panic; 8 | 9 | fn main() { 10 | tracing_subscriber::fmt() 11 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 12 | .init(); 13 | 14 | let panic_message = Arc::new(RwLock::new(None)); 15 | let deadlock_message = Arc::new(RwLock::new(None)); 16 | 17 | let (backend_recv, backend_handle, frontend_recv, frontend_handle) = bridge::handle::create_pair(); 18 | 19 | crate::panic::install_hook(panic_message.clone(), frontend_handle.clone()); 20 | 21 | // Start deadlock detection 22 | std::thread::spawn({ 23 | let deadlock_message = deadlock_message.clone(); 24 | let frontend_handle = frontend_handle.clone(); 25 | move || { 26 | loop { 27 | std::thread::sleep(std::time::Duration::from_secs(10)); 28 | let deadlocks = parking_lot::deadlock::check_deadlock(); 29 | if deadlocks.is_empty() { 30 | continue; 31 | } 32 | 33 | let mut message = String::new(); 34 | _ = writeln!(&mut message, "{} deadlock(s) detected", deadlocks.len()); 35 | for (i, threads) in deadlocks.iter().enumerate() { 36 | _ = writeln!(&mut message, "==== Deadlock #{} ({} threads) ====", i, threads.len()); 37 | for (thread_index, t) in threads.iter().enumerate() { 38 | _ = writeln!(&mut message, "== Thread #{} ({:?}) ==", thread_index, t.thread_id()); 39 | _ = writeln!(&mut message, "{:#?}", t.backtrace()); 40 | } 41 | } 42 | 43 | eprintln!("{}", message); 44 | *deadlock_message.write().unwrap() = Some(message); 45 | frontend_handle.send(bridge::message::MessageToFrontend::Refresh); 46 | return; 47 | } 48 | } 49 | }); 50 | 51 | backend::start(frontend_handle, backend_handle.clone(), backend_recv); 52 | frontend::start(panic_message, deadlock_message, backend_handle, frontend_recv); 53 | } 54 | -------------------------------------------------------------------------------- /crates/frontend/src/component/instance_dropdown.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use gpui::{prelude::*, *}; 4 | use gpui_component::{ 5 | IndexPath, 6 | select::{SelectDelegate, SelectItem, SelectState}, 7 | }; 8 | 9 | use crate::{component::search_helper::SearchHelper, entity::instance::InstanceEntry}; 10 | 11 | pub struct InstanceDropdown { 12 | instances: Arc<[InstanceEntry]>, 13 | search: SearchHelper, 14 | } 15 | 16 | impl InstanceDropdown { 17 | pub fn create(instances: Arc<[InstanceEntry]>, window: &mut Window, cx: &mut App) -> Entity> { 18 | cx.new(|cx| { 19 | let instance_list = Self { 20 | instances: instances.clone(), 21 | search: SearchHelper::new(instances, |item| item.name.clone()), 22 | }; 23 | SelectState::new(instance_list, None, window, cx).searchable(true) 24 | }) 25 | } 26 | 27 | pub fn get(&self, index: usize) -> Option<&InstanceEntry> { 28 | self.search.get(index) 29 | } 30 | } 31 | 32 | impl SelectDelegate for InstanceDropdown { 33 | type Item = InstanceEntry; 34 | 35 | fn items_count(&self, _section: usize) -> usize { 36 | self.search.len() 37 | } 38 | 39 | fn item(&self, ix: gpui_component::IndexPath) -> Option<&Self::Item> { 40 | self.search.get(ix.row) 41 | } 42 | 43 | fn position(&self, value: &V) -> Option 44 | where 45 | Self::Item: gpui_component::select::SelectItem, 46 | V: PartialEq, 47 | { 48 | if let Some(searched_iter) = self.search.iter() { 49 | for (ix, item) in searched_iter.enumerate() { 50 | if item.value() == value { 51 | return Some(IndexPath::default().row(ix)); 52 | } 53 | } 54 | } else { 55 | for (ix, item) in self.instances.iter().enumerate() { 56 | if item.value() == value { 57 | return Some(IndexPath::default().row(ix)); 58 | } 59 | } 60 | } 61 | 62 | None 63 | } 64 | 65 | fn perform_search( 66 | &mut self, 67 | query: &str, 68 | _window: &mut Window, 69 | _: &mut Context>, 70 | ) -> Task<()> { 71 | self.search.search(query); 72 | Task::ready(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/backend/src/directories.rs: -------------------------------------------------------------------------------- 1 | use std::{path::{Path, PathBuf}, sync::Arc}; 2 | 3 | pub struct LauncherDirectories { 4 | pub instances_dir: Arc, 5 | 6 | pub metadata_dir: Arc, 7 | 8 | pub assets_root_dir: Arc, 9 | pub assets_index_dir: Arc, 10 | pub assets_objects_dir: Arc, 11 | pub virtual_legacy_assets_dir: Arc, 12 | 13 | pub libraries_dir: Arc, 14 | pub log_configs_dir: Arc, 15 | pub runtime_base_dir: Arc, 16 | 17 | pub content_library_dir: Arc, 18 | pub content_meta_dir: Arc, 19 | 20 | pub temp_dir: Arc, 21 | pub temp_natives_base_dir: Arc, 22 | 23 | pub accounts_json: Arc, 24 | } 25 | 26 | impl LauncherDirectories { 27 | pub fn new(launcher_dir: PathBuf) -> Self { 28 | let instances_dir = launcher_dir.join("instances"); 29 | 30 | let metadata_dir = launcher_dir.join("metadata"); 31 | 32 | let assets_root_dir = launcher_dir.join("assets"); 33 | let assets_index_dir = assets_root_dir.join("indexes"); 34 | let assets_objects_dir = assets_root_dir.join("objects"); 35 | let virtual_legacy_assets_dir = assets_index_dir.join("virtual").join("legacy"); 36 | 37 | let libraries_dir = launcher_dir.join("libraries"); 38 | 39 | let log_configs_dir = launcher_dir.join("logconfigs"); 40 | 41 | let runtime_base_dir = launcher_dir.join("runtime"); 42 | 43 | let content_library_dir = launcher_dir.join("contentlibrary"); 44 | let content_meta_dir = launcher_dir.join("contentmeta"); 45 | 46 | let temp_dir = launcher_dir.join("temp"); 47 | let temp_natives_base_dir = temp_dir.join("natives"); 48 | 49 | let accounts_json = launcher_dir.join("accounts.json"); 50 | 51 | Self { 52 | instances_dir: instances_dir.into(), 53 | 54 | metadata_dir: metadata_dir.into(), 55 | 56 | assets_root_dir: assets_root_dir.into(), 57 | assets_index_dir: assets_index_dir.into(), 58 | assets_objects_dir: assets_objects_dir.into(), 59 | virtual_legacy_assets_dir: virtual_legacy_assets_dir.into(), 60 | 61 | libraries_dir: libraries_dir.into(), 62 | log_configs_dir: log_configs_dir.into(), 63 | runtime_base_dir: runtime_base_dir.into(), 64 | 65 | content_library_dir: content_library_dir.into(), 66 | content_meta_dir: content_meta_dir.into(), 67 | 68 | temp_dir: temp_dir.into(), 69 | temp_natives_base_dir: temp_natives_base_dir.into(), 70 | 71 | accounts_json: accounts_json.into(), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /assets/icons/window-maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /crates/frontend/locales/locales.yml: -------------------------------------------------------------------------------- 1 | _version: 2 2 | 3 | # Modrinth categories 4 | bungeecord: 5 | en: BungeeCord 6 | liteloader: 7 | en: LiteLoader 8 | forge: 9 | en: Forge 10 | neoforge: 11 | en: NeoForge 12 | fabric: 13 | en: Fabric 14 | babric: 15 | en: Babric 16 | quilt: 17 | en: Quilt 18 | cursed: 19 | en: Cursed 20 | technology: 21 | en: Technology 22 | challenging: 23 | en: Challenging 24 | decoration: 25 | en: Decoration 26 | library: 27 | en: Library 28 | adventure: 29 | en: Adventure 30 | path-tracing: 31 | en: Path Tracing 32 | realistic: 33 | en: Realistic 34 | low: 35 | en: Low 36 | medium: 37 | en: Medium 38 | high: 39 | en: High 40 | atmosphere: 41 | en: Atmosphere 42 | fantasy: 43 | en: Fantasy 44 | foliage: 45 | en: Foliage 46 | bloom: 47 | en: Bloom 48 | vanilla-like: 49 | en: Vanilla-like 50 | cartoon: 51 | en: Cartoon 52 | potato: 53 | en: Potato 54 | shadows: 55 | en: Shadows 56 | pbr: 57 | en: PBR 58 | semi-realistic: 59 | en: Semi-realistic 60 | reflections: 61 | en: Reflections 62 | screenshot: 63 | en: Screenshot 64 | colored-lighting: 65 | en: Colored Lighting 66 | economy: 67 | en: Economy 68 | management: 69 | en: Management 70 | optimization: 71 | en: Optimization 72 | mobs: 73 | en: Mobs 74 | transportation: 75 | en: Transportation 76 | kitchen-sink: 77 | en: Kitchen sink 78 | blocks: 79 | en: Blocks 80 | audio: 81 | en: Audio 82 | combat: 83 | en: Combat 84 | modded: 85 | en: Modded 86 | environment: 87 | en: Environment 88 | entities: 89 | en: Entities 90 | game-mechanics: 91 | en: Game Mechanics 92 | utility: 93 | en: Utility 94 | core-shaders: 95 | en: Core Shaders 96 | tweaks: 97 | en: Tweaks 98 | items: 99 | en: Items 100 | models: 101 | en: Models 102 | equipment: 103 | en: Equipment 104 | fonts: 105 | en: Fonts 106 | simplistic: 107 | en: Simplistic 108 | themed: 109 | en: Themed 110 | magic: 111 | en: Magic 112 | storage: 113 | en: Storage 114 | food: 115 | en: Food 116 | gui: 117 | en: GUI 118 | worldgen: 119 | en: World Generation 120 | multiplayer: 121 | en: Multiplayer 122 | quests: 123 | en: Quests 124 | lightweight: 125 | en: Lightweight 126 | social: 127 | en: Social 128 | minigame: 129 | en: Minigame 130 | 131 | # Modrinth environments 132 | client_and_server: 133 | en: Client and server 134 | client_only: 135 | en: Client only 136 | client_only_server_optional: 137 | en: Client (server optional) 138 | server_only: 139 | en: Server only 140 | server_only_client_optional: 141 | en: Server (client optional) 142 | client_or_server: 143 | en: Client or server 144 | unknown_environment: 145 | en: Unknown environment 146 | -------------------------------------------------------------------------------- /crates/backend/src/account.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Cursor, sync::Arc}; 2 | 3 | use auth::models::{MinecraftAccessToken, MinecraftProfileResponse}; 4 | use bridge::{account::Account, message::MessageToFrontend}; 5 | use image::imageops::FilterType; 6 | use rustc_hash::FxHashMap; 7 | use serde::{Deserialize, Serialize}; 8 | use uuid::Uuid; 9 | 10 | pub struct MinecraftLoginInfo { 11 | pub uuid: Uuid, 12 | pub username: Arc, 13 | pub access_token: MinecraftAccessToken, 14 | } 15 | 16 | #[derive(Default, Debug, Serialize, Deserialize)] 17 | pub struct BackendAccountInfo { 18 | pub accounts: FxHashMap, 19 | pub selected_account: Option, 20 | } 21 | 22 | impl BackendAccountInfo { 23 | pub fn create_update_message(&self) -> MessageToFrontend { 24 | let mut accounts = Vec::with_capacity(self.accounts.len()); 25 | for (uuid, account) in &self.accounts { 26 | accounts.push(Account { 27 | uuid: *uuid, 28 | username: account.username.clone(), 29 | head: account.head_32x.clone(), 30 | }); 31 | } 32 | accounts.sort_by(|a, b| lexical_sort::natural_lexical_cmp(&a.username, &b.username)); 33 | MessageToFrontend::AccountsUpdated { 34 | accounts: accounts.into(), 35 | selected_account: self.selected_account, 36 | } 37 | } 38 | } 39 | 40 | #[derive(Debug, Serialize, Deserialize)] 41 | pub struct BackendAccount { 42 | pub username: Arc, 43 | pub head: Option>, 44 | 45 | #[serde(skip)] 46 | pub head_32x: Option>, 47 | #[serde(skip)] 48 | pub downloaded_head: bool, 49 | } 50 | 51 | impl BackendAccount { 52 | pub fn new_from_profile(profile: &MinecraftProfileResponse) -> Self { 53 | Self { 54 | username: profile.name.clone(), 55 | head_32x: None, 56 | head: None, 57 | downloaded_head: false, 58 | } 59 | } 60 | 61 | pub fn try_load_head_32x_from_head(&mut self) { 62 | let Some(head) = &self.head else { 63 | return; 64 | }; 65 | let head = Arc::clone(head); 66 | 67 | let Ok(image) = image::load_from_memory(&head) else { 68 | return; 69 | }; 70 | 71 | if image.width() != 32 || image.height() != 32 { 72 | let resized = image.resize_exact(32, 32, FilterType::Nearest); 73 | 74 | let mut head_png = Vec::new(); 75 | let mut cursor = Cursor::new(&mut head_png); 76 | if resized.write_to(&mut cursor, image::ImageFormat::Png).is_ok() { 77 | self.head_32x = Some(head_png.into()); 78 | } else { 79 | self.head_32x = Some(head); 80 | } 81 | } else { 82 | self.head_32x = Some(head); 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/frontend/src/component/progress_bar.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | App, Hsla, IntoElement, ParentElement, RenderOnce, Styled, Window, div, prelude::FluentBuilder, px, relative, 3 | }; 4 | use gpui_component::ActiveTheme; 5 | 6 | #[derive(Default)] 7 | pub enum ProgressBarColor { 8 | #[default] 9 | Normal, 10 | Error, 11 | Success, 12 | } 13 | 14 | #[derive(IntoElement)] 15 | pub struct ProgressBar { 16 | pub amount: f32, 17 | pub color_scale: f32, 18 | pub color: ProgressBarColor, 19 | } 20 | 21 | impl Default for ProgressBar { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | impl ProgressBar { 28 | pub fn new() -> Self { 29 | Self { 30 | amount: 0.0, 31 | color_scale: 1.0, 32 | color: ProgressBarColor::Normal, 33 | } 34 | } 35 | } 36 | 37 | fn lerp(from: Hsla, to: Hsla, amount: f32) -> Hsla { 38 | if amount <= 0.0 { 39 | from 40 | } else if amount >= 1.0 { 41 | to 42 | } else { 43 | let mut hue_delta = to.h - from.h; 44 | if hue_delta < -0.5 { 45 | hue_delta += 1.0; 46 | } else if hue_delta > 0.5 { 47 | hue_delta -= 1.0; 48 | } 49 | let mut hue = from.h + hue_delta * amount; 50 | if hue < 0.0 { 51 | hue += 1.0; 52 | } else if hue > 1.0 { 53 | hue -= 1.0; 54 | } 55 | Hsla { 56 | h: hue, 57 | s: from.s + (to.s - from.s) * amount, 58 | l: from.l + (to.l - from.l) * amount, 59 | a: from.a + (to.a - from.a) * amount, 60 | } 61 | } 62 | } 63 | 64 | impl RenderOnce for ProgressBar { 65 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 66 | // Match the theme radius, if theme radius is zero use it. 67 | let radius = px(4.0).min(cx.theme().radius); 68 | let relative_w = relative(self.amount); 69 | 70 | let progress_bar_color = cx.theme().progress_bar; 71 | let color = match self.color { 72 | ProgressBarColor::Normal => progress_bar_color, 73 | ProgressBarColor::Error => lerp(progress_bar_color, cx.theme().red, self.color_scale), 74 | ProgressBarColor::Success => lerp(progress_bar_color, cx.theme().green, self.color_scale), 75 | }; 76 | 77 | div().w_full().relative().h(px(8.0)).rounded(radius).bg(color.opacity(0.2)).child( 78 | div() 79 | .absolute() 80 | .top_0() 81 | .left_0() 82 | .h_full() 83 | .w(relative_w) 84 | .bg(color) 85 | .map(|this| match self.amount { 86 | v if v >= 1.0 => this.rounded(radius), 87 | _ => this.rounded_l(radius), 88 | }), 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/bridge/src/instance.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, path::Path, sync::{atomic::AtomicBool, Arc}}; 2 | 3 | use schema::modification::ModrinthModpackFileDownload; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 6 | pub struct InstanceID { 7 | pub index: usize, 8 | pub generation: usize, 9 | } 10 | 11 | impl InstanceID { 12 | pub fn dangling() -> Self { 13 | Self { 14 | index: usize::MAX, 15 | generation: usize::MAX, 16 | } 17 | } 18 | } 19 | 20 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 21 | pub struct InstanceModID { 22 | pub index: usize, 23 | pub generation: usize, 24 | } 25 | 26 | impl InstanceModID { 27 | pub fn dangling() -> Self { 28 | Self { 29 | index: usize::MAX, 30 | generation: usize::MAX, 31 | } 32 | } 33 | } 34 | 35 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 36 | pub enum InstanceStatus { 37 | NotRunning, 38 | Launching, 39 | Running, 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct InstanceWorldSummary { 44 | pub title: Arc, 45 | pub subtitle: Arc, 46 | pub level_path: Arc, 47 | pub last_played: i64, 48 | pub png_icon: Option>, 49 | } 50 | 51 | #[derive(Debug, Clone)] 52 | pub struct InstanceServerSummary { 53 | pub name: Arc, 54 | pub ip: Arc, 55 | pub png_icon: Option>, 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | pub struct InstanceModSummary { 60 | pub mod_summary: Arc, 61 | pub id: InstanceModID, 62 | pub filename: Arc, 63 | pub lowercase_filename: Arc, 64 | pub filename_hash: u64, 65 | pub path: Arc, 66 | pub enabled: bool, 67 | pub disabled_children: HashSet, 68 | } 69 | 70 | #[derive(Debug, Clone)] 71 | pub struct ModSummary { 72 | pub id: Arc, 73 | pub hash: [u8; 20], 74 | pub name: Arc, 75 | pub lowercase_search_key: Arc, 76 | pub version_str: Arc, 77 | pub authors: Arc, 78 | pub png_icon: Option>, 79 | pub update_status: Arc, 80 | pub extra: LoaderSpecificModSummary, 81 | } 82 | 83 | #[derive(Debug, Clone)] 84 | pub enum LoaderSpecificModSummary { 85 | Fabric, 86 | ModrinthModpack { 87 | downloads: Arc<[ModrinthModpackFileDownload]>, 88 | summaries: Arc<[Option>]>, 89 | overrides: Arc<[(Arc, Arc<[u8]>)]>, 90 | }, 91 | } 92 | 93 | 94 | #[atomic_enum::atomic_enum] 95 | #[derive(PartialEq, Eq)] 96 | pub enum ContentUpdateStatus { 97 | Unknown, 98 | ManualInstall, 99 | ErrorNotFound, 100 | ErrorInvalidHash, 101 | AlreadyUpToDate, 102 | Modrinth, 103 | } 104 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["crates/backend", "crates/bridge", "crates/reqwest_client", "crates/frontend", "crates/pandora_launcher", "crates/nbt", "crates/schema", "crates/auth"] 4 | 5 | default-members = ["crates/pandora_launcher"] 6 | 7 | [workspace.package] 8 | edition = "2024" 9 | 10 | [profile.release] 11 | debug = "limited" 12 | lto = "thin" 13 | codegen-units = 1 14 | # Enable for more accurate flamegraphs 15 | # debug = true 16 | 17 | [profile.dev.package."*"] 18 | opt-level = 3 19 | 20 | [workspace.dependencies] 21 | auth = { path = "crates/auth" } 22 | backend = { path = "crates/backend" } 23 | bridge = { path = "crates/bridge" } 24 | frontend = { path = "crates/frontend" } 25 | schema = { path = "crates/schema" } 26 | nbt = { path = "crates/nbt" } 27 | reqwest_client = { path = "crates/reqwest_client" } 28 | 29 | anyhow = "1.0.100" 30 | atomic-time = "0.1.5" 31 | atomic_enum = "0.3.0" 32 | backtrace = "0.3.76" 33 | byteorder = "1.5.0" 34 | bytes = "1.10.1" 35 | cesu8 = "1.1.0" 36 | chrono = { version = "0.4.42", features = ["serde"] } 37 | directories = "6.0.0" 38 | flate2 = "1.1.5" 39 | ftree = { path = "crates/ftree"} 40 | futures = "0.3.31" 41 | gpui = "0.2.2" 42 | gpui-component = { git = "https://github.com/Moulberry/gpui-component.git", rev = "6525ac151af1bdd3f294f89b0ec2c259abbe73c4" } 43 | hex = { version = "0.4.3", features = ["serde"] } 44 | image = "0.25.8" 45 | indexmap = { version = "2.12.0", features = ["serde"] } 46 | lexical-sort = "0.3.1" 47 | lru = "0.16.2" 48 | mini-moka = "0.10.3" 49 | notify = "8.2.0" 50 | notify-debouncer-full = "0.6.0" 51 | num = "0.4.3" 52 | once_cell = "1.21.3" 53 | os_info = "3.12.0" 54 | paste = "1.0.15" 55 | quick-xml = "0.38.3" 56 | rand = "0.8.5" 57 | regex = "1.12.2" 58 | reqwest = { version = "0.12.24", features = ["json", "rustls-tls", "stream"] } 59 | rust-embed = "8.7.2" 60 | rustc-hash = "2.1.1" 61 | sanitize-filename = "0.6.0" 62 | serde = { version = "1.0.228", features = ["rc", "alloc", "derive"] } 63 | serde-untagged = "0.1.9" 64 | serde_json = "1.0.145" 65 | sha1 = "0.10.6" 66 | slab = "0.4.11" 67 | thiserror = "2.0.17" 68 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net"] } 69 | tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } 70 | ustr = { version = "1.1.0", features = ["serde"] } 71 | zip = "6.0.0" 72 | base64 = "0.22.1" 73 | cvlib = "0.1.2" 74 | oauth2 = { version = "5.0.0", features = ["reqwest"] } 75 | p256 = "0.13.2" 76 | base64ct = "1.8.0" 77 | sha2 = "0.10.9" 78 | url = "2.5.7" 79 | nt-time = { version = "0.12.1", features = ["chrono"] } 80 | obfstr = "0.4.4" 81 | uuid = "1.18.1" 82 | keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "linux-native-sync-persistent"] } 83 | xmlparser = "0.13.6" 84 | heapless = "0.9.1" 85 | intrusive-collections = "0.9.7" 86 | tokio-util = "0.7.17" 87 | enumset = "1.1.10" 88 | serde_with = "3.15.1" 89 | typed-path = "0.12.0" 90 | parking_lot = { version = "0.12.5", features = ["deadlock_detection"] } 91 | lzma-rs = "0.3.0" 92 | httparse = "1.10.1" 93 | -------------------------------------------------------------------------------- /crates/auth/src/credentials.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use chrono::Utc; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::models::{MinecraftAccessToken, TokenWithExpiry, XstsToken}; 7 | 8 | #[derive(Default, Deserialize, Serialize)] 9 | pub struct AccountCredentials { 10 | pub msa_refresh: Option>, 11 | pub msa_access: Option, 12 | pub xbl: Option, 13 | pub xsts: Option, 14 | pub access_token: Option, 15 | } 16 | 17 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] 18 | #[repr(u8)] 19 | pub enum AuthStage { 20 | Initial, 21 | MsaRefresh, 22 | MsaAccess, 23 | XboxLive, 24 | XboxSecure, 25 | AccessToken, 26 | } 27 | pub const AUTH_STAGE_COUNT: u8 = 6; 28 | 29 | pub enum AuthStageWithData { 30 | Initial, 31 | MsaRefresh(Arc), 32 | MsaAccess(Arc), 33 | XboxLive(Arc), 34 | XboxSecure { 35 | xsts: Arc, 36 | userhash: Arc, 37 | }, 38 | AccessToken(MinecraftAccessToken), 39 | } 40 | 41 | impl AuthStageWithData { 42 | pub fn stage(&self) -> AuthStage { 43 | match self { 44 | AuthStageWithData::Initial => AuthStage::Initial, 45 | AuthStageWithData::MsaRefresh(..) => AuthStage::MsaRefresh, 46 | AuthStageWithData::MsaAccess(..) => AuthStage::MsaAccess, 47 | AuthStageWithData::XboxLive(..) => AuthStage::XboxLive, 48 | AuthStageWithData::XboxSecure { .. } => AuthStage::XboxSecure, 49 | AuthStageWithData::AccessToken(..) => AuthStage::AccessToken, 50 | } 51 | } 52 | } 53 | 54 | impl AccountCredentials { 55 | pub fn stage(&mut self) -> AuthStageWithData { 56 | let now = Utc::now(); 57 | 58 | // Try returning access token 59 | if let Some(access_token) = &self.access_token && now < access_token.expiry { 60 | return AuthStageWithData::AccessToken(MinecraftAccessToken(Arc::clone(&access_token.token))); 61 | } 62 | self.access_token = None; 63 | 64 | // Try returning XboxSecure 65 | if let Some(xsts) = &self.xsts && now < xsts.expiry { 66 | return AuthStageWithData::XboxSecure { 67 | xsts: Arc::clone(&xsts.token), 68 | userhash: Arc::clone(&xsts.userhash), 69 | }; 70 | } 71 | self.xsts = None; 72 | 73 | // Try returning XboxLive 74 | if let Some(xbl) = &self.xbl && now < xbl.expiry { 75 | return AuthStageWithData::XboxLive(Arc::clone(&xbl.token)); 76 | } 77 | self.xbl = None; 78 | 79 | // Try returning MsaAccess 80 | if let Some(msa_access) = &self.msa_access && now < msa_access.expiry { 81 | return AuthStageWithData::MsaAccess(Arc::clone(&msa_access.token)); 82 | } 83 | self.msa_access = None; 84 | 85 | // Try returning MsaRefresh 86 | if let Some(msa_refresh) = &self.msa_refresh { 87 | return AuthStageWithData::MsaRefresh(Arc::clone(msa_refresh)); 88 | } 89 | 90 | // No valid stage, return initial stage 91 | AuthStageWithData::Initial 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_must_use)] 2 | 3 | mod backend; 4 | use std::{ffi::OsString, io::Write, path::{Path, PathBuf}}; 5 | 6 | pub use backend::*; 7 | use sha1::{Digest, Sha1}; 8 | 9 | mod backend_filesystem; 10 | mod backend_handler; 11 | 12 | mod account; 13 | mod arcfactory; 14 | mod directories; 15 | mod install_content; 16 | mod instance; 17 | mod launch; 18 | mod launch_wrapper; 19 | mod log_reader; 20 | mod metadata; 21 | mod mod_metadata; 22 | mod id_slab; 23 | 24 | pub(crate) fn is_single_component_path(path: &str) -> bool { 25 | let path = std::path::Path::new(path); 26 | let mut components = path.components().peekable(); 27 | 28 | if let Some(first) = components.peek() && !matches!(first, std::path::Component::Normal(_)) { 29 | return false; 30 | } 31 | 32 | components.count() == 1 33 | } 34 | 35 | pub(crate) fn is_relative_normal_path(path: &Path) -> bool { 36 | if path.is_absolute() { 37 | return false; 38 | } 39 | 40 | if path.components().count() == 0 { 41 | return false; 42 | } 43 | 44 | if !path.components().all(|component| matches!(component, std::path::Component::Normal(_))) { 45 | return false; 46 | } 47 | 48 | true 49 | } 50 | 51 | pub(crate) fn check_sha1_hash(path: &Path, expected_hash: [u8; 20]) -> std::io::Result { 52 | let mut file = std::fs::File::open(path)?; 53 | let mut hasher = Sha1::new(); 54 | let _ = std::io::copy(&mut file, &mut hasher)?; 55 | 56 | let actual_hash = hasher.finalize(); 57 | 58 | Ok(expected_hash == *actual_hash) 59 | } 60 | 61 | pub(crate) fn write_safe(path: impl AsRef, content: impl AsRef<[u8]>) -> std::io::Result<()> { 62 | let path = path.as_ref(); 63 | let content = content.as_ref(); 64 | 65 | if let Some(parent) = path.parent() { 66 | let _ = std::fs::create_dir_all(parent); 67 | } 68 | 69 | let mut temp = path.to_path_buf(); 70 | temp.add_extension("new"); 71 | 72 | let mut temp_file = std::fs::File::create(&temp)?; 73 | 74 | temp_file.write_all(content)?; 75 | temp_file.flush()?; 76 | temp_file.sync_all()?; 77 | 78 | drop(temp_file); 79 | 80 | std::fs::rename(temp, path)?; 81 | 82 | Ok(()) 83 | } 84 | 85 | pub(crate) fn child_state_path(path: &Path) -> Option { 86 | let mut new_path = path.to_path_buf(); 87 | 88 | if let Some(extension) = new_path.extension() { 89 | if extension == "disabled" { 90 | new_path.set_extension(""); 91 | } 92 | } 93 | 94 | let Some(filename) = new_path.file_name() else { 95 | return None; 96 | }; 97 | 98 | let mut new_filename = OsString::new(); 99 | new_filename.push("."); 100 | new_filename.push(filename); 101 | new_filename.push(".pandorachildstate"); 102 | new_path.set_file_name(new_filename); 103 | 104 | Some(new_path) 105 | } 106 | 107 | pub(crate) fn create_content_library_path(content_library_dir: &Path, expected_hash: [u8; 20], extension: Option<&std::ffi::OsStr>) -> PathBuf { 108 | let hash_as_str = hex::encode(expected_hash); 109 | 110 | let hash_folder = content_library_dir.join(&hash_as_str[..2]); 111 | let mut path = hash_folder.join(hash_as_str); 112 | 113 | if let Some(extension) = extension { 114 | path.set_extension(extension); 115 | } 116 | 117 | path 118 | } 119 | -------------------------------------------------------------------------------- /crates/pandora_launcher/src/panic.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | 3 | use bridge::handle::FrontendHandle; 4 | 5 | pub fn install_hook(panic_message: Arc>>, frontend_handle: FrontendHandle) { 6 | let old_hook = std::panic::take_hook(); 7 | std::panic::set_hook(Box::new(move |info| { 8 | let thread = std::thread::current(); 9 | if thread.name() == Some("tokio-runtime-worker") { 10 | let payload = match info.payload().downcast_ref::<&'static str>() { 11 | Some(s) => *s, 12 | None => match info.payload().downcast_ref::() { 13 | Some(s) => &**s, 14 | None => "Box", 15 | }, 16 | }; 17 | 18 | let backtrace = backtrace::Backtrace::new(); 19 | 20 | let message = match info.location() { 21 | Some(location) => { 22 | format!( 23 | "Backend panicked at {}:{}:{}\n{}\n{:?}", 24 | location.file(), 25 | location.line(), 26 | location.column(), 27 | payload, 28 | PrettyBacktrace(backtrace) 29 | ) 30 | }, 31 | None => format!("Backend panicked\n{}\n{:?}", payload, PrettyBacktrace(backtrace)), 32 | }; 33 | 34 | eprintln!("{}", message); 35 | *panic_message.write().unwrap() = Some(message); 36 | frontend_handle.send(bridge::message::MessageToFrontend::Refresh); 37 | } else { 38 | (old_hook)(info); 39 | } 40 | })); 41 | } 42 | 43 | struct PrettyBacktrace(backtrace::Backtrace); 44 | 45 | impl std::fmt::Debug for PrettyBacktrace { 46 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | let cwd = std::env::current_dir(); 48 | let mut print_path = 49 | move |fmt: &mut std::fmt::Formatter<'_>, path: backtrace::BytesOrWideString<'_>| { 50 | let path = path.into_path_buf(); 51 | if let Ok(cwd) = &cwd && let Ok(suffix) = path.strip_prefix(cwd) { 52 | return std::fmt::Display::fmt(&suffix.display(), fmt); 53 | } 54 | std::fmt::Display::fmt(&path.display(), fmt) 55 | }; 56 | 57 | let mut f = backtrace::BacktraceFmt::new(fmt, backtrace::PrintFmt::Short, &mut print_path); 58 | f.add_context()?; 59 | let frames = self.0.frames(); 60 | let ignore_start = &[ 61 | "backtrace::backtrace::trace", 62 | "backtrace::capture::Backtrace::create", 63 | "backtrace::capture::Backtrace::new", 64 | "pandora_launcher::panic::install_hook::{{closure}}", 65 | "__rustc::rust_begin_unwind", 66 | ]; 67 | let mut start = 0; 68 | for (index, frame) in frames.iter().enumerate() { 69 | for symbol in frame.symbols() { 70 | if let Some(name) = symbol.name() { 71 | let name_str = format!("{name:#}"); 72 | if ignore_start.contains(&name_str.as_str()) { 73 | start = index; 74 | } 75 | } 76 | } 77 | } 78 | for frame in &frames[start..] { 79 | f.frame().backtrace_frame(frame)?; 80 | } 81 | f.finish()?; 82 | Ok(()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/frontend/src/modals/delete_instance.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::{AtomicBool, AtomicU8, Ordering}, Arc}; 2 | 3 | use bridge::{handle::BackendHandle, instance::InstanceID}; 4 | use gpui::{prelude::*, *}; 5 | use gpui_component::{ 6 | button::{Button, ButtonVariants}, dialog::DialogButtonProps, input::{Input, InputEvent, InputState}, notification::Notification, v_flex, Disableable, WindowExt 7 | }; 8 | 9 | pub fn open_delete_instance( 10 | instance: InstanceID, 11 | instance_name: SharedString, 12 | backend_handle: BackendHandle, 13 | window: &mut Window, 14 | cx: &mut App, 15 | ) { 16 | let stage = Arc::new(AtomicU8::new(0)); 17 | let correct_name = Arc::new(AtomicBool::new(false)); 18 | 19 | let title = SharedString::new(format!("Delete Instance: {}", instance_name)); 20 | let warning_message = SharedString::new(format!("This will permanently delete the '{}' instance and associated saves, resourcepacks, mods, configuration files, and more. These files will not be recoverable", instance_name)); 21 | let confirm_message = SharedString::new(format!("To confirm, type '{}' in the box below", instance_name)); 22 | 23 | let input_state = cx.new(|cx| InputState::new(window, cx)); 24 | 25 | let correct_name2 = correct_name.clone(); 26 | let instance_name2 = instance_name.clone(); 27 | let _input_subscription = cx.subscribe(&input_state, move |state, event: &InputEvent, cx| { 28 | if let InputEvent::Change = event { 29 | let value = state.read(cx).value(); 30 | correct_name2.store(value == instance_name2, Ordering::Relaxed); 31 | } 32 | }); 33 | 34 | window.open_dialog(cx, move |dialog, _, _| { 35 | let _ = &_input_subscription; 36 | 37 | let content = match stage.load(Ordering::Relaxed) { 38 | 0 => { 39 | v_flex() 40 | .child(Button::new("delete").label("I want to delete this instance").on_click({ 41 | let stage = stage.clone(); 42 | move |_, _, _| { 43 | stage.store(1, Ordering::Relaxed); 44 | } 45 | })) 46 | } 47 | 1 => { 48 | v_flex() 49 | .gap_2() 50 | .child(warning_message.clone()) 51 | .child(Button::new("confirm").label("I have read and understand these effects").on_click({ 52 | let stage = stage.clone(); 53 | move |_, _, _| { 54 | stage.store(2, Ordering::Relaxed); 55 | } 56 | })) 57 | } 58 | 2 => { 59 | let correct = correct_name.load(Ordering::Relaxed); 60 | v_flex() 61 | .gap_2() 62 | .child(confirm_message.clone()) 63 | .child(Input::new(&input_state).border_color(gpui::red())) 64 | .child(Button::new("confirm").label("Delete this instance").danger().disabled(!correct).on_click({ 65 | let backend_handle = backend_handle.clone(); 66 | move |_, window, cx| { 67 | backend_handle.send(bridge::message::MessageToBackend::DeleteInstance { 68 | id: instance 69 | }); 70 | window.close_all_dialogs(cx); 71 | } 72 | })) 73 | } 74 | _ => { 75 | unreachable!() 76 | } 77 | }; 78 | 79 | dialog 80 | .title(title.clone()) 81 | .child(content) 82 | }); 83 | 84 | } 85 | -------------------------------------------------------------------------------- /assets/icons/window-restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/frontend/src/component/search_helper.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use gpui::SharedString; 4 | 5 | pub struct SearchHelper { 6 | last_search: SharedString, 7 | items: Arc<[T]>, 8 | lower_keys: Vec, 9 | searched_starts_with: Vec, 10 | searched_contains: Vec, 11 | searched: bool, 12 | } 13 | 14 | impl SearchHelper { 15 | pub fn new(items: Arc<[T]>, key: impl Fn(&T) -> SharedString) -> Self { 16 | let lower_keys = items 17 | .iter() 18 | .map(key) 19 | .map(|key| { 20 | if key.chars().any(char::is_uppercase) { 21 | key.to_lowercase().into() 22 | } else { 23 | key.clone() 24 | } 25 | }) 26 | .collect(); 27 | Self { 28 | last_search: SharedString::default(), 29 | items, 30 | lower_keys, 31 | searched_starts_with: Vec::new(), 32 | searched_contains: Vec::new(), 33 | searched: false, 34 | } 35 | } 36 | 37 | pub fn get(&self, index: usize) -> Option<&T> { 38 | if self.searched { 39 | let item_idx = if index >= self.searched_starts_with.len() { 40 | self.searched_contains.get(index - self.searched_starts_with.len())? 41 | } else { 42 | self.searched_starts_with.get(index)? 43 | }; 44 | self.items.get(*item_idx) 45 | } else { 46 | self.items.get(index) 47 | } 48 | } 49 | 50 | pub fn iter(&self) -> Option> { 51 | if self.searched { 52 | Some( 53 | self.searched_starts_with 54 | .iter() 55 | .chain(self.searched_contains.iter()) 56 | .map(|i| self.items.get(*i).unwrap()), 57 | ) 58 | } else { 59 | None 60 | } 61 | } 62 | 63 | pub fn len(&self) -> usize { 64 | if self.searched { 65 | self.searched_starts_with.len() + self.searched_contains.len() 66 | } else { 67 | self.items.len() 68 | } 69 | } 70 | 71 | pub fn search(&mut self, query: &str) { 72 | if query.is_empty() { 73 | self.searched = false; 74 | self.last_search = SharedString::default(); 75 | return; 76 | } 77 | 78 | if self.searched && query == self.last_search.as_str() { 79 | return; 80 | } 81 | 82 | if self.searched && query.starts_with(self.last_search.as_str()) { 83 | self.searched_contains.retain(|i| self.lower_keys[*i].contains(query)); 84 | self.searched_starts_with.retain(|i| { 85 | let key = &self.lower_keys[*i]; 86 | if !key.starts_with(query) { 87 | if key.contains(query) 88 | && let Err(insert_at) = self.searched_contains.binary_search(i) 89 | { 90 | self.searched_contains.insert(insert_at, *i); 91 | } 92 | false 93 | } else { 94 | true 95 | } 96 | }); 97 | } else { 98 | self.searched_contains.clear(); 99 | self.searched_starts_with.clear(); 100 | for (index, key) in self.lower_keys.iter().enumerate() { 101 | if key.starts_with(query) { 102 | self.searched_starts_with.push(index); 103 | } else if key.contains(query) { 104 | self.searched_contains.push(index); 105 | } 106 | } 107 | } 108 | 109 | self.searched = true; 110 | self.last_search = SharedString::new(query); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/auth/src/models.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use oauth2::{CsrfToken, PkceCodeVerifier}; 5 | use serde::{Deserialize, Serialize}; 6 | use url::Url; 7 | use uuid::Uuid; 8 | 9 | pub struct MinecraftAccessToken(pub(crate) Arc); 10 | 11 | impl MinecraftAccessToken { 12 | pub fn secret(&self) -> &str { 13 | &self.0 14 | } 15 | } 16 | 17 | #[derive(Deserialize, Serialize)] 18 | pub struct TokenWithExpiry { 19 | pub token: Arc, 20 | pub expiry: DateTime, 21 | } 22 | 23 | #[derive(Deserialize, Serialize)] 24 | pub struct XstsToken { 25 | pub token: Arc, 26 | pub expiry: DateTime, 27 | pub userhash: Arc, 28 | } 29 | 30 | pub struct PendingAuthorization { 31 | pub url: Url, 32 | pub csrf_token: CsrfToken, 33 | pub pkce_verifier: PkceCodeVerifier, 34 | } 35 | 36 | pub struct FinishedAuthorization { 37 | pub pending: PendingAuthorization, 38 | pub code: String, 39 | } 40 | 41 | pub struct MsaTokens { 42 | pub access: TokenWithExpiry, 43 | pub refresh: Option>, 44 | } 45 | 46 | #[derive(Serialize)] 47 | #[serde(rename_all = "PascalCase")] 48 | pub struct XboxLiveAuthenticateRequest<'a> { 49 | pub properties: XboxLiveAuthenticateRequestProperties<'a>, 50 | pub relying_party: &'a str, 51 | pub token_type: &'a str, 52 | } 53 | 54 | #[derive(Serialize)] 55 | #[serde(rename_all = "PascalCase")] 56 | pub struct XboxLiveAuthenticateRequestProperties<'a> { 57 | pub auth_method: &'a str, 58 | pub site_name: &'a str, 59 | pub rps_ticket: &'a str, 60 | } 61 | 62 | #[derive(Deserialize)] 63 | #[serde(rename_all = "PascalCase")] 64 | pub struct XboxLiveAuthenticateResponse { 65 | pub issue_instant: DateTime, 66 | pub not_after: DateTime, 67 | pub token: Arc, 68 | } 69 | 70 | #[derive(Serialize)] 71 | #[serde(rename_all = "PascalCase")] 72 | pub struct XboxLiveSecurityTokenRequest<'a> { 73 | pub properties: XboxLiveSecurityTokenRequestProperties<'a>, 74 | pub relying_party: &'a str, 75 | pub token_type: &'a str, 76 | } 77 | 78 | #[derive(Serialize)] 79 | #[serde(rename_all = "PascalCase")] 80 | pub struct XboxLiveSecurityTokenRequestProperties<'a> { 81 | pub sandbox_id: &'a str, 82 | pub user_tokens: &'a [&'a str], 83 | } 84 | 85 | #[derive(Deserialize)] 86 | #[serde(rename_all = "PascalCase")] 87 | pub struct XboxLiveSecurityTokenResponse { 88 | pub issue_instant: DateTime, 89 | pub not_after: DateTime, 90 | pub token: Arc, 91 | pub display_claims: XboxUserIdentityDisplayClaims, 92 | } 93 | 94 | #[derive(Deserialize)] 95 | pub struct XboxUserIdentityDisplayClaims { 96 | pub xui: Vec>, 97 | } 98 | 99 | #[derive(Serialize)] 100 | #[serde(rename_all = "camelCase")] 101 | pub struct MinecraftLoginWithXboxRequest<'a> { 102 | pub identity_token: &'a str, 103 | } 104 | 105 | #[derive(Deserialize)] 106 | #[serde(rename_all = "snake_case")] 107 | pub struct MinecraftLoginWithXboxResponse { 108 | pub username: Arc, 109 | pub access_token: Arc, 110 | pub expires_in: usize, 111 | } 112 | 113 | #[derive(Deserialize)] 114 | pub struct MinecraftProfileResponse { 115 | pub id: Uuid, 116 | pub name: Arc, 117 | pub skins: Vec, 118 | } 119 | 120 | #[derive(Clone, Deserialize)] 121 | pub struct MinecraftProfileSkin { 122 | pub url: Arc, 123 | pub state: SkinState, 124 | pub variant: SkinVariant, 125 | } 126 | 127 | #[derive(Clone, Copy, Eq, PartialEq, Deserialize)] 128 | #[serde(rename_all = "UPPERCASE")] 129 | pub enum SkinState { 130 | Active, 131 | #[serde(other)] 132 | Inactive, 133 | } 134 | 135 | #[derive(Clone, Copy, Eq, PartialEq, Deserialize)] 136 | #[serde(rename_all = "UPPERCASE")] 137 | pub enum SkinVariant { 138 | Classic, 139 | Slim, 140 | #[serde(other)] 141 | Other, 142 | } 143 | -------------------------------------------------------------------------------- /assets/icons/pandora.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 52 | 53 | 55 | 59 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /assets/OFL.txt: -------------------------------------------------------------------------------- 1 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 2 | This license is copied below, and is also available with a FAQ at: 3 | https://openfontlicense.org 4 | 5 | 6 | ----------------------------------------------------------- 7 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 8 | ----------------------------------------------------------- 9 | 10 | PREAMBLE 11 | The goals of the Open Font License (OFL) are to stimulate worldwide 12 | development of collaborative font projects, to support the font creation 13 | efforts of academic and linguistic communities, and to provide a free and 14 | open framework in which fonts may be shared and improved in partnership 15 | with others. 16 | 17 | The OFL allows the licensed fonts to be used, studied, modified and 18 | redistributed freely as long as they are not sold by themselves. The 19 | fonts, including any derivative works, can be bundled, embedded, 20 | redistributed and/or sold with any software provided that any reserved 21 | names are not used by derivative works. The fonts and derivatives, 22 | however, cannot be released under any other type of license. The 23 | requirement for fonts to remain under this license does not apply 24 | to any document created using the fonts or their derivatives. 25 | 26 | DEFINITIONS 27 | "Font Software" refers to the set of files released by the Copyright 28 | Holder(s) under this license and clearly marked as such. This may 29 | include source files, build scripts and documentation. 30 | 31 | "Reserved Font Name" refers to any names specified as such after the 32 | copyright statement(s). 33 | 34 | "Original Version" refers to the collection of Font Software components as 35 | distributed by the Copyright Holder(s). 36 | 37 | "Modified Version" refers to any derivative made by adding to, deleting, 38 | or substituting -- in part or in whole -- any of the components of the 39 | Original Version, by changing formats or by porting the Font Software to a 40 | new environment. 41 | 42 | "Author" refers to any designer, engineer, programmer, technical 43 | writer or other person who contributed to the Font Software. 44 | 45 | PERMISSION & CONDITIONS 46 | Permission is hereby granted, free of charge, to any person obtaining 47 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 48 | redistribute, and sell modified and unmodified copies of the Font 49 | Software, subject to the following conditions: 50 | 51 | 1) Neither the Font Software nor any of its individual components, 52 | in Original or Modified Versions, may be sold by itself. 53 | 54 | 2) Original or Modified Versions of the Font Software may be bundled, 55 | redistributed and/or sold with any software, provided that each copy 56 | contains the above copyright notice and this license. These can be 57 | included either as stand-alone text files, human-readable headers or 58 | in the appropriate machine-readable metadata fields within text or 59 | binary files as long as those fields can be easily viewed by the user. 60 | 61 | 3) No Modified Version of the Font Software may use the Reserved Font 62 | Name(s) unless explicit written permission is granted by the corresponding 63 | Copyright Holder. This restriction only applies to the primary font name as 64 | presented to the users. 65 | 66 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 67 | Software shall not be used to promote, endorse or advertise any 68 | Modified Version, except to acknowledge the contribution(s) of the 69 | Copyright Holder(s) and the Author(s) or with their explicit written 70 | permission. 71 | 72 | 5) The Font Software, modified or unmodified, in part or in whole, 73 | must be distributed entirely under this license, and must not be 74 | distributed under any other license. The requirement for fonts to 75 | remain under this license does not apply to any document created 76 | using the Font Software. 77 | 78 | TERMINATION 79 | This license becomes null and void if any of the above conditions are 80 | not met. 81 | 82 | DISCLAIMER 83 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 84 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 85 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 86 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 87 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 88 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 89 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 90 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 91 | OTHER DEALINGS IN THE FONT SOFTWARE. 92 | -------------------------------------------------------------------------------- /assets/fonts/inter/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION AND CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | 94 | -------------------------------------------------------------------------------- /assets/fonts/roboto-mono/OFL.txt: -------------------------------------------------------------------------------- 1 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 2 | This license is copied below, and is also available with a FAQ at: 3 | https://openfontlicense.org 4 | 5 | 6 | ----------------------------------------------------------- 7 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 8 | ----------------------------------------------------------- 9 | 10 | PREAMBLE 11 | The goals of the Open Font License (OFL) are to stimulate worldwide 12 | development of collaborative font projects, to support the font creation 13 | efforts of academic and linguistic communities, and to provide a free and 14 | open framework in which fonts may be shared and improved in partnership 15 | with others. 16 | 17 | The OFL allows the licensed fonts to be used, studied, modified and 18 | redistributed freely as long as they are not sold by themselves. The 19 | fonts, including any derivative works, can be bundled, embedded, 20 | redistributed and/or sold with any software provided that any reserved 21 | names are not used by derivative works. The fonts and derivatives, 22 | however, cannot be released under any other type of license. The 23 | requirement for fonts to remain under this license does not apply 24 | to any document created using the fonts or their derivatives. 25 | 26 | DEFINITIONS 27 | "Font Software" refers to the set of files released by the Copyright 28 | Holder(s) under this license and clearly marked as such. This may 29 | include source files, build scripts and documentation. 30 | 31 | "Reserved Font Name" refers to any names specified as such after the 32 | copyright statement(s). 33 | 34 | "Original Version" refers to the collection of Font Software components as 35 | distributed by the Copyright Holder(s). 36 | 37 | "Modified Version" refers to any derivative made by adding to, deleting, 38 | or substituting -- in part or in whole -- any of the components of the 39 | Original Version, by changing formats or by porting the Font Software to a 40 | new environment. 41 | 42 | "Author" refers to any designer, engineer, programmer, technical 43 | writer or other person who contributed to the Font Software. 44 | 45 | PERMISSION & CONDITIONS 46 | Permission is hereby granted, free of charge, to any person obtaining 47 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 48 | redistribute, and sell modified and unmodified copies of the Font 49 | Software, subject to the following conditions: 50 | 51 | 1) Neither the Font Software nor any of its individual components, 52 | in Original or Modified Versions, may be sold by itself. 53 | 54 | 2) Original or Modified Versions of the Font Software may be bundled, 55 | redistributed and/or sold with any software, provided that each copy 56 | contains the above copyright notice and this license. These can be 57 | included either as stand-alone text files, human-readable headers or 58 | in the appropriate machine-readable metadata fields within text or 59 | binary files as long as those fields can be easily viewed by the user. 60 | 61 | 3) No Modified Version of the Font Software may use the Reserved Font 62 | Name(s) unless explicit written permission is granted by the corresponding 63 | Copyright Holder. This restriction only applies to the primary font name as 64 | presented to the users. 65 | 66 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 67 | Software shall not be used to promote, endorse or advertise any 68 | Modified Version, except to acknowledge the contribution(s) of the 69 | Copyright Holder(s) and the Author(s) or with their explicit written 70 | permission. 71 | 72 | 5) The Font Software, modified or unmodified, in part or in whole, 73 | must be distributed entirely under this license, and must not be 74 | distributed under any other license. The requirement for fonts to 75 | remain under this license does not apply to any document created 76 | using the Font Software. 77 | 78 | TERMINATION 79 | This license becomes null and void if any of the above conditions are 80 | not met. 81 | 82 | DISCLAIMER 83 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 84 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 85 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 86 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 87 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 88 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 89 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 90 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 91 | OTHER DEALINGS IN THE FONT SOFTWARE. 92 | -------------------------------------------------------------------------------- /crates/nbt/src/encode.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use bytes::BufMut; 4 | 5 | pub fn write_named(nbt: &NBT) -> Vec { 6 | let mut vec = Vec::new(); 7 | write_named_into(nbt, &mut vec); 8 | vec 9 | } 10 | 11 | pub fn write_named_into(nbt: &NBT, vec: &mut Vec) { 12 | write_node(vec, &nbt.nodes, Some(&nbt.root_name), &nbt.nodes[nbt.root_index]); 13 | } 14 | 15 | pub fn write_protocol(nbt: &NBT) -> Vec { 16 | let mut vec = Vec::new(); 17 | write_protocol_into(nbt, &mut vec); 18 | vec 19 | } 20 | 21 | pub fn write_protocol_into(nbt: &NBT, vec: &mut Vec) { 22 | vec.push(TAG_COMPOUND_ID.0); 23 | write_node(vec, &nbt.nodes, None, &nbt.nodes[nbt.root_index]); 24 | } 25 | 26 | fn write_node(vec: &mut Vec, nodes: &Slab, name: Option<&str>, node: &NBTNode) { 27 | match node { 28 | NBTNode::Byte(value) => { 29 | if let Some(name) = name { 30 | vec.push(TAG_BYTE_ID.0); 31 | write_string(vec, name); 32 | } 33 | vec.put_i8(*value); 34 | }, 35 | NBTNode::Short(value) => { 36 | if let Some(name) = name { 37 | vec.push(TAG_SHORT_ID.0); 38 | write_string(vec, name); 39 | } 40 | vec.put_i16(*value); 41 | }, 42 | NBTNode::Int(value) => { 43 | if let Some(name) = name { 44 | vec.push(TAG_INT_ID.0); 45 | write_string(vec, name); 46 | } 47 | vec.put_i32(*value); 48 | }, 49 | NBTNode::Long(value) => { 50 | if let Some(name) = name { 51 | vec.push(TAG_LONG_ID.0); 52 | write_string(vec, name); 53 | } 54 | vec.put_i64(*value); 55 | }, 56 | NBTNode::Float(value) => { 57 | if let Some(name) = name { 58 | vec.push(TAG_FLOAT_ID.0); 59 | write_string(vec, name); 60 | } 61 | vec.put_f32(*value); 62 | }, 63 | NBTNode::Double(value) => { 64 | if let Some(name) = name { 65 | vec.push(TAG_DOUBLE_ID.0); 66 | write_string(vec, name); 67 | } 68 | vec.put_f64(*value); 69 | }, 70 | NBTNode::ByteArray(values) => { 71 | if let Some(name) = name { 72 | vec.push(TAG_BYTE_ARRAY_ID.0); 73 | write_string(vec, name); 74 | } 75 | vec.put_i32(values.len() as _); 76 | vec.extend_from_slice(unsafe { std::mem::transmute(values.as_slice()) }); 77 | }, 78 | NBTNode::String(value) => { 79 | if let Some(name) = name { 80 | vec.push(TAG_STRING_ID.0); 81 | write_string(vec, name); 82 | } 83 | write_string(vec, value); 84 | }, 85 | NBTNode::List { type_id, children } => { 86 | if let Some(name) = name { 87 | vec.push(TAG_LIST_ID.0); 88 | write_string(vec, name); 89 | } 90 | vec.push(type_id.0); 91 | vec.put_i32(children.len() as _); 92 | for child in children { 93 | let child = &nodes[*child]; 94 | write_node(vec, nodes, None, child); 95 | } 96 | }, 97 | NBTNode::Compound(value) => { 98 | if let Some(name) = name { 99 | vec.push(TAG_COMPOUND_ID.0); 100 | write_string(vec, name); 101 | } 102 | write_compound(vec, nodes, value); 103 | }, 104 | NBTNode::IntArray(values) => { 105 | if let Some(name) = name { 106 | vec.push(TAG_INT_ARRAY_ID.0); 107 | write_string(vec, name); 108 | } 109 | vec.put_i32(values.len() as _); 110 | for value in values { 111 | vec.put_i32(*value); 112 | } 113 | }, 114 | NBTNode::LongArray(values) => { 115 | if let Some(name) = name { 116 | vec.push(TAG_LONG_ARRAY_ID.0); 117 | write_string(vec, name); 118 | } 119 | vec.put_i32(values.len() as _); 120 | for value in values { 121 | vec.put_i64(*value); 122 | } 123 | }, 124 | } 125 | } 126 | 127 | fn write_compound(vec: &mut Vec, nodes: &Slab, children: &NBTCompound) { 128 | for (child_name, child_idx) in &children.0 { 129 | let child = &nodes[*child_idx]; 130 | write_node(vec, nodes, Some(child_name), child); 131 | } 132 | 133 | vec.push(TAG_END_ID.0); 134 | } 135 | 136 | fn write_string(vec: &mut Vec, value: &str) { 137 | vec.put_u16(value.len() as _); 138 | vec.extend_from_slice(value.as_bytes()); 139 | } 140 | -------------------------------------------------------------------------------- /crates/frontend/src/entity/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use bridge::{handle::BackendHandle, keep_alive::KeepAliveHandle, message::MessageToBackend, meta::{MetadataRequest, MetadataResult}}; 4 | use gpui::{prelude::*, *}; 5 | use schema::{modrinth::{ModrinthProjectVersionsResult, ModrinthSearchResult}, version_manifest::MinecraftVersionManifest}; 6 | 7 | #[derive(Debug)] 8 | pub enum FrontendMetadataState { 9 | Loading, 10 | Loaded { 11 | result: Result>, 12 | keep_alive: Option, 13 | }, 14 | } 15 | 16 | pub enum FrontendMetadataResult<'a, T> { 17 | Loading, 18 | Loaded(&'a T), 19 | Error(SharedString), 20 | } 21 | 22 | pub struct FrontendMetadata { 23 | pub data: HashMap>, 24 | pub backend_handle: BackendHandle, 25 | } 26 | 27 | impl FrontendMetadata { 28 | pub fn new(backend_handle: BackendHandle) -> Self { 29 | Self { 30 | data: HashMap::new(), 31 | backend_handle, 32 | } 33 | } 34 | 35 | pub fn force_reload(entity: &Entity, request: MetadataRequest, cx: &mut App) -> Entity { 36 | entity.update(cx, |this, cx| { 37 | if let Some(existing) = this.data.get(&request) { 38 | this.backend_handle.send(MessageToBackend::RequestMetadata { 39 | request: request.clone(), 40 | force_reload: true, 41 | }); 42 | return existing.clone(); 43 | } 44 | 45 | let loading = cx.new(|_| FrontendMetadataState::Loading); 46 | this.backend_handle.send(MessageToBackend::RequestMetadata { 47 | request: request.clone(), 48 | force_reload: true, 49 | }); 50 | this.data.insert(request, loading.clone()); 51 | loading 52 | }) 53 | } 54 | 55 | pub fn request(entity: &Entity, request: MetadataRequest, cx: &mut App) -> Entity { 56 | entity.update(cx, |this, cx| { 57 | if let Some(existing) = this.data.get(&request) { 58 | if let FrontendMetadataState::Loaded { keep_alive, .. } = existing.read(cx) { 59 | if !keep_alive.as_ref().map(|k| k.is_alive()).unwrap_or(true) { 60 | this.backend_handle.send(MessageToBackend::RequestMetadata { 61 | request: request.clone(), 62 | force_reload: false, 63 | }); 64 | } 65 | } 66 | return existing.clone(); 67 | } 68 | 69 | let loading = cx.new(|_| FrontendMetadataState::Loading); 70 | this.backend_handle.send(MessageToBackend::RequestMetadata { 71 | request: request.clone(), 72 | force_reload: false, 73 | }); 74 | this.data.insert(request, loading.clone()); 75 | loading 76 | }) 77 | } 78 | 79 | pub fn set( 80 | entity: &Entity, 81 | request: MetadataRequest, 82 | result: Result>, 83 | keep_alive: Option, 84 | cx: &mut C, 85 | ) { 86 | entity.update(cx, |this, cx| { 87 | this.data.get(&request).unwrap().update(cx, |value, cx| { 88 | *value = FrontendMetadataState::Loaded { result, keep_alive }; 89 | cx.notify(); 90 | }); 91 | }); 92 | } 93 | } 94 | 95 | pub trait AsMetadataResult { 96 | fn result(&self) -> FrontendMetadataResult<'_, T>; 97 | } 98 | 99 | macro_rules! define_as_metadata_result { 100 | ($t:ident) => { 101 | impl AsMetadataResult<$t> for FrontendMetadataState { 102 | fn result(&self) -> FrontendMetadataResult<'_, $t> { 103 | match self { 104 | FrontendMetadataState::Loading => FrontendMetadataResult::Loading, 105 | FrontendMetadataState::Loaded { result, .. } => { 106 | match result { 107 | Ok(MetadataResult::$t(result)) => FrontendMetadataResult::Loaded(&*result), 108 | Ok(_) => FrontendMetadataResult::Error(SharedString::new_static("Wrong metadata type! Pandora bug!")), 109 | Err(error) => FrontendMetadataResult::Error(SharedString::new(error.clone())), 110 | } 111 | }, 112 | } 113 | } 114 | } 115 | }; 116 | } 117 | 118 | define_as_metadata_result!(MinecraftVersionManifest); 119 | define_as_metadata_result!(ModrinthSearchResult); 120 | define_as_metadata_result!(ModrinthProjectVersionsResult); 121 | -------------------------------------------------------------------------------- /crates/frontend/src/png_render_cache.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | rc::Rc, 3 | sync::{Arc, Mutex, atomic::Ordering}, 4 | time::{Duration, Instant}, 5 | }; 6 | 7 | use atomic_time::AtomicInstant; 8 | use gpui::{App, RenderImage}; 9 | use image::Frame; 10 | use intrusive_collections::{LinkedList, LinkedListLink, intrusive_adapter}; 11 | use rustc_hash::FxHashMap; 12 | 13 | struct CacheEntry { 14 | link: LinkedListLink, 15 | ptrs: Mutex>, 16 | source: Arc<[u8]>, 17 | expiring: AtomicInstant, 18 | value: Option>, 19 | } 20 | 21 | intrusive_adapter!(CacheEntryAdapter = Rc: CacheEntry { link: LinkedListLink }); 22 | 23 | #[derive(Default)] 24 | struct PngRenderCache { 25 | by_ptr: FxHashMap>, 26 | by_arc: FxHashMap, Rc>, 27 | expiring: LinkedList, 28 | submitted_cleanup: bool, 29 | } 30 | 31 | impl gpui::Global for PngRenderCache {} 32 | 33 | const EXPIRY_SECONDS: u64 = 1; 34 | 35 | pub fn render(image: Arc<[u8]>, cx: &mut App) -> gpui::Img { 36 | let cache = cx.default_global::(); 37 | 38 | let result = if let Some(result) = cache.get_or_create(image) { 39 | gpui::img(result) 40 | } else { 41 | gpui::img(gpui::ImageSource::Resource(gpui::Resource::Embedded("images/missing.png".into()))) 42 | }; 43 | 44 | if !cache.submitted_cleanup { 45 | cache.submitted_cleanup = true; 46 | cx.spawn(async |cx| { 47 | let _ = cx.update_global(|cache: &mut PngRenderCache, cx| { 48 | let now = Instant::now(); 49 | let mut cursor = cache.expiring.front_mut(); 50 | while let Some(entry) = cursor.get() { 51 | if now > entry.expiring.load(Ordering::Relaxed) { 52 | let entry = cursor.remove().expect("present"); 53 | for ptr in entry.ptrs.lock().unwrap().iter() { 54 | cache.by_ptr.remove(ptr).expect("present"); 55 | } 56 | cache.by_arc.remove(&entry.source).expect("present"); 57 | 58 | debug_assert_eq!(Rc::strong_count(&entry), 1); 59 | 60 | if let Some(image) = &entry.value { 61 | cx.drop_image(image.clone(), None); 62 | } 63 | } else { 64 | break; 65 | } 66 | } 67 | cache.submitted_cleanup = false; 68 | }); 69 | }).detach(); 70 | } 71 | 72 | result 73 | } 74 | 75 | impl PngRenderCache { 76 | fn get_or_create(&mut self, image: Arc<[u8]>) -> Option> { 77 | let ptr = Arc::as_ptr(&image).addr(); 78 | 79 | if let Some(result) = self.by_ptr.get(&ptr) { 80 | // Update expiry 81 | result.expiring.store(Instant::now() + Duration::from_secs(EXPIRY_SECONDS), Ordering::Relaxed); 82 | unsafe { 83 | self.expiring.cursor_mut_from_ptr(Rc::as_ptr(result)).remove(); 84 | } 85 | self.expiring.push_back(result.clone()); 86 | 87 | return result.value.clone(); 88 | } 89 | 90 | if let Some(result) = self.by_arc.get(&image) { 91 | // Update expiry 92 | result.expiring.store(Instant::now() + Duration::from_secs(EXPIRY_SECONDS), Ordering::Relaxed); 93 | unsafe { 94 | self.expiring.cursor_mut_from_ptr(Rc::as_ptr(result)).remove(); 95 | } 96 | self.expiring.push_back(result.clone()); 97 | 98 | // Add ptr 99 | result.ptrs.lock().unwrap().push(ptr); 100 | self.by_ptr.insert(ptr, result.clone()); 101 | 102 | return result.value.clone(); 103 | } 104 | 105 | let result = image::load_from_memory_with_format(&image, image::ImageFormat::Png).map(|data| { 106 | let mut data = data.into_rgba8(); 107 | 108 | // Convert from RGBA to BGRA. 109 | for pixel in data.chunks_exact_mut(4) { 110 | pixel.swap(0, 2); 111 | } 112 | 113 | RenderImage::new([Frame::new(data)]) 114 | }); 115 | 116 | let render_image = match result { 117 | Ok(render_image) => Some(Arc::new(render_image)), 118 | Err(error) => { 119 | eprintln!("Error loading png: {error:?}"); 120 | None 121 | }, 122 | }; 123 | 124 | let entry = Rc::new(CacheEntry { 125 | link: LinkedListLink::new(), 126 | ptrs: Mutex::new(vec![ptr]), 127 | source: Arc::clone(&image), 128 | expiring: AtomicInstant::new(Instant::now() + Duration::from_secs(EXPIRY_SECONDS)), 129 | value: render_image.clone(), 130 | }); 131 | self.by_ptr.insert(ptr, entry.clone()); 132 | self.by_arc.insert(Arc::clone(&image), entry.clone()); 133 | self.expiring.push_back(entry); 134 | 135 | render_image 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /crates/bridge/src/message.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, path::{Path, PathBuf}, sync::Arc}; 2 | 3 | use schema::loader::Loader; 4 | use ustr::Ustr; 5 | use uuid::Uuid; 6 | 7 | use crate::{ 8 | account::Account, game_output::GameOutputLogLevel, install::ContentInstall, instance::{ 9 | InstanceID, InstanceModID, InstanceModSummary, InstanceServerSummary, InstanceStatus, InstanceWorldSummary, 10 | }, keep_alive::{KeepAlive, KeepAliveHandle}, meta::{MetadataRequest, MetadataResult}, modal_action::ModalAction, serial::Serial 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub enum MessageToBackend { 15 | RequestMetadata { 16 | request: MetadataRequest, 17 | force_reload: bool, 18 | }, 19 | CreateInstance { 20 | name: Ustr, 21 | version: Ustr, 22 | loader: Loader, 23 | }, 24 | DeleteInstance { 25 | id: InstanceID, 26 | }, 27 | RenameInstance { 28 | id: InstanceID, 29 | name: Ustr, 30 | }, 31 | KillInstance { 32 | id: InstanceID, 33 | }, 34 | StartInstance { 35 | id: InstanceID, 36 | quick_play: Option, 37 | modal_action: ModalAction, 38 | }, 39 | RequestLoadWorlds { 40 | id: InstanceID, 41 | }, 42 | RequestLoadServers { 43 | id: InstanceID, 44 | }, 45 | RequestLoadMods { 46 | id: InstanceID, 47 | }, 48 | SetModEnabled { 49 | id: InstanceID, 50 | mod_id: InstanceModID, 51 | enabled: bool, 52 | }, 53 | SetModChildEnabled { 54 | id: InstanceID, 55 | mod_id: InstanceModID, 56 | path: Arc, 57 | enabled: bool, 58 | }, 59 | DeleteMod { 60 | id: InstanceID, 61 | mod_id: InstanceModID, 62 | }, 63 | UpdateAccountHeadPng { 64 | uuid: Uuid, 65 | head_png: Arc<[u8]>, 66 | head_png_32x: Arc<[u8]>, 67 | }, 68 | InstallContent { 69 | content: ContentInstall, 70 | modal_action: ModalAction, 71 | }, 72 | DownloadAllMetadata, 73 | UpdateCheck { instance: InstanceID, modal_action: ModalAction }, 74 | UpdateMod { 75 | instance: InstanceID, 76 | mod_id: InstanceModID, 77 | modal_action: ModalAction, 78 | }, 79 | Sleep5s, 80 | ReadLog { 81 | path: Arc, 82 | send: tokio::sync::mpsc::Sender> 83 | }, 84 | GetLogFiles { 85 | instance: InstanceID, 86 | channel: tokio::sync::oneshot::Sender, 87 | }, 88 | CleanupOldLogFiles { 89 | instance: InstanceID, 90 | }, 91 | UploadLogFile { 92 | path: Arc, 93 | modal_action: ModalAction, 94 | } 95 | } 96 | 97 | #[derive(Debug)] 98 | pub enum MessageToFrontend { 99 | InstanceAdded { 100 | id: InstanceID, 101 | name: Ustr, 102 | version: Ustr, 103 | loader: Loader, 104 | worlds_state: Arc, 105 | servers_state: Arc, 106 | mods_state: Arc, 107 | }, 108 | InstanceRemoved { 109 | id: InstanceID, 110 | }, 111 | InstanceModified { 112 | id: InstanceID, 113 | name: Ustr, 114 | version: Ustr, 115 | loader: Loader, 116 | status: InstanceStatus, 117 | }, 118 | InstanceWorldsUpdated { 119 | id: InstanceID, 120 | worlds: Arc<[InstanceWorldSummary]>, 121 | }, 122 | InstanceServersUpdated { 123 | id: InstanceID, 124 | servers: Arc<[InstanceServerSummary]>, 125 | }, 126 | InstanceModsUpdated { 127 | id: InstanceID, 128 | mods: Arc<[InstanceModSummary]>, 129 | }, 130 | CreateGameOutputWindow { 131 | id: usize, 132 | keep_alive: KeepAlive, 133 | }, 134 | AddGameOutput { 135 | id: usize, 136 | time: i64, 137 | thread: Arc, 138 | level: GameOutputLogLevel, 139 | text: Arc<[Arc]>, 140 | }, 141 | AddNotification { 142 | notification_type: BridgeNotificationType, 143 | message: Arc, 144 | }, 145 | AccountsUpdated { 146 | accounts: Arc<[Account]>, 147 | selected_account: Option, 148 | }, 149 | Refresh, 150 | CloseModal, 151 | MoveInstanceToTop { 152 | id: InstanceID, 153 | }, 154 | MetadataResult { 155 | request: MetadataRequest, 156 | result: Result>, 157 | keep_alive_handle: Option, 158 | }, 159 | } 160 | 161 | #[derive(Debug)] 162 | pub struct LogFiles { 163 | pub paths: Vec>, 164 | pub total_gzipped_size: usize, 165 | } 166 | 167 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 168 | pub enum BridgeNotificationType { 169 | Success, 170 | Info, 171 | Error, 172 | Warning, 173 | } 174 | 175 | #[atomic_enum::atomic_enum] 176 | #[derive(PartialEq, Eq)] 177 | pub enum BridgeDataLoadState { 178 | Unloaded, 179 | LoadingDirty, 180 | LoadedDirty, 181 | Loading, 182 | Loaded, 183 | } 184 | 185 | impl BridgeDataLoadState { 186 | pub fn should_send_load_request(self) -> bool { 187 | match self { 188 | BridgeDataLoadState::Unloaded => true, 189 | BridgeDataLoadState::LoadingDirty => false, 190 | BridgeDataLoadState::LoadedDirty => true, 191 | BridgeDataLoadState::Loading => false, 192 | BridgeDataLoadState::Loaded => false, 193 | } 194 | } 195 | } 196 | 197 | #[derive(Debug, Clone, PartialEq, Eq)] 198 | pub enum QuickPlayLaunch { 199 | Singleplayer(OsString), 200 | Multiplayer(OsString), 201 | Realms(OsString), 202 | } 203 | -------------------------------------------------------------------------------- /crates/nbt/src/stringified/to_snbt.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::fmt::Write; 3 | 4 | pub fn to_snbt_string(nbt: &NBT) -> String { 5 | let mut snbt = String::new(); 6 | to_snbt(&mut snbt, nbt).expect("string writing is infallible"); 7 | snbt 8 | } 9 | 10 | pub fn to_snbt(writer: &mut T, nbt: &NBT) -> std::fmt::Result { 11 | write_node(writer, &nbt.nodes, &nbt.nodes[nbt.root_index]) 12 | } 13 | 14 | fn write_node(writer: &mut T, nodes: &Slab, node: &NBTNode) -> std::fmt::Result { 15 | match node { 16 | NBTNode::Byte(value) => write_byte(writer, *value), 17 | NBTNode::Short(value) => write_short(writer, *value), 18 | NBTNode::Int(value) => write_int(writer, *value), 19 | NBTNode::Long(value) => write_long(writer, *value), 20 | NBTNode::Float(value) => write_float(writer, *value), 21 | NBTNode::Double(value) => write_double(writer, *value), 22 | NBTNode::ByteArray(values) => write_byte_array(writer, values), 23 | NBTNode::String(value) => write_string(writer, value), 24 | NBTNode::List { type_id: _, children } => write_list(writer, children, nodes), 25 | NBTNode::Compound(value) => write_compound(writer, nodes, value), 26 | NBTNode::IntArray(values) => write_int_array(writer, values), 27 | NBTNode::LongArray(values) => write_long_array(writer, values), 28 | } 29 | } 30 | 31 | fn write_compound(writer: &mut T, nodes: &Slab, children: &NBTCompound) -> std::fmt::Result { 32 | writer.write_char('{')?; 33 | 34 | let mut first = true; 35 | 36 | for (child_name, child_idx) in &children.0 { 37 | if first { 38 | first = false; 39 | } else { 40 | writer.write_str(", ")?; 41 | } 42 | 43 | write_key(writer, child_name)?; 44 | writer.write_str(": ")?; 45 | 46 | let child = &nodes[*child_idx]; 47 | write_node(writer, nodes, child)?; 48 | } 49 | 50 | writer.write_char('}')?; 51 | Ok(()) 52 | } 53 | 54 | fn write_key(writer: &mut T, value: &str) -> std::fmt::Result { 55 | // String must match `[A-Za-z0-9._+-]+` to be unquoted 56 | for c in value.chars() { 57 | if matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '+' | '-') { 58 | // Contains invalid character, write a quoted string instead 59 | return write_string(writer, value); 60 | } 61 | } 62 | 63 | // All good to go - write the unquoted string 64 | writer.write_str(value) 65 | } 66 | 67 | fn write_string(writer: &mut T, value: &str) -> std::fmt::Result { 68 | writer.write_char('"')?; 69 | 70 | for c in value.chars() { 71 | // Escape backslashes and quotes 72 | if c == '\\' || c == '"' { 73 | writer.write_char('\\')?; 74 | } 75 | // Push the char 76 | writer.write_char(c)?; 77 | } 78 | 79 | writer.write_char('"') 80 | } 81 | 82 | // Note: doing write!("{}") and then push('b') 83 | // is about 25% faster than doing write!("{}b") 84 | 85 | fn write_byte(writer: &mut T, value: i8) -> std::fmt::Result { 86 | write!(writer, "{}", value)?; 87 | writer.write_char('b') 88 | } 89 | 90 | fn write_short(writer: &mut T, value: i16) -> std::fmt::Result { 91 | write!(writer, "{}", value)?; 92 | writer.write_char('s') 93 | } 94 | 95 | fn write_int(writer: &mut T, value: i32) -> std::fmt::Result { 96 | write!(writer, "{}", value) 97 | } 98 | 99 | fn write_long(writer: &mut T, value: i64) -> std::fmt::Result { 100 | write!(writer, "{}", value)?; 101 | writer.write_char('L') 102 | } 103 | 104 | fn write_float(writer: &mut T, value: f32) -> std::fmt::Result { 105 | write!(writer, "{}", value)?; 106 | writer.write_char('f') 107 | } 108 | 109 | fn write_double(writer: &mut T, value: f64) -> std::fmt::Result { 110 | write!(writer, "{}", value)?; 111 | writer.write_char('d') 112 | } 113 | 114 | fn write_byte_array(writer: &mut T, values: &Vec) -> std::fmt::Result { 115 | writer.write_str("[B;")?; 116 | let mut first = true; 117 | for byte in values { 118 | if first { 119 | first = false; 120 | } else { 121 | writer.write_char(',')?; 122 | } 123 | write_byte(writer, *byte)?; 124 | } 125 | writer.write_char(']') 126 | } 127 | 128 | fn write_list(writer: &mut T, children: &Vec, nodes: &Slab) -> std::fmt::Result { 129 | writer.write_str("[")?; 130 | let mut first = true; 131 | for child in children { 132 | if first { 133 | first = false; 134 | } else { 135 | writer.write_str(", ")?; 136 | } 137 | 138 | let child = &nodes[*child]; 139 | write_node(writer, nodes, child)?; 140 | } 141 | writer.write_char(']') 142 | } 143 | 144 | fn write_int_array(writer: &mut T, values: &Vec) -> std::fmt::Result { 145 | writer.write_str("[I;")?; 146 | let mut first = true; 147 | for int in values { 148 | if first { 149 | first = false; 150 | } else { 151 | writer.write_char(',')?; 152 | } 153 | write_int(writer, *int)?; 154 | } 155 | writer.write_char(']') 156 | } 157 | 158 | fn write_long_array(writer: &mut T, values: &Vec) -> std::fmt::Result { 159 | writer.write_str("[L;")?; 160 | let mut first = true; 161 | for long in values { 162 | if first { 163 | first = false; 164 | } else { 165 | writer.write_char(',')?; 166 | } 167 | write_long(writer, *long)?; 168 | } 169 | writer.write_char(']') 170 | } 171 | --------------------------------------------------------------------------------