diff options
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | make_table.py | 19 | ||||
-rw-r--r-- | src/api/legends.rs | 23 | ||||
-rw-r--r-- | src/api/mod.rs | 11 | ||||
-rw-r--r-- | src/bt.rs | 129 | ||||
-rw-r--r-- | src/main.rs | 79 | ||||
-rw-r--r-- | src/skill_palette.json | 1 |
7 files changed, 248 insertions, 15 deletions
@@ -18,3 +18,4 @@ xdg = "2.2.0" quick-error = "1.2.0" itertools = "0.8.0" md5 = "0.7" +base64 = "0.11" diff --git a/make_table.py b/make_table.py new file mode 100644 index 0000000..18a30fa --- /dev/null +++ b/make_table.py @@ -0,0 +1,19 @@ +import requests +import json +from lxml import html, etree + +data = requests.get("https://wiki.guildwars2.com/wiki/Chat_link_format") +parsed = html.fromstring(data.content) +body = parsed.find(".//table") +iterator = iter(body) +next(iterator) +result = [] +for row in iterator: + if "-" in row[3].text: + continue + ids = row[3].text.strip().split(";") + palette_id = int(row[4].text) + for skill_id in ids: + skill_id = int(skill_id) + result.append((skill_id, palette_id)) +print(json.dumps(result)) diff --git a/src/api/legends.rs b/src/api/legends.rs new file mode 100644 index 0000000..395d6b9 --- /dev/null +++ b/src/api/legends.rs @@ -0,0 +1,23 @@ +//! Struct definitions for the legends API endpoint. +//! +//! * [Example](https://api.guildwars2.com/v2/legends/Legend2) +//! * [Wiki](https://wiki.guildwars2.com/wiki/API:2/legends) + +use super::HasId; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Legend { + /// The legend id. + pub id: String, + pub heal: u32, + pub elite: u32, + pub utilities: Vec<u32>, +} + +impl HasId for Legend { + type Id = String; + fn get_id(&self) -> String { + self.id.clone() + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 6e1b457..41034ec 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,13 +3,15 @@ //! It contains `Deserialize`able definitions for the required API responses, as well as a wrapper //! around the HTTP library. Note that only the required fields are modelled here, which means this //! does not provide a full-featured mapping for all API types. +pub mod legends; pub mod professions; pub mod skills; pub mod specializations; pub mod traits; pub use self::{ - professions::Profession, skills::Skill, specializations::Specialization, traits::Trait, + legends::Legend, professions::Profession, skills::Skill, specializations::Specialization, + traits::Trait, }; use image::DynamicImage; @@ -187,6 +189,13 @@ impl Api { self.get_multiple_cached("traits", "traits/", ids) } + /// Retrieve detailed information about the given legends. + /// + /// Traits that are found in the cache are taken from there. + pub fn get_legends(&mut self, ids: &[String]) -> Result<Vec<Legend>, ApiError> { + self.get_multiple_cached("legends", "legends/", ids) + } + /// Loads the image from the given URL. /// /// This automatically caches and also decodes the resulting data. @@ -68,6 +68,60 @@ impl FromStr for TraitChoice { } } +/// Represents a revenenant legend. +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +pub enum Legend { + None = 0, + Dragon = 13, + Assassin = 14, + Dwarf = 15, + Demon = 16, + Renegade = 17, + Centaur = 18, +} + +impl Legend { + pub fn get_api_id(self) -> Option<String> { + Some( + match self { + Legend::None => return None, + Legend::Dragon => "Legend1", + Legend::Assassin => "Legend2", + Legend::Dwarf => "Legend3", + Legend::Demon => "Legend4", + Legend::Renegade => "Legend5", + Legend::Centaur => "Legend6", + } + .to_owned(), + ) + } +} + +impl FromStr for Legend { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let lower = s.to_lowercase(); + match &lower as &str { + "" | "none" => Ok(Legend::None), + "dragon" | "glint" => Ok(Legend::Dragon), + "assassin" | "shiro" => Ok(Legend::Assassin), + "dwarf" | "jalis" => Ok(Legend::Dwarf), + "demon" | "mallyx" => Ok(Legend::Demon), + "renegade" | "kalla" => Ok(Legend::Renegade), + "centaur" | "ventari" => Ok(Legend::Centaur), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone)] +pub enum ExtraData { + None, + Legends([Legend; 4]), +} + /// Represents a traitline. /// /// A traitline consists of the chosen specialization including 3 possible trait choices. @@ -75,6 +129,7 @@ pub type Traitline = (Specialization, [TraitChoice; 3]); pub const SKILL_COUNT: usize = 5; pub const TRAITLINE_COUNT: usize = 3; +pub const LEGEND_COUNT: usize = 4; /// Represents a build template. /// @@ -92,6 +147,8 @@ pub struct BuildTemplate { skills: [Option<Skill>; SKILL_COUNT], /// The traitlines of the build. traitlines: [Option<Traitline>; TRAITLINE_COUNT], + /// Extra data, such as revenant legends or ranger pets. + extra_data: ExtraData, } impl BuildTemplate { @@ -101,6 +158,7 @@ impl BuildTemplate { profession, skills: [None, None, None, None, None], traitlines: [None, None, None], + extra_data: ExtraData::None, } } @@ -111,6 +169,7 @@ impl BuildTemplate { profession: Profession, skills: &[Skill], traitlines: &[Traitline], + extra_data: ExtraData, ) -> Option<BuildTemplate> { if skills.len() > SKILL_COUNT { return None; @@ -130,6 +189,7 @@ impl BuildTemplate { profession, skills: skill_array, traitlines: trait_array, + extra_data, }) } @@ -157,4 +217,73 @@ impl BuildTemplate { pub fn traitline_count(&self) -> u32 { self.traitlines.iter().filter(|x| x.is_some()).count() as u32 } + + pub fn extra_data(&self) -> &ExtraData { + &self.extra_data + } + + pub fn chatlink(&self) -> String { + let mut bytes = vec![0x0Du8]; + let prof_byte = self.profession() as u8; + bytes.push(prof_byte); + + for traitline in self.traitlines().iter() { + if let Some((spec, choices)) = traitline { + bytes.push(spec.id as u8); + let selected = choices[0] as u8 | (choices[1] as u8) << 2 | (choices[2] as u8) << 4; + bytes.push(selected); + } else { + bytes.push(0); + bytes.push(0); + } + } + + for skill in self.skills() { + // Terrestric + match skill { + None => { + bytes.push(0); + bytes.push(0); + } + Some(s) => { + let palette_id = skill_id_to_palette_id(s.id); + bytes.push((palette_id & 0xFF) as u8); + bytes.push(((palette_id >> 8) & 0xFF) as u8); + } + } + // Aquatic + bytes.push(0); + bytes.push(0); + } + + match *self.extra_data() { + ExtraData::None => { + bytes.extend_from_slice(&[0, 0, 0, 0]); + } + ExtraData::Legends(ref legends) => { + bytes.extend(legends.iter().map(|l| *l as u8)); + } + } + + // Weird padding, achieved by looking at other chat links. + for _ in 0..12 { + bytes.push(0); + } + + println!("Length: {}", bytes.len()); + println!("{:?}", bytes); + format!("[&{}]", base64::encode(&bytes)) + } +} + +static JSON_PALETTE: &'static str = include_str!("skill_palette.json"); + +fn skill_id_to_palette_id(input: u32) -> u32 { + let lookup_table: Vec<(u32, u32)> = serde_json::from_str(JSON_PALETTE).unwrap(); + for (skill, palette) in &lookup_table { + if *skill == input { + return *palette; + } + } + 0 } diff --git a/src/main.rs b/src/main.rs index 9ffa1eb..36d7a21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +extern crate base64; extern crate clap; extern crate image; extern crate imageproc; @@ -20,7 +21,7 @@ mod render; use clap::{App, Arg, ArgMatches}; use api::{Api, Profession, Skill}; -use bt::{BuildTemplate, TraitChoice, Traitline}; +use bt::{BuildTemplate, ExtraData, Legend, TraitChoice, Traitline}; const APP_NAME: &str = "kondou"; @@ -121,14 +122,51 @@ fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplat .expect("clap handles missing argument"); let profession = find_profession(api, requested_profession)?; + let prof_enum = profession + .name + .parse() + .expect("Profession object has unparseable name"); - let skills = matches - .values_of("skill") + let legends = matches + .values_of("legend") .map(Iterator::collect::<Vec<_>>) .unwrap_or_default() .into_iter() - .map(|s| resolve_skill(api, &profession, s)) - .collect::<Result<Vec<_>, _>>()?; + .map(str::parse) + .map(Result::unwrap) + .collect::<Vec<_>>(); + + let extra_data = if prof_enum == bt::Profession::Revenant { + let mut array_legends = [Legend::None; 4]; + for (i, l) in legends.iter().enumerate() { + array_legends[i] = *l; + } + ExtraData::Legends(array_legends) + } else { + ExtraData::None + }; + + let skills = if prof_enum != bt::Profession::Revenant { + matches + .values_of("skill") + .map(Iterator::collect::<Vec<_>>) + .unwrap_or_default() + .into_iter() + .map(|s| resolve_skill(api, &profession, s)) + .collect::<Result<Vec<_>, _>>()? + } else { + if let Some(l) = legends.first() { + let l = api.get_legends(&[l.get_api_id().unwrap()])?.remove(0); + let mut result = Vec::new(); + for skill_id in (&[l.heal]).iter().chain(&l.utilities).chain(&[l.elite]) { + let skill = api.get_skills(&[*skill_id])?.remove(0); + result.push(skill); + } + result + } else { + Vec::new() + } + }; let traitlines = matches .values_of("traitline") @@ -144,15 +182,8 @@ fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplat "got too many traitlines" ); - let build = BuildTemplate::new( - profession - .name - .parse() - .expect("Profession object has unparseable name"), - &skills, - &traitlines, - ) - .expect("BuildTemplate could not be constructed"); + let build = BuildTemplate::new(prof_enum, &skills, &traitlines, extra_data) + .expect("BuildTemplate could not be constructed"); Ok(build) } @@ -176,6 +207,13 @@ fn validate_traitline_format(input: String) -> Result<(), String> { Ok(()) } +fn validate_legend(input: String) -> Result<(), String> { + input + .parse::<Legend>() + .map(|_| ()) + .map_err(|_| "invalid legend name".to_owned()) +} + fn run() -> MainResult<()> { let matches = App::new(APP_NAME) .version("0.1") @@ -208,6 +246,17 @@ fn run() -> MainResult<()> { .max_values(bt::TRAITLINE_COUNT as u64), ) .arg( + Arg::with_name("legend") + .help("Selects a revenant legend.") + .takes_value(true) + .number_of_values(1) + .long("legend") + .short("l") + .multiple(true) + .max_values(bt::LEGEND_COUNT as u64) + .validator(validate_legend), + ) + .arg( Arg::with_name("chatlink") .help("Specifies a chat link to parse.") .short("c") @@ -231,6 +280,8 @@ fn run() -> MainResult<()> { true => unimplemented!(), }; + println!("Chat code: {}", build.chatlink()); + let mut renderer = render::Renderer::new(&mut api, Default::default()); let img = renderer.render_buildtemplate(&build).unwrap(); let filename = matches.value_of("outfile").unwrap(); diff --git a/src/skill_palette.json b/src/skill_palette.json new file mode 100644 index 0000000..62e7200 --- /dev/null +++ b/src/skill_palette.json @@ -0,0 +1 @@ +[[14402, 166], [21815, 3881], [14389, 112], [14401, 167], [30189, 4850], [41100, 5959], [14409, 174], [14403, 168], [14575, 482], [14372, 106], [14412, 178], [14528, 429], [14407, 172], [14405, 170], [14408, 176], [14406, 171], [14516, 173], [14413, 179], [14392, 113], [14368, 105], [14502, 418], [14410, 175], [14404, 169], [14479, 317], [14388, 110], [14354, 10], [30258, 4823], [30074, 4828], [29613, 4769], [29941, 4804], [43123, 5671], [45380, 5904], [41919, 5738], [43745, 5750], [14419, 238], [14483, 380], [14355, 156], [30343, 4802], [45333, 5789], [9083, 127], [21664, 3878], [9102, 259], [9158, 312], [30025, 4796], [41714, 5963], [9152, 309], [9084, 301], [9085, 138], [9153, 310], [9093, 254], [9241, 254], [9175, 329], [9248, 260], [9253, 331], [9125, 256], [9247, 327], [9246, 441], [9187, 332], [9128, 278], [9182, 330], [9242, 326], [9150, 326], [9163, 306], [9243, 306], [9151, 305], [9244, 305], [9245, 376], [9168, 328], [9251, 255], [30553, 4740], [30871, 4858], [30364, 4746], [29786, 4862], [46148, 5909], [45460, 5971], [40915, 5754], [44266, 5754], [44080, 5827], [29965, 4745], [9154, 311], [30461, 4721], [30273, 4789], [43357, 5656], [26937, 4572], [29148, 4572], [28219, 4572], [27220, 4572], [27372, 4572], [56661, 4572], [45686, 4572], [27107, 4564], [29209, 4614], [28231, 4651], [26821, 4614], [29310, 4614], [27025, 4651], [29082, 4651], [27715, 4564], [29197, 4564], [27917, 4564], [31100, 4564], [27322, 4614], [27505, 4651], [26644, 4564], [28379, 4614], [27014, 4651], [26557, 4564], [56752, 4564], [28516, 4614], [56841, 4614], [26679, 4651], [56662, 4651], [41220, 4564], [42949, 4614], [40485, 4651], [28406, 4554], [31294, 4554], [27356, 4554], [29114, 4554], [28287, 4554], [27760, 4554], [27975, 4554], [56773, 4554], [45773, 4554], [21659, 3882], [30881, 3882], [5834, 276], [5857, 296], [5802, 132], [30357, 4825], [40507, 5717], [5812, 263], [5821, 257], [5860, 396], [5933, 405], [5968, 353], [5861, 350], [5862, 351], [5836, 290], [5927, 403], [5805, 134], [6020, 134], [5837, 291], [5811, 136], [29991, 136], [5818, 163], [5910, 397], [29522, 397], [5912, 398], [5825, 275], [30828, 275], [6161, 294], [30337, 294], [5838, 292], [5904, 394], [5865, 352], [29591, 352], [31248, 4903], [30101, 4878], [29739, 4812], [29921, 4782], [44646, 5685], [42842, 5719], [43739, 5861], [41218, 5679], [30800, 4857], [5832, 274], [20451, 274], [5868, 393], [30815, 4739], [42009, 5616], [31914, 121], [12489, 161], [12483, 120], [21773, 3877], [31407, 4873], [44948, 5934], [12632, 183], [12631, 187], [34309, 180], [12633, 421], [12499, 190], [12497, 188], [12492, 181], [12494, 184], [12501, 193], [12550, 406], [12537, 191], [12502, 194], [12500, 427], [12542, 154], [12491, 428], [12476, 27], [12495, 185], [12493, 182], [12498, 189], [12496, 186], [31322, 4821], [31746, 4838], [31582, 4792], [30238, 4776], [45789, 5882], [45142, 5889], [45970, 5684], [40498, 5865], [12516, 237], [12580, 192], [12569, 407], [31677, 4788], [45717, 5678], [13027, 268], [13050, 133], [21778, 3876], [13021, 266], [30400, 4756], [45088, 5617], [13046, 307], [13044, 308], [13028, 269], [13093, 341], [13066, 347], [13096, 346], [13064, 344], [13057, 340], [13056, 339], [13038, 283], [13026, 267], [13035, 281], [13020, 137], [13117, 443], [13002, 88], [13062, 343], [13060, 342], [13055, 318], [13065, 345], [13037, 303], [30661, 4790], [30568, 4727], [30868, 4784], [30369, 4905], [41205, 5860], [41372, 5920], [41158, 5804], [46335, 5663], [13132, 270], [13085, 415], [13082, 40], [29516, 4846], [45508, 5693], [21656, 3879], [5507, 117], [5569, 279], [5503, 116], [29535, 4807], [44239, 5632], [5539, 336], [5635, 333], [5641, 246], [5703, 246], [5638, 334], [22572, 334], [5639, 335], [5535, 142], [5546, 230], [5540, 202], [5567, 261], [5624, 322], [5506, 115], [5502, 114], [5573, 285], [5734, 446], [5536, 144], [5554, 235], [15795, 235], [5572, 284], [5571, 145], [5542, 203], [5570, 143], [30432, 4724], [30047, 4726], [30662, 4803], [29948, 4773], [29844, 4773], [40183, 5941], [44926, 5851], [45746, 5621], [44612, 5755], [5516, 151], [5666, 38], [5534, 150], [29968, 4761], [43638, 5906], [10548, 162], [21762, 3880], [10547, 155], [10527, 18], [10670, 18], [30488, 4801], [43148, 5758], [10544, 128], [10689, 139], [10602, 304], [10606, 409], [10562, 245], [10622, 373], [10611, 367], [10612, 374], [10583, 250], [10620, 375], [10608, 364], [10685, 445], [10533, 118], [10541, 228], [10543, 302], [10589, 368], [10545, 371], [10671, 371], [10607, 320], [10672, 320], [10609, 372], [10673, 372], [10546, 129], [10674, 129], [29666, 4843], [30772, 4879], [30670, 4774], [29414, 4849], [42935, 5924], [42917, 5752], [41615, 5921], [40274, 5746], [10550, 378], [10549, 146], [10646, 149], [30105, 4867], [42355, 5984], [10176, 366], [10213, 365], [10177, 271], [21750, 3875], [30305, 4848], [40200, 5614], [10185, 280], [10200, 357], [10201, 358], [10302, 383], [10244, 388], [10237, 389], [10204, 359], [10211, 361], [10207, 360], [29578, 438], [10202, 363], [10203, 362], [10341, 390], [10267, 399], [10197, 356], [10232, 385], [10247, 386], [10236, 384], [10234, 387], [10187, 282], [30814, 4815], [30525, 4868], [29526, 4755], [29856, 4743], [41065, 5600], [45046, 5770], [42851, 5810], [43064, 5639], [10245, 410], [29519, 4845], [10311, 444], [10377, 444], [30359, 4787], [45449, 5958], [12320, 12], [12319, 2333], [12318, 9], [12323, 14], [12324, 369], [12325, 17], [12338, 8], [12339, 33], [12337, 4], [12343, 1], [12344, 456], [12340, 3939], [12360, 210], [12361, 324], [12362, 337], [12373, 7], [12363, 338], [12367, 349], [12387, 13], [12417, 2], [12385, 152], [12403, 31], [12401, 29], [12391, 20], [12440, 21], [12453, 28], [12456, 30], [12447, 23], [12450, 25], [12457, 37]] |