chore: initial commit

This commit is contained in:
Anna 2021-10-03 23:17:09 -04:00
commit 095ceadc78
24 changed files with 5950 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/.idea
/config.toml

1850
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

31
Cargo.toml Normal file
View File

@ -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"

1
assets/icons.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

5
config.example.toml Normal file
View File

@ -0,0 +1,5 @@
[web]
host = "127.0.0.1:1234"
[mongo]
url = "mongodb://example.com:1234"

17
src/base64_sestring.rs Normal file
View File

@ -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)
}

18
src/config.rs Normal file
View File

@ -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,
}

15
src/ffxiv.rs Normal file
View File

@ -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,
};

1266
src/ffxiv/auto_translate.rs Normal file

File diff suppressed because it is too large Load Diff

573
src/ffxiv/duties.rs Normal file
View File

@ -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)",
};
}

45
src/ffxiv/jobs.rs Normal file
View File

@ -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),
};
}

41
src/ffxiv/roulettes.rs Normal file
View File

@ -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",
};
}

View File

@ -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",
};
}

75
src/ffxiv/worlds.rs Normal file
View File

@ -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,
};
}

375
src/listing.rs Normal file
View File

@ -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
}
}

33
src/listing_container.rs Normal file
View File

@ -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())
}
}

54
src/main.rs Normal file
View File

@ -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)
}

21
src/sestring_ext.rs Normal file
View File

@ -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()
}
}

10
src/template/listings.rs Normal file
View File

@ -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>,
}

1
src/template/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod listings;

89
src/test.rs Normal file
View File

@ -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(),
);
}

231
src/web.rs Normal file
View File

@ -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
}

60
templates/_frame.html Normal file
View File

@ -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>

330
templates/listings.html Normal file
View File

@ -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 %}