Automatic mod install

This commit is contained in:
IQuant 2024-05-23 21:02:39 +03:00
parent 8a79173997
commit 42e390cdb9
10 changed files with 1636 additions and 41 deletions

View file

@ -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 Discord server: https://discord.gg/RVmAzBNE
Special thanks to: Special thanks to:
- Contributors.
- @EvaisaDev for allowing to use code from Noita Arena mod. - @EvaisaDev for allowing to use code from Noita Arena mod.
- @dextered for NoitaPatcher. - @dextered for NoitaPatcher.
- Creators of other libraries used in this project. - Creators of other libraries used in this project.

1001
noita-proxy/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ eframe = { version="0.27.2", features = ["persistence", "glow", "default_fonts"]
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing = "0.1.40" tracing = "0.1.40"
tangled = { path = "tangled" } tangled = { path = "tangled" }
serde = { version = "1.0.199", features = ["serde_derive"] } serde = { version = "1.0.199", features = ["serde_derive", "derive"] }
bitcode = "0.6.0" bitcode = "0.6.0"
lz4_flex = { version = "0.11.3", default_features = false, features = ["std"]} lz4_flex = { version = "0.11.3", default_features = false, features = ["std"]}
rand = "0.8.5" rand = "0.8.5"
@ -22,6 +22,12 @@ steamworks = "0.11.0"
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] } crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
clipboard = "0.5.0" clipboard = "0.5.0"
socket2 = { version = "0.5.7", features = ["all"] } 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] [profile.dev]
opt-level = 1 opt-level = 1

View file

@ -4,12 +4,16 @@ use std::{
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use clipboard::{ClipboardContext, ClipboardProvider}; 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 steamworks::{LobbyId, SteamAPIInitError};
use tangled::Peer; use tangled::Peer;
use tracing::info; use tracing::info;
pub mod messages; pub mod messages;
mod mod_manager;
pub mod releases;
#[derive(Debug, Decode, Encode, Clone)] #[derive(Debug, Decode, Encode, Clone)]
pub struct GameSettings { pub struct GameSettings {
@ -20,7 +24,8 @@ pub struct GameSettings {
pub mod net; pub mod net;
enum AppState { enum AppState {
Init, Connect,
ModManager,
Netman { netman: Arc<net::NetManager> }, Netman { netman: Arc<net::NetManager> },
Error { message: String }, Error { message: String },
} }
@ -34,7 +39,8 @@ impl SteamState {
if env::var_os("NP_DISABLE_STEAM").is_some() { if env::var_os("NP_DISABLE_STEAM").is_some() {
return Err(SteamAPIInitError::FailedGeneric("Disabled by env variable".to_string())) 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 || { thread::spawn(move || {
info!("Spawned steam callback thread"); info!("Spawned steam callback thread");
loop { loop {
@ -46,15 +52,51 @@ impl SteamState {
} }
} }
pub struct App { #[derive(Debug, Serialize, Deserialize)]
state: AppState, struct AppSavedState {
steam_state: Result<SteamState, SteamAPIInitError>,
addr: String, addr: String,
debug_mode: bool, debug_mode: bool,
use_constant_seed: 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 { 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) { fn start_server(&mut self) {
let bind_addr = "0.0.0.0:5123".parse().unwrap(); let bind_addr = "0.0.0.0:5123".parse().unwrap();
let peer = Peer::host(bind_addr, None).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>) { fn set_netman_settings(&mut self, netman: &Arc<net::NetManager>) {
let mut settings = netman.settings.lock().unwrap(); let mut settings = netman.settings.lock().unwrap();
settings.debug_mode = self.debug_mode; settings.debug_mode = self.saved_state.debug_mode;
if !self.use_constant_seed { if !self.saved_state.use_constant_seed {
settings.seed = rand::random(); settings.seed = rand::random();
} }
netman.accept_local.store(true, Ordering::SeqCst); 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 { impl eframe::App for App {
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint_after(Duration::from_secs(1)); ctx.request_repaint_after(Duration::from_secs(1));
match &self.state { match &self.state {
AppState::Init => { AppState::Connect => {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Noita Entangled Worlds proxy"); ui.heading("Noita Entangled Worlds proxy");
ui.checkbox(&mut self.debug_mode, "Debug mode"); ui.checkbox(&mut self.saved_state.debug_mode, "Debug mode");
ui.checkbox(&mut self.use_constant_seed, "Use specified seed"); ui.checkbox(&mut self.saved_state.use_constant_seed, "Use specified seed");
if ui.button("Host").clicked() { if ui.button("Host").clicked() {
self.start_server(); self.start_server();
} }
ui.separator(); ui.separator();
ui.text_edit_singleline(&mut self.addr); ui.text_edit_singleline(&mut self.saved_state.addr);
let addr = self.addr.parse(); let addr = self.saved_state.addr.parse();
ui.add_enabled_ui(addr.is_ok(), |ui| { ui.add_enabled_ui(addr.is_ok(), |ui| {
if ui.button("Connect").clicked() { if ui.button("Connect").clicked() {
if let Ok(addr) = addr { if let Ok(addr) = addr {
@ -221,9 +250,23 @@ impl eframe::App for App {
}) })
.inner .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);
}
} }

View file

@ -1,4 +1,4 @@
use eframe::NativeOptions; use eframe::{egui::ViewportBuilder, NativeOptions};
use noita_proxy::App; use noita_proxy::App;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter; 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"); tracing::subscriber::set_global_default(my_subscriber).expect("setting tracing default failed");
eframe::run_native( eframe::run_native(
"Noita Proxy", "Noita Proxy",
NativeOptions::default(), NativeOptions {
Box::new(|_cc| Box::new(App::default())), 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))),
) )
} }

View 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
View 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")
}
}

View file

@ -2,6 +2,12 @@ import subprocess
from zipfile import ZipFile, ZIP_DEFLATED as COMPRESS_TYPE from zipfile import ZipFile, ZIP_DEFLATED as COMPRESS_TYPE
import shutil import shutil
import os 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): def try_remove(path):
try: try:
@ -47,3 +53,6 @@ with ZipFile("target/noita-proxy-linux.zip", "w") as release:
print("Writing mod release...") print("Writing mod release...")
shutil.make_archive("target/quant.ew", "zip", "quant.ew") 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}"')

View file

@ -1 +0,0 @@
return "dev-build"

View file

@ -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 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 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.lua", "mods/quant.ew/files/append/gun.lua")
ModLuaFileAppend("data/scripts/gun/gun_actions.lua", "mods/quant.ew/files/append/action_fix.lua") ModLuaFileAppend("data/scripts/gun/gun_actions.lua", "mods/quant.ew/files/append/action_fix.lua")