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"] }
|
||||
log = "0.4.17"
|
||||
byte-unit = "4.0.18"
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"]}
|
||||
anyhow = "1.0.72"
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
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(),
|
||||
event_time,
|
||||
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(),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::common::consts::*;
|
|||
pub mod button_config;
|
||||
pub mod consts;
|
||||
pub mod dev_config;
|
||||
pub mod dialog;
|
||||
pub mod events;
|
||||
pub mod input;
|
||||
pub mod menu;
|
||||
|
|
|
@ -1,74 +1,224 @@
|
|||
use std::fs;
|
||||
|
||||
use skyline_web::dialog_ok::DialogOk;
|
||||
|
||||
use crate::consts::{
|
||||
LEGACY_MENU_OPTIONS_PATH, MENU_DEFAULT_OPTIONS_PATH, MENU_OPTIONS_PATH, VERSION_TXT_PATH,
|
||||
};
|
||||
#![allow(clippy::unnecessary_unwrap)]
|
||||
use crate::consts::*;
|
||||
use crate::dialog;
|
||||
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");
|
||||
|
||||
enum VersionCheck {
|
||||
Current,
|
||||
NoFile,
|
||||
Update,
|
||||
lazy_static! {
|
||||
pub static ref CURRENT_VERSION: Mutex<String> =
|
||||
Mutex::new(get_current_version().expect("Could not determine current version!"));
|
||||
}
|
||||
|
||||
fn is_current_version(fpath: &str) -> VersionCheck {
|
||||
// Create a blank version file if it doesn't exists
|
||||
if fs::metadata(fpath).is_err() {
|
||||
fs::File::create(fpath).expect("Could not create version file!");
|
||||
return VersionCheck::NoFile;
|
||||
}
|
||||
|
||||
if fs::read_to_string(fpath).unwrap_or_else(|_| "".to_string()) == CURRENT_VERSION {
|
||||
VersionCheck::Current
|
||||
} else {
|
||||
VersionCheck::Update
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Release {
|
||||
pub url: String,
|
||||
pub tag: String,
|
||||
pub published_at: String,
|
||||
}
|
||||
|
||||
fn record_current_version(fpath: &str) {
|
||||
// Write the current version to the version file
|
||||
fs::write(fpath, CURRENT_VERSION).expect("Could not record current version!")
|
||||
}
|
||||
|
||||
pub fn version_check() {
|
||||
match is_current_version(VERSION_TXT_PATH) {
|
||||
VersionCheck::Current => {
|
||||
// Version is current, no need to take any action
|
||||
}
|
||||
VersionCheck::Update => {
|
||||
// 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."
|
||||
)
|
||||
impl Release {
|
||||
/// Downloads and installs the release
|
||||
pub fn install(self: &Release) -> Result<()> {
|
||||
info!("Installing asset from URL: {}", &self.url);
|
||||
let response = minreq::get(&self.url)
|
||||
.with_header("User-Agent", "UltimateTrainingModpack")
|
||||
.with_header("Accept", "application/octet-stream")
|
||||
.send_lazy()?;
|
||||
info!(
|
||||
"Ok response from Github. Status Code: {}",
|
||||
&response.status_code
|
||||
);
|
||||
// Remove old menu selections, silently ignoring errors (i.e. if the file doesn't exist)
|
||||
[
|
||||
MENU_OPTIONS_PATH,
|
||||
MENU_DEFAULT_OPTIONS_PATH,
|
||||
LEGACY_MENU_OPTIONS_PATH,
|
||||
]
|
||||
.iter()
|
||||
.for_each(|path| {
|
||||
fs::remove_file(path).unwrap_or_else(|_| error!("Couldn't remove {path}"))
|
||||
let mut vec = Vec::new();
|
||||
for result in response {
|
||||
let (byte, length) = result?;
|
||||
vec.reserve(length);
|
||||
vec.push(byte);
|
||||
}
|
||||
info!("Finished receiving .zip file from GitHub.");
|
||||
info!("Unpacking .zip file...");
|
||||
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 => {
|
||||
// Display dialog box on fresh installation
|
||||
DialogOk::ok(
|
||||
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_release.is_some() && stable_release.is_some() {
|
||||
// Don't iterate needlessly, we already found both releases
|
||||
break;
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
info!("Initialized.");
|
||||
|
||||
unsafe {
|
||||
EVENT_QUEUE.push(Event::smash_open());
|
||||
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();
|
||||
|
||||
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 {
|
||||
notification("Training Modpack".to_string(), "Welcome!".to_string(), 60);
|
||||
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_json = "1"
|
||||
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 }
|
||||
toml = "0.5.9"
|
||||
anyhow = "1.0.72"
|
||||
|
||||
[features]
|
||||
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";
|
||||
pub const MENU_DEFAULT_OPTIONS_PATH: &str =
|
||||
"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 mod files;
|
||||
pub use files::*;
|
||||
pub mod config;
|
||||
pub use config::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||
|
@ -87,6 +89,7 @@ pub struct TrainingModpackMenu {
|
|||
pub input_record: ButtonConfig,
|
||||
pub input_playback: ButtonConfig,
|
||||
pub recording_crop: OnOff,
|
||||
pub update_policy: UpdatePolicy,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
@ -191,6 +194,7 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
|
|||
input_record: ButtonConfig::ZR.union(ButtonConfig::DPAD_DOWN),
|
||||
input_playback: ButtonConfig::ZR.union(ButtonConfig::DPAD_UP),
|
||||
recording_crop: OnOff::On,
|
||||
update_policy: UpdatePolicy::default(),
|
||||
};
|
||||
|
||||
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
|
||||
|
@ -774,6 +778,14 @@ pub unsafe fn ui_menu(menu: TrainingModpackMenu) -> UiMenu {
|
|||
true,
|
||||
&(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);
|
||||
|
||||
let mut input_tab = Tab {
|
||||
|
|
|
@ -1663,3 +1663,43 @@ impl fmt::Display for ButtonConfig {
|
|||
|
||||
extra_bitflag_impls! {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