Initial implementation - still very much WIP
This commit is contained in:
parent
a544050b22
commit
90c3246f3e
8 changed files with 4159 additions and 0 deletions
1365
Cargo.lock
generated
Normal file
1365
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "ikamaki"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.6", features = ["derive"] }
|
||||
rand = "0.10"
|
||||
roead = { version = "1.0", default-features = false, features = ["byml", "yaml"] }
|
||||
BIN
VSSetting.byaml
Normal file
BIN
VSSetting.byaml
Normal file
Binary file not shown.
2364
VSSetting.yml
Normal file
2364
VSSetting.yml
Normal file
File diff suppressed because it is too large
Load diff
261
src/exporter.rs
Normal file
261
src/exporter.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
use crate::schedule::Schedule;
|
||||
use roead::Endian;
|
||||
use roead::byml::Byml;
|
||||
|
||||
/*
|
||||
Fixes the formatting in the human readable YAML.
|
||||
Not the prettiest, but it works.
|
||||
*/
|
||||
fn fix_yaml(byml: &Byml) -> Byml {
|
||||
match byml {
|
||||
Byml::I32(v) => Byml::I64(*v as i64),
|
||||
Byml::Array(arr) => Byml::Array(arr.iter().map(fix_yaml).collect()),
|
||||
Byml::Map(map) => {
|
||||
let mut new_map = roead::byml::Map::default();
|
||||
new_map.reserve(map.len());
|
||||
for (k, v) in map.iter() {
|
||||
new_map.insert(k.clone(), fix_yaml(v));
|
||||
}
|
||||
Byml::Map(new_map)
|
||||
}
|
||||
_ => byml.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
const MAP_FIRST_APPEAR: [(&str, i32); 16] = [
|
||||
("2015-04-17", 0),
|
||||
("2015-04-17", 1),
|
||||
("2015-04-17", 2),
|
||||
("2015-04-17", 3),
|
||||
("2015-04-17", 4),
|
||||
("2015-06-02", 6),
|
||||
("2015-06-11", 7),
|
||||
("2015-06-20", 9),
|
||||
("2015-07-11", 8),
|
||||
("2015-07-25", 5),
|
||||
("2015-08-21", 11),
|
||||
("2015-09-18", 10),
|
||||
("2015-11-14", 12),
|
||||
("2015-12-04", 15),
|
||||
("2015-12-29", 14),
|
||||
("2016-01-22", 13),
|
||||
];
|
||||
|
||||
const RULE_FIRST_APPEAR: [(&str, &str); 3] = [
|
||||
("2015-06-02", "cVar"),
|
||||
("2015-07-02", "cVlf"),
|
||||
("2015-08-15", "cVgl"),
|
||||
];
|
||||
|
||||
const WEAPON_UNLOCK: [(&str, i32); 58] = [
|
||||
("2015-06-02", 1060),
|
||||
("2015-06-06", 2030),
|
||||
("2015-06-13", 1020),
|
||||
("2015-06-17", 1160),
|
||||
("2015-06-17", 4031),
|
||||
("2015-06-24", 1110),
|
||||
("2015-06-27", 2000),
|
||||
("2015-06-27", 1091),
|
||||
("2015-07-08", 1000),
|
||||
("2015-07-08", 1081),
|
||||
("2015-07-18", 2040),
|
||||
("2015-07-18", 1061),
|
||||
("2015-07-22", 4040),
|
||||
("2015-07-22", 1021),
|
||||
("2015-08-01", 1130),
|
||||
("2015-08-01", 2031),
|
||||
("2015-08-06", 3000),
|
||||
("2015-08-06", 5010),
|
||||
("2015-08-29", 4050),
|
||||
("2015-08-29", 1161),
|
||||
("2015-09-05", 5000),
|
||||
("2015-09-12", 1170),
|
||||
("2015-09-26", 3010),
|
||||
("2015-10-03", 2001),
|
||||
("2015-10-10", 1131),
|
||||
("2015-10-10", 4041),
|
||||
("2015-10-17", 1150),
|
||||
("2015-10-30", 1111),
|
||||
("2015-10-30", 1171),
|
||||
("2015-11-07", 5011),
|
||||
("2015-11-11", 1001),
|
||||
("2015-11-18", 4051),
|
||||
("2015-11-21", 5020),
|
||||
("2015-11-25", 3001),
|
||||
("2015-11-28", 3020),
|
||||
("2015-12-12", 5001),
|
||||
("2015-12-19", 3011),
|
||||
("2015-12-25", 1151),
|
||||
("'2016-01-01T19:00:00+09:00'", 2041),
|
||||
("2016-01-09", 3021),
|
||||
("2016-01-16", 5021),
|
||||
("'2016-04-13T11:00:00+09:00'", 1042),
|
||||
("'2016-04-13T11:00:00+09:00'", 1072),
|
||||
("'2016-04-13T11:00:00+09:00'", 2022),
|
||||
("'2016-04-13T11:00:00+09:00'", 2032),
|
||||
("'2016-04-13T11:00:00+09:00'", 3002),
|
||||
("'2016-04-13T11:00:00+09:00'", 4002),
|
||||
("'2016-04-13T11:00:00+09:00'", 4052),
|
||||
("'2016-04-13T11:00:00+09:00'", 5002),
|
||||
("'2016-06-08T11:00:00+09:00'", 1002),
|
||||
("'2016-06-08T11:00:00+09:00'", 1032),
|
||||
("'2016-06-08T11:00:00+09:00'", 1062),
|
||||
("'2016-06-08T11:00:00+09:00'", 1132),
|
||||
("'2016-06-08T11:00:00+09:00'", 1172),
|
||||
("'2016-06-08T11:00:00+09:00'", 2012),
|
||||
("'2016-06-08T11:00:00+09:00'", 4012),
|
||||
("'2016-06-08T11:00:00+09:00'", 4022),
|
||||
("'2016-06-08T11:00:00+09:00'", 5012),
|
||||
];
|
||||
|
||||
const HEADER: &str = "Version: 1\n\
|
||||
IsBigEndian: True\n\
|
||||
SupportPaths: False\n\
|
||||
HasReferenceNodes: False\n\
|
||||
root:\n";
|
||||
|
||||
fn build_map_first_appear() -> Byml {
|
||||
Byml::Array(
|
||||
MAP_FIRST_APPEAR
|
||||
.iter()
|
||||
.map(|(date, id)| {
|
||||
let mut map = roead::byml::Map::default();
|
||||
map.insert("Date".into(), Byml::String((*date).into()));
|
||||
map.insert("MapID".into(), Byml::I32(*id));
|
||||
Byml::Map(map)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_rule_first_appear() -> Byml {
|
||||
Byml::Array(
|
||||
RULE_FIRST_APPEAR
|
||||
.iter()
|
||||
.map(|(date, rule)| {
|
||||
let mut map = roead::byml::Map::default();
|
||||
map.insert("Date".into(), Byml::String((*date).into()));
|
||||
map.insert("GachiRule".into(), Byml::String((*rule).into()));
|
||||
Byml::Map(map)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_weapon_unlock() -> Byml {
|
||||
Byml::Array(
|
||||
WEAPON_UNLOCK
|
||||
.iter()
|
||||
.map(|(date, id)| {
|
||||
let mut map = roead::byml::Map::default();
|
||||
map.insert("Date".into(), Byml::String((*date).into()));
|
||||
map.insert("WeaponSetID".into(), Byml::I32(*id));
|
||||
Byml::Map(map)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_byml(schedule: &Schedule, hash_kick: bool) -> Byml {
|
||||
let mut root = roead::byml::Map::default();
|
||||
|
||||
root.insert("AddFirstMatchingTime".into(), Byml::I32(30));
|
||||
root.insert("AddMatchingTime".into(), Byml::I32(0));
|
||||
root.insert(
|
||||
"AfterFesBonusStart".into(),
|
||||
Byml::String("2016-07-24T19:00:00+09:00".into()),
|
||||
);
|
||||
root.insert("BottleneckThreasholdFrame".into(), Byml::I32(480));
|
||||
let aligned = crate::default_timestamp();
|
||||
let date_time = chrono::DateTime::from_timestamp(aligned, 0)
|
||||
.unwrap()
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
root.insert("DateTime".into(), Byml::String(date_time.into()));
|
||||
root.insert("DisconnectByMemoryHash".into(), Byml::Bool(hash_kick));
|
||||
root.insert("TimeoutAfterJoin".into(), Byml::I32(120));
|
||||
root.insert("Version".into(), Byml::I32(3));
|
||||
root.insert("WaitMatchingTime".into(), Byml::I32(25));
|
||||
root.insert("WebPost".into(), Byml::Bool(true));
|
||||
|
||||
root.insert("MapFirstAppear".into(), build_map_first_appear());
|
||||
root.insert("RuleFirstAppear".into(), build_rule_first_appear());
|
||||
root.insert("WeaponUnlock".into(), build_weapon_unlock());
|
||||
|
||||
let phases_array: Vec<Byml> = schedule
|
||||
.phases
|
||||
.iter()
|
||||
.map(|phase| {
|
||||
let mut dict = roead::byml::Map::default();
|
||||
|
||||
let duration_hours = ((phase.end - phase.start) / 3600) as i32;
|
||||
dict.insert("Time".into(), Byml::I32(duration_hours));
|
||||
|
||||
let regular_stages: Vec<Byml> = phase
|
||||
.regular
|
||||
.iter()
|
||||
.map(|&m| {
|
||||
let mut stage_map = roead::byml::Map::default();
|
||||
stage_map.insert("MapID".into(), Byml::I32(m));
|
||||
Byml::Map(stage_map)
|
||||
})
|
||||
.collect();
|
||||
dict.insert("RegularStages".into(), Byml::Array(regular_stages));
|
||||
|
||||
let gachi_stages: Vec<Byml> = phase
|
||||
.gachi
|
||||
.iter()
|
||||
.map(|&m| {
|
||||
let mut stage_map = roead::byml::Map::default();
|
||||
stage_map.insert("MapID".into(), Byml::I32(m));
|
||||
Byml::Map(stage_map)
|
||||
})
|
||||
.collect();
|
||||
dict.insert("GachiStages".into(), Byml::Array(gachi_stages));
|
||||
|
||||
dict.insert(
|
||||
"RegularRule".into(),
|
||||
Byml::String(phase.regular_rule.clone().into()),
|
||||
);
|
||||
dict.insert(
|
||||
"GachiRule".into(),
|
||||
Byml::String(phase.gachi_rule.clone().into()),
|
||||
);
|
||||
|
||||
Byml::Map(dict)
|
||||
})
|
||||
.collect();
|
||||
|
||||
root.insert("Phases".into(), Byml::Array(phases_array));
|
||||
|
||||
Byml::Map(root)
|
||||
}
|
||||
|
||||
// Export the BYAML file
|
||||
pub fn export(
|
||||
schedule: &Schedule,
|
||||
path: &str,
|
||||
hash_kick: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let byml = build_byml(schedule, hash_kick);
|
||||
// Version 1 is being used on console
|
||||
let bytes = byml.to_binary_with_version(Endian::Big, 1);
|
||||
std::fs::write(path, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Export the human-readable YAML file
|
||||
pub fn export_text(
|
||||
schedule: &Schedule,
|
||||
path: &str,
|
||||
hash_kick: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let body = fix_yaml(&build_byml(schedule, hash_kick)).to_text();
|
||||
let mut out = String::from(HEADER);
|
||||
for line in body.lines() {
|
||||
out.push_str(" ");
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
std::fs::write(path, out)?;
|
||||
Ok(())
|
||||
}
|
||||
62
src/generator.rs
Normal file
62
src/generator.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use rand::Rng;
|
||||
use rand::seq::SliceRandom;
|
||||
use crate::schedule::{Rotation, Schedule};
|
||||
|
||||
// Splatoon Map IDs
|
||||
const MAP_IDS: &[i32] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
|
||||
// Ranked modes
|
||||
const RULES: &[&str] = &["cVar", "cVlf", "cVgl"];
|
||||
|
||||
// Duration of a rotation
|
||||
const PHASE_DURATION: i64 = 2 * 60 * 60;
|
||||
|
||||
// Generates the rotation schedule
|
||||
pub fn generate(count: usize, start_time: i64) -> Schedule {
|
||||
let mut rng = rand::rng();
|
||||
let mut phases = Vec::with_capacity(count);
|
||||
|
||||
let mut prev_regular: Vec<i32> = Vec::new();
|
||||
let mut prev_gachi: Vec<i32> = Vec::new();
|
||||
|
||||
for i in 0..count {
|
||||
let start = start_time + (i as i64 * PHASE_DURATION);
|
||||
let end = start + PHASE_DURATION;
|
||||
|
||||
let regular = pick_maps(&mut rng, MAP_IDS, &prev_regular);
|
||||
let gachi = pick_maps(&mut rng, MAP_IDS, &prev_gachi);
|
||||
let gachi_rule = RULES[i % RULES.len()].to_string();
|
||||
|
||||
prev_regular = regular.clone();
|
||||
prev_gachi = gachi.clone();
|
||||
|
||||
phases.push(Rotation {
|
||||
start,
|
||||
end,
|
||||
regular,
|
||||
gachi,
|
||||
regular_rule: "cPnt".to_string(),
|
||||
gachi_rule,
|
||||
});
|
||||
}
|
||||
|
||||
Schedule { phases }
|
||||
}
|
||||
|
||||
// Pick two maps from the pool, avoid any maps present in the previous rotation
|
||||
fn pick_maps<R: Rng>(rng: &mut R, pool: &[i32], prev: &[i32]) -> Vec<i32> {
|
||||
let filtered: Vec<i32> = pool
|
||||
.iter()
|
||||
.filter(|&&m| !prev.contains(&m))
|
||||
.copied()
|
||||
.collect();
|
||||
let source = if filtered.len() >= 2 {
|
||||
filtered
|
||||
} else {
|
||||
pool.to_vec()
|
||||
};
|
||||
|
||||
let mut shuffled = source;
|
||||
shuffled.shuffle(rng);
|
||||
shuffled.into_iter().take(2).collect()
|
||||
}
|
||||
79
src/main.rs
Normal file
79
src/main.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use std::path::Path;
|
||||
|
||||
mod exporter;
|
||||
mod generator;
|
||||
mod schedule;
|
||||
|
||||
// 3 weeks by default
|
||||
const DEFAULT: usize = 21 * 24 / 2;
|
||||
|
||||
// If no start time is specified, use the last even hour
|
||||
pub fn default_timestamp() -> i64 {
|
||||
let now = Utc::now().timestamp();
|
||||
let block = 2 * 60 * 60;
|
||||
now - (now % block)
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "ikamaki")]
|
||||
struct Args {
|
||||
// How many rotations to generate
|
||||
#[arg(short, long)]
|
||||
count: Option<usize>,
|
||||
|
||||
// UNIX timestamp to start the schedule from
|
||||
#[arg(short, long)]
|
||||
start: Option<i64>,
|
||||
|
||||
// Output file path
|
||||
#[arg(short, long, default_value = "VSSetting.byaml")]
|
||||
output: String,
|
||||
|
||||
// Generate YAML in addition to the BYAML
|
||||
#[arg(long)]
|
||||
yml: bool,
|
||||
|
||||
// Set DisconnectByMemoryHash to true
|
||||
#[arg(long)]
|
||||
hash_kick: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
let count = args.count.unwrap_or(DEFAULT);
|
||||
let start_time = args.start.unwrap_or_else(default_timestamp);
|
||||
let schedule = generator::generate(count, start_time);
|
||||
exporter::export(&schedule, &args.output, args.hash_kick)?;
|
||||
|
||||
if args.yml {
|
||||
let yml_path = derive_yml_path(&args.output);
|
||||
exporter::export_text(&schedule, &yml_path, args.hash_kick)?;
|
||||
println!(
|
||||
"Successfully generated {} rotation(s) -> {} + {}",
|
||||
count, args.output, yml_path
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Successfully generated {} rotation(s) -> {}",
|
||||
count, args.output
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn derive_yml_path(output: &str) -> String {
|
||||
let path = Path::new(output);
|
||||
if let Some(stem) = path.file_stem() {
|
||||
if let Some(parent) = path.parent() {
|
||||
return parent
|
||||
.join(format!("{}.yml", stem.to_string_lossy()))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
}
|
||||
return format!("{}.yml", stem.to_string_lossy());
|
||||
}
|
||||
format!("{}.yml", output)
|
||||
}
|
||||
18
src/schedule.rs
Normal file
18
src/schedule.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
pub struct Rotation {
|
||||
// UNIX timestamp for the start
|
||||
pub start: i64,
|
||||
// UNIX timestamp for the end
|
||||
pub end: i64,
|
||||
// Map IDs for Turf
|
||||
pub regular: Vec<i32>,
|
||||
// Map IDs for Ranked
|
||||
pub gachi: Vec<i32>,
|
||||
// Regular isn't hardcoded to Turf War for some reason
|
||||
pub regular_rule: String,
|
||||
// Ranked Mode (cVar, cVlf, cVgl)
|
||||
pub gachi_rule: String,
|
||||
}
|
||||
|
||||
pub struct Schedule {
|
||||
pub phases: Vec<Rotation>,
|
||||
}
|
||||
Loading…
Reference in a new issue