chore: initial commit
This commit is contained in:
commit
095ceadc78
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/.idea
|
||||
/config.toml
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "remote-party-finder"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
askama = { version = "0.10", features = ["with-warp"] }
|
||||
askama_warp = "0.11"
|
||||
base64 = "0.13"
|
||||
bitflags = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono-humanize = "0.2"
|
||||
ffxiv_types = "1"
|
||||
lazy_static = "1"
|
||||
maplit = "1"
|
||||
mime = "0.3"
|
||||
mongodb = { version = "2", features = ["bson-chrono-0_4"] }
|
||||
sestring = { version = "0.1", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_repr = "0.1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1"
|
||||
toml = "0.5"
|
||||
warp = { version = "0.3", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1"
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 64 KiB |
|
@ -0,0 +1,5 @@
|
|||
[web]
|
||||
host = "127.0.0.1:1234"
|
||||
|
||||
[mongo]
|
||||
url = "mongodb://example.com:1234"
|
|
@ -0,0 +1,17 @@
|
|||
use sestring::SeString;
|
||||
use serde::{Deserializer, Deserialize, Serializer, Serialize};
|
||||
|
||||
pub fn deserialize<'de, D>(de: D) -> Result<SeString, D::Error>
|
||||
where D: Deserializer<'de>,
|
||||
{
|
||||
let b64 = String::deserialize(de)?;
|
||||
let bytes = base64::decode(&b64).map_err(|e| serde::de::Error::custom(format!("invalid base64: {:?}", e)))?;
|
||||
SeString::parse(&bytes).map_err(|e| serde::de::Error::custom(format!("invalid sestring: {:?}", e)))
|
||||
}
|
||||
|
||||
pub fn serialize<S>(sestring: &SeString, ser: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer,
|
||||
{
|
||||
let bytes = sestring.encode();
|
||||
base64::encode(&bytes).serialize(ser)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use std::net::SocketAddr;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub web: Web,
|
||||
pub mongo: Mongo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Web {
|
||||
pub host: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Mongo {
|
||||
pub url: String,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
pub mod auto_translate;
|
||||
pub mod duties;
|
||||
pub mod jobs;
|
||||
pub mod roulettes;
|
||||
pub mod territory_names;
|
||||
pub mod worlds;
|
||||
|
||||
pub use self::{
|
||||
auto_translate::AUTO_TRANSLATE,
|
||||
duties::DUTIES,
|
||||
jobs::JOBS,
|
||||
roulettes::ROULETTES,
|
||||
territory_names::TERRITORY_NAMES,
|
||||
worlds::WORLDS,
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,573 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DUTIES: HashMap<u32, &'static str> = maplit::hashmap! {
|
||||
1 => "The Thousand Maws of TotoRak",
|
||||
2 => "The TamTara Deepcroft",
|
||||
3 => "Copperbell Mines",
|
||||
4 => "Sastasha",
|
||||
5 => "The Aurum Vale",
|
||||
6 => "Haukke Manor",
|
||||
7 => "Halatali",
|
||||
8 => "Brayflox's Longstop",
|
||||
9 => "The Sunken Temple of Qarn",
|
||||
10 => "The Wanderer's Palace",
|
||||
11 => "The Stone Vigil",
|
||||
12 => "Cutter's Cry",
|
||||
13 => "Dzemael Darkhold",
|
||||
14 => "Amdapor Keep",
|
||||
15 => "Castrum Meridianum",
|
||||
16 => "The Praetorium",
|
||||
17 => "Pharos Sirius",
|
||||
18 => "Copperbell Mines (Hard)",
|
||||
19 => "Haukke Manor (Hard)",
|
||||
20 => "Brayflox's Longstop (Hard)",
|
||||
21 => "Halatali (Hard)",
|
||||
22 => "The Lost City of Amdapor",
|
||||
23 => "Hullbreaker Isle",
|
||||
24 => "The TamTara Deepcroft (Hard)",
|
||||
25 => "The Stone Vigil (Hard)",
|
||||
26 => "The Sunken Temple of Qarn (Hard)",
|
||||
27 => "Snowcloak",
|
||||
28 => "Sastasha (Hard)",
|
||||
29 => "Amdapor Keep (Hard)",
|
||||
30 => "The Wanderer's Palace (Hard)",
|
||||
31 => "The Great Gubal Library",
|
||||
32 => "The Keeper of the Lake",
|
||||
33 => "Neverreap",
|
||||
34 => "The Vault",
|
||||
35 => "The Fractal Continuum",
|
||||
36 => "The Dusk Vigil",
|
||||
37 => "Sohm Al",
|
||||
38 => "The Aetherochemical Research Facility",
|
||||
39 => "The Aery",
|
||||
40 => "Pharos Sirius (Hard)",
|
||||
41 => "Saint Mocianne's Arboretum",
|
||||
42 => "Basic Training: Enemy Parties",
|
||||
43 => "Under the Armor",
|
||||
44 => "Basic Training: Enemy Strongholds",
|
||||
45 => "Hero on the Half Shell",
|
||||
46 => "Pulling Poison Posies",
|
||||
47 => "Stinging Back",
|
||||
48 => "All's Well that Ends in the Well",
|
||||
49 => "Flicking Sticks and Taking Names",
|
||||
50 => "More than a Feeler",
|
||||
51 => "Annoy the Void",
|
||||
52 => "Shadow and Claw",
|
||||
53 => "Long Live the Queen",
|
||||
54 => "Ward Up",
|
||||
55 => "Solemn Trinity",
|
||||
56 => "The Bowl of Embers",
|
||||
57 => "The Navel",
|
||||
58 => "The Howling Eye",
|
||||
59 => "The Bowl of Embers (Hard)",
|
||||
60 => "The Navel (Hard)",
|
||||
61 => "The Howling Eye (Hard)",
|
||||
62 => "Cape Westwind",
|
||||
63 => "The Bowl of Embers (Extreme)",
|
||||
64 => "The Navel (Extreme)",
|
||||
65 => "The Howling Eye (Extreme)",
|
||||
66 => "Thornmarch (Hard)",
|
||||
67 => "Thornmarch (Extreme)",
|
||||
68 => "The Minstrel's Ballad: Ultima's Bane",
|
||||
69 => "Special Event III",
|
||||
70 => "Special Event I",
|
||||
71 => "Special Event II",
|
||||
72 => "The Whorleater (Hard)",
|
||||
73 => "The Whorleater (Extreme)",
|
||||
74 => "A Relic Reborn: the Chimera",
|
||||
75 => "A Relic Reborn: the Hydra",
|
||||
76 => "Battle on the Big Bridge",
|
||||
77 => "The Striking Tree (Hard)",
|
||||
78 => "The Striking Tree (Extreme)",
|
||||
79 => "The Akh Afah Amphitheatre (Hard)",
|
||||
80 => "The Akh Afah Amphitheatre (Extreme)",
|
||||
81 => "The Dragon's Neck",
|
||||
82 => "Urth's Fount",
|
||||
83 => "The Steps of Faith",
|
||||
84 => "The Chrysalis",
|
||||
85 => "Battle in the Big Keep",
|
||||
86 => "Thok ast Thok (Hard)",
|
||||
87 => "Thok ast Thok (Extreme)",
|
||||
88 => "The Limitless Blue (Hard)",
|
||||
89 => "The Limitless Blue (Extreme)",
|
||||
90 => "The Singularity Reactor",
|
||||
91 => "The Minstrel's Ballad: Thordan's Reign",
|
||||
92 => "The Labyrinth of the Ancients",
|
||||
93 => "The Binding Coil of Bahamut - Turn 1",
|
||||
94 => "The Binding Coil of Bahamut - Turn 2",
|
||||
95 => "The Binding Coil of Bahamut - Turn 3",
|
||||
96 => "The Binding Coil of Bahamut - Turn 4",
|
||||
97 => "The Binding Coil of Bahamut - Turn 5",
|
||||
98 => "The Second Coil of Bahamut - Turn 1",
|
||||
99 => "The Second Coil of Bahamut - Turn 2",
|
||||
100 => "The Second Coil of Bahamut - Turn 3",
|
||||
101 => "The Second Coil of Bahamut - Turn 4",
|
||||
102 => "Syrcus Tower",
|
||||
103 => "The Second Coil of Bahamut (Savage) - Turn 1",
|
||||
104 => "The Second Coil of Bahamut (Savage) - Turn 2",
|
||||
105 => "The Second Coil of Bahamut (Savage) - Turn 3",
|
||||
106 => "The Second Coil of Bahamut (Savage) - Turn 4",
|
||||
107 => "The Final Coil of Bahamut - Turn 1",
|
||||
108 => "The Final Coil of Bahamut - Turn 2",
|
||||
109 => "The Final Coil of Bahamut - Turn 3",
|
||||
110 => "The Final Coil of Bahamut - Turn 4",
|
||||
111 => "The World of Darkness",
|
||||
112 => "Alexander - The Fist of the Father",
|
||||
113 => "Alexander - The Cuff of the Father",
|
||||
114 => "Alexander - The Arm of the Father",
|
||||
115 => "Alexander - The Burden of the Father",
|
||||
116 => "Alexander - The Fist of the Father (Savage)",
|
||||
117 => "Alexander - The Cuff of the Father (Savage)",
|
||||
118 => "Alexander - The Arm of the Father (Savage)",
|
||||
119 => "Alexander - The Burden of the Father (Savage)",
|
||||
120 => "The Void Ark",
|
||||
127 => "The Borderland Ruins (Secure)",
|
||||
130 => "Seal Rock (Seize)",
|
||||
131 => "The Diadem (Easy)",
|
||||
132 => "The Diadem",
|
||||
133 => "The Diadem (Hard)",
|
||||
134 => "Containment Bay S1T7",
|
||||
135 => "Containment Bay S1T7 (Extreme)",
|
||||
136 => "Alexander - The Fist of the Son",
|
||||
137 => "Alexander - The Cuff of the Son",
|
||||
138 => "Alexander - The Arm of the Son",
|
||||
139 => "Alexander - The Burden of the Son",
|
||||
140 => "The Lost City of Amdapor (Hard)",
|
||||
141 => "The Antitower",
|
||||
143 => "The Feast (4 on 4 - Training)",
|
||||
145 => "The Feast (4 on 4 - Ranked)",
|
||||
147 => "Alexander - The Fist of the Son (Savage)",
|
||||
148 => "Alexander - The Cuff of the Son (Savage)",
|
||||
149 => "Alexander - The Arm of the Son (Savage)",
|
||||
150 => "Alexander - The Burden of the Son (Savage)",
|
||||
151 => "Avoid Area of Effect Attacks",
|
||||
152 => "Execute a Combo to Increase Enmity",
|
||||
153 => "Execute a Combo in Battle",
|
||||
154 => "Accrue Enmity from Multiple Targets",
|
||||
155 => "Engage Multiple Targets",
|
||||
156 => "Execute a Ranged Attack to Increase Enmity",
|
||||
157 => "Engage Enemy Reinforcements",
|
||||
158 => "Assist Allies in Defeating a Target",
|
||||
159 => "Defeat an Occupied Target",
|
||||
160 => "Avoid Engaged Targets",
|
||||
161 => "Engage Enemy Reinforcements",
|
||||
162 => "Interact with the Battlefield",
|
||||
163 => "Heal an Ally",
|
||||
164 => "Heal Multiple Allies",
|
||||
165 => "Avoid Engaged Targets",
|
||||
166 => "Final Exercise",
|
||||
167 => "A Spectacle for the Ages",
|
||||
168 => "The Weeping City of Mhach",
|
||||
169 => "The Final Steps of Faith",
|
||||
170 => "The Minstrel's Ballad: Nidhogg's Rage",
|
||||
171 => "Sohr Khai",
|
||||
172 => "Hullbreaker Isle (Hard)",
|
||||
173 => "A Bloody Reunion",
|
||||
174 => "The Palace of the Dead (Floors 1-10)",
|
||||
175 => "The Palace of the Dead (Floors 11-20)",
|
||||
176 => "The Palace of the Dead (Floors 21-30)",
|
||||
177 => "The Palace of the Dead (Floors 31-40)",
|
||||
178 => "The Palace of the Dead (Floors 41-50)",
|
||||
179 => "The Aquapolis",
|
||||
180 => "The Fields of Glory (Shatter)",
|
||||
181 => "The Haunted Manor",
|
||||
182 => "Xelphatol",
|
||||
183 => "Containment Bay P1T6",
|
||||
184 => "Containment Bay P1T6 (Extreme)",
|
||||
186 => "Alexander - The Eyes of the Creator",
|
||||
187 => "Alexander - The Breath of the Creator",
|
||||
188 => "Alexander - The Heart of the Creator",
|
||||
189 => "Alexander - The Soul of the Creator",
|
||||
190 => "Alexander - The Eyes of the Creator (Savage)",
|
||||
191 => "Alexander - The Breath of the Creator (Savage)",
|
||||
192 => "Alexander - The Heart of the Creator (Savage)",
|
||||
193 => "Alexander - The Soul of the Creator (Savage)",
|
||||
194 => "One Life for One World",
|
||||
195 => "The Triple Triad Battlehall",
|
||||
196 => "The Great Gubal Library (Hard)",
|
||||
197 => "LoVM: Player Battle (RP)",
|
||||
198 => "LoVM: Tournament",
|
||||
199 => "LoVM: Player Battle (Non-RP)",
|
||||
201 => "The Feast (Custom Match - Feasting Grounds)",
|
||||
202 => "The Diadem Hunting Grounds (Easy)",
|
||||
203 => "The Diadem Hunting Grounds",
|
||||
204 => "The Palace of the Dead (Floors 51-60)",
|
||||
205 => "The Palace of the Dead (Floors 61-70)",
|
||||
206 => "The Palace of the Dead (Floors 71-80)",
|
||||
207 => "The Palace of the Dead (Floors 81-90)",
|
||||
208 => "The Palace of the Dead (Floors 91-100)",
|
||||
209 => "The Palace of the Dead (Floors 101-110)",
|
||||
210 => "The Palace of the Dead (Floors 111-120)",
|
||||
211 => "The Palace of the Dead (Floors 121-130)",
|
||||
212 => "The Palace of the Dead (Floors 131-140)",
|
||||
213 => "The Palace of the Dead (Floors 141-150)",
|
||||
214 => "The Palace of the Dead (Floors 151-160)",
|
||||
215 => "The Palace of the Dead (Floors 161-170)",
|
||||
216 => "The Palace of the Dead (Floors 171-180)",
|
||||
217 => "The Palace of the Dead (Floors 181-190)",
|
||||
218 => "The Palace of the Dead (Floors 191-200)",
|
||||
219 => "Baelsar's Wall",
|
||||
220 => "Dun Scaith",
|
||||
221 => "Sohm Al (Hard)",
|
||||
222 => "The Carteneau Flats: Heliodrome",
|
||||
223 => "Containment Bay Z1T9",
|
||||
224 => "Containment Bay Z1T9 (Extreme)",
|
||||
225 => "The Diadem - Trials of the Fury",
|
||||
228 => "The Feast (4 on 4 - Training)",
|
||||
230 => "The Feast (4 on 4 - Ranked)",
|
||||
233 => "The Feast (Custom Match - Lichenweed)",
|
||||
234 => "The Diadem - Trials of the Matron",
|
||||
235 => "Shisui of the Violet Tides",
|
||||
236 => "The Temple of the Fist",
|
||||
237 => "It's Probably a Trap",
|
||||
238 => "The Sirensong Sea",
|
||||
239 => "The Royal Menagerie",
|
||||
240 => "Bardam's Mettle",
|
||||
241 => "Doma Castle",
|
||||
242 => "Castrum Abania",
|
||||
243 => "The Pool of Tribute",
|
||||
244 => "The Pool of Tribute (Extreme)",
|
||||
245 => "With Heart and Steel",
|
||||
246 => "Naadam",
|
||||
247 => "Ala Mhigo",
|
||||
248 => "Blood on the Deck",
|
||||
249 => "The Face of True Evil",
|
||||
250 => "Matsuba Mayhem",
|
||||
251 => "The Battle on Bekko",
|
||||
252 => "Deltascape V1.0",
|
||||
253 => "Deltascape V2.0",
|
||||
254 => "Deltascape V3.0",
|
||||
255 => "Deltascape V4.0",
|
||||
256 => "Deltascape V1.0 (Savage)",
|
||||
257 => "Deltascape V2.0 (Savage)",
|
||||
258 => "Deltascape V3.0 (Savage)",
|
||||
259 => "Deltascape V4.0 (Savage)",
|
||||
260 => "Curious Gorge Meets His Match",
|
||||
261 => "In Thal's Name",
|
||||
262 => "Kugane Castle",
|
||||
263 => "Emanation",
|
||||
264 => "Emanation (Extreme)",
|
||||
265 => "Our Unsung Heroes",
|
||||
266 => "The Heart of the Problem",
|
||||
267 => "Dark as the Night Sky",
|
||||
268 => "The Lost Canals of Uznair",
|
||||
269 => "The Resonant",
|
||||
270 => "Raising the Sword",
|
||||
271 => "The Orphans and the Broken Blade",
|
||||
272 => "Our Compromise",
|
||||
273 => "Dragon Sound",
|
||||
274 => "When Clans Collide",
|
||||
275 => "Interdimensional Rift",
|
||||
276 => "The Hidden Canals of Uznair",
|
||||
277 => "Astragalos",
|
||||
278 => "The Minstrel's Ballad: Shinryu's Domain",
|
||||
279 => "The Drowned City of Skalla",
|
||||
280 => "The Unending Coil of Bahamut (Ultimate)",
|
||||
281 => "The Royal City of Rabanastre",
|
||||
282 => "Return of the Bull",
|
||||
283 => "The Forbidden Land, Eureka Anemos",
|
||||
284 => "Hells' Lid",
|
||||
285 => "The Fractal Continuum (Hard)",
|
||||
286 => "Sigmascape V1.0",
|
||||
287 => "Sigmascape V2.0",
|
||||
288 => "Sigmascape V3.0",
|
||||
289 => "Sigmascape V4.0",
|
||||
290 => "The Jade Stoa",
|
||||
291 => "The Jade Stoa (Extreme)",
|
||||
292 => "Sigmascape V1.0 (Savage)",
|
||||
293 => "Sigmascape V2.0 (Savage)",
|
||||
294 => "Sigmascape V3.0 (Savage)",
|
||||
295 => "Sigmascape V4.0 (Savage)",
|
||||
473 => "The Valentione's Ceremony",
|
||||
474 => "The Great Hunt",
|
||||
475 => "The Great Hunt (Extreme)",
|
||||
476 => "The Feast (Team Ranked)",
|
||||
478 => "The Feast (Ranked)",
|
||||
479 => "The Feast (Training)",
|
||||
480 => "The Feast (Custom Match - Crystal Tower)",
|
||||
481 => "Chocobo Race: Tutorial",
|
||||
482 => "Race 1 - Hugging the Inside",
|
||||
483 => "Race 2 - Keep Away",
|
||||
484 => "Race 3 - Inability",
|
||||
485 => "Race 4 - Heavy Hooves",
|
||||
486 => "Race 5 - Defending the Rush",
|
||||
487 => "Race 6 - Road Rivals",
|
||||
488 => "Race 7 - Field of Dreams",
|
||||
489 => "Race 8 - Playing Both Ends",
|
||||
490 => "Race 9 - Stamina",
|
||||
491 => "Race 10 - Cat and Mouse",
|
||||
492 => "Race 11 - Mad Dash",
|
||||
493 => "Race 12 - Bag of Tricks",
|
||||
494 => "Race 13 - Tag Team",
|
||||
495 => "Race 14 - Heavier Hooves",
|
||||
496 => "Race 15 - Ultimatum",
|
||||
497 => "Chocobo Race: Sagolii Road",
|
||||
498 => "Chocobo Race: Costa del Sol",
|
||||
499 => "Chocobo Race: Tranquil Paths",
|
||||
500 => "Chocobo Race: Sagolii Road",
|
||||
501 => "Chocobo Race: Costa del Sol",
|
||||
502 => "Chocobo Race: Tranquil Paths",
|
||||
503 => "Chocobo Race: Sagolii Road",
|
||||
504 => "Chocobo Race: Costa del Sol",
|
||||
505 => "Chocobo Race: Tranquil Paths",
|
||||
506 => "Chocobo Race: Sagolii Road",
|
||||
507 => "Chocobo Race: Costa del Sol",
|
||||
508 => "Chocobo Race: Tranquil Paths",
|
||||
509 => "Chocobo Race: Sagolii Road",
|
||||
510 => "Chocobo Race: Costa del Sol",
|
||||
511 => "Chocobo Race: Tranquil Paths",
|
||||
512 => "Chocobo Race: Sagolii Road",
|
||||
513 => "Chocobo Race: Costa del Sol",
|
||||
514 => "Chocobo Race: Tranquil Paths",
|
||||
515 => "Chocobo Race: Sagolii Road",
|
||||
516 => "Chocobo Race: Costa del Sol",
|
||||
517 => "Chocobo Race: Tranquil Paths",
|
||||
518 => "Chocobo Race: Sagolii Road",
|
||||
519 => "Chocobo Race: Costa del Sol",
|
||||
520 => "Chocobo Race: Tranquil Paths",
|
||||
521 => "Chocobo Race: Sagolii Road",
|
||||
522 => "Chocobo Race: Costa del Sol",
|
||||
523 => "Chocobo Race: Tranquil Paths",
|
||||
524 => "Chocobo Race: Sagolii Road",
|
||||
525 => "Chocobo Race: Costa del Sol",
|
||||
526 => "Chocobo Race: Tranquil Paths",
|
||||
527 => "Chocobo Race: Sagolii Road",
|
||||
528 => "Chocobo Race: Costa del Sol",
|
||||
529 => "Chocobo Race: Tranquil Paths",
|
||||
530 => "Chocobo Race: Sagolii Road",
|
||||
531 => "Chocobo Race: Costa del Sol",
|
||||
532 => "Chocobo Race: Tranquil Paths",
|
||||
533 => "Chocobo Race: Sagolii Road",
|
||||
534 => "Chocobo Race: Costa del Sol",
|
||||
535 => "Chocobo Race: Tranquil Paths",
|
||||
536 => "The Swallow's Compass",
|
||||
537 => "Castrum Fluminis",
|
||||
538 => "The Minstrel's Ballad: Tsukuyomi's Pain",
|
||||
539 => "The Weapon's Refrain (Ultimate)",
|
||||
540 => "Heaven-on-High (Floors 1-10)",
|
||||
541 => "Heaven-on-High (Floors 11-20)",
|
||||
542 => "Heaven-on-High (Floors 21-30)",
|
||||
543 => "Heaven-on-High (Floors 31-40)",
|
||||
544 => "Heaven-on-High (Floors 41-50)",
|
||||
545 => "Heaven-on-High (Floors 51-60)",
|
||||
546 => "Heaven-on-High (Floors 61-70)",
|
||||
547 => "Heaven-on-High (Floors 71-80)",
|
||||
548 => "Heaven-on-High (Floors 81-90)",
|
||||
549 => "Heaven-on-High (Floors 91-100)",
|
||||
550 => "The Ridorana Lighthouse",
|
||||
552 => "Stage 1: Tutorial",
|
||||
553 => "Stage 2: Hatching a Plan",
|
||||
554 => "Stage 3: The First Move",
|
||||
555 => "Stage 4: Little Big Beast",
|
||||
556 => "Stage 5: Turning Tribes",
|
||||
557 => "Stage 6: Off the Deepcroft",
|
||||
558 => "Stage 7: Rivals",
|
||||
559 => "Stage 8: Always Darkest",
|
||||
560 => "Stage 9: Mine Your Minions",
|
||||
561 => "Stage 10: Children of Mandragora",
|
||||
562 => "Stage 11: The Queen and I",
|
||||
563 => "Stage 12: Breakout",
|
||||
564 => "Stage 13: My Name Is Cid",
|
||||
565 => "Stage 14: Like a Nut",
|
||||
566 => "Stage 15: Urth's Spout",
|
||||
567 => "Stage 16: Exodus",
|
||||
568 => "Stage 17: Over the Wall",
|
||||
569 => "Stage 18: The Hunt",
|
||||
570 => "Stage 19: Battle on the Bitty Bridge",
|
||||
571 => "Stage 20: Guiding Light",
|
||||
572 => "Stage 21: Wise Words",
|
||||
573 => "Stage 22: World of Poor Lighting",
|
||||
574 => "Stage 23: The Binding Coil",
|
||||
575 => "Stage 24: The Final Coil",
|
||||
576 => "LoVM: Master Battle",
|
||||
577 => "LoVM: Master Battle (Hard)",
|
||||
578 => "LoVM: Master Battle (Extreme)",
|
||||
579 => "LoVM: Master Tournament",
|
||||
580 => "The Feast (Team Custom Match - Crystal Tower)",
|
||||
581 => "The Forbidden Land, Eureka Pagos",
|
||||
582 => "Emissary of the Dawn",
|
||||
583 => "The Calamity Retold",
|
||||
584 => "Saint Mocianne's Arboretum (Hard)",
|
||||
585 => "The Burn",
|
||||
586 => "The Shifting Altars of Uznair",
|
||||
587 => "Alphascape V1.0",
|
||||
588 => "Alphascape V2.0",
|
||||
589 => "Alphascape V3.0",
|
||||
590 => "Alphascape V4.0",
|
||||
591 => "Alphascape V1.0 (Savage)",
|
||||
592 => "Alphascape V2.0 (Savage)",
|
||||
593 => "Alphascape V3.0 (Savage)",
|
||||
594 => "Alphascape V4.0 (Savage)",
|
||||
595 => "Kugane Ohashi",
|
||||
596 => "Hells' Kier",
|
||||
597 => "Hells' Kier (Extreme)",
|
||||
598 => "The Forbidden Land, Eureka Pyros",
|
||||
599 => "Hidden Gorge",
|
||||
600 => "Leap of Faith",
|
||||
601 => "Leap of Faith",
|
||||
602 => "Leap of Faith",
|
||||
603 => "Leap of Faith",
|
||||
604 => "Leap of Faith",
|
||||
605 => "Leap of Faith",
|
||||
606 => "Leap of Faith",
|
||||
607 => "Leap of Faith",
|
||||
608 => "Leap of Faith",
|
||||
609 => "The Will of the Moon",
|
||||
610 => "All's Well That Starts Well",
|
||||
611 => "The Ghimlyt Dark",
|
||||
612 => "Much Ado About Pudding",
|
||||
613 => "Waiting for Golem",
|
||||
614 => "Gentlemen Prefer Swords",
|
||||
615 => "The Threepenny Turtles",
|
||||
616 => "Eye Society",
|
||||
617 => "A Chorus Slime",
|
||||
618 => "Bomb-edy of Errors",
|
||||
619 => "To Kill a Mockingslime",
|
||||
620 => "A Little Knight Music",
|
||||
621 => "Some Like It Excruciatingly Hot",
|
||||
622 => "The Plant-om of the Opera",
|
||||
623 => "Beauty and a Beast",
|
||||
624 => "Blobs in the Woods",
|
||||
625 => "The Me Nobody Nodes",
|
||||
626 => "Sunset Bull-evard",
|
||||
627 => "The Sword of Music",
|
||||
628 => "Midsummer Night's Explosion",
|
||||
629 => "On a Clear Day You Can Smell Forever",
|
||||
630 => "Miss Typhon",
|
||||
631 => "Chimera on a Hot Tin Roof",
|
||||
632 => "Here Comes the Boom",
|
||||
633 => "Behemoths and Broomsticks",
|
||||
634 => "Amazing Technicolor Pit Fiends",
|
||||
635 => "Dirty Rotten Azulmagia",
|
||||
636 => "The Orbonne Monastery",
|
||||
637 => "The Wreath of Snakes",
|
||||
638 => "The Wreath of Snakes (Extreme)",
|
||||
639 => "The Forbidden Land, Eureka Hydatos",
|
||||
640 => "Air Force One",
|
||||
641 => "Air Force One",
|
||||
642 => "Air Force One",
|
||||
643 => "Novice Mahjong (Full Ranked Match)",
|
||||
644 => "Advanced Mahjong (Full Ranked Match)",
|
||||
645 => "Four-player Mahjong (Full Match, Kuitan Enabled)",
|
||||
646 => "Messenger of the Winds",
|
||||
648 => "A Requiem for Heroes",
|
||||
649 => "Dohn Mheg",
|
||||
650 => "Four-player Mahjong (Full Match, Kuitan Disabled)",
|
||||
651 => "The Qitana Ravel",
|
||||
652 => "Amaurot",
|
||||
653 => "Eden's Gate: Resurrection",
|
||||
654 => "Eden's Gate: Resurrection (Savage)",
|
||||
655 => "The Twinning",
|
||||
656 => "Malikah's Well",
|
||||
657 => "The Dancing Plague",
|
||||
658 => "The Dancing Plague (Extreme)",
|
||||
659 => "Mt. Gulg",
|
||||
661 => "Akadaemia Anyder",
|
||||
666 => "The Crown of the Immaculate",
|
||||
667 => "The Crown of the Immaculate (Extreme)",
|
||||
676 => "Holminster Switch",
|
||||
678 => "The Hardened Heart",
|
||||
679 => "The Lost and the Found",
|
||||
680 => "Coming Clean",
|
||||
681 => "Legend of the Not-so-hidden Temple",
|
||||
682 => "Eden's Gate: Inundation",
|
||||
683 => "Eden's Gate: Inundation (Savage)",
|
||||
684 => "Eden's Gate: Descent",
|
||||
685 => "Eden's Gate: Descent (Savage)",
|
||||
686 => "Nyelbert's Lament",
|
||||
687 => "The Dying Gasp",
|
||||
688 => "The Dungeons of Lyhe Ghiah",
|
||||
689 => "Eden's Gate: Sepulture",
|
||||
690 => "Eden's Gate: Sepulture (Savage)",
|
||||
691 => "The Hunter's Legacy",
|
||||
692 => "The Grand Cosmos",
|
||||
693 => "The Minstrel's Ballad: Hades's Elegy",
|
||||
694 => "The Epic of Alexander (Ultimate)",
|
||||
695 => "Papa Mia",
|
||||
696 => "Lock up Your Snorters",
|
||||
697 => "Dangerous When Dead",
|
||||
698 => "Red, Fraught, and Blue",
|
||||
699 => "The Catch of the Siegfried",
|
||||
700 => "The Copied Factory",
|
||||
701 => "Onsal Hakair (Danshig Naadam)",
|
||||
702 => "Vows of Virtue, Deeds of Cruelty",
|
||||
703 => "As the Heart Bids",
|
||||
705 => "Leap of Faith",
|
||||
706 => "Leap of Faith",
|
||||
707 => "Leap of Faith",
|
||||
708 => "Leap of Faith",
|
||||
709 => "Leap of Faith",
|
||||
710 => "Leap of Faith",
|
||||
711 => "Leap of Faith",
|
||||
712 => "Leap of Faith",
|
||||
713 => "Leap of Faith",
|
||||
714 => "Anamnesis Anyder",
|
||||
715 => "Eden's Verse: Fulmination",
|
||||
716 => "Eden's Verse: Fulmination (Savage)",
|
||||
717 => "Cinder Drift",
|
||||
718 => "Cinder Drift (Extreme)",
|
||||
719 => "Eden's Verse: Furor",
|
||||
720 => "Eden's Verse: Furor (Savage)",
|
||||
721 => "Ocean Fishing",
|
||||
722 => "The Diadem",
|
||||
723 => "The Bozja Incident",
|
||||
724 => "A Sleep Disturbed",
|
||||
725 => "Memoria Misera (Extreme)",
|
||||
726 => "Eden's Verse: Iconoclasm",
|
||||
727 => "Eden's Verse: Iconoclasm (Savage)",
|
||||
728 => "Eden's Verse: Refulgence",
|
||||
729 => "Eden's Verse: Refulgence (Savage)",
|
||||
730 => "Ocean Fishing",
|
||||
731 => "Ocean Fishing",
|
||||
732 => "Ocean Fishing",
|
||||
733 => "Ocean Fishing",
|
||||
734 => "Ocean Fishing",
|
||||
735 => "The Bozjan Southern Front",
|
||||
736 => "The Puppets' Bunker",
|
||||
737 => "The Heroes' Gauntlet",
|
||||
738 => "The Seat of Sacrifice",
|
||||
739 => "The Seat of Sacrifice (Extreme)",
|
||||
740 => "Sleep Now in Sapphire",
|
||||
741 => "Sleep Now in Sapphire",
|
||||
742 => "The Diadem",
|
||||
743 => "Faded Memories",
|
||||
745 => "The Shifting Oubliettes of Lyhe Ghiah",
|
||||
746 => "Matoya's Relict",
|
||||
747 => "Eden's Promise: Litany",
|
||||
748 => "Eden's Promise: Litany (Savage)",
|
||||
749 => "Eden's Promise: Umbra",
|
||||
750 => "Eden's Promise: Umbra (Savage)",
|
||||
751 => "Eden's Promise: Anamorphosis",
|
||||
752 => "Eden's Promise: Anamorphosis (Savage)",
|
||||
753 => "The Diadem",
|
||||
754 => "Anything Gogo's",
|
||||
755 => "Triple Triad Open Tournament",
|
||||
756 => "Triple Triad Invitational Parlor",
|
||||
758 => "Eden's Promise: Eternity",
|
||||
759 => "Eden's Promise: Eternity (Savage)",
|
||||
760 => "Delubrum Reginae",
|
||||
761 => "Delubrum Reginae (Savage)",
|
||||
762 => "Castrum Marinum",
|
||||
763 => "Castrum Marinum (Extreme)",
|
||||
764 => "The Great Ship Vylbrand",
|
||||
765 => "Fit for a Queen",
|
||||
766 => "Novice Mahjong (Quick Ranked Match)",
|
||||
767 => "Advanced Mahjong (Quick Ranked Match)",
|
||||
768 => "Four-player Mahjong (Quick Match, Kuitan Enabled)",
|
||||
769 => "Four-player Mahjong (Quick Match, Kuitan Disabled)",
|
||||
770 => "Ocean Fishing",
|
||||
771 => "Ocean Fishing",
|
||||
772 => "Ocean Fishing",
|
||||
773 => "Ocean Fishing",
|
||||
774 => "Ocean Fishing",
|
||||
775 => "Ocean Fishing",
|
||||
776 => "The Whorleater (Unreal)",
|
||||
777 => "Paglth'an",
|
||||
778 => "Zadnor",
|
||||
779 => "The Tower at Paradigm's Breach",
|
||||
780 => "Death Unto Dawn",
|
||||
781 => "The Cloud Deck",
|
||||
782 => "The Cloud Deck (Extreme)",
|
||||
};
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
use std::collections::HashMap;
|
||||
use ffxiv_types::jobs::{ClassJob, Class, Job, NonCombatJob};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref JOBS: HashMap<u32, ClassJob> = maplit::hashmap! {
|
||||
1 => ClassJob::Class(Class::Gladiator),
|
||||
2 => ClassJob::Class(Class::Pugilist),
|
||||
3 => ClassJob::Class(Class::Marauder),
|
||||
4 => ClassJob::Class(Class::Lancer),
|
||||
5 => ClassJob::Class(Class::Archer),
|
||||
6 => ClassJob::Class(Class::Conjurer),
|
||||
7 => ClassJob::Class(Class::Thaumaturge),
|
||||
8 => ClassJob::NonCombat(NonCombatJob::Carpenter),
|
||||
9 => ClassJob::NonCombat(NonCombatJob::Blacksmith),
|
||||
10 => ClassJob::NonCombat(NonCombatJob::Armorer),
|
||||
11 => ClassJob::NonCombat(NonCombatJob::Goldsmith),
|
||||
12 => ClassJob::NonCombat(NonCombatJob::Leatherworker),
|
||||
13 => ClassJob::NonCombat(NonCombatJob::Weaver),
|
||||
14 => ClassJob::NonCombat(NonCombatJob::Alchemist),
|
||||
15 => ClassJob::NonCombat(NonCombatJob::Culinarian),
|
||||
16 => ClassJob::NonCombat(NonCombatJob::Miner),
|
||||
17 => ClassJob::NonCombat(NonCombatJob::Botanist),
|
||||
18 => ClassJob::NonCombat(NonCombatJob::Fisher),
|
||||
19 => ClassJob::Job(Job::Paladin),
|
||||
20 => ClassJob::Job(Job::Monk),
|
||||
21 => ClassJob::Job(Job::Warrior),
|
||||
22 => ClassJob::Job(Job::Dragoon),
|
||||
23 => ClassJob::Job(Job::Bard),
|
||||
24 => ClassJob::Job(Job::WhiteMage),
|
||||
25 => ClassJob::Job(Job::BlackMage),
|
||||
26 => ClassJob::Class(Class::Arcanist),
|
||||
27 => ClassJob::Job(Job::Summoner),
|
||||
28 => ClassJob::Job(Job::Scholar),
|
||||
29 => ClassJob::Class(Class::Rogue),
|
||||
30 => ClassJob::Job(Job::Ninja),
|
||||
31 => ClassJob::Job(Job::Machinist),
|
||||
32 => ClassJob::Job(Job::DarkKnight),
|
||||
33 => ClassJob::Job(Job::Astrologian),
|
||||
34 => ClassJob::Job(Job::Samurai),
|
||||
35 => ClassJob::Job(Job::RedMage),
|
||||
36 => ClassJob::Job(Job::BlueMage),
|
||||
37 => ClassJob::Job(Job::Gunbreaker),
|
||||
38 => ClassJob::Job(Job::Dancer),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref ROULETTES: HashMap<u32, &'static str> = maplit::hashmap! {
|
||||
1 => "Duty Roulette: Leveling",
|
||||
2 => "Duty Roulette: Level 50/60/70 Dungeons",
|
||||
3 => "Duty Roulette: Main Scenario",
|
||||
4 => "Duty Roulette: Guildhests",
|
||||
5 => "Duty Roulette: Expert",
|
||||
6 => "Duty Roulette: Trials",
|
||||
7 => "Daily Challenge: Frontline",
|
||||
8 => "Duty Roulette: Level 80 Dungeons",
|
||||
9 => "Duty Roulette: Mentor",
|
||||
11 => "The Feast (Training Match)",
|
||||
13 => "The Feast (Ranked Match)",
|
||||
15 => "Duty Roulette: Alliance Raids",
|
||||
16 => "The Feast (Team Ranked Match)",
|
||||
17 => "Duty Roulette: Normal Raids",
|
||||
18 => "Chocobo Race: Sagolii Road",
|
||||
19 => "Chocobo Race: Costa del Sol",
|
||||
20 => "Chocobo Race: Tranquil Paths",
|
||||
21 => "Chocobo Race: Random",
|
||||
22 => "Chocobo Race: Sagolii Road (No Rewards)",
|
||||
23 => "Chocobo Race: Costa del Sol (No Rewards)",
|
||||
24 => "Chocobo Race: Tranquil Paths (No Rewards)",
|
||||
25 => "Chocobo Race: Random (No Rewards)",
|
||||
26 => "Chocobo Race: Random",
|
||||
27 => "Chocobo Race: Random",
|
||||
28 => "Chocobo Race: Random",
|
||||
29 => "Chocobo Race: Random",
|
||||
30 => "Chocobo Race: Random",
|
||||
31 => "Chocobo Race: Random",
|
||||
32 => "Chocobo Race: Random",
|
||||
33 => "Chocobo Race: Random",
|
||||
34 => "Chocobo Race: Random",
|
||||
35 => "Chocobo Race: Random",
|
||||
36 => "Chocobo Race: Random",
|
||||
37 => "Chocobo Race: Random",
|
||||
38 => "Chocobo Race: Random",
|
||||
};
|
||||
}
|
|
@ -0,0 +1,806 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TERRITORY_NAMES: HashMap<u32, &'static str> = maplit::hashmap! {
|
||||
128 => "Limsa Lominsa Upper Decks",
|
||||
129 => "Limsa Lominsa Lower Decks",
|
||||
130 => "Ul'dah - Steps of Nald",
|
||||
131 => "Ul'dah - Steps of Thal",
|
||||
132 => "New Gridania",
|
||||
133 => "Old Gridania",
|
||||
134 => "Middle La Noscea",
|
||||
135 => "Lower La Noscea",
|
||||
136 => "Mist",
|
||||
137 => "Eastern La Noscea",
|
||||
138 => "Western La Noscea",
|
||||
139 => "Upper La Noscea",
|
||||
140 => "Western Thanalan",
|
||||
141 => "Central Thanalan",
|
||||
142 => "Halatali",
|
||||
143 => "Steps of Faith",
|
||||
144 => "The Gold Saucer",
|
||||
145 => "Eastern Thanalan",
|
||||
146 => "Southern Thanalan",
|
||||
147 => "Northern Thanalan",
|
||||
148 => "Central Shroud",
|
||||
149 => "The Feasting Grounds",
|
||||
150 => "The Keeper of the Lake",
|
||||
151 => "The World of Darkness",
|
||||
152 => "East Shroud",
|
||||
153 => "South Shroud",
|
||||
154 => "North Shroud",
|
||||
155 => "Coerthas Central Highlands",
|
||||
156 => "Mor Dhona",
|
||||
157 => "Sastasha",
|
||||
158 => "Brayflox's Longstop",
|
||||
159 => "The Wanderer's Palace",
|
||||
160 => "Pharos Sirius",
|
||||
161 => "Copperbell Mines",
|
||||
162 => "Halatali",
|
||||
163 => "The Sunken Temple of Qarn",
|
||||
164 => "The Tam-Tara Deepcroft",
|
||||
166 => "Haukke Manor",
|
||||
167 => "Amdapor Keep",
|
||||
168 => "Stone Vigil",
|
||||
169 => "The Thousand Maws of Toto-Rak",
|
||||
170 => "Cutter's Cry",
|
||||
171 => "Dzemael Darkhold",
|
||||
172 => "Aurum Vale",
|
||||
174 => "Labyrinth of the Ancients",
|
||||
176 => "Mordion Gaol",
|
||||
177 => "Mizzenmast Inn",
|
||||
178 => "The Hourglass",
|
||||
179 => "The Roost",
|
||||
180 => "Outer La Noscea",
|
||||
181 => "Limsa Lominsa",
|
||||
182 => "Ul'dah - Steps of Nald",
|
||||
183 => "New Gridania",
|
||||
188 => "The Wanderer's Palace",
|
||||
189 => "Amdapor Keep",
|
||||
190 => "Central Shroud",
|
||||
191 => "East Shroud",
|
||||
192 => "South Shroud",
|
||||
193 => "IC-06 Central Decks",
|
||||
194 => "IC-06 Regeneration Grid",
|
||||
195 => "IC-06 Main Bridge",
|
||||
196 => "The Burning Heart",
|
||||
198 => "Command Room",
|
||||
202 => "Bowl of Embers",
|
||||
204 => "Seat of the First Bow",
|
||||
205 => "Lotus Stand",
|
||||
206 => "The Navel",
|
||||
207 => "Thornmarch",
|
||||
208 => "The Howling Eye",
|
||||
210 => "Heart of the Sworn",
|
||||
212 => "The Waking Sands",
|
||||
214 => "Middle La Noscea",
|
||||
215 => "Western Thanalan",
|
||||
216 => "Central Thanalan",
|
||||
217 => "Castrum Meridianum",
|
||||
219 => "Central Shroud",
|
||||
220 => "South Shroud",
|
||||
221 => "Upper La Noscea",
|
||||
222 => "Lower La Noscea",
|
||||
223 => "Coerthas Central Highlands",
|
||||
224 => "The Praetorium",
|
||||
225 => "Central Shroud",
|
||||
226 => "Central Shroud",
|
||||
227 => "Central Shroud",
|
||||
228 => "North Shroud",
|
||||
229 => "South Shroud",
|
||||
230 => "Central Shroud",
|
||||
231 => "South Shroud",
|
||||
232 => "South Shroud",
|
||||
233 => "Central Shroud",
|
||||
234 => "East Shroud",
|
||||
235 => "South Shroud",
|
||||
236 => "South Shroud",
|
||||
237 => "Central Shroud",
|
||||
238 => "Old Gridania",
|
||||
239 => "Central Shroud",
|
||||
240 => "North Shroud",
|
||||
241 => "Upper Aetheroacoustic Exploratory Site",
|
||||
242 => "Lower Aetheroacoustic Exploratory Site",
|
||||
243 => "The Ragnarok",
|
||||
244 => "Ragnarok Drive Cylinder",
|
||||
245 => "Ragnarok Central Core",
|
||||
246 => "IC-04 Main Bridge",
|
||||
247 => "Ragnarok Main Bridge",
|
||||
248 => "Central Thanalan",
|
||||
249 => "Lower La Noscea",
|
||||
250 => "Wolves' Den Pier",
|
||||
251 => "Ul'dah - Steps of Nald",
|
||||
252 => "Middle La Noscea",
|
||||
253 => "Central Thanalan",
|
||||
254 => "Ul'dah - Steps of Nald",
|
||||
255 => "Western Thanalan",
|
||||
256 => "Eastern Thanalan",
|
||||
257 => "Eastern Thanalan",
|
||||
258 => "Central Thanalan",
|
||||
259 => "Ul'dah - Steps of Nald",
|
||||
260 => "Southern Thanalan",
|
||||
261 => "Southern Thanalan",
|
||||
262 => "Lower La Noscea",
|
||||
263 => "Western La Noscea",
|
||||
264 => "Lower La Noscea",
|
||||
265 => "Lower La Noscea",
|
||||
266 => "Eastern Thanalan",
|
||||
267 => "Western Thanalan",
|
||||
268 => "Eastern Thanalan",
|
||||
269 => "Western Thanalan",
|
||||
270 => "Central Thanalan",
|
||||
271 => "Central Thanalan",
|
||||
272 => "Middle La Noscea",
|
||||
273 => "Western Thanalan",
|
||||
274 => "Ul'dah - Steps of Nald",
|
||||
275 => "Eastern Thanalan",
|
||||
276 => "Hall of Summoning",
|
||||
277 => "East Shroud",
|
||||
278 => "Western Thanalan",
|
||||
279 => "Lower La Noscea",
|
||||
280 => "Western La Noscea",
|
||||
281 => "The Whorleater",
|
||||
282 => "Private Cottage - Mist",
|
||||
283 => "Private House - Mist",
|
||||
284 => "Private Mansion - Mist",
|
||||
285 => "Middle La Noscea",
|
||||
286 => "Rhotano Sea",
|
||||
287 => "Lower La Noscea",
|
||||
288 => "Rhotano Sea",
|
||||
289 => "East Shroud",
|
||||
290 => "East Shroud",
|
||||
291 => "South Shroud",
|
||||
292 => "Bowl of Embers",
|
||||
293 => "The Navel",
|
||||
294 => "The Howling Eye",
|
||||
295 => "Bowl of Embers",
|
||||
296 => "The Navel",
|
||||
297 => "The Howling Eye",
|
||||
298 => "Coerthas Central Highlands",
|
||||
299 => "Mor Dhona",
|
||||
300 => "Mor Dhona",
|
||||
301 => "Coerthas Central Highlands",
|
||||
302 => "Coerthas Central Highlands",
|
||||
303 => "East Shroud",
|
||||
304 => "Coerthas Central Highlands",
|
||||
305 => "Mor Dhona",
|
||||
306 => "Southern Thanalan",
|
||||
307 => "Lower La Noscea",
|
||||
308 => "Mor Dhona",
|
||||
309 => "Mor Dhona",
|
||||
310 => "Eastern La Noscea",
|
||||
311 => "Eastern La Noscea",
|
||||
312 => "Southern Thanalan",
|
||||
313 => "Coerthas Central Highlands",
|
||||
314 => "Central Thanalan",
|
||||
315 => "Mor Dhona",
|
||||
316 => "Coerthas Central Highlands",
|
||||
317 => "South Shroud",
|
||||
318 => "Southern Thanalan",
|
||||
319 => "Central Shroud",
|
||||
320 => "Central Shroud",
|
||||
321 => "North Shroud",
|
||||
322 => "Coerthas Central Highlands",
|
||||
323 => "Southern Thanalan",
|
||||
324 => "North Shroud",
|
||||
325 => "Outer La Noscea",
|
||||
326 => "Mor Dhona",
|
||||
327 => "Eastern La Noscea",
|
||||
328 => "Upper La Noscea",
|
||||
329 => "The Wanderer's Palace",
|
||||
330 => "Western La Noscea",
|
||||
331 => "The Howling Eye",
|
||||
332 => "Western Thanalan",
|
||||
335 => "Mor Dhona",
|
||||
338 => "Eorzean Subterrane",
|
||||
339 => "Mist",
|
||||
340 => "The Lavender Beds",
|
||||
341 => "The Goblet",
|
||||
342 => "Private Cottage - The Lavender Beds",
|
||||
343 => "Private House - The Lavender Beds",
|
||||
344 => "Private Mansion - The Lavender Beds",
|
||||
345 => "Private Cottage - The Goblet",
|
||||
346 => "Private House - The Goblet",
|
||||
347 => "Private Mansion - The Goblet",
|
||||
348 => "Porta Decumana",
|
||||
349 => "Copperbell Mines",
|
||||
350 => "Haukke Manor",
|
||||
351 => "The Rising Stones",
|
||||
353 => "Kugane Ohashi",
|
||||
354 => "The Dancing Plague",
|
||||
355 => "Dalamud's Shadow",
|
||||
356 => "The Outer Coil",
|
||||
357 => "Central Decks",
|
||||
358 => "The Holocharts",
|
||||
359 => "The Whorleater",
|
||||
360 => "Halatali",
|
||||
361 => "Hullbreaker Isle",
|
||||
362 => "Brayflox's Longstop",
|
||||
363 => "The Lost City of Amdapor",
|
||||
364 => "Thornmarch",
|
||||
365 => "Stone Vigil",
|
||||
366 => "Griffin Crossing",
|
||||
367 => "The Sunken Temple of Qarn",
|
||||
368 => "The Weeping Saint",
|
||||
369 => "Hall of the Bestiarii",
|
||||
370 => "Main Bridge",
|
||||
371 => "Snowcloak",
|
||||
372 => "Syrcus Tower",
|
||||
373 => "The Tam-Tara Deepcroft",
|
||||
374 => "The Striking Tree",
|
||||
375 => "The Striking Tree",
|
||||
376 => "Carteneau Flats: Borderland Ruins",
|
||||
377 => "Akh Afah Amphitheatre",
|
||||
378 => "Akh Afah Amphitheatre",
|
||||
379 => "Mor Dhona",
|
||||
380 => "Dalamud's Shadow",
|
||||
381 => "The Outer Coil",
|
||||
382 => "Central Decks",
|
||||
383 => "The Holocharts",
|
||||
384 => "Private Chambers - Mist",
|
||||
385 => "Private Chambers - The Lavender Beds",
|
||||
386 => "Private Chambers - The Goblet",
|
||||
387 => "Sastasha",
|
||||
388 => "Chocobo Square",
|
||||
389 => "Chocobo Square",
|
||||
390 => "Chocobo Square",
|
||||
391 => "Chocobo Square",
|
||||
392 => "Sanctum of the Twelve",
|
||||
393 => "Sanctum of the Twelve",
|
||||
394 => "South Shroud",
|
||||
395 => "Intercessory",
|
||||
396 => "Amdapor Keep",
|
||||
397 => "Coerthas Western Highlands",
|
||||
398 => "The Dravanian Forelands",
|
||||
399 => "The Dravanian Hinterlands",
|
||||
400 => "The Churning Mists",
|
||||
401 => "The Sea of Clouds",
|
||||
402 => "Azys Lla",
|
||||
403 => "Ala Mhigo",
|
||||
404 => "Limsa Lominsa Lower Decks",
|
||||
405 => "Western La Noscea",
|
||||
406 => "Western La Noscea",
|
||||
407 => "Rhotano Sea",
|
||||
408 => "Eastern La Noscea",
|
||||
409 => "Limsa Lominsa Upper Decks",
|
||||
410 => "Northern Thanalan",
|
||||
411 => "Eastern La Noscea",
|
||||
412 => "Upper La Noscea",
|
||||
413 => "Western La Noscea",
|
||||
414 => "Eastern La Noscea",
|
||||
415 => "Lower La Noscea",
|
||||
416 => "The Great Gubal Library",
|
||||
417 => "Chocobo Square",
|
||||
418 => "Foundation",
|
||||
419 => "The Pillars",
|
||||
420 => "Neverreap",
|
||||
421 => "The Vault",
|
||||
423 => "Company Workshop - Mist",
|
||||
424 => "Company Workshop - The Goblet",
|
||||
425 => "Company Workshop - The Lavender Beds",
|
||||
426 => "The Chrysalis",
|
||||
427 => "Saint Endalim's Scholasticate",
|
||||
428 => "Seat of the Lord Commander",
|
||||
429 => "Cloud Nine",
|
||||
430 => "The Fractal Continuum",
|
||||
431 => "Seal Rock",
|
||||
432 => "Thok ast Thok",
|
||||
433 => "Fortemps Manor",
|
||||
434 => "Dusk Vigil",
|
||||
435 => "The Aery",
|
||||
436 => "The Limitless Blue",
|
||||
437 => "Singularity Reactor",
|
||||
438 => "Aetherochemical Research Facility",
|
||||
439 => "The Lightfeather Proving Grounds",
|
||||
440 => "Ruling Chamber",
|
||||
441 => "Sohm Al",
|
||||
442 => "The Fist of the Father",
|
||||
443 => "The Cuff of the Father",
|
||||
444 => "The Arm of the Father",
|
||||
445 => "The Burden of the Father",
|
||||
446 => "Thok ast Thok",
|
||||
447 => "The Limitless Blue",
|
||||
448 => "Singularity Reactor",
|
||||
449 => "The Fist of the Father",
|
||||
450 => "The Cuff of the Father",
|
||||
451 => "The Arm of the Father",
|
||||
452 => "The Burden of the Father",
|
||||
453 => "Western La Noscea",
|
||||
454 => "Upper La Noscea",
|
||||
455 => "The Sea of Clouds",
|
||||
456 => "Ruling Chamber",
|
||||
457 => "Akh Afah Amphitheatre",
|
||||
458 => "Foundation",
|
||||
459 => "Azys Lla",
|
||||
460 => "Halatali",
|
||||
461 => "The Sea of Clouds",
|
||||
462 => "Sacrificial Chamber",
|
||||
463 => "Matoya's Cave",
|
||||
464 => "The Dravanian Forelands",
|
||||
465 => "Eastern Thanalan",
|
||||
466 => "Upper La Noscea",
|
||||
467 => "Coerthas Western Highlands",
|
||||
468 => "Coerthas Central Highlands",
|
||||
469 => "Coerthas Central Highlands",
|
||||
470 => "Coerthas Western Highlands",
|
||||
471 => "Eastern La Noscea",
|
||||
472 => "Coerthas Western Highlands",
|
||||
473 => "South Shroud",
|
||||
474 => "Limsa Lominsa Upper Decks",
|
||||
475 => "Coerthas Central Highlands",
|
||||
476 => "The Dravanian Hinterlands",
|
||||
477 => "Coerthas Western Highlands",
|
||||
478 => "Idyllshire",
|
||||
479 => "Coerthas Western Highlands",
|
||||
480 => "Mor Dhona",
|
||||
481 => "The Dravanian Forelands",
|
||||
482 => "The Dravanian Forelands",
|
||||
483 => "Northern Thanalan",
|
||||
484 => "Lower La Noscea",
|
||||
485 => "The Dravanian Hinterlands",
|
||||
486 => "Outer La Noscea",
|
||||
487 => "Coerthas Central Highlands",
|
||||
488 => "Coerthas Central Highlands",
|
||||
489 => "Coerthas Western Highlands",
|
||||
490 => "Hullbreaker Isle",
|
||||
491 => "Southern Thanalan",
|
||||
492 => "The Sea of Clouds",
|
||||
493 => "Coerthas Western Highlands",
|
||||
494 => "Eastern Thanalan",
|
||||
495 => "Lower La Noscea",
|
||||
496 => "Coerthas Central Highlands",
|
||||
497 => "Coerthas Western Highlands",
|
||||
498 => "Coerthas Western Highlands",
|
||||
499 => "The Pillars",
|
||||
500 => "Coerthas Central Highlands",
|
||||
501 => "The Churning Mists",
|
||||
502 => "Carteneau Flats: Borderland Ruins",
|
||||
503 => "The Dravanian Hinterlands",
|
||||
504 => "The Eighteenth Floor",
|
||||
505 => "Alexander",
|
||||
506 => "Chocobo Square",
|
||||
507 => "Central Azys Lla",
|
||||
508 => "Void Ark",
|
||||
509 => "The Navel",
|
||||
510 => "Pharos Sirius",
|
||||
511 => "Saint Mocianne's Arboretum",
|
||||
512 => "The Diadem",
|
||||
513 => "The Vault",
|
||||
514 => "The Diadem",
|
||||
515 => "The Diadem",
|
||||
516 => "The Antitower",
|
||||
517 => "Containment Bay S1T7",
|
||||
519 => "The Lost City of Amdapor",
|
||||
520 => "The Fist of the Son",
|
||||
521 => "The Cuff of the Son",
|
||||
522 => "The Arm of the Son",
|
||||
523 => "The Burden of the Son",
|
||||
524 => "Containment Bay S1T7",
|
||||
525 => "The Feasting Grounds",
|
||||
527 => "The Feasting Grounds",
|
||||
529 => "The Fist of the Son",
|
||||
530 => "The Cuff of the Son",
|
||||
531 => "The Arm of the Son",
|
||||
532 => "The Burden of the Son",
|
||||
533 => "Coerthas Central Highlands",
|
||||
534 => "Twin Adder Barracks",
|
||||
535 => "Flame Barracks",
|
||||
536 => "Maelstrom Barracks",
|
||||
537 => "The Fold",
|
||||
538 => "The Fold",
|
||||
539 => "The Fold",
|
||||
540 => "The Fold",
|
||||
541 => "The Fold",
|
||||
542 => "The Fold",
|
||||
543 => "The Fold",
|
||||
544 => "The Fold",
|
||||
545 => "The Fold",
|
||||
546 => "The Fold",
|
||||
547 => "The Fold",
|
||||
548 => "The Fold",
|
||||
549 => "The Fold",
|
||||
550 => "The Fold",
|
||||
551 => "The Fold",
|
||||
552 => "Western La Noscea",
|
||||
553 => "Alexander",
|
||||
554 => "The Fields of Glory",
|
||||
555 => "Sohr Khai",
|
||||
556 => "The Weeping City of Mhach",
|
||||
557 => "Hullbreaker Isle",
|
||||
558 => "The Aquapolis",
|
||||
559 => "Steps of Faith",
|
||||
560 => "Aetherochemical Research Facility",
|
||||
561 => "The Palace of the Dead",
|
||||
562 => "The Palace of the Dead",
|
||||
563 => "The Palace of the Dead",
|
||||
564 => "The Palace of the Dead",
|
||||
565 => "The Palace of the Dead",
|
||||
566 => "Steps of Faith",
|
||||
567 => "The Parrock",
|
||||
568 => "Leofard's Chambers",
|
||||
569 => "Steps of Faith",
|
||||
570 => "The Palace of the Dead",
|
||||
571 => "Haunted Manor",
|
||||
572 => "Xelphatol",
|
||||
573 => "Topmast Apartment Lobby",
|
||||
574 => "Lily Hills Apartment Lobby",
|
||||
575 => "Sultana's Breath Apartment Lobby",
|
||||
576 => "Containment Bay P1T6",
|
||||
577 => "Containment Bay P1T6",
|
||||
578 => "The Great Gubal Library",
|
||||
579 => "The Battlehall",
|
||||
580 => "Eyes of the Creator",
|
||||
581 => "Breath of the Creator",
|
||||
582 => "Heart of the Creator",
|
||||
583 => "Soul of the Creator",
|
||||
584 => "Eyes of the Creator",
|
||||
585 => "Breath of the Creator",
|
||||
586 => "Heart of the Creator",
|
||||
587 => "Soul of the Creator",
|
||||
588 => "Heart of the Creator",
|
||||
589 => "Chocobo Square",
|
||||
590 => "Chocobo Square",
|
||||
591 => "Chocobo Square",
|
||||
592 => "Bowl of Embers",
|
||||
593 => "The Palace of the Dead",
|
||||
594 => "The Palace of the Dead",
|
||||
595 => "The Palace of the Dead",
|
||||
596 => "The Palace of the Dead",
|
||||
597 => "The Palace of the Dead",
|
||||
598 => "The Palace of the Dead",
|
||||
599 => "The Palace of the Dead",
|
||||
600 => "The Palace of the Dead",
|
||||
601 => "The Palace of the Dead",
|
||||
602 => "The Palace of the Dead",
|
||||
603 => "The Palace of the Dead",
|
||||
604 => "The Palace of the Dead",
|
||||
605 => "The Palace of the Dead",
|
||||
606 => "The Palace of the Dead",
|
||||
607 => "The Palace of the Dead",
|
||||
608 => "Topmast Apartment",
|
||||
609 => "Lily Hills Apartment",
|
||||
610 => "Sultana's Breath Apartment",
|
||||
611 => "Frondale's Home for Friendless Foundlings",
|
||||
612 => "The Fringes",
|
||||
613 => "The Ruby Sea",
|
||||
614 => "Yanxia",
|
||||
615 => "Baelsar's Wall",
|
||||
616 => "Shisui of the Violet Tides",
|
||||
617 => "Sohm Al",
|
||||
619 => "The Feasting Grounds",
|
||||
620 => "The Peaks",
|
||||
621 => "The Lochs",
|
||||
622 => "The Azim Steppe",
|
||||
623 => "Bardam's Mettle",
|
||||
624 => "The Diadem",
|
||||
625 => "The Diadem",
|
||||
626 => "The Sirensong Sea",
|
||||
627 => "Dun Scaith",
|
||||
628 => "Kugane",
|
||||
629 => "Bokairo Inn",
|
||||
630 => "The Diadem",
|
||||
632 => "Lichenweed",
|
||||
633 => "Carteneau Flats: Borderland Ruins",
|
||||
634 => "Yanxia",
|
||||
635 => "Rhalgr's Reach",
|
||||
636 => "Omega Control",
|
||||
637 => "Containment Bay Z1T9",
|
||||
638 => "Containment Bay Z1T9",
|
||||
639 => "Ruby Bazaar Offices",
|
||||
640 => "The Fringes",
|
||||
641 => "Shirogane",
|
||||
644 => "Lichenweed",
|
||||
646 => "Lichenweed",
|
||||
647 => "The Fringes",
|
||||
648 => "The Fringes",
|
||||
649 => "Private Cottage - Shirogane",
|
||||
650 => "Private House - Shirogane",
|
||||
651 => "Private Mansion - Shirogane",
|
||||
652 => "Private Chambers - Shirogane",
|
||||
653 => "Company Workshop - Shirogane",
|
||||
654 => "Kobai Goten Apartment Lobby",
|
||||
655 => "Kobai Goten Apartment",
|
||||
656 => "The Diadem",
|
||||
657 => "The Ruby Sea",
|
||||
658 => "The Interdimensional Rift",
|
||||
659 => "Rhalgr's Reach",
|
||||
660 => "Doma Castle",
|
||||
661 => "Castrum Abania",
|
||||
662 => "Kugane Castle",
|
||||
663 => "The Temple of the Fist",
|
||||
664 => "Kugane",
|
||||
665 => "Kugane",
|
||||
666 => "Ul'dah - Steps of Thal",
|
||||
667 => "Kugane",
|
||||
668 => "Eastern Thanalan",
|
||||
669 => "Southern Thanalan",
|
||||
670 => "The Fringes",
|
||||
671 => "The Fringes",
|
||||
672 => "Mor Dhona",
|
||||
673 => "Sohm Al",
|
||||
674 => "The Blessed Treasury",
|
||||
675 => "Western La Noscea",
|
||||
676 => "The Great Gubal Library",
|
||||
677 => "The Blessed Treasury",
|
||||
678 => "The Fringes",
|
||||
679 => "The Royal Airship Landing",
|
||||
680 => "The Misery",
|
||||
681 => "The House of the Fierce",
|
||||
682 => "The Doman Enclave",
|
||||
683 => "The First Altar of Djanan Qhat",
|
||||
684 => "The Lochs",
|
||||
685 => "Yanxia",
|
||||
686 => "The Lochs",
|
||||
687 => "The Lochs",
|
||||
688 => "The Azim Steppe",
|
||||
689 => "Ala Mhigo",
|
||||
690 => "The Interdimensional Rift",
|
||||
691 => "Deltascape V1.0",
|
||||
692 => "Deltascape V2.0",
|
||||
693 => "Deltascape V3.0",
|
||||
694 => "Deltascape V4.0",
|
||||
695 => "Deltascape V1.0",
|
||||
696 => "Deltascape V2.0",
|
||||
697 => "Deltascape V3.0",
|
||||
698 => "Deltascape V4.0",
|
||||
699 => "Coerthas Central Highlands",
|
||||
700 => "Foundation",
|
||||
701 => "Seal Rock",
|
||||
702 => "Aetherochemical Research Facility",
|
||||
703 => "The Fringes",
|
||||
704 => "Dalamud's Shadow",
|
||||
705 => "Ul'dah - Steps of Thal",
|
||||
706 => "Ul'dah - Steps of Thal",
|
||||
707 => "The Weeping City of Mhach",
|
||||
708 => "Rhotano Sea",
|
||||
709 => "Coerthas Western Highlands",
|
||||
710 => "Kugane",
|
||||
711 => "The Ruby Sea",
|
||||
712 => "The Lost Canals of Uznair",
|
||||
713 => "The Azim Steppe",
|
||||
714 => "Bardam's Mettle",
|
||||
715 => "The Churning Mists",
|
||||
716 => "The Peaks",
|
||||
717 => "Wolves' Den Pier",
|
||||
718 => "The Azim Steppe",
|
||||
719 => "Emanation",
|
||||
720 => "Emanation",
|
||||
721 => "Amdapor Keep",
|
||||
722 => "The Lost City of Amdapor",
|
||||
723 => "The Azim Steppe",
|
||||
724 => "The Interdimensional Rift",
|
||||
725 => "The Lost Canals of Uznair",
|
||||
726 => "The Ruby Sea",
|
||||
727 => "The Royal Menagerie",
|
||||
728 => "Mordion Gaol",
|
||||
729 => "Astragalos",
|
||||
730 => "Transparency",
|
||||
731 => "The Drowned City of Skalla",
|
||||
732 => "Eureka Anemos",
|
||||
733 => "The Binding Coil of Bahamut",
|
||||
734 => "The Royal City of Rabanastre",
|
||||
735 => "The Prima Vista Tiring Room",
|
||||
736 => "The Prima Vista Bridge",
|
||||
737 => "Royal Palace",
|
||||
738 => "The Resonatorium",
|
||||
739 => "The Doman Enclave",
|
||||
740 => "The Royal Menagerie",
|
||||
741 => "Sanctum of the Twelve",
|
||||
742 => "Hells' Lid",
|
||||
743 => "The Fractal Continuum",
|
||||
744 => "Kienkan",
|
||||
745 => "Crystal Tower Training Grounds",
|
||||
746 => "The Jade Stoa",
|
||||
748 => "Sigmascape V1.0",
|
||||
749 => "Sigmascape V2.0",
|
||||
750 => "Sigmascape V3.0",
|
||||
751 => "Sigmascape V4.0",
|
||||
752 => "Sigmascape V1.0",
|
||||
753 => "Sigmascape V2.0",
|
||||
754 => "Sigmascape V3.0",
|
||||
755 => "Sigmascape V4.0",
|
||||
756 => "The Interdimensional Rift",
|
||||
757 => "The Ruby Sea",
|
||||
758 => "The Jade Stoa",
|
||||
759 => "The Doman Enclave",
|
||||
760 => "The Fringes",
|
||||
761 => "The Great Hunt",
|
||||
762 => "The Great Hunt",
|
||||
763 => "Eureka Pagos",
|
||||
764 => "Reisen Temple",
|
||||
765 => "Crystal Tower Training Grounds",
|
||||
766 => "Crystal Tower Training Grounds",
|
||||
767 => "Crystal Tower Training Grounds",
|
||||
768 => "The Swallow's Compass",
|
||||
769 => "The Burn",
|
||||
770 => "Heaven-on-High",
|
||||
771 => "Heaven-on-High",
|
||||
772 => "Heaven-on-High",
|
||||
773 => "Heaven-on-High",
|
||||
774 => "Heaven-on-High",
|
||||
775 => "Heaven-on-High",
|
||||
776 => "The Ridorana Lighthouse",
|
||||
777 => "Ultimacy",
|
||||
778 => "Castrum Fluminis",
|
||||
779 => "Castrum Fluminis",
|
||||
780 => "Heaven-on-High",
|
||||
781 => "Reisen Temple Road",
|
||||
782 => "Heaven-on-High",
|
||||
783 => "Heaven-on-High",
|
||||
784 => "Heaven-on-High",
|
||||
785 => "Heaven-on-High",
|
||||
786 => "Castrum Fluminis",
|
||||
787 => "The Ridorana Cataract",
|
||||
788 => "Saint Mocianne's Arboretum",
|
||||
789 => "The Burn",
|
||||
790 => "Ul'dah - Steps of Nald",
|
||||
791 => "Hidden Gorge",
|
||||
792 => "The Fall of Belah'dia",
|
||||
793 => "The Ghimlyt Dark",
|
||||
794 => "The Shifting Altars of Uznair",
|
||||
795 => "Eureka Pyros",
|
||||
796 => "Blue Sky",
|
||||
797 => "The Azim Steppe",
|
||||
798 => "Psiscape V1.0",
|
||||
799 => "Psiscape V2.0",
|
||||
800 => "The Interdimensional Rift",
|
||||
801 => "The Interdimensional Rift",
|
||||
802 => "Psiscape V1.0",
|
||||
803 => "Psiscape V2.0",
|
||||
804 => "The Interdimensional Rift",
|
||||
805 => "The Interdimensional Rift",
|
||||
806 => "Kugane Ohashi",
|
||||
807 => "The Interdimensional Rift",
|
||||
808 => "The Interdimensional Rift",
|
||||
809 => "Haunted Manor",
|
||||
810 => "Hells' Kier",
|
||||
811 => "Hells' Kier",
|
||||
812 => "The Interdimensional Rift",
|
||||
813 => "Lakeland",
|
||||
814 => "Kholusia",
|
||||
815 => "Amh Araeng",
|
||||
816 => "Il Mheg",
|
||||
817 => "The Rak'tika Greatwood",
|
||||
818 => "The Tempest",
|
||||
819 => "The Crystarium",
|
||||
820 => "Eulmore",
|
||||
821 => "Dohn Mheg",
|
||||
822 => "Mt. Gulg",
|
||||
823 => "The Qitana Ravel",
|
||||
824 => "The Wreath of Snakes",
|
||||
825 => "The Wreath of Snakes",
|
||||
826 => "The Orbonne Monastery",
|
||||
827 => "Eureka Hydatos",
|
||||
828 => "The Prima Vista Tiring Room",
|
||||
829 => "Eorzean Alliance Headquarters",
|
||||
830 => "The Ghimlyt Dark",
|
||||
831 => "The Manderville Tables",
|
||||
832 => "The Gold Saucer",
|
||||
833 => "The Howling Eye",
|
||||
834 => "The Howling Eye",
|
||||
836 => "Malikah's Well",
|
||||
837 => "Holminster Switch",
|
||||
838 => "Amaurot",
|
||||
839 => "East Shroud",
|
||||
840 => "The Twinning",
|
||||
841 => "Akadaemia Anyder",
|
||||
842 => "The Syrcus Trench",
|
||||
843 => "The Pendants Personal Suite",
|
||||
844 => "The Ocular",
|
||||
845 => "The Dancing Plague",
|
||||
846 => "The Crown of the Immaculate",
|
||||
847 => "The Dying Gasp",
|
||||
848 => "The Crown of the Immaculate",
|
||||
849 => "The Core",
|
||||
850 => "The Halo",
|
||||
851 => "The Nereus Trench",
|
||||
852 => "Atlas Peak",
|
||||
853 => "The Core",
|
||||
854 => "The Halo",
|
||||
855 => "The Nereus Trench",
|
||||
856 => "Atlas Peak",
|
||||
857 => "The Core",
|
||||
858 => "The Dancing Plague",
|
||||
859 => "The Confessional of Toupasa the Elder",
|
||||
860 => "Amh Araeng",
|
||||
861 => "Lakeland",
|
||||
862 => "Lakeland",
|
||||
863 => "Eulmore",
|
||||
864 => "Kholusia",
|
||||
865 => "Old Gridania",
|
||||
866 => "Coerthas Western Highlands",
|
||||
867 => "Eastern La Noscea",
|
||||
868 => "The Peaks",
|
||||
869 => "Il Mheg",
|
||||
870 => "Kholusia",
|
||||
871 => "The Rak'tika Greatwood",
|
||||
872 => "Amh Araeng",
|
||||
873 => "The Dancing Plague",
|
||||
874 => "The Rak'tika Greatwood",
|
||||
875 => "The Rak'tika Greatwood",
|
||||
876 => "The Nabaath Mines",
|
||||
877 => "Lakeland",
|
||||
878 => "The Empty",
|
||||
879 => "The Dungeons of Lyhe Ghiah",
|
||||
880 => "The Crown of the Immaculate",
|
||||
881 => "The Dying Gasp",
|
||||
882 => "The Copied Factory",
|
||||
884 => "The Grand Cosmos",
|
||||
885 => "The Dying Gasp",
|
||||
886 => "The Firmament",
|
||||
887 => "Liminal Space",
|
||||
888 => "Onsal Hakair",
|
||||
889 => "Lyhe Mheg",
|
||||
890 => "Lyhe Mheg",
|
||||
891 => "Lyhe Mheg",
|
||||
892 => "Lyhe Mheg",
|
||||
893 => "The Imperial Palace",
|
||||
894 => "Lyhe Mheg",
|
||||
895 => "Excavation Tunnels",
|
||||
896 => "The Copied Factory",
|
||||
897 => "Cinder Drift",
|
||||
898 => "Anamnesis Anyder",
|
||||
899 => "The Falling City of Nym",
|
||||
900 => "The Endeavor",
|
||||
901 => "The Diadem",
|
||||
902 => "The Gandof Thunder Plains",
|
||||
903 => "Ashfall",
|
||||
904 => "The Halo",
|
||||
905 => "Great Glacier",
|
||||
906 => "The Gandof Thunder Plains",
|
||||
907 => "Ashfall",
|
||||
908 => "The Halo",
|
||||
909 => "Great Glacier",
|
||||
911 => "Cid's Memory",
|
||||
912 => "Cinder Drift",
|
||||
913 => "Transmission Control",
|
||||
914 => "Trial's Threshold",
|
||||
915 => "Gangos",
|
||||
916 => "The Heroes' Gauntlet",
|
||||
917 => "The Puppets' Bunker",
|
||||
918 => "Anamnesis Anyder",
|
||||
919 => "Terncliff",
|
||||
920 => "Bozjan Southern Front",
|
||||
921 => "Frondale's Home for Friendless Foundlings",
|
||||
922 => "The Seat of Sacrifice",
|
||||
923 => "The Seat of Sacrifice",
|
||||
924 => "The Shifting Oubliettes of Lyhe Ghiah",
|
||||
925 => "Terncliff Bay",
|
||||
926 => "Terncliff Bay",
|
||||
928 => "The Puppets' Bunker",
|
||||
929 => "The Diadem",
|
||||
930 => "Akh Afah Amphitheatre",
|
||||
931 => "The Seat of Sacrifice",
|
||||
932 => "The Tempest",
|
||||
933 => "Matoya's Relict",
|
||||
934 => "Castrum Marinum Drydocks",
|
||||
935 => "Castrum Marinum Drydocks",
|
||||
936 => "Delubrum Reginae",
|
||||
937 => "Delubrum Reginae",
|
||||
938 => "Paglth'an",
|
||||
939 => "The Diadem",
|
||||
940 => "The Battlehall",
|
||||
941 => "The Battlehall",
|
||||
942 => "Sphere of Naught",
|
||||
943 => "Laxan Loft",
|
||||
944 => "Bygone Gaol",
|
||||
945 => "The Garden of Nowhere",
|
||||
946 => "Sphere of Naught",
|
||||
947 => "Laxan Loft",
|
||||
948 => "Bygone Gaol",
|
||||
949 => "The Garden of Nowhere",
|
||||
950 => "G-Savior Deck",
|
||||
951 => "G-Savior Deck",
|
||||
953 => "The Navel",
|
||||
954 => "The Navel",
|
||||
955 => "The Last Trace",
|
||||
964 => "The Last Trace",
|
||||
965 => "The Empty",
|
||||
966 => "The Tower at Paradigm's Breach",
|
||||
967 => "Castrum Marinum Drydocks",
|
||||
972 => "The Whorleater",
|
||||
975 => "Zadnor",
|
||||
977 => "Carteneau Flats: Borderland Ruins",
|
||||
991 => "G-Savior Deck",
|
||||
};
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
use std::collections::HashMap;
|
||||
use ffxiv_types::World;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref WORLDS: HashMap<u32, World> = maplit::hashmap! {
|
||||
23 => World::Asura,
|
||||
24 => World::Belias,
|
||||
28 => World::Pandaemonium,
|
||||
29 => World::Shinryu,
|
||||
30 => World::Unicorn,
|
||||
31 => World::Yojimbo,
|
||||
32 => World::Zeromus,
|
||||
33 => World::Twintania,
|
||||
34 => World::Brynhildr,
|
||||
35 => World::Famfrit,
|
||||
36 => World::Lich,
|
||||
37 => World::Mateus,
|
||||
39 => World::Omega,
|
||||
40 => World::Jenova,
|
||||
41 => World::Zalera,
|
||||
42 => World::Zodiark,
|
||||
43 => World::Alexander,
|
||||
44 => World::Anima,
|
||||
45 => World::Carbuncle,
|
||||
46 => World::Fenrir,
|
||||
47 => World::Hades,
|
||||
48 => World::Ixion,
|
||||
49 => World::Kujata,
|
||||
50 => World::Typhon,
|
||||
51 => World::Ultima,
|
||||
52 => World::Valefor,
|
||||
53 => World::Exodus,
|
||||
54 => World::Faerie,
|
||||
55 => World::Lamia,
|
||||
56 => World::Phoenix,
|
||||
57 => World::Siren,
|
||||
58 => World::Garuda,
|
||||
59 => World::Ifrit,
|
||||
60 => World::Ramuh,
|
||||
61 => World::Titan,
|
||||
62 => World::Diabolos,
|
||||
63 => World::Gilgamesh,
|
||||
64 => World::Leviathan,
|
||||
65 => World::Midgardsormr,
|
||||
66 => World::Odin,
|
||||
67 => World::Shiva,
|
||||
68 => World::Atomos,
|
||||
69 => World::Bahamut,
|
||||
70 => World::Chocobo,
|
||||
71 => World::Moogle,
|
||||
72 => World::Tonberry,
|
||||
73 => World::Adamantoise,
|
||||
74 => World::Coeurl,
|
||||
75 => World::Malboro,
|
||||
76 => World::Tiamat,
|
||||
77 => World::Ultros,
|
||||
78 => World::Behemoth,
|
||||
79 => World::Cactuar,
|
||||
80 => World::Cerberus,
|
||||
81 => World::Goblin,
|
||||
82 => World::Mandragora,
|
||||
83 => World::Louisoix,
|
||||
85 => World::Spriggan,
|
||||
90 => World::Aegis,
|
||||
91 => World::Balmung,
|
||||
92 => World::Durandal,
|
||||
93 => World::Excalibur,
|
||||
94 => World::Gungnir,
|
||||
95 => World::Hyperion,
|
||||
96 => World::Masamune,
|
||||
97 => World::Ragnarok,
|
||||
98 => World::Ridill,
|
||||
99 => World::Sargatanas,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,375 @@
|
|||
use std::borrow::Cow;
|
||||
use bitflags::bitflags;
|
||||
use ffxiv_types::jobs::{ClassJob, Class, Job};
|
||||
use ffxiv_types::{Role, World};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use sestring::SeString;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct PartyFinderListing {
|
||||
pub id: u32,
|
||||
pub content_id_lower: u32,
|
||||
#[serde(with = "crate::base64_sestring")]
|
||||
pub name: SeString,
|
||||
#[serde(with = "crate::base64_sestring")]
|
||||
pub description: SeString,
|
||||
pub created_world: u8,
|
||||
pub home_world: u8,
|
||||
pub current_world: u8,
|
||||
pub category: DutyCategory,
|
||||
pub duty: u16,
|
||||
pub duty_type: DutyType,
|
||||
pub beginners_welcome: bool,
|
||||
pub seconds_remaining: u16,
|
||||
pub min_item_level: u16,
|
||||
pub num_parties: u8,
|
||||
pub slots_available: u8,
|
||||
pub objective: ObjectiveFlags,
|
||||
pub conditions: ConditionFlags,
|
||||
pub duty_finder_settings: DutyFinderSettingsFlags,
|
||||
pub loot_rules: LootRuleFlags,
|
||||
pub search_area: SearchAreaFlags,
|
||||
pub slots: Vec<PartyFinderSlot>,
|
||||
pub jobs_present: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PartyFinderListing {
|
||||
pub fn slots_filled(&self) -> usize {
|
||||
self.jobs_present.iter().filter(|&&job| job > 0).count()
|
||||
}
|
||||
|
||||
pub fn is_cross_world(&self) -> bool {
|
||||
self.search_area.contains(SearchAreaFlags::DATA_CENTRE)
|
||||
}
|
||||
|
||||
pub fn duty_name(&self) -> Cow<str> {
|
||||
match (&self.duty_type, &self.category) {
|
||||
(DutyType::Other, DutyCategory::Fates) => {
|
||||
if let Some(&name) = crate::ffxiv::TERRITORY_NAMES.get(&u32::from(self.duty)) {
|
||||
return Cow::from(name);
|
||||
}
|
||||
|
||||
return Cow::from("Fates");
|
||||
}
|
||||
(DutyType::Other, DutyCategory::TheHunt) => return Cow::from("The Hunt"),
|
||||
(DutyType::Other, DutyCategory::Duty) if self.duty == 0 => return Cow::from("None"),
|
||||
(DutyType::Normal, _) => {
|
||||
if let Some(&name) = crate::ffxiv::DUTIES.get(&u32::from(self.duty)) {
|
||||
return Cow::from(name);
|
||||
}
|
||||
}
|
||||
(DutyType::Roulette, _) => {
|
||||
if let Some(&name) = crate::ffxiv::ROULETTES.get(&u32::from(self.duty)) {
|
||||
return Cow::from(name);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Cow::from(format!("{:?}", self.category))
|
||||
}
|
||||
|
||||
pub fn slots(&self) -> Vec<std::result::Result<ClassJob, (String, String)>> {
|
||||
let mut slots = Vec::with_capacity(self.slots_available as usize);
|
||||
for i in 0..self.slots_available as usize {
|
||||
if i >= self.jobs_present.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let cj = match crate::ffxiv::JOBS.get(&u32::from(self.jobs_present[i])).copied() {
|
||||
Some(cj) => Ok(cj),
|
||||
None => Err((
|
||||
self.slots[i].html_classes(),
|
||||
self.slots[i].codes(),
|
||||
)),
|
||||
};
|
||||
slots.push(cj);
|
||||
}
|
||||
|
||||
slots
|
||||
}
|
||||
|
||||
pub fn created_world(&self) -> Option<World> {
|
||||
crate::ffxiv::WORLDS.get(&u32::from(self.created_world)).copied()
|
||||
}
|
||||
|
||||
pub fn created_world_string(&self) -> Cow<str> {
|
||||
self.created_world()
|
||||
.map(|world| Cow::from(world.name()))
|
||||
.unwrap_or_else(|| Cow::from(self.created_world.to_string()))
|
||||
}
|
||||
|
||||
pub fn home_world(&self) -> Option<World> {
|
||||
crate::ffxiv::WORLDS.get(&u32::from(self.home_world)).copied()
|
||||
}
|
||||
|
||||
pub fn home_world_string(&self) -> Cow<str> {
|
||||
self.home_world()
|
||||
.map(|world| Cow::from(world.name()))
|
||||
.unwrap_or_else(|| Cow::from(self.home_world.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct PartyFinderSlot {
|
||||
pub accepting: JobFlags,
|
||||
}
|
||||
|
||||
impl PartyFinderSlot {
|
||||
pub fn html_classes(&self) -> String {
|
||||
if self.accepting == JobFlags::all() {
|
||||
return "empty".into();
|
||||
}
|
||||
|
||||
let mut classes = Vec::with_capacity(3);
|
||||
let cjs = self.accepting.classjobs();
|
||||
|
||||
if cjs.iter().any(|cj| cj.role() == Some(Role::Healer)) {
|
||||
classes.push("healer");
|
||||
}
|
||||
|
||||
if cjs.iter().any(|cj| cj.role() == Some(Role::Tank)) {
|
||||
classes.push("tank");
|
||||
}
|
||||
|
||||
if cjs.iter().any(|cj| cj.role() == Some(Role::Dps)) {
|
||||
classes.push("dps");
|
||||
}
|
||||
|
||||
classes.join(" ")
|
||||
}
|
||||
|
||||
pub fn codes(&self) -> String {
|
||||
self.accepting.classjobs()
|
||||
.iter()
|
||||
.map(|cj| cj.code())
|
||||
.intersperse(" ")
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize_repr, Serialize_repr, PartialEq)]
|
||||
#[repr(u32)]
|
||||
pub enum DutyCategory {
|
||||
Duty = 0,
|
||||
QuestBattles = 1 << 0,
|
||||
Fates = 1 << 1,
|
||||
TreasureHunt = 1 << 2,
|
||||
TheHunt = 1 << 3,
|
||||
GatheringForays = 1 << 4,
|
||||
DeepDungeons = 1 << 5,
|
||||
AdventuringForays = 1 << 6,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize_repr, Serialize_repr, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum DutyType {
|
||||
Other = 0,
|
||||
Roulette = 1 << 0,
|
||||
Normal = 1 << 1,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ObjectiveFlags : u32 {
|
||||
const NONE = 0;
|
||||
const DUTY_COMPLETION = 1 << 0;
|
||||
const PRACTICE = 1 << 1;
|
||||
const LOOT = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ConditionFlags : u32 {
|
||||
const NONE = 1 << 0;
|
||||
const DUTY_COMPLETE = 1 << 1;
|
||||
const DUTY_INCOMPLETE = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct DutyFinderSettingsFlags : u32 {
|
||||
const NONE = 0;
|
||||
const UNDERSIZED_PARTY = 1 << 0;
|
||||
const MINIMUM_ITEM_LEVEL = 1 << 1;
|
||||
const SILENCE_ECHO = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct LootRuleFlags : u32 {
|
||||
const NONE = 0;
|
||||
const GREED_ONLY = 1 << 0;
|
||||
const LOOTMASTER = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SearchAreaFlags : u32 {
|
||||
const DATA_CENTRE = 1 << 0;
|
||||
const PRIVATE = 1 << 1;
|
||||
const ALLIANCE_RAID = 1 << 2;
|
||||
const WORLD = 1 << 3;
|
||||
const ONE_PLAYER_PER_JOB = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct JobFlags : u32 {
|
||||
const GLADIATOR = 1 << 1;
|
||||
const PUGILIST = 1 << 2;
|
||||
const MARAUDER = 1 << 3;
|
||||
const LANCER = 1 << 4;
|
||||
const ARCHER = 1 << 5;
|
||||
const CONJURER = 1 << 6;
|
||||
const THAUMATURGE = 1 << 7;
|
||||
const PALADIN = 1 << 8;
|
||||
const MONK = 1 << 9;
|
||||
const WARRIOR = 1 << 10;
|
||||
const DRAGOON = 1 << 11;
|
||||
const BARD = 1 << 12;
|
||||
const WHITE_MAGE = 1 << 13;
|
||||
const BLACK_MAGE = 1 << 14;
|
||||
const ARCANIST = 1 << 15;
|
||||
const SUMMONER = 1 << 16;
|
||||
const SCHOLAR = 1 << 17;
|
||||
const ROGUE = 1 << 18;
|
||||
const NINJA = 1 << 19;
|
||||
const MACHINIST = 1 << 20;
|
||||
const DARK_KNIGHT = 1 << 21;
|
||||
const ASTROLOGIAN = 1 << 22;
|
||||
const SAMURAI = 1 << 23;
|
||||
const RED_MAGE = 1 << 24;
|
||||
const BLUE_MAGE = 1 << 25;
|
||||
const GUNBREAKER = 1 << 26;
|
||||
const DANCER = 1 << 27;
|
||||
}
|
||||
}
|
||||
|
||||
impl JobFlags {
|
||||
pub fn classjobs(&self) -> Vec<ClassJob> {
|
||||
let mut cjs = Vec::new();
|
||||
|
||||
if self.contains(Self::GLADIATOR) {
|
||||
cjs.push(ClassJob::Class(Class::Gladiator));
|
||||
}
|
||||
|
||||
if self.contains(Self::PUGILIST) {
|
||||
cjs.push(ClassJob::Class(Class::Pugilist));
|
||||
}
|
||||
|
||||
if self.contains(Self::MARAUDER) {
|
||||
cjs.push(ClassJob::Class(Class::Marauder));
|
||||
}
|
||||
|
||||
if self.contains(Self::LANCER) {
|
||||
cjs.push(ClassJob::Class(Class::Lancer));
|
||||
}
|
||||
|
||||
if self.contains(Self::ARCHER) {
|
||||
cjs.push(ClassJob::Class(Class::Archer));
|
||||
}
|
||||
|
||||
if self.contains(Self::CONJURER) {
|
||||
cjs.push(ClassJob::Class(Class::Conjurer));
|
||||
}
|
||||
|
||||
if self.contains(Self::THAUMATURGE) {
|
||||
cjs.push(ClassJob::Class(Class::Thaumaturge));
|
||||
}
|
||||
|
||||
if self.contains(Self::PALADIN) {
|
||||
cjs.push(ClassJob::Job(Job::Paladin));
|
||||
}
|
||||
|
||||
if self.contains(Self::MONK) {
|
||||
cjs.push(ClassJob::Job(Job::Monk));
|
||||
}
|
||||
|
||||
if self.contains(Self::WARRIOR) {
|
||||
cjs.push(ClassJob::Job(Job::Warrior));
|
||||
}
|
||||
|
||||
if self.contains(Self::DRAGOON) {
|
||||
cjs.push(ClassJob::Job(Job::Dragoon));
|
||||
}
|
||||
|
||||
if self.contains(Self::BARD) {
|
||||
cjs.push(ClassJob::Job(Job::Bard));
|
||||
}
|
||||
|
||||
if self.contains(Self::WHITE_MAGE) {
|
||||
cjs.push(ClassJob::Job(Job::WhiteMage));
|
||||
}
|
||||
|
||||
if self.contains(Self::BLACK_MAGE) {
|
||||
cjs.push(ClassJob::Job(Job::BlackMage));
|
||||
}
|
||||
|
||||
if self.contains(Self::ARCANIST) {
|
||||
cjs.push(ClassJob::Class(Class::Arcanist));
|
||||
}
|
||||
|
||||
if self.contains(Self::SUMMONER) {
|
||||
cjs.push(ClassJob::Job(Job::Summoner));
|
||||
}
|
||||
|
||||
if self.contains(Self::SCHOLAR) {
|
||||
cjs.push(ClassJob::Job(Job::Scholar));
|
||||
}
|
||||
|
||||
if self.contains(Self::ROGUE) {
|
||||
cjs.push(ClassJob::Class(Class::Rogue));
|
||||
}
|
||||
|
||||
if self.contains(Self::NINJA) {
|
||||
cjs.push(ClassJob::Job(Job::Ninja));
|
||||
}
|
||||
|
||||
if self.contains(Self::MACHINIST) {
|
||||
cjs.push(ClassJob::Job(Job::Machinist));
|
||||
}
|
||||
|
||||
if self.contains(Self::DARK_KNIGHT) {
|
||||
cjs.push(ClassJob::Job(Job::DarkKnight));
|
||||
}
|
||||
|
||||
if self.contains(Self::ASTROLOGIAN) {
|
||||
cjs.push(ClassJob::Job(Job::Astrologian));
|
||||
}
|
||||
|
||||
if self.contains(Self::SAMURAI) {
|
||||
cjs.push(ClassJob::Job(Job::Samurai));
|
||||
}
|
||||
|
||||
if self.contains(Self::RED_MAGE) {
|
||||
cjs.push(ClassJob::Job(Job::RedMage));
|
||||
}
|
||||
|
||||
if self.contains(Self::BLUE_MAGE) {
|
||||
cjs.push(ClassJob::Job(Job::BlueMage));
|
||||
}
|
||||
|
||||
if self.contains(Self::GUNBREAKER) {
|
||||
cjs.push(ClassJob::Job(Job::Gunbreaker));
|
||||
}
|
||||
|
||||
if self.contains(Self::DANCER) {
|
||||
cjs.push(ClassJob::Job(Job::Dancer));
|
||||
}
|
||||
|
||||
cjs
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use chrono::{DateTime, Duration, Utc};
|
||||
use chrono_humanize::HumanTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::listing::PartyFinderListing;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ListingContainer {
|
||||
#[serde(with = "mongodb::bson::serde_helpers::chrono_datetime_as_bson_datetime")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub listing: PartyFinderListing,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct QueriedListing {
|
||||
#[serde(with = "mongodb::bson::serde_helpers::chrono_datetime_as_bson_datetime")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub time_left: f64,
|
||||
pub listing: PartyFinderListing,
|
||||
}
|
||||
|
||||
impl QueriedListing {
|
||||
pub fn human_time_left(&self) -> HumanTime {
|
||||
HumanTime::from(Duration::milliseconds((self.time_left * 1000f64) as i64))
|
||||
}
|
||||
|
||||
pub fn since_updated(&self) -> Duration {
|
||||
Utc::now() - self.updated_at
|
||||
}
|
||||
|
||||
pub fn human_since_updated(&self) -> HumanTime {
|
||||
HumanTime::from(-self.since_updated())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
#![feature(try_blocks, iter_intersperse)]
|
||||
|
||||
use anyhow::Context;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use crate::config::Config;
|
||||
|
||||
mod config;
|
||||
mod listing;
|
||||
mod listing_container;
|
||||
mod base64_sestring;
|
||||
mod sestring_ext;
|
||||
mod web;
|
||||
mod template;
|
||||
mod ffxiv;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let config_path = if args.is_empty() {
|
||||
Cow::from("./config.toml")
|
||||
} else {
|
||||
Cow::from(args.remove(0))
|
||||
};
|
||||
|
||||
let config = match get_config(&*config_path).await {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("error: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = self::web::start(Arc::new(config)).await {
|
||||
eprintln!("error: {}", e);
|
||||
eprintln!(" {:?}", e);
|
||||
eprintln!("{}", e.backtrace());
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_config<P: AsRef<Path>>(path: P) -> anyhow::Result<Config> {
|
||||
let mut f = File::open(path).await.context("could not open config file")?;
|
||||
let mut toml = String::new();
|
||||
f.read_to_string(&mut toml).await.context("could not read config file")?;
|
||||
let config = toml::from_str(&toml).context("could not parse config file")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
use sestring::{Payload, SeString};
|
||||
|
||||
pub trait SeStringExt {
|
||||
fn full_text(&self) -> String;
|
||||
}
|
||||
|
||||
impl SeStringExt for SeString {
|
||||
fn full_text(&self) -> String {
|
||||
self.0.iter()
|
||||
.flat_map(|payload| {
|
||||
match payload {
|
||||
Payload::Text(t) => Some(&*t.0),
|
||||
Payload::AutoTranslate(at) => crate::ffxiv::AUTO_TRANSLATE
|
||||
.get(&(u32::from(at.group), at.key))
|
||||
.map(std::ops::Deref::deref),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
use askama::Template;
|
||||
use crate::listing_container::QueriedListing;
|
||||
use std::borrow::Borrow;
|
||||
use crate::sestring_ext::SeStringExt;
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "listings.html")]
|
||||
pub struct ListingsTemplate {
|
||||
pub containers: Vec<QueriedListing>,
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod listings;
|
|
@ -0,0 +1,89 @@
|
|||
use crate::listing::{PartyFinderListing, DutyType, ObjectiveFlags, ConditionFlags, DutyFinderSettingsFlags, LootRuleFlags, SearchAreaFlags, DutyCategory, PartyFinderSlot, JobFlags};
|
||||
use sestring::SeString;
|
||||
|
||||
const LISTING: &str = r###"
|
||||
{
|
||||
"id": 123,
|
||||
"content_id_lower": 456,
|
||||
"name": "VGVzdCBOYW1l",
|
||||
"description": "VGhpcyBpcyBteSB0ZXN0IGRlc2NyaXB0aW9uLg==",
|
||||
"created_world": 73,
|
||||
"home_world": 73,
|
||||
"current_world": 73,
|
||||
"category": 0,
|
||||
"duty": 55,
|
||||
"duty_type": 2,
|
||||
"beginners_welcome": false,
|
||||
"seconds_remaining": 3300,
|
||||
"min_item_level": 0,
|
||||
"num_parties": 1,
|
||||
"slots_available": 7,
|
||||
"objective": 3,
|
||||
"conditions": 1,
|
||||
"duty_finder_settings": 0,
|
||||
"loot_rules": 0,
|
||||
"search_area": 1,
|
||||
"slots": [
|
||||
{
|
||||
"accepting": 167772160
|
||||
}
|
||||
],
|
||||
"jobs_present": [
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}"###;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref EXPECTED: PartyFinderListing = PartyFinderListing {
|
||||
id: 123,
|
||||
content_id_lower: 456,
|
||||
name: SeString::parse(b"Test Name").unwrap(),
|
||||
description: SeString::parse(b"This is my test description.").unwrap(),
|
||||
created_world: 73,
|
||||
home_world: 73,
|
||||
current_world: 73,
|
||||
category: DutyCategory::Duty,
|
||||
duty: 55,
|
||||
duty_type: DutyType::Normal,
|
||||
beginners_welcome: false,
|
||||
seconds_remaining: 3300,
|
||||
min_item_level: 0,
|
||||
num_parties: 1,
|
||||
slots_available: 7,
|
||||
objective: ObjectiveFlags::PRACTICE | ObjectiveFlags::DUTY_COMPLETION,
|
||||
conditions: ConditionFlags::NONE,
|
||||
duty_finder_settings: DutyFinderSettingsFlags::NONE,
|
||||
loot_rules: LootRuleFlags::NONE,
|
||||
search_area: SearchAreaFlags::DATA_CENTRE,
|
||||
slots: vec![
|
||||
PartyFinderSlot {
|
||||
accepting: JobFlags::DANCER | JobFlags::BLUE_MAGE,
|
||||
},
|
||||
],
|
||||
jobs_present: vec![5, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialise_listing() {
|
||||
let listing: PartyFinderListing = serde_json::from_str(LISTING).unwrap();
|
||||
assert_eq!(
|
||||
listing,
|
||||
*EXPECTED,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialise_listing() {
|
||||
assert_eq!(
|
||||
serde_json::to_string_pretty(&*EXPECTED).unwrap(),
|
||||
LISTING.trim(),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
use std::convert::{Infallible, TryFrom};
|
||||
use anyhow::{Result, Context};
|
||||
use std::sync::Arc;
|
||||
use mongodb::{Client as MongoClient, Collection};
|
||||
use mongodb::options::UpdateOptions;
|
||||
use mongodb::results::UpdateResult;
|
||||
use tokio_stream::StreamExt;
|
||||
use warp::{Filter, Reply};
|
||||
use warp::filters::BoxedFilter;
|
||||
use warp::http::Uri;
|
||||
use crate::config::Config;
|
||||
use crate::listing::PartyFinderListing;
|
||||
use crate::listing_container::{ListingContainer, QueriedListing};
|
||||
use crate::template::listings::ListingsTemplate;
|
||||
|
||||
pub async fn start(config: Arc<Config>) -> Result<()> {
|
||||
let state = State::new(Arc::clone(&config)).await?;
|
||||
|
||||
println!("listening at {}", config.web.host);
|
||||
warp::serve(router(state))
|
||||
.run(config.web.host)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct State {
|
||||
config: Arc<Config>,
|
||||
mongo: MongoClient,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn new(config: Arc<Config>) -> Result<Arc<Self>> {
|
||||
let mongo = MongoClient::with_uri_str(&config.mongo.url)
|
||||
.await
|
||||
.context("could not create mongodb client")?;
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
config,
|
||||
mongo,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn collection(&self) -> Collection<ListingContainer> {
|
||||
self.mongo.database("rpf").collection("listings")
|
||||
}
|
||||
}
|
||||
|
||||
fn router(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||
index()
|
||||
.or(listings(Arc::clone(&state)))
|
||||
.or(contribute(Arc::clone(&state)))
|
||||
.or(contribute_multiple(Arc::clone(&state)))
|
||||
.or(assets())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn assets() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::get()
|
||||
.and(warp::path("assets"))
|
||||
.and(
|
||||
icons()
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn icons() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("icons.svg")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/icons.svg"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn index() -> BoxedFilter<(impl Reply, )> {
|
||||
let route = warp::path::end()
|
||||
.map(|| warp::redirect(Uri::from_static("/listings")));
|
||||
warp::get().and(route).boxed()
|
||||
}
|
||||
|
||||
fn listings(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||
async fn logic(state: Arc<State>) -> std::result::Result<impl Reply, Infallible> {
|
||||
use mongodb::bson::doc;
|
||||
|
||||
let res = state
|
||||
.collection()
|
||||
.aggregate(
|
||||
[
|
||||
doc! {
|
||||
"$set": {
|
||||
"time_left": {
|
||||
"$divide": [
|
||||
{
|
||||
"$subtract": [
|
||||
{ "$multiply": ["$listing.seconds_remaining", 1000] },
|
||||
{ "$subtract": ["$$NOW", "$updated_at"] },
|
||||
]
|
||||
},
|
||||
1000,
|
||||
]
|
||||
},
|
||||
"updated_minute": {
|
||||
"$dateTrunc": {
|
||||
"date": "$updated_at",
|
||||
"unit": "minute",
|
||||
"binSize": 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$match": {
|
||||
"time_left": {
|
||||
"$gte": 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$sort": {
|
||||
"updated_minute": -1,
|
||||
"time_left": 1,
|
||||
}
|
||||
}
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
Ok(match res {
|
||||
Ok(mut cursor) => {
|
||||
let mut containers = Vec::new();
|
||||
|
||||
while let Ok(Some(container)) = cursor.try_next().await {
|
||||
let res: Result<QueriedListing> = try {
|
||||
let json = serde_json::to_vec(&container)?;
|
||||
let result: QueriedListing = serde_json::from_slice(&json)?;
|
||||
result
|
||||
};
|
||||
if let Ok(listing) = res {
|
||||
containers.push(listing);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ListingsTemplate {
|
||||
containers,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{:#?}", e);
|
||||
Ok(ListingsTemplate {
|
||||
containers: Default::default(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let route = warp::path("listings")
|
||||
.and(warp::path::end())
|
||||
.and_then(move || logic(Arc::clone(&state)));
|
||||
|
||||
warp::get().and(route).boxed()
|
||||
}
|
||||
|
||||
fn contribute(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||
async fn logic(state: Arc<State>, listing: PartyFinderListing) -> std::result::Result<impl Reply, Infallible> {
|
||||
if listing.seconds_remaining > 60 * 60 {
|
||||
return Ok("invalid listing".to_string());
|
||||
}
|
||||
|
||||
let result = insert_listing(&*state, listing).await;
|
||||
Ok(format!("{:#?}", result))
|
||||
}
|
||||
|
||||
let route = warp::path("contribute")
|
||||
.and(warp::path::end())
|
||||
.and(warp::body::json())
|
||||
.and_then(move |listing: PartyFinderListing| logic(Arc::clone(&state), listing));
|
||||
warp::post().and(route).boxed()
|
||||
}
|
||||
|
||||
fn contribute_multiple(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||
async fn logic(state: Arc<State>, listings: Vec<PartyFinderListing>) -> std::result::Result<impl Reply, Infallible> {
|
||||
let total = listings.len();
|
||||
let mut successful = 0;
|
||||
|
||||
for listing in listings {
|
||||
if listing.seconds_remaining > 60 * 60 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = insert_listing(&*state, listing).await;
|
||||
if result.is_ok() {
|
||||
successful += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format!("{}/{} updated", successful, total))
|
||||
}
|
||||
|
||||
let route = warp::path("contribute")
|
||||
.and(warp::path("multiple"))
|
||||
.and(warp::path::end())
|
||||
.and(warp::body::json())
|
||||
.and_then(move |listings: Vec<PartyFinderListing>| logic(Arc::clone(&state), listings));
|
||||
warp::post().and(route).boxed()
|
||||
}
|
||||
|
||||
async fn insert_listing(state: &State, listing: PartyFinderListing) -> mongodb::error::Result<UpdateResult> {
|
||||
use mongodb::bson::doc;
|
||||
|
||||
let opts = UpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.build();
|
||||
let value = serde_json::to_value(&listing).unwrap();
|
||||
let bson_value = mongodb::bson::Bson::try_from(value).unwrap();
|
||||
state
|
||||
.collection()
|
||||
.update_one(
|
||||
doc! {
|
||||
"listing.id": listing.id,
|
||||
"listing.content_id_lower": listing.content_id_lower,
|
||||
},
|
||||
doc! {
|
||||
"$currentDate": {
|
||||
"updated_at": true,
|
||||
},
|
||||
"$set": {
|
||||
"listing": bson_value,
|
||||
}
|
||||
},
|
||||
opts,
|
||||
)
|
||||
.await
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<style>
|
||||
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
|
||||
html, body, p, ol, ul, li, dl, dt, dd, blockquote, figure, fieldset, legend, textarea, pre, iframe, hr, h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: 100%;
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none
|
||||
}
|
||||
|
||||
button, input, select {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit
|
||||
}
|
||||
|
||||
img, video {
|
||||
height: auto;
|
||||
max-width: 100%
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1em 1em 0;
|
||||
}
|
||||
</style>
|
||||
{%- block head %}{% endblock %}
|
||||
</head>
|
||||
<body>{% block body %}{% endblock %}</body>
|
||||
</html>
|
|
@ -0,0 +1,330 @@
|
|||
{% extends "_frame.html" %}
|
||||
|
||||
{% block title -%}
|
||||
Remote Party Finder
|
||||
{%- endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
:root {
|
||||
--background: #2C2F34;
|
||||
--text: #A0A0A0;
|
||||
--local-duty-text: #E6A73A;
|
||||
--cross-duty-text: #79C7EC;
|
||||
--gold-text: #FFC240;
|
||||
--light-blue-text: #5BE2FF;
|
||||
--green-text: #37DE99;
|
||||
--meta-text: #D3EEE9;
|
||||
--ui-text: #EEE1C5;
|
||||
--text-bright: #FFF;
|
||||
|
||||
--row-background: #2C2F34;
|
||||
--row-background-alternate: #373A3E;
|
||||
|
||||
--slot-background: #868180;
|
||||
--slot-empty: #AAA3A2;
|
||||
--tank-blue: #455CCB;
|
||||
--healer-green: #487B39;
|
||||
--dps-red: #813B3C;
|
||||
--icon-gold: #ECB7A2;
|
||||
|
||||
/*--slot-empty: #ADA99C;*/
|
||||
/*--tank-blue: #4A5DC6;*/
|
||||
/*--healer-green: #4A7939;*/
|
||||
/*--dps-red: #7E3938;*/
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
font-family: sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
body > div {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
background-color: var(--row-background);
|
||||
}
|
||||
|
||||
body > div .left {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
body > div:nth-child(2n) {
|
||||
background-color: var(--row-background-alternate);
|
||||
}
|
||||
|
||||
body > div .description {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
body > div .duty {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
body > div .duty.cross {
|
||||
color: var(--cross-duty-text);
|
||||
}
|
||||
|
||||
body > div .duty.local {
|
||||
color: var(--local-duty-text);
|
||||
}
|
||||
|
||||
body > div .meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin-left: 1em;
|
||||
|
||||
color: var(--meta-text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body > div .meta > .item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
/*body > div .meta > .item > .text {*/
|
||||
/* align-self: center;*/
|
||||
/*}*/
|
||||
|
||||
body > div .meta > .item .icon {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
fill: var(--text);
|
||||
margin-left: .5em;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
body > div .party {
|
||||
margin-top: .5em;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 2em);
|
||||
gap: .5em;
|
||||
|
||||
/*display: flex;*/
|
||||
/*flex-direction: row;*/
|
||||
/*flex-wrap: wrap;*/
|
||||
/*align-items: center;*/
|
||||
/*margin-top: .5em;*/
|
||||
}
|
||||
|
||||
body > div .party > .total {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
body > div .party > .slot {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border: 1px solid currentColor;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
body > div .party > .slot:not(.filled).dps.tank {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--tank-blue) 0%,
|
||||
var(--tank-blue) 50%,
|
||||
var(--dps-red) 50%
|
||||
);
|
||||
}
|
||||
|
||||
body > div .party > .slot:not(.filled).dps.healer {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--healer-green) 0%,
|
||||
var(--healer-green) 50%,
|
||||
var(--dps-red) 50%
|
||||
);
|
||||
}
|
||||
|
||||
body > div .party > .slot:not(.filled).tank.healer {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--tank-blue) 0%,
|
||||
var(--tank-blue) 50%,
|
||||
var(--healer-green) 50%
|
||||
);
|
||||
}
|
||||
|
||||
body > div .party > .slot:not(.filled).tank.healer.dps {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--tank-blue) 0%,
|
||||
var(--tank-blue) 33%,
|
||||
var(--healer-green) 33%,
|
||||
var(--healer-green) 66%,
|
||||
var(--dps-red) 66%
|
||||
);
|
||||
}
|
||||
|
||||
body > div .party > .slot:not(.filled).dps {
|
||||
background-color: var(--dps-red);
|
||||
}
|
||||
|
||||
/* background: linear-gradient(to top, #FF44AD, #CD60B2); */
|
||||
|
||||
body > div .party > .slot.filled {
|
||||
background-color: var(--slot-background);
|
||||
border-color: var(--icon-gold);
|
||||
}
|
||||
|
||||
body > div .party > .slot.empty {
|
||||
background-color: var(--slot-empty);
|
||||
}
|
||||
|
||||
body > div .party > .slot.dps {
|
||||
background-color: var(--dps-red);
|
||||
}
|
||||
|
||||
body > div .party > .slot.healer {
|
||||
background-color: var(--healer-green);
|
||||
}
|
||||
|
||||
body > div .party > .slot.tank {
|
||||
background-color: var(--tank-blue);
|
||||
}
|
||||
|
||||
body > div .party > .slot > svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: var(--icon-gold);
|
||||
}
|
||||
|
||||
body > div .party > .slot.filled:not(.dps):not(.healer):not(.tank) > svg {
|
||||
fill: #C6C6C6;
|
||||
}
|
||||
|
||||
/* Really, this could be 26em */
|
||||
@media (max-width: 30em) {
|
||||
body > div {
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
body > div .meta {
|
||||
flex-grow: 1;
|
||||
|
||||
margin-top: .5em;
|
||||
margin-left: 0;
|
||||
text-align: unset;
|
||||
}
|
||||
|
||||
body > div .meta > .item {
|
||||
align-self: unset;
|
||||
}
|
||||
|
||||
body > div .meta > .item .icon {
|
||||
order: 1;
|
||||
margin-left: 0;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
body > div .meta > .item > .text {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% for container in containers %}
|
||||
{% let listing = container.listing.borrow() %}
|
||||
<div data-id="{{ listing.id }}" data-updated-minutes="{{ container.since_updated().num_minutes() }}">
|
||||
<div class="left">
|
||||
{% let duty_class %}
|
||||
{% if listing.is_cross_world() %}
|
||||
{% let duty_class = " cross" %}
|
||||
{% else %}
|
||||
{% let duty_class = " local" %}
|
||||
{% endif %}
|
||||
<div class="duty{{ duty_class }}">{{ listing.duty_name() }}</div>
|
||||
<div class="description">
|
||||
{%- let desc = listing.description.full_text() %}
|
||||
{%- if desc.is_empty() -%}
|
||||
<em>None</em>
|
||||
{%- else -%}
|
||||
{{ desc }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="party">
|
||||
{% for slot in listing.slots() %}
|
||||
{% let filled %}
|
||||
{% let title %}
|
||||
{% let role_class %}
|
||||
{% match slot %}
|
||||
{% when Ok with (slot) %}
|
||||
{% let filled = " filled" %}
|
||||
{% match slot.role() %}
|
||||
{% when Some with (role) %}
|
||||
{% let role_class = " {}"|format(role.as_str().to_lowercase()) %}
|
||||
{% when None %}
|
||||
{% let role_class = "".to_string() %}
|
||||
{% endmatch %}
|
||||
{% let title = slot.code().to_string() %}
|
||||
{% when Err with (tuple) %}
|
||||
{% let filled = "" %}
|
||||
{% let title = tuple.1.clone() %}
|
||||
{% let role_class = " {}"|format(tuple.0) %}
|
||||
{% endmatch %}
|
||||
<div class="slot{{ filled }}{{ role_class }}" title="{{ title }}">
|
||||
{% if !filled.is_empty() %}
|
||||
<svg viewBox="0 0 32 32">
|
||||
<use href="/assets/icons.svg#{{ title }}"></use>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="total">{{ listing.slots_filled() }}/{{ listing.slots_available }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right meta">
|
||||
<div class="item creator">
|
||||
<span class="text">{{ listing.name.full_text() }} @ {{ listing.home_world_string() }}</span>
|
||||
<span title="Creator">
|
||||
<svg class="icon" viewBox="0 0 32 32">
|
||||
<use href="/assets/icons.svg#user"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item world">
|
||||
<span class="text">{{ listing.created_world_string() }}</span>
|
||||
<span title="Created on">
|
||||
<svg class="icon" viewBox="0 0 32 32">
|
||||
<use href="/assets/icons.svg#sphere"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item expires">
|
||||
<span class="text">{{ container.human_time_left() }}</span>
|
||||
<span title="Expires">
|
||||
<svg class="icon" viewBox="0 0 32 32">
|
||||
<use href="/assets/icons.svg#stopwatch"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item updated">
|
||||
<span class="text">{{ container.human_since_updated() }}</span>
|
||||
<span title="Updated">
|
||||
<svg class="icon" viewBox="0 0 32 32">
|
||||
<use href="/assets/icons.svg#clock"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
Loading…
Reference in New Issue