diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/api/mod.rs | 14 | ||||
-rw-r--r-- | src/api/professions.rs | 51 | ||||
-rw-r--r-- | src/bt.rs | 140 | ||||
-rw-r--r-- | src/cache.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 27 | ||||
-rw-r--r-- | src/major_trait_mask.png | bin | 0 -> 3058 bytes | |||
-rw-r--r-- | src/minor_trait_mask.png | bin | 4287 -> 4210 bytes | |||
-rw-r--r-- | src/output.rs | 2 | ||||
-rw-r--r-- | src/render.rs | 8 | ||||
-rw-r--r-- | src/skill_palette.json | 1 |
10 files changed, 134 insertions, 111 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 27f0da1..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. @@ -123,7 +125,7 @@ impl Api { P: AsRef<Path>, { match self.cache.get(name.as_ref())? { - Some(data) => Ok(serde_json::from_slice(&data)?), + Some(data) => serde_json::from_slice(&data).or(Ok(None)), None => Ok(None), } } @@ -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,6 +1,7 @@ -use super::api::{Api, ApiError, Skill, Specialization}; +use super::api::{Api, ApiError, Profession, Skill, Specialization}; +use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use std::{convert::TryFrom, error::Error, fmt, str::FromStr}; +use std::{convert::TryFrom, error::Error, fmt, io::Cursor, str::FromStr}; #[derive(Debug)] pub enum ChatlinkError { @@ -11,11 +12,16 @@ 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, } +impl From<std::io::Error> for ChatlinkError { + fn from(_err: std::io::Error) -> Self { + panic!("The reading cursor should never return an error!"); + } +} + impl fmt::Display for ChatlinkError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -34,48 +40,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 +140,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 +171,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 +209,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 +243,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,9 +265,8 @@ impl BuildTemplate { 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); + let palette_id = self.profession().skill_id_to_palette_id(s.id).unwrap_or(0); + bytes.write_u16::<LE>(palette_id as u16).unwrap(); } } // Aquatic @@ -314,7 +283,10 @@ impl BuildTemplate { } } - // Weird padding, achieved by looking at other chat links. + // 12 more bytes are used to save the order of the skills in the inactive revenant + // utilities (the active ones are saved in the normal skill slots). + // The order is terrestric 1/2/3 and then aquatic 1/2/3, with 2 bytes per skill. + // We don't care about that, so just do whatever. for _ in 0..12 { bytes.push(0); } @@ -338,12 +310,14 @@ impl BuildTemplate { } bytes.remove(0); - let profession = Profession::try_from(bytes.remove(0))?; + let mut reader = Cursor::new(bytes); - let mut traitlines = BuildTemplate::empty(profession).traitlines; + let profession = code_to_profession(api, reader.read_u8()? as u32)?; + + let mut traitlines = EMPTY_TRAITLINES; for i in traitlines.iter_mut() { - let spec_id = bytes.remove(0); - let trait_choices = bytes.remove(0); + let spec_id = reader.read_u8()?; + let trait_choices = reader.read_u8()?; if spec_id == 0 { continue; } @@ -355,28 +329,27 @@ 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; + let palette_id = reader.read_u16::<LE>()? as u32; 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); } // Aquatic - bytes.remove(0); - bytes.remove(0); + reader.read_u16::<LE>()?; } - 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))?; + *i = Legend::try_from(reader.read_u8()?)?; } ExtraData::Legends(legends) } @@ -392,29 +365,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/cache.rs b/src/cache.rs index 2b490ec..09c8512 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -66,7 +66,7 @@ impl Cache for FileCache { } /// A cache that does nothing. -struct NoopCache; +pub struct NoopCache; impl NoopCache { pub fn new() -> Self { diff --git a/src/main.rs b/src/main.rs index f990319..d8e66a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ extern crate base64; +extern crate byteorder; extern crate clap; extern crate image; extern crate imageproc; @@ -11,8 +12,6 @@ extern crate rusttype; extern crate termcolor; extern crate toml; extern crate xdg; -#[macro_use] -extern crate lazy_static; use std::error::Error as StdError; use std::fmt; @@ -39,7 +38,7 @@ mod useropts; 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. @@ -184,10 +183,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") @@ -198,7 +193,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; @@ -208,7 +203,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<_>>) @@ -242,7 +237,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) @@ -348,6 +343,12 @@ fn run() -> MainResult<()> { .takes_value(true), ) .arg( + Arg::with_name("no-cache") + .help("Disables the cache") + .long("no-cache") + .takes_value(false), + ) + .arg( Arg::with_name("config") .help("Specifies the render option file.") .long("config") @@ -355,7 +356,11 @@ fn run() -> MainResult<()> { ) .get_matches(); - let mut api = Api::new(cache::FileCache::new()); + let mut api = if matches.is_present("no-cache") { + Api::new(cache::NoopCache::new()) + } else { + Api::new(cache::FileCache::new()) + }; let build = if matches.is_present("chatlink") { run_chatlink(&mut api, &matches)? } else { diff --git a/src/major_trait_mask.png b/src/major_trait_mask.png Binary files differnew file mode 100644 index 0000000..1b9abf2 --- /dev/null +++ b/src/major_trait_mask.png diff --git a/src/minor_trait_mask.png b/src/minor_trait_mask.png Binary files differindex 4fed74d..f825b0e 100644 --- a/src/minor_trait_mask.png +++ b/src/minor_trait_mask.png 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/render.rs b/src/render.rs index 6513cd5..9341848 100644 --- a/src/render.rs +++ b/src/render.rs @@ -120,18 +120,23 @@ pub struct Renderer<'r> { api: &'r mut Api, options: RenderOptions, minor_mask: DynamicImage, + major_mask: DynamicImage, } impl<'r> Renderer<'r> { /// Create a new renderer using the given API. pub fn new(api: &mut Api, options: RenderOptions) -> Renderer<'_> { let minor_mask = image::load_from_memory(include_bytes!("minor_trait_mask.png")) - .expect("Mask image could not be loaded") + .expect("Minor mask image could not be loaded") + .resize(options.trait_size, options.trait_size, CatmullRom); + let major_mask = image::load_from_memory(include_bytes!("major_trait_mask.png")) + .expect("Major mask image could not be loaded") .resize(options.trait_size, options.trait_size, CatmullRom); Renderer { api, options, minor_mask, + major_mask, } } @@ -191,6 +196,7 @@ impl<'r> Renderer<'r> { } else { major_img.to_rgba() }; + let major_img = with_mask(&major_img, &self.major_mask); let y_slice = buffer.height() / TRAITS_PER_TIER; let y_pos = vertical_pos as u32 * y_slice + half(y_slice - major_img.height()); let x_slice = (buffer.width() - self.options.traitline_x_offset) / TOTAL_COLUMN_COUNT; 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]] |