mirror of
https://github.com/IntQuant/noita_entangled_worlds.git
synced 2025-10-19 07:03:16 +00:00
Automatic mod install
This commit is contained in:
parent
8a79173997
commit
42e390cdb9
10 changed files with 1636 additions and 41 deletions
|
@ -14,6 +14,7 @@ There is a video guide/showcase of the mod by Gudyni (in russian): https://www.y
|
|||
Discord server: https://discord.gg/RVmAzBNE
|
||||
|
||||
Special thanks to:
|
||||
- Contributors.
|
||||
- @EvaisaDev for allowing to use code from Noita Arena mod.
|
||||
- @dextered for NoitaPatcher.
|
||||
- Creators of other libraries used in this project.
|
||||
|
|
1001
noita-proxy/Cargo.lock
generated
1001
noita-proxy/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,7 @@ eframe = { version="0.27.2", features = ["persistence", "glow", "default_fonts"]
|
|||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracing = "0.1.40"
|
||||
tangled = { path = "tangled" }
|
||||
serde = { version = "1.0.199", features = ["serde_derive"] }
|
||||
serde = { version = "1.0.199", features = ["serde_derive", "derive"] }
|
||||
bitcode = "0.6.0"
|
||||
lz4_flex = { version = "0.11.3", default_features = false, features = ["std"]}
|
||||
rand = "0.8.5"
|
||||
|
@ -22,6 +22,12 @@ steamworks = "0.11.0"
|
|||
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
|
||||
clipboard = "0.5.0"
|
||||
socket2 = { version = "0.5.7", features = ["all"] }
|
||||
egui-file-dialog = "0.5.0"
|
||||
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
|
||||
serde_json = "1.0.117"
|
||||
thiserror = "1.0.61"
|
||||
poll-promise = "0.3.0"
|
||||
zip = "1.3.1"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
|
|
@ -4,12 +4,16 @@ use std::{
|
|||
|
||||
use bitcode::{Decode, Encode};
|
||||
use clipboard::{ClipboardContext, ClipboardProvider};
|
||||
use eframe::egui::{self, Color32, Layout};
|
||||
use eframe::egui::{self, Align2, Color32, Layout};
|
||||
use mod_manager::{Modmanager, ModmanagerSettings};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use steamworks::{LobbyId, SteamAPIInitError};
|
||||
use tangled::Peer;
|
||||
use tracing::info;
|
||||
|
||||
pub mod messages;
|
||||
mod mod_manager;
|
||||
pub mod releases;
|
||||
|
||||
#[derive(Debug, Decode, Encode, Clone)]
|
||||
pub struct GameSettings {
|
||||
|
@ -20,7 +24,8 @@ pub struct GameSettings {
|
|||
pub mod net;
|
||||
|
||||
enum AppState {
|
||||
Init,
|
||||
Connect,
|
||||
ModManager,
|
||||
Netman { netman: Arc<net::NetManager> },
|
||||
Error { message: String },
|
||||
}
|
||||
|
@ -34,7 +39,8 @@ impl SteamState {
|
|||
if env::var_os("NP_DISABLE_STEAM").is_some() {
|
||||
return Err(SteamAPIInitError::FailedGeneric("Disabled by env variable".to_string()))
|
||||
}
|
||||
let (client, single) = steamworks::Client::init_app(881100)?;
|
||||
let app_id = env::var("NP_APPID").ok().and_then(|x| x.parse().ok());
|
||||
let (client, single) = steamworks::Client::init_app(app_id.unwrap_or(881100))?;
|
||||
thread::spawn(move || {
|
||||
info!("Spawned steam callback thread");
|
||||
loop {
|
||||
|
@ -46,15 +52,51 @@ impl SteamState {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
state: AppState,
|
||||
steam_state: Result<SteamState, SteamAPIInitError>,
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AppSavedState {
|
||||
addr: String,
|
||||
debug_mode: bool,
|
||||
use_constant_seed: bool,
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl Default for AppSavedState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addr: "127.0.0.1:5123".to_string(),
|
||||
debug_mode: false,
|
||||
use_constant_seed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
state: AppState,
|
||||
modmanager: Modmanager,
|
||||
steam_state: Result<SteamState, SteamAPIInitError>,
|
||||
saved_state: AppSavedState,
|
||||
modmanager_settings: ModmanagerSettings
|
||||
}
|
||||
|
||||
const MODMANAGER: &str = "modman";
|
||||
|
||||
impl App {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let saved_state = cc.storage.and_then(|storage| eframe::get_value(storage, eframe::APP_KEY))
|
||||
.unwrap_or_default();
|
||||
let modmanager_settings = cc.storage.and_then(|storage| eframe::get_value(storage, MODMANAGER))
|
||||
.unwrap_or_default();
|
||||
|
||||
info!("Creating the app...");
|
||||
Self {
|
||||
state: AppState::ModManager,
|
||||
modmanager: Modmanager::default(),
|
||||
steam_state: SteamState::new(),saved_state,
|
||||
modmanager_settings,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_server(&mut self) {
|
||||
let bind_addr = "0.0.0.0:5123".parse().unwrap();
|
||||
let peer = Peer::host(bind_addr, None).unwrap();
|
||||
|
@ -66,8 +108,8 @@ impl App {
|
|||
|
||||
fn set_netman_settings(&mut self, netman: &Arc<net::NetManager>) {
|
||||
let mut settings = netman.settings.lock().unwrap();
|
||||
settings.debug_mode = self.debug_mode;
|
||||
if !self.use_constant_seed {
|
||||
settings.debug_mode = self.saved_state.debug_mode;
|
||||
if !self.saved_state.use_constant_seed {
|
||||
settings.seed = rand::random();
|
||||
}
|
||||
netman.accept_local.store(true, Ordering::SeqCst);
|
||||
|
@ -107,34 +149,21 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
info!("Creating the app...");
|
||||
Self {
|
||||
state: AppState::Init,
|
||||
addr: "127.0.0.1:5123".to_string(),
|
||||
debug_mode: false,
|
||||
use_constant_seed: false,
|
||||
steam_state: SteamState::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.request_repaint_after(Duration::from_secs(1));
|
||||
match &self.state {
|
||||
AppState::Init => {
|
||||
AppState::Connect => {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Noita Entangled Worlds proxy");
|
||||
ui.checkbox(&mut self.debug_mode, "Debug mode");
|
||||
ui.checkbox(&mut self.use_constant_seed, "Use specified seed");
|
||||
ui.checkbox(&mut self.saved_state.debug_mode, "Debug mode");
|
||||
ui.checkbox(&mut self.saved_state.use_constant_seed, "Use specified seed");
|
||||
if ui.button("Host").clicked() {
|
||||
self.start_server();
|
||||
}
|
||||
ui.separator();
|
||||
ui.text_edit_singleline(&mut self.addr);
|
||||
let addr = self.addr.parse();
|
||||
ui.text_edit_singleline(&mut self.saved_state.addr);
|
||||
let addr = self.saved_state.addr.parse();
|
||||
ui.add_enabled_ui(addr.is_ok(), |ui| {
|
||||
if ui.button("Connect").clicked() {
|
||||
if let Ok(addr) = addr {
|
||||
|
@ -221,9 +250,23 @@ impl eframe::App for App {
|
|||
})
|
||||
.inner
|
||||
{
|
||||
self.state = AppState::Init;
|
||||
self.state = AppState::Connect;
|
||||
}
|
||||
}
|
||||
AppState::ModManager => {
|
||||
egui::Window::new("Mod manager").auto_sized().anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.show(ctx, |ui| {
|
||||
self.modmanager.update(ctx, ui, &mut self.modmanager_settings, self.steam_state.as_mut().ok())
|
||||
});
|
||||
if self.modmanager.is_done() {
|
||||
self.state = AppState::Connect;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
eframe::set_value(storage, eframe::APP_KEY, &self.saved_state);
|
||||
eframe::set_value(storage, MODMANAGER, &self.modmanager_settings);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use eframe::NativeOptions;
|
||||
use eframe::{egui::ViewportBuilder, NativeOptions};
|
||||
use noita_proxy::App;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
@ -14,7 +14,11 @@ fn main() -> Result<(), eframe::Error> {
|
|||
tracing::subscriber::set_global_default(my_subscriber).expect("setting tracing default failed");
|
||||
eframe::run_native(
|
||||
"Noita Proxy",
|
||||
NativeOptions::default(),
|
||||
Box::new(|_cc| Box::new(App::default())),
|
||||
NativeOptions {
|
||||
viewport: ViewportBuilder::default().with_min_inner_size([800.0, 600.0]),
|
||||
follow_system_theme: false,
|
||||
..Default::default()
|
||||
},
|
||||
Box::new(|cc| Box::new(App::new(cc))),
|
||||
)
|
||||
}
|
||||
|
|
298
noita-proxy/src/mod_manager.rs
Normal file
298
noita-proxy/src/mod_manager.rs
Normal file
|
@ -0,0 +1,298 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use eframe::egui::{Align2, Context, Ui};
|
||||
use egui_file_dialog::{DialogState, FileDialog};
|
||||
use poll_promise::Promise;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use steamworks::AppId;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
releases::{get_release_by_tag, Downloader, ReleasesError, Version},
|
||||
SteamState,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
JustStarted,
|
||||
IsAutomaticPathOk,
|
||||
SelectPath,
|
||||
PreCheckMod,
|
||||
InvalidPath,
|
||||
CheckMod,
|
||||
Done,
|
||||
DownloadMod(Promise<Result<Downloader, ReleasesError>>),
|
||||
Error(io::Error),
|
||||
ReleasesError(ReleasesError),
|
||||
UnpackMod(Promise<Result<(), ReleasesError>>),
|
||||
ConfirmInstall,
|
||||
}
|
||||
|
||||
pub struct Modmanager {
|
||||
state: State,
|
||||
file_dialog: FileDialog,
|
||||
}
|
||||
|
||||
impl Default for Modmanager {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
file_dialog: FileDialog::default()
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.title("Select path to noita.exe"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ModmanagerSettings {
|
||||
game_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ModmanagerSettings {
|
||||
fn try_find_game_path(&mut self, steam_state: Option<&mut SteamState>) {
|
||||
info!("Trying to find game path");
|
||||
if let Some(state) = steam_state {
|
||||
let apps = state.client.apps();
|
||||
let app_id = AppId::from(881100);
|
||||
if apps.is_app_installed(app_id) {
|
||||
let app_install_dir = apps.app_install_dir(app_id);
|
||||
self.game_path = PathBuf::from(app_install_dir).join("noita.exe");
|
||||
info!("Found game path with steam: {}", self.game_path.display())
|
||||
} else {
|
||||
info!("App not installed");
|
||||
}
|
||||
}
|
||||
}
|
||||
fn mod_path(&self) -> PathBuf {
|
||||
let mut path = self.game_path.clone();
|
||||
path.pop();
|
||||
path.push("mods");
|
||||
path.push("quant.ew");
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Modmanager {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
settings: &mut ModmanagerSettings,
|
||||
steam_state: Option<&mut SteamState>,
|
||||
) {
|
||||
if let State::JustStarted = self.state {
|
||||
if check_path_valid(&settings.game_path) {
|
||||
info!("Path is valid, checking mod now");
|
||||
self.state = State::PreCheckMod;
|
||||
} else {
|
||||
settings.try_find_game_path(steam_state);
|
||||
let could_find_automatically = check_path_valid(&settings.game_path);
|
||||
if could_find_automatically {
|
||||
self.state = State::IsAutomaticPathOk;
|
||||
} else {
|
||||
self.select_noita_file();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &self.state {
|
||||
State::JustStarted => unreachable!(),
|
||||
State::IsAutomaticPathOk => {
|
||||
ui.heading("Found a path automatically:");
|
||||
ui.label(settings.game_path.display().to_string());
|
||||
if ui.button("Use this one").clicked() {
|
||||
self.state = State::PreCheckMod;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
if ui.button("Select manually").clicked() {
|
||||
self.select_noita_file();
|
||||
}
|
||||
}
|
||||
State::SelectPath => {
|
||||
if let Some(path) = self.file_dialog.update(ctx).selected() {
|
||||
settings.game_path = path.to_path_buf();
|
||||
if !check_path_valid(&settings.game_path) {
|
||||
self.state = State::InvalidPath;
|
||||
}
|
||||
}
|
||||
if self.file_dialog.state() == DialogState::Cancelled {
|
||||
// self.select_noita_file()
|
||||
self.state = State::JustStarted
|
||||
}
|
||||
}
|
||||
State::InvalidPath => {
|
||||
ui.label("This path is not valid");
|
||||
if ui.button("Select again").clicked() {
|
||||
self.select_noita_file();
|
||||
}
|
||||
}
|
||||
State::PreCheckMod => {
|
||||
// settings.game_path = PathBuf::new();
|
||||
// self.state = State::JustStarted;
|
||||
ui.label("Will check mod install now...");
|
||||
self.state = State::CheckMod;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
State::CheckMod => {
|
||||
ctx.request_repaint();
|
||||
let mod_path = settings.mod_path();
|
||||
info!("Mod path: {}", mod_path.display());
|
||||
|
||||
self.state = match is_mod_ok(&mod_path) {
|
||||
Ok(true) => State::Done,
|
||||
Ok(false) => State::ConfirmInstall,
|
||||
Err(err) => {
|
||||
error!("Could not check if mod is ok: {}", err);
|
||||
State::Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
State::ConfirmInstall => {
|
||||
let mod_path = settings.mod_path();
|
||||
ui.label(format!(
|
||||
"Proxy will install the mod to {}",
|
||||
mod_path.display()
|
||||
));
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Confirm").clicked() {
|
||||
let download_path = PathBuf::from("mod.zip");
|
||||
let tag = Version::current().into();
|
||||
let promise = Promise::spawn_thread("release-request", move || {
|
||||
mod_downloader_for(tag, download_path)
|
||||
});
|
||||
// Make sure we are deleting the right thing
|
||||
assert!(mod_path.ends_with("quant.ew"));
|
||||
fs::remove_dir_all(mod_path).ok();
|
||||
info!("Current mod deleted");
|
||||
|
||||
self.state = State::DownloadMod(promise)
|
||||
}
|
||||
if ui.button("Select a different path").clicked() {
|
||||
self.select_noita_file()
|
||||
}
|
||||
});
|
||||
}
|
||||
State::DownloadMod(promise) => {
|
||||
ui.label("Downloading mod...");
|
||||
match promise.ready() {
|
||||
Some(Ok(downloader)) => {
|
||||
downloader.show_progress(ui);
|
||||
match downloader.ready() {
|
||||
Some(Ok(_)) => {
|
||||
let path = downloader.path().to_path_buf();
|
||||
let directory = settings.mod_path();
|
||||
let promise: Promise<Result<(), ReleasesError>> =
|
||||
Promise::spawn_thread("unpack", move || {
|
||||
extract_and_remove_zip(path, directory)
|
||||
});
|
||||
self.state = State::UnpackMod(promise);
|
||||
}
|
||||
Some(Err(err)) => self.state = State::ReleasesError(err.clone()),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Some(Err(err)) => self.state = State::ReleasesError(err.clone()),
|
||||
None => {
|
||||
ui.label("Receiving release info...");
|
||||
ui.spinner();
|
||||
}
|
||||
}
|
||||
}
|
||||
State::UnpackMod(promise) => match promise.ready() {
|
||||
Some(Ok(_)) => {
|
||||
ui.label("Mod has been installed!");
|
||||
if ui.button("Continue").clicked() {
|
||||
self.state = State::Done;
|
||||
};
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
self.state = State::ReleasesError(err.clone());
|
||||
}
|
||||
None => {
|
||||
ui.label("Unpacking mod");
|
||||
}
|
||||
},
|
||||
State::Error(err) => {
|
||||
ui.label(format!("Encountered an error: {}", err));
|
||||
if ui.button("Retry").clicked() {
|
||||
self.state = State::JustStarted;
|
||||
}
|
||||
}
|
||||
State::ReleasesError(err) => {
|
||||
ui.label(format!("Encountered an error: {}", err));
|
||||
if ui.button("Retry").clicked() {
|
||||
self.state = State::JustStarted;
|
||||
}
|
||||
}
|
||||
State::Done => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_noita_file(&mut self) {
|
||||
self.state = State::SelectPath;
|
||||
self.file_dialog.select_file();
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
if let State::Done = self.state {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mod_downloader_for(
|
||||
tag: crate::releases::Tag,
|
||||
download_path: PathBuf,
|
||||
) -> Result<Downloader, ReleasesError> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(None)
|
||||
.build()
|
||||
.unwrap();
|
||||
get_release_by_tag(&client, tag)
|
||||
.and_then(|release| release.get_release_assets(&client))
|
||||
.and_then(|asset_list| asset_list.find_by_name("quant.ew.zip").cloned())
|
||||
.and_then(|asset| asset.download(&client, &download_path))
|
||||
}
|
||||
|
||||
fn extract_and_remove_zip(zip_file: PathBuf, extact_to: PathBuf) -> Result<(), ReleasesError> {
|
||||
let reader = File::open(&zip_file)?;
|
||||
let mut zip = zip::ZipArchive::new(reader)?;
|
||||
info!("Extracting zip file");
|
||||
zip.extract(extact_to)?;
|
||||
info!("Zip file extracted");
|
||||
fs::remove_file(&zip_file).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_mod_ok(mod_path: &Path) -> io::Result<bool> {
|
||||
if !mod_path.try_exists()? {
|
||||
return Ok(false);
|
||||
}
|
||||
let version_path = mod_path.join("files/version.lua");
|
||||
let version = fs::read_to_string(&version_path)
|
||||
.ok()
|
||||
.and_then(|v| Version::parse_from_mod(&v));
|
||||
|
||||
info!("Mod version: {:?}", version);
|
||||
|
||||
if Some(Version::current()) != version {
|
||||
info!("Mod version differs");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
info!("Mod is ok");
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn check_path_valid(game_path: &Path) -> bool {
|
||||
game_path.ends_with("noita.exe") && game_path.exists()
|
||||
}
|
250
noita-proxy/src/releases.rs
Normal file
250
noita-proxy/src/releases.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicU64, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use eframe::egui::{self, Ui};
|
||||
use poll_promise::Promise;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use zip::result::ZipError;
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum ReleasesError {
|
||||
#[error("Could not complete request: {0}")]
|
||||
Request(Arc<reqwest::Error>),
|
||||
#[error("Asset not found")]
|
||||
AssetNotFound,
|
||||
#[error("Io error: {0}")]
|
||||
Io(Arc<io::Error>),
|
||||
#[error("Zip error: {0}")]
|
||||
Zip(Arc<ZipError>),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ReleasesError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::Request(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for ReleasesError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ZipError> for ReleasesError {
|
||||
fn from(value: ZipError) -> Self {
|
||||
Self::Zip(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Release {
|
||||
pub tag_name: String,
|
||||
assets_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Asset {
|
||||
url: String,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
pub fn download(&self, client: &Client, path: &Path) -> Result<Downloader, ReleasesError> {
|
||||
let shared = Arc::new(DownloaderSharedState {
|
||||
progress: AtomicU64::new(0),
|
||||
});
|
||||
let client = client.clone();
|
||||
let url = self.url.clone();
|
||||
let file = File::create(path)?;
|
||||
let handle = {
|
||||
let shared = shared.clone();
|
||||
Promise::spawn_thread("downloader", move || {
|
||||
download_thread(client, url, shared, file)
|
||||
})
|
||||
};
|
||||
|
||||
let downloader = Ok(Downloader {
|
||||
shared,
|
||||
handle,
|
||||
path: path.to_path_buf(),
|
||||
size: self.size,
|
||||
});
|
||||
downloader
|
||||
}
|
||||
}
|
||||
|
||||
fn download_thread(
|
||||
client: Client,
|
||||
url: String,
|
||||
shared: Arc<DownloaderSharedState>,
|
||||
mut file: File,
|
||||
) -> Result<(), ReleasesError> {
|
||||
let mut response = client
|
||||
.get(&url)
|
||||
.header("Accept", "application/octet-stream")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.header("User-agent", "noita proxy")
|
||||
.send()?;
|
||||
let mut buf = [0; 4096];
|
||||
|
||||
loop {
|
||||
let len = response.read(&mut buf)?;
|
||||
shared
|
||||
.progress
|
||||
.fetch_add(len as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
file.write_all(&buf[..len])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct DownloaderSharedState {
|
||||
progress: AtomicU64,
|
||||
}
|
||||
|
||||
pub struct Downloader {
|
||||
shared: Arc<DownloaderSharedState>,
|
||||
size: u64,
|
||||
handle: Promise<Result<(), ReleasesError>>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
pub fn progress(&self) -> (u64, u64) {
|
||||
let written = self
|
||||
.shared
|
||||
.progress
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
(written, self.size)
|
||||
}
|
||||
|
||||
pub fn show_progress(&self, ui: &mut Ui) {
|
||||
let (current, max) = self.progress();
|
||||
ui.label(format!("{} out of {} bytes", current, max));
|
||||
ui.add(egui::ProgressBar::new(current as f32 / max as f32));
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
pub fn ready(&self) -> Option<&Result<(), ReleasesError>> {
|
||||
self.handle.ready()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AssetList(Vec<Asset>);
|
||||
|
||||
impl AssetList {
|
||||
pub fn find_by_name(&self, name: &str) -> Result<&Asset, ReleasesError> {
|
||||
self.0
|
||||
.iter()
|
||||
.find(|asset| asset.name == name)
|
||||
.ok_or(ReleasesError::AssetNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Tag(String);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Version {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn parse_from_mod(version: &str) -> Option<Self> {
|
||||
let strip_suffix = version.strip_prefix("return \"")?.strip_suffix("\"")?;
|
||||
Self::parse_from_string(strip_suffix)
|
||||
}
|
||||
fn parse_from_string(version: &str) -> Option<Self> {
|
||||
let mut nums = version.split(".");
|
||||
let major = nums.next()?.parse().ok()?;
|
||||
let minor = nums.next()?.parse().ok()?;
|
||||
let patch = nums.next()?.parse().ok()?;
|
||||
Some(Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
pub fn current() -> Self {
|
||||
Self::parse_from_string(env!("CARGO_PKG_VERSION")).expect("can always parse crate version")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for Tag {
|
||||
fn from(value: Version) -> Self {
|
||||
Self(format!("v{}.{}.{}", value.major, value.minor, value.patch))
|
||||
}
|
||||
}
|
||||
|
||||
impl Release {
|
||||
pub fn get_release_assets(&self, client: &Client) -> Result<AssetList, ReleasesError> {
|
||||
let response = client
|
||||
.get(&self.assets_url)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.header("User-agent", "noita proxy")
|
||||
.send()?;
|
||||
|
||||
Ok(response.json()?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_release(client: &Client) -> Result<Release, ReleasesError> {
|
||||
let response = client
|
||||
.get("https://api.github.com/repos/IntQuant/noita_entangled_worlds/releases/latest")
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.header("User-agent", "noita proxy")
|
||||
.send()?;
|
||||
|
||||
Ok(response.json()?)
|
||||
}
|
||||
|
||||
pub fn get_release_by_tag(client: &Client, tag: Tag) -> Result<Release, ReleasesError> {
|
||||
let response = client
|
||||
.get(format!(
|
||||
"https://api.github.com/repos/IntQuant/noita_entangled_worlds/releases/tags/{}",
|
||||
tag.0
|
||||
))
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.header("User-agent", "noita proxy")
|
||||
.send()?;
|
||||
|
||||
Ok(response.json()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::releases::{get_release_by_tag, Tag};
|
||||
|
||||
#[test]
|
||||
fn release_assets() {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
// let release = get_latest_release(&client).unwrap();
|
||||
let release = get_release_by_tag(&client, Tag("v0.4.1".to_string())).unwrap();
|
||||
let assets = release.get_release_assets(&client).unwrap();
|
||||
println!("{:?}", release);
|
||||
println!("{:?}", assets);
|
||||
let mod_asset = assets.find_by_name("quant.ew.zip").unwrap();
|
||||
println!("{:?}", mod_asset);
|
||||
assert_eq!(mod_asset.name, "quant.ew.zip")
|
||||
}
|
||||
}
|
|
@ -2,6 +2,12 @@ import subprocess
|
|||
from zipfile import ZipFile, ZIP_DEFLATED as COMPRESS_TYPE
|
||||
import shutil
|
||||
import os
|
||||
import tomllib
|
||||
|
||||
cargo_manifest = tomllib.load(open("noita-proxy/Cargo.toml", "rb"))
|
||||
version = cargo_manifest["package"]["version"]
|
||||
|
||||
print("Current version: ", version)
|
||||
|
||||
def try_remove(path):
|
||||
try:
|
||||
|
@ -47,3 +53,6 @@ with ZipFile("target/noita-proxy-linux.zip", "w") as release:
|
|||
print("Writing mod release...")
|
||||
|
||||
shutil.make_archive("target/quant.ew", "zip", "quant.ew")
|
||||
|
||||
with ZipFile("target/quant.ew.zip", "a") as release:
|
||||
release.writestr("files/version.lua", f'return "{version}"')
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
return "dev-build"
|
|
@ -21,7 +21,7 @@ local enemy_sync = ctx.dofile_and_add_hooks("mods/quant.ew/files/src/enemy_sync.
|
|||
local world_sync = ctx.dofile_and_add_hooks("mods/quant.ew/files/src/world_sync.lua")
|
||||
local item_sync = ctx.dofile_and_add_hooks("mods/quant.ew/files/src/item_sync.lua")
|
||||
|
||||
local version = dofile_once("mods/quant.ew/files/version.lua")
|
||||
local version = dofile_once("mods/quant.ew/files/version.lua") or "unknown (dev build)"
|
||||
|
||||
ModLuaFileAppend("data/scripts/gun/gun.lua", "mods/quant.ew/files/append/gun.lua")
|
||||
ModLuaFileAppend("data/scripts/gun/gun_actions.lua", "mods/quant.ew/files/append/action_fix.lua")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue