aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/mod.rs14
-rw-r--r--src/api/professions.rs51
-rw-r--r--src/bt.rs140
-rw-r--r--src/cache.rs2
-rw-r--r--src/main.rs27
-rw-r--r--src/major_trait_mask.pngbin0 -> 3058 bytes
-rw-r--r--src/minor_trait_mask.pngbin4287 -> 4210 bytes
-rw-r--r--src/output.rs2
-rw-r--r--src/render.rs8
-rw-r--r--src/skill_palette.json1
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,
+ })
+ }
+}
diff --git a/src/bt.rs b/src/bt.rs
index cf95f3c..97fb8c5 100644
--- a/src/bt.rs
+++ b/src/bt.rs
@@ -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
new file mode 100644
index 0000000..1b9abf2
--- /dev/null
+++ b/src/major_trait_mask.png
Binary files differ
diff --git a/src/minor_trait_mask.png b/src/minor_trait_mask.png
index 4fed74d..f825b0e 100644
--- a/src/minor_trait_mask.png
+++ b/src/minor_trait_mask.png
Binary files differ
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]]