mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-24 02:44:17 +00:00
Automatically Keep the Modpack Up-to-Date (#595)
* Grab releases from Github and install * Refactor * Fix crash * Work on fixing install() crash * Fix the crash by increasing stack size * Block main() execution on the auto-updater * Improve error handling * Delete hash.txt * Fix get_update_policy() * Use current time as default last_update_version; Compare publish date to last_update_version to determine if update should be applied. * Use skyline_web dialogs for user_wants_to_install. Use default selections on emulator instead. * Fix some logic * Convert CURRENT_VERSION to a Mutex; implement functions to update the config file * Adjust logging * Remove unneeded file * Allow unwrap after is_some() check * Fix format * Auto-updater (squashed) * Move update policy from config file to menu * Skip version check on emulator * Rustfmt, clippy --------- Co-authored-by: jugeeya <jugeeya@live.com>
This commit is contained in:
parent
c360ce1ad5
commit
cc78dc7e4a
12 changed files with 1386 additions and 953 deletions
4
.cargo/config.toml
Normal file
4
.cargo/config.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[env]
|
||||||
|
AUTHOR = "jugeeya"
|
||||||
|
REPO_NAME = "UltimateTrainingModpack"
|
||||||
|
USER_AGENT = "UltimateTrainingModpack"
|
|
@ -35,6 +35,9 @@ training_mod_tui = { path = "training_mod_tui" }
|
||||||
native-tls = { version = "0.2.11", features = ["vendored"] }
|
native-tls = { version = "0.2.11", features = ["vendored"] }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
byte-unit = "4.0.18"
|
byte-unit = "4.0.18"
|
||||||
|
zip = { version = "0.6", default-features = false, features = ["deflate"]}
|
||||||
|
anyhow = "1.0.72"
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" }
|
native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" }
|
||||||
|
|
62
src/common/dialog.rs
Normal file
62
src/common/dialog.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::common::is_emulator;
|
||||||
|
use skyline_web::dialog::{Dialog, DialogOption};
|
||||||
|
use skyline_web::dialog_ok::DialogOk;
|
||||||
|
|
||||||
|
/// Returns true for yes, false for no
|
||||||
|
/// Returns false if you cancel with B
|
||||||
|
/// Returns `emulator_default` if you're on emulator
|
||||||
|
pub fn yes_no(prompt: String, emulator_default: bool) -> bool {
|
||||||
|
if is_emulator() {
|
||||||
|
return emulator_default;
|
||||||
|
}
|
||||||
|
Dialog::yes_no(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true for yes, false for no
|
||||||
|
/// Returns false if you cancel with B
|
||||||
|
/// Returns `emulator_default` if you're on emulator
|
||||||
|
pub fn no_yes(prompt: String, emulator_default: bool) -> bool {
|
||||||
|
if is_emulator() {
|
||||||
|
return emulator_default;
|
||||||
|
}
|
||||||
|
Dialog::no_yes(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true for ok, false for cancel
|
||||||
|
/// Returns false if you cancel with B
|
||||||
|
/// Returns `emulator_default` if you're on emulator
|
||||||
|
pub fn ok_cancel(prompt: String, emulator_default: bool) -> bool {
|
||||||
|
if is_emulator() {
|
||||||
|
return emulator_default;
|
||||||
|
}
|
||||||
|
Dialog::ok_cancel(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `left` for the left option,
|
||||||
|
/// Returns `right` for the right option
|
||||||
|
/// Returns `default` if you cancel with B
|
||||||
|
/// Returns `emulator_default` if you're on emulator
|
||||||
|
pub fn left_right(
|
||||||
|
prompt: String,
|
||||||
|
left: String,
|
||||||
|
right: String,
|
||||||
|
default: String,
|
||||||
|
emulator_default: String,
|
||||||
|
) -> String {
|
||||||
|
if is_emulator() {
|
||||||
|
return emulator_default;
|
||||||
|
}
|
||||||
|
match Dialog::new(prompt, left.clone(), right.clone()).show() {
|
||||||
|
DialogOption::Left => left,
|
||||||
|
DialogOption::Right => right,
|
||||||
|
DialogOption::Default => default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Always returns true after you accept the prompt
|
||||||
|
pub fn dialog_ok(prompt: String) -> bool {
|
||||||
|
if is_emulator() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
DialogOk::ok(prompt)
|
||||||
|
}
|
|
@ -151,7 +151,7 @@ impl Event {
|
||||||
device_id: DEVICE_ID.get().unwrap().to_string(),
|
device_id: DEVICE_ID.get().unwrap().to_string(),
|
||||||
event_time,
|
event_time,
|
||||||
session_id: SESSION_ID.get().unwrap().to_string(),
|
session_id: SESSION_ID.get().unwrap().to_string(),
|
||||||
mod_version: CURRENT_VERSION.to_string(),
|
mod_version: CURRENT_VERSION.lock().to_string(),
|
||||||
smash_version: smash_version(),
|
smash_version: smash_version(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::common::consts::*;
|
||||||
pub mod button_config;
|
pub mod button_config;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod dev_config;
|
pub mod dev_config;
|
||||||
|
pub mod dialog;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
|
|
@ -1,74 +1,224 @@
|
||||||
use std::fs;
|
#![allow(clippy::unnecessary_unwrap)]
|
||||||
|
use crate::consts::*;
|
||||||
use skyline_web::dialog_ok::DialogOk;
|
use crate::dialog;
|
||||||
|
|
||||||
use crate::consts::{
|
|
||||||
LEGACY_MENU_OPTIONS_PATH, MENU_DEFAULT_OPTIONS_PATH, MENU_OPTIONS_PATH, VERSION_TXT_PATH,
|
|
||||||
};
|
|
||||||
use crate::logging::*;
|
use crate::logging::*;
|
||||||
|
use crate::MENU;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
lazy_static! {
|
||||||
|
pub static ref CURRENT_VERSION: Mutex<String> =
|
||||||
enum VersionCheck {
|
Mutex::new(get_current_version().expect("Could not determine current version!"));
|
||||||
Current,
|
|
||||||
NoFile,
|
|
||||||
Update,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_current_version(fpath: &str) -> VersionCheck {
|
#[derive(Debug)]
|
||||||
// Create a blank version file if it doesn't exists
|
pub struct Release {
|
||||||
if fs::metadata(fpath).is_err() {
|
pub url: String,
|
||||||
fs::File::create(fpath).expect("Could not create version file!");
|
pub tag: String,
|
||||||
return VersionCheck::NoFile;
|
pub published_at: String,
|
||||||
}
|
|
||||||
|
|
||||||
if fs::read_to_string(fpath).unwrap_or_else(|_| "".to_string()) == CURRENT_VERSION {
|
|
||||||
VersionCheck::Current
|
|
||||||
} else {
|
|
||||||
VersionCheck::Update
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_current_version(fpath: &str) {
|
impl Release {
|
||||||
// Write the current version to the version file
|
/// Downloads and installs the release
|
||||||
fs::write(fpath, CURRENT_VERSION).expect("Could not record current version!")
|
pub fn install(self: &Release) -> Result<()> {
|
||||||
}
|
info!("Installing asset from URL: {}", &self.url);
|
||||||
|
let response = minreq::get(&self.url)
|
||||||
pub fn version_check() {
|
.with_header("User-Agent", "UltimateTrainingModpack")
|
||||||
match is_current_version(VERSION_TXT_PATH) {
|
.with_header("Accept", "application/octet-stream")
|
||||||
VersionCheck::Current => {
|
.send_lazy()?;
|
||||||
// Version is current, no need to take any action
|
info!(
|
||||||
}
|
"Ok response from Github. Status Code: {}",
|
||||||
VersionCheck::Update => {
|
&response.status_code
|
||||||
// Display dialog box on launch if changing versions
|
|
||||||
DialogOk::ok(
|
|
||||||
format!(
|
|
||||||
"Thank you for installing version {CURRENT_VERSION} of the Training Modpack.\n\n\
|
|
||||||
Due to a breaking change in this version, your menu selections and defaults must be reset once.\n\n\
|
|
||||||
Please refer to the Github page and the Discord server for a full list of recent features, bugfixes, and other changes."
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
// Remove old menu selections, silently ignoring errors (i.e. if the file doesn't exist)
|
let mut vec = Vec::new();
|
||||||
[
|
for result in response {
|
||||||
MENU_OPTIONS_PATH,
|
let (byte, length) = result?;
|
||||||
MENU_DEFAULT_OPTIONS_PATH,
|
vec.reserve(length);
|
||||||
LEGACY_MENU_OPTIONS_PATH,
|
vec.push(byte);
|
||||||
]
|
}
|
||||||
.iter()
|
info!("Finished receiving .zip file from GitHub.");
|
||||||
.for_each(|path| {
|
info!("Unpacking .zip file...");
|
||||||
fs::remove_file(path).unwrap_or_else(|_| error!("Couldn't remove {path}"))
|
let mut zip = ZipArchive::new(std::io::Cursor::new(vec))?;
|
||||||
|
zip.extract(UNPACK_PATH)?;
|
||||||
|
info!("Finished unpacking update");
|
||||||
|
|
||||||
|
info!("Updating config file with last update time...");
|
||||||
|
TrainingModpackConfig::change_last_update_version(&self.published_at)?;
|
||||||
|
dialog::dialog_ok(
|
||||||
|
"The Training Modpack has been updated.\n\n\
|
||||||
|
Your game will now restart."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
info!("Finished. Restarting...");
|
||||||
|
unsafe {
|
||||||
|
skyline::nn::oe::RequestToRelaunchApplication();
|
||||||
|
}
|
||||||
|
// Don't need a return type here because this area is unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(self: &Release) -> String {
|
||||||
|
format!("{} - {}", self.tag, self.published_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_older_than_installed(self: &Release) -> bool {
|
||||||
|
// String comparison is good enough because for RFC3339 format,
|
||||||
|
// alphabetical order == chronological order
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3339#section-5.1
|
||||||
|
let current_version = CURRENT_VERSION.lock();
|
||||||
|
self.published_at.as_str() < current_version.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_update_policy() -> UpdatePolicy {
|
||||||
|
unsafe { MENU.update_policy }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_release(beta: bool) -> Result<Release> {
|
||||||
|
// Get the list of releases from Github
|
||||||
|
let url = format!(
|
||||||
|
"https://api.github.com/repos/{}/{}/releases",
|
||||||
|
env!("AUTHOR"),
|
||||||
|
env!("REPO_NAME")
|
||||||
|
);
|
||||||
|
let response = minreq::get(url)
|
||||||
|
.with_header("User-Agent", env!("USER_AGENT"))
|
||||||
|
.with_header("Accept", "application/json")
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let json: Vec<Value> = serde_json::from_str(response.as_str()?)?;
|
||||||
|
|
||||||
|
// Parse the list to determine the latest stable and beta release
|
||||||
|
let mut stable_release: Option<Release> = None;
|
||||||
|
let mut beta_release: Option<Release> = None;
|
||||||
|
for release in json.into_iter() {
|
||||||
|
// The list is ordered by date w/ most recent releases first
|
||||||
|
// so we only need to get the first of each type
|
||||||
|
let is_prerelease = release["prerelease"]
|
||||||
|
.as_bool()
|
||||||
|
.ok_or_else(|| anyhow!("prerelease is not a bool"))?;
|
||||||
|
if is_prerelease && beta_release.is_none() {
|
||||||
|
// Assumes that the first asset exists and is the right one
|
||||||
|
let url = release["assets"][0]["url"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Could not parse beta asset url"))?;
|
||||||
|
let tag = release["tag_name"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Could not parse beta asset tag_name"))?;
|
||||||
|
let published_at = release["published_at"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Could not parse beta asset published_at"))?;
|
||||||
|
beta_release = Some(Release {
|
||||||
|
url: url.to_string(),
|
||||||
|
tag: tag.to_string(),
|
||||||
|
published_at: published_at.to_string(),
|
||||||
|
});
|
||||||
|
} else if !is_prerelease && stable_release.is_none() {
|
||||||
|
// Assumes that the first asset exists and is the right one
|
||||||
|
let url = release["assets"][0]["url"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Could not parse stable asset url"))?;
|
||||||
|
let tag = release["tag_name"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Could not parse stable asset tag_name"))?;
|
||||||
|
let published_at = release["published_at"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow!("Could not parse stable asset published_at"))?;
|
||||||
|
stable_release = Some(Release {
|
||||||
|
url: url.to_string(),
|
||||||
|
tag: tag.to_string(),
|
||||||
|
published_at: published_at.to_string(),
|
||||||
});
|
});
|
||||||
record_current_version(VERSION_TXT_PATH);
|
|
||||||
}
|
}
|
||||||
VersionCheck::NoFile => {
|
if beta_release.is_some() && stable_release.is_some() {
|
||||||
// Display dialog box on fresh installation
|
// Don't iterate needlessly, we already found both releases
|
||||||
DialogOk::ok(
|
break;
|
||||||
format!(
|
}
|
||||||
"Thank you for installing version {CURRENT_VERSION} of the Training Modpack.\n\n\
|
}
|
||||||
Please refer to the Github page and the Discord server for a full list of features and instructions on how to utilize the improved Training Mode."
|
if beta && beta_release.is_some() {
|
||||||
|
Ok(beta_release.unwrap())
|
||||||
|
} else if !beta && stable_release.is_some() {
|
||||||
|
Ok(stable_release.unwrap())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"The specified release was not found in the GitHub JSON response!"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_wants_to_install() -> bool {
|
||||||
|
dialog::yes_no(
|
||||||
|
"There is a new update available for the Training Modpack. \n\n\
|
||||||
|
Do you want to install it?"
|
||||||
|
.to_string(),
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
record_current_version(VERSION_TXT_PATH);
|
|
||||||
|
fn get_current_version() -> Result<String> {
|
||||||
|
let config = TrainingModpackConfig::load();
|
||||||
|
match config {
|
||||||
|
Ok(c) => {
|
||||||
|
info!("Config file found and parsed. Loading...");
|
||||||
|
Ok(c.update.last_update_version)
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
if e.is::<Error>()
|
||||||
|
&& e.downcast_ref::<Error>().unwrap().kind() == ErrorKind::NotFound =>
|
||||||
|
{
|
||||||
|
warn!("No config file found, creating default...");
|
||||||
|
TrainingModpackConfig::create_default()?;
|
||||||
|
get_current_version()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Some other error, re-raise it
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn perform_version_check() {
|
||||||
|
let update_policy = get_update_policy();
|
||||||
|
info!("Update Policy is {}", update_policy);
|
||||||
|
let mut release_to_apply = match update_policy {
|
||||||
|
UpdatePolicy::Stable => get_release(false),
|
||||||
|
UpdatePolicy::Beta => get_release(true),
|
||||||
|
UpdatePolicy::Disabled => {
|
||||||
|
// User does not want to update at all
|
||||||
|
Err(anyhow!("Updates are disabled per UpdatePolicy"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if release_to_apply.is_ok() {
|
||||||
|
let published_at = release_to_apply.as_ref().unwrap().published_at.clone();
|
||||||
|
let current_version = CURRENT_VERSION.lock();
|
||||||
|
info!("Current version: {}", current_version);
|
||||||
|
info!("Github version: {}", published_at);
|
||||||
|
drop(current_version); // Explicitly unlock, since we also acquire a lock in is_older_than_installed()
|
||||||
|
if release_to_apply.as_ref().unwrap().is_older_than_installed() {
|
||||||
|
release_to_apply = Err(anyhow!(
|
||||||
|
"Update is older than the current installed version.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform Update
|
||||||
|
match release_to_apply {
|
||||||
|
Ok(release) => {
|
||||||
|
if user_wants_to_install() {
|
||||||
|
info!("Installing update: {}", &release.to_string());
|
||||||
|
if let Err(e) = release.install() {
|
||||||
|
error!("Failed to install the update. Reason: {:?}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("User declined the update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Did not install update. Reason: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
src/lib.rs
17
src/lib.rs
|
@ -71,6 +71,7 @@ pub fn main() {
|
||||||
init_logger().unwrap();
|
init_logger().unwrap();
|
||||||
|
|
||||||
info!("Initialized.");
|
info!("Initialized.");
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
EVENT_QUEUE.push(Event::smash_open());
|
EVENT_QUEUE.push(Event::smash_open());
|
||||||
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
|
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
|
||||||
|
@ -99,11 +100,21 @@ pub fn main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Performing version check...");
|
|
||||||
release::version_check();
|
|
||||||
|
|
||||||
menu::load_from_file();
|
menu::load_from_file();
|
||||||
|
|
||||||
|
if !is_emulator() {
|
||||||
|
info!("Performing version check...");
|
||||||
|
let _updater = std::thread::Builder::new()
|
||||||
|
.stack_size(0x20000)
|
||||||
|
.spawn(move || {
|
||||||
|
release::perform_version_check();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let _result = _updater.join();
|
||||||
|
} else {
|
||||||
|
info!("Skipping version check because we are using an emulator");
|
||||||
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
|
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
|
||||||
notification("Open Menu".to_string(), MENU.menu_open.to_string(), 120);
|
notification("Open Menu".to_string(), MENU.menu_open.to_string(), 120);
|
||||||
|
|
|
@ -15,7 +15,10 @@ serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_repr = "0.1.8"
|
serde_repr = "0.1.8"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
bitflags_serde_shim = "0.2"
|
bitflags_serde_shim = "0.2"
|
||||||
|
skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" }
|
||||||
skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true }
|
skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git", optional = true }
|
||||||
|
toml = "0.5.9"
|
||||||
|
anyhow = "1.0.72"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smash"]
|
default = ["smash"]
|
||||||
|
|
146
training_mod_consts/src/config.rs
Normal file
146
training_mod_consts/src/config.rs
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
use crate::files::*;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use skyline::nn::time;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
/// Top level struct which represents the entirety of the modpack config
|
||||||
|
/// (Does not include in-game menu settings)
|
||||||
|
/// Each field here is a section of training_modpack.toml
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct TrainingModpackConfig {
|
||||||
|
pub update: UpdaterConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrainingModpackConfig {
|
||||||
|
pub fn new() -> TrainingModpackConfig {
|
||||||
|
TrainingModpackConfig {
|
||||||
|
update: UpdaterConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to load the config from file
|
||||||
|
pub fn load() -> Result<TrainingModpackConfig> {
|
||||||
|
if fs::metadata(TRAINING_MODPACK_TOML_PATH).is_ok() {
|
||||||
|
let toml_config_str = fs::read_to_string(TRAINING_MODPACK_TOML_PATH)?;
|
||||||
|
let parsed = toml::from_str::<TrainingModpackConfig>(&toml_config_str)?;
|
||||||
|
Ok(parsed)
|
||||||
|
} else {
|
||||||
|
Err(Error::from(ErrorKind::NotFound).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_or_create() -> Result<TrainingModpackConfig> {
|
||||||
|
match TrainingModpackConfig::load() {
|
||||||
|
Ok(c) => Ok(c),
|
||||||
|
Err(e)
|
||||||
|
if e.is::<Error>()
|
||||||
|
&& e.downcast_ref::<Error>().unwrap().kind() == ErrorKind::NotFound =>
|
||||||
|
{
|
||||||
|
TrainingModpackConfig::create_default()?;
|
||||||
|
TrainingModpackConfig::load()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Some other error, re-raise it
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a default config and saves to file
|
||||||
|
/// Returns Err if the file already exists
|
||||||
|
pub fn create_default() -> Result<()> {
|
||||||
|
if fs::metadata(TRAINING_MODPACK_TOML_PATH).is_ok() {
|
||||||
|
Err(Error::from(ErrorKind::AlreadyExists).into())
|
||||||
|
} else {
|
||||||
|
let default_config: TrainingModpackConfig = TrainingModpackConfig::new();
|
||||||
|
let contents = toml::to_string(&default_config)?;
|
||||||
|
fs::write(TRAINING_MODPACK_TOML_PATH, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_last_update_version(last_update_version: &str) -> Result<()> {
|
||||||
|
let mut config = TrainingModpackConfig::load()?;
|
||||||
|
config.update.last_update_version = last_update_version.to_string();
|
||||||
|
let contents = toml::to_string(&config)?;
|
||||||
|
fs::write(TRAINING_MODPACK_TOML_PATH, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Since we can't rely on most time based libraries, this is a seconds -> date/time string based on the `chrono` crates implementation
|
||||||
|
/// God bless blujay and Raytwo
|
||||||
|
/// https://github.com/Raytwo/ARCropolis/blob/9dc1d59d1e8a3dcac433b10a90bb5b3fabad6c00/src/logging.rs#L15-L49
|
||||||
|
fn format_time_string(seconds: u64) -> String {
|
||||||
|
let leapyear = |year| -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) };
|
||||||
|
|
||||||
|
static YEAR_TABLE: [[u64; 12]; 2] = [
|
||||||
|
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
|
||||||
|
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut year = 1970;
|
||||||
|
|
||||||
|
let seconds_in_day = seconds % 86400;
|
||||||
|
let mut day_number = seconds / 86400;
|
||||||
|
|
||||||
|
let sec = seconds_in_day % 60;
|
||||||
|
let min = (seconds_in_day % 3600) / 60;
|
||||||
|
let hours = seconds_in_day / 3600;
|
||||||
|
loop {
|
||||||
|
let year_length = if leapyear(year) { 366 } else { 365 };
|
||||||
|
|
||||||
|
if day_number >= year_length {
|
||||||
|
day_number -= year_length;
|
||||||
|
year += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut month = 0;
|
||||||
|
while day_number >= YEAR_TABLE[if leapyear(year) { 1 } else { 0 }][month] {
|
||||||
|
day_number -= YEAR_TABLE[if leapyear(year) { 1 } else { 0 }][month];
|
||||||
|
month += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||||
|
year,
|
||||||
|
month + 1,
|
||||||
|
day_number + 1,
|
||||||
|
hours,
|
||||||
|
min,
|
||||||
|
sec
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_utc() -> String {
|
||||||
|
unsafe {
|
||||||
|
time::Initialize();
|
||||||
|
}
|
||||||
|
let current_epoch_seconds = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards")
|
||||||
|
.as_secs();
|
||||||
|
format_time_string(current_epoch_seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Config section for the automatic updater
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct UpdaterConfig {
|
||||||
|
pub last_update_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdaterConfig {
|
||||||
|
pub fn default() -> UpdaterConfig {
|
||||||
|
UpdaterConfig {
|
||||||
|
last_update_version: now_utc(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,3 +12,4 @@ pub const LEGACY_MENU_OPTIONS_PATH: &str =
|
||||||
"sd:/ultimate/TrainingModpack/training_modpack_menu.json";
|
"sd:/ultimate/TrainingModpack/training_modpack_menu.json";
|
||||||
pub const MENU_DEFAULT_OPTIONS_PATH: &str =
|
pub const MENU_DEFAULT_OPTIONS_PATH: &str =
|
||||||
"sd:/ultimate/TrainingModpack/training_modpack_menu_defaults.conf";
|
"sd:/ultimate/TrainingModpack/training_modpack_menu_defaults.conf";
|
||||||
|
pub const UNPACK_PATH: &str = "sd:/";
|
||||||
|
|
|
@ -14,6 +14,8 @@ pub mod options;
|
||||||
pub use options::*;
|
pub use options::*;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
|
pub mod config;
|
||||||
|
pub use config::*;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
@ -87,6 +89,7 @@ pub struct TrainingModpackMenu {
|
||||||
pub input_record: ButtonConfig,
|
pub input_record: ButtonConfig,
|
||||||
pub input_playback: ButtonConfig,
|
pub input_playback: ButtonConfig,
|
||||||
pub recording_crop: OnOff,
|
pub recording_crop: OnOff,
|
||||||
|
pub update_policy: UpdatePolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
@ -191,6 +194,7 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
|
||||||
input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN),
|
input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN),
|
||||||
input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP),
|
input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP),
|
||||||
recording_crop: OnOff::On,
|
recording_crop: OnOff::On,
|
||||||
|
update_policy: UpdatePolicy::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
|
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
|
||||||
|
@ -774,6 +778,14 @@ pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu {
|
||||||
true,
|
true,
|
||||||
&(menu.hud as u32),
|
&(menu.hud as u32),
|
||||||
);
|
);
|
||||||
|
misc_tab.add_submenu_with_toggles::<UpdatePolicy>(
|
||||||
|
"Auto-Update".to_string(),
|
||||||
|
"update_policy".to_string(),
|
||||||
|
"Auto-Update: What type of Training Modpack updates to automatically apply. (CONSOLE ONLY)"
|
||||||
|
.to_string(),
|
||||||
|
true,
|
||||||
|
&(menu.update_policy as u32),
|
||||||
|
);
|
||||||
overall_menu.tabs.push(misc_tab);
|
overall_menu.tabs.push(misc_tab);
|
||||||
|
|
||||||
let mut input_tab = Tab {
|
let mut input_tab = Tab {
|
||||||
|
|
|
@ -1663,3 +1663,43 @@ impl fmt::Display for ButtonConfig {
|
||||||
|
|
||||||
extra_bitflag_impls! {ButtonConfig}
|
extra_bitflag_impls! {ButtonConfig}
|
||||||
impl_serde_for_bitflags!(ButtonConfig);
|
impl_serde_for_bitflags!(ButtonConfig);
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize_repr, Deserialize_repr,
|
||||||
|
)]
|
||||||
|
pub enum UpdatePolicy {
|
||||||
|
Stable,
|
||||||
|
Beta,
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdatePolicy {
|
||||||
|
pub const fn default() -> UpdatePolicy {
|
||||||
|
UpdatePolicy::Stable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for UpdatePolicy {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
|
UpdatePolicy::Stable => "Stable",
|
||||||
|
UpdatePolicy::Beta => "Beta",
|
||||||
|
UpdatePolicy::Disabled => "Disabled",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToggleTrait for UpdatePolicy {
|
||||||
|
fn to_toggle_vals() -> Vec<u32> {
|
||||||
|
UpdatePolicy::iter().map(|i| i as u32).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_toggle_strings() -> Vec<String> {
|
||||||
|
UpdatePolicy::iter().map(|i| i.to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue