diff options
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | make_table.py | 38 | ||||
-rw-r--r-- | src/api/mod.rs | 12 | ||||
-rw-r--r-- | src/api/professions.rs | 51 | ||||
-rw-r--r-- | src/bt.rs | 111 | ||||
-rw-r--r-- | src/main.rs | 16 | ||||
-rw-r--r-- | src/output.rs | 2 | ||||
-rw-r--r-- | src/skill_palette.json | 1 |
8 files changed, 97 insertions, 135 deletions
@@ -21,4 +21,3 @@ base64 = "0.11" termcolor = "1.0" num_enum = "0.4" num-traits = "0.2" -lazy_static = "1.4" diff --git a/make_table.py b/make_table.py deleted file mode 100644 index 444211d..0000000 --- a/make_table.py +++ /dev/null @@ -1,38 +0,0 @@ -"""This script is used to downlaod the skill palette data. - -The Guild Wars 2 chat links for build templates use a different skill ID than -the API, so skills need to be mapped from their "skill ID" to the corresponding -"palette ID". - -Some game updates may require this mapping to be updated as well, otherwise the -chat link parsing/generating functionality might be broken. - -To use this script, pipe the output to src/skill_palette.json and rebuild -kondou. -""" -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 - # For some reason, some skills have multiple IDs. It is not quite clear - # what they are for, it sometimes seems to be the traited version, and - # sometimes the underwater version. Then again, some of the IDs are also - # invalid API IDs, which makes the whole thing a bit weird. Since we don't - # care about underwater combat at the moment (and so does nobody else in - # GW2) nor about the traited version of skills, we'll just stick with the - # first ID. For rendering it shouldn't matter anyway, as all of them - # usually share the same icon. - ids = row[3].text.strip().split(";") - palette_id = int(row[4].text) - skill_id = int(ids[0]) - result.append((skill_id, palette_id)) -print(json.dumps(result)) diff --git a/src/api/mod.rs b/src/api/mod.rs index ffed5ea..c4aa1f7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -113,7 +113,9 @@ impl Api { /// Combines the given endpoint with the `base_url` of this API. fn make_url(&self, endpoint: &str) -> Url { - self.base_url.join(endpoint).expect("Invalid API endpoint") + let mut result = self.base_url.join(endpoint).expect("Invalid API endpoint"); + result.set_query(Some("v=2019-19-12T0:00")); + result } /// Get and deserialize a cached value. @@ -208,6 +210,14 @@ impl Api { self.get_multiple_cached("professions", "professions/", ids) } + /// Retrieve all available professions. + /// + /// This is a shortcut around `get_profession_ids` and `get_professions`. + pub fn get_all_professions(&mut self) -> Result<Vec<Profession>, ApiError> { + let ids = self.get_profession_ids()?; + self.get_professions(&ids) + } + /// Retrieve detailed information about the given skills. /// /// Skills that are found in the cache are taken from there. diff --git a/src/api/professions.rs b/src/api/professions.rs index 2716a1a..f3d1f94 100644 --- a/src/api/professions.rs +++ b/src/api/professions.rs @@ -4,7 +4,7 @@ //! * [Wiki](https://wiki.guildwars2.com/wiki/API:2/professions) use super::HasId; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Profession { @@ -12,10 +12,32 @@ pub struct Profession { pub id: String, /// The name of the profession. pub name: String, + /// The numeric code of the profession, e.g. for chat links. + pub code: u32, /// List of specialization ids. pub specializations: Vec<u32>, /// List of skills. pub skills: Vec<Skill>, + /// Conversion of palette ID to skill ID. + pub skills_by_palette: Vec<PaletteEntry>, +} + +impl Profession { + /// Resolves a given palette ID to the corresponding skill ID. + pub fn palette_id_to_skill_id(&self, palette_id: u32) -> Option<u32> { + self.skills_by_palette + .iter() + .find(|entry| entry.palette_id == palette_id) + .map(|entry| entry.skill_id) + } + + /// Resolves a given skill ID to the corresponding palette ID. + pub fn skill_id_to_palette_id(&self, skill_id: u32) -> Option<u32> { + self.skills_by_palette + .iter() + .find(|entry| entry.skill_id == skill_id) + .map(|entry| entry.palette_id) + } } impl HasId for Profession { @@ -34,3 +56,30 @@ pub struct Skill { #[serde(rename = "type")] pub typ: String, } + +#[derive(Debug, Clone)] +pub struct PaletteEntry { + /// The palette ID, as used in the chat link. + /// + /// Note that the actual palette only allows 2 bytes for this number, i.e. an `u16`. To stay + /// consistent with other integers that are handled here however, this struct uses a `u32`. + pub palette_id: u32, + /// The skill ID, as used in the API. + pub skill_id: u32, +} + +impl Serialize for PaletteEntry { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + (self.palette_id, self.skill_id).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PaletteEntry { + fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + let (palette_id, skill_id) = Deserialize::deserialize(deserializer)?; + Ok(PaletteEntry { + palette_id, + skill_id, + }) + } +} @@ -1,4 +1,4 @@ -use super::api::{Api, ApiError, Skill, Specialization}; +use super::api::{Api, ApiError, Profession, Skill, Specialization}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::{convert::TryFrom, error::Error, fmt, str::FromStr}; @@ -11,7 +11,6 @@ pub enum ChatlinkError { error_froms! { ChatlinkError, err: ApiError => ChatlinkError::ApiError(err), _err: base64::DecodeError => ChatlinkError::MalformedInput, - _err: num_enum::TryFromPrimitiveError<Profession> => ChatlinkError::MalformedInput, _err: num_enum::TryFromPrimitiveError<TraitChoice> => ChatlinkError::MalformedInput, _err: num_enum::TryFromPrimitiveError<Legend> => ChatlinkError::MalformedInput, } @@ -34,48 +33,6 @@ impl Error for ChatlinkError { } } -/// The profession of the template. -/// -/// Can be cast to an `u8` to get the right ID for building chat links. -#[repr(u8)] -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] -pub enum Profession { - Guardian = 1, - Warrior = 2, - Engineer = 3, - Ranger = 4, - Thief = 5, - Elementalist = 6, - Mesmer = 7, - Necromancer = 8, - Revenant = 9, -} - -impl fmt::Display for Profession { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self, f) - } -} - -impl FromStr for Profession { - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "Guardian" => Ok(Profession::Guardian), - "Warrior" => Ok(Profession::Warrior), - "Engineer" => Ok(Profession::Engineer), - "Ranger" => Ok(Profession::Ranger), - "Thief" => Ok(Profession::Thief), - "Elementalist" => Ok(Profession::Elementalist), - "Mesmer" => Ok(Profession::Mesmer), - "Necromancer" => Ok(Profession::Necromancer), - "Revenant" => Ok(Profession::Revenant), - _ => Err(()), - } - } -} - /// Represents the selected trait. #[repr(u8)] #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] @@ -176,6 +133,12 @@ pub const SKILL_COUNT: usize = 5; pub const TRAITLINE_COUNT: usize = 3; pub const LEGEND_COUNT: usize = 4; +/// The code for the revenant profession. +pub const CODE_REVENANT: u32 = 9; + +pub const EMPTY_SKILLS: [Option<Skill>; SKILL_COUNT] = [None, None, None, None, None]; +pub const EMPTY_TRAITLINES: [Option<Traitline>; TRAITLINE_COUNT] = [None,None, None]; + /// Represents a build template. /// /// This struct is made with the same limitations as the game imposes. That is, even though the @@ -201,8 +164,8 @@ impl BuildTemplate { pub fn empty(profession: Profession) -> BuildTemplate { BuildTemplate { profession, - skills: [None, None, None, None, None], - traitlines: [None, None, None], + skills: EMPTY_SKILLS, + traitlines: EMPTY_TRAITLINES, extra_data: ExtraData::None, } } @@ -239,8 +202,8 @@ impl BuildTemplate { } /// Returns the profession of this build. - pub fn profession(&self) -> Profession { - self.profession + pub fn profession(&self) -> &Profession { + &self.profession } /// Returns the skills of this build. @@ -273,7 +236,7 @@ impl BuildTemplate { /// The returned link is ready to be copy-and-pasted into Guild Wars 2. pub fn chatlink(&self) -> String { let mut bytes = vec![0x0Du8]; - let prof_byte = self.profession() as u8; + let prof_byte = self.profession().code as u8; bytes.push(prof_byte); for traitline in self.traitlines().iter() { @@ -295,7 +258,10 @@ impl BuildTemplate { bytes.push(0); } Some(s) => { - let palette_id = skill_id_to_palette_id(s.id); + let palette_id = self + .profession() + .skill_id_to_palette_id(s.id) + .unwrap_or(0); bytes.push((palette_id & 0xFF) as u8); bytes.push(((palette_id >> 8) & 0xFF) as u8); } @@ -338,9 +304,9 @@ impl BuildTemplate { } bytes.remove(0); - let profession = Profession::try_from(bytes.remove(0))?; + let profession = code_to_profession(api, bytes.remove(0) as u32)?; - let mut traitlines = BuildTemplate::empty(profession).traitlines; + let mut traitlines = EMPTY_TRAITLINES; for i in traitlines.iter_mut() { let spec_id = bytes.remove(0); let trait_choices = bytes.remove(0); @@ -355,14 +321,16 @@ impl BuildTemplate { *i = Some((spec, [c_0, c_1, c_2])); } - let mut skills = BuildTemplate::empty(profession).skills; + let mut skills = EMPTY_SKILLS; for i in skills.iter_mut() { // Terrestrial let byte_1 = bytes.remove(0); let byte_2 = bytes.remove(0); let palette_id = byte_1 as u32 | (byte_2 as u32) << 8; if palette_id != 0 { - let skill_id = palette_id_to_skill_id(palette_id); + let skill_id = profession + .palette_id_to_skill_id(palette_id) + .ok_or(ChatlinkError::MalformedInput)?; let skill = api.get_skills(&[skill_id])?.remove(0); *i = Some(skill); } @@ -372,8 +340,8 @@ impl BuildTemplate { bytes.remove(0); } - let extra_data = match profession { - Profession::Revenant => { + let extra_data = match profession.code { + CODE_REVENANT => { let mut legends = [Legend::None; LEGEND_COUNT]; for i in legends.iter_mut() { *i = Legend::try_from(bytes.remove(0))?; @@ -392,29 +360,10 @@ impl BuildTemplate { } } -lazy_static! { - static ref PALETTE_MAPPING: Vec<(u32, u32)> = - serde_json::from_str(include_str!("skill_palette.json")).unwrap(); -} - -// Those functions do linear searches, but the list only has about 400 items, which should be okay. -// If performance becomes an issue, we can always create hash tables or do a binary search, -// however, since we need both directions, we would need double the memory to keep the second map. - -fn skill_id_to_palette_id(input: u32) -> u32 { - for (skill, palette) in PALETTE_MAPPING.iter() { - if *skill == input { - return *palette; - } - } - 0 -} - -fn palette_id_to_skill_id(input: u32) -> u32 { - for (skill, palette) in PALETTE_MAPPING.iter() { - if *palette == input { - return *skill; - } - } - 0 +fn code_to_profession(api: &mut Api, code: u32) -> Result<Profession, ChatlinkError> { + let professions = api.get_all_professions()?; + professions + .into_iter() + .find(|p| p.code == code) + .ok_or(ChatlinkError::MalformedInput) } diff --git a/src/main.rs b/src/main.rs index 395e683..47ccabd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,6 @@ extern crate reqwest; extern crate rusttype; extern crate termcolor; extern crate xdg; -#[macro_use] -extern crate lazy_static; use std::error::Error as StdError; use std::fmt; @@ -37,7 +35,7 @@ mod render; use clap::{App, Arg, ArgMatches}; use api::{Api, Profession, Skill}; -use bt::{BuildTemplate, ExtraData, Legend, TraitChoice, Traitline}; +use bt::{BuildTemplate, ExtraData, Legend, TraitChoice, Traitline, CODE_REVENANT}; use render::RenderError; /// The name of this application. @@ -182,10 +180,6 @@ 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 legends = matches .values_of("legend") @@ -196,7 +190,7 @@ fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplat .map(Result::unwrap) .collect::<Vec<_>>(); - let extra_data = if prof_enum == bt::Profession::Revenant { + let extra_data = if profession.code == CODE_REVENANT { let mut array_legends = [Legend::None; 4]; for (i, l) in legends.iter().enumerate() { array_legends[i] = *l; @@ -206,7 +200,7 @@ fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplat ExtraData::None }; - let skills = if prof_enum != bt::Profession::Revenant { + let skills = if profession.code != CODE_REVENANT { matches .values_of("skill") .map(Iterator::collect::<Vec<_>>) @@ -240,7 +234,7 @@ fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplat "got too many traitlines" ); - let build = BuildTemplate::new(prof_enum, &skills, &traitlines, extra_data) + let build = BuildTemplate::new(profession, &skills, &traitlines, extra_data) .expect("BuildTemplate could not be constructed"); Ok(build) @@ -368,7 +362,7 @@ fn run() -> MainResult<()> { if !matches.is_present("quiet") { println!("Image saved in {}", filename); } - }, + } Err(RenderError::EmptyBuild) => (), Err(err) => { eprintln!("Image could not be rendered:"); diff --git a/src/output.rs b/src/output.rs index 9406571..8d71909 100644 --- a/src/output.rs +++ b/src/output.rs @@ -36,7 +36,7 @@ pub fn show_build_template(build: &BuildTemplate) -> io::Result<()> { color_spec.set_fg(Some(HEADER_COLOR)); color_spec.set_bold(true); - let mut fields = vec![("Profession:", build.profession().to_string())]; + let mut fields = vec![("Profession:", build.profession().name.clone())]; fields.push(("Skills:", format_skill(&build.skills()[0]))); for skill in build.skills().iter().skip(1) { diff --git a/src/skill_palette.json b/src/skill_palette.json deleted file mode 100644 index 67ac962..0000000 --- a/src/skill_palette.json +++ /dev/null @@ -1 +0,0 @@ -[[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], [9175, 329], [9248, 260], [9253, 331], [9125, 256], [9247, 327], [9246, 441], [9187, 332], [9128, 278], [9182, 330], [9242, 326], [9163, 306], [9151, 305], [9245, 376], [9168, 328], [9251, 255], [30553, 4740], [30871, 4858], [30364, 4746], [29786, 4862], [46148, 5909], [45460, 5971], [40915, 5754], [44080, 5827], [29965, 4745], [9154, 311], [30461, 4721], [30273, 4789], [43357, 5656], [26937, 4572], [29148, 4572], [28219, 4572], [27220, 4572], [27372, 4572], [45686, 4572], [27107, 4564], [29209, 4614], [28231, 4651], [26821, 4614], [27025, 4651], [27715, 4564], [27917, 4564], [27322, 4614], [27505, 4651], [26644, 4564], [28379, 4614], [27014, 4651], [26557, 4564], [28516, 4614], [26679, 4651], [41220, 4564], [42949, 4614], [40485, 4651], [28406, 4554], [27356, 4554], [28287, 4554], [27760, 4554], [27975, 4554], [45773, 4554], [21659, 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], [5837, 291], [5811, 136], [5818, 163], [5910, 397], [5912, 398], [5825, 275], [6161, 294], [5838, 292], [5904, 394], [5865, 352], [31248, 4903], [30101, 4878], [29739, 4812], [29921, 4782], [44646, 5685], [42842, 5719], [43739, 5861], [41218, 5679], [30800, 4857], [5832, 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], [5638, 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], [5572, 284], [5571, 145], [5542, 203], [5570, 143], [30432, 4724], [30047, 4726], [30662, 4803], [29948, 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], [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], [10607, 320], [10609, 372], [10546, 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], [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]] |