chore: initial commit
This commit is contained in:
commit
1f6342ed37
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
/.idea
|
||||||
|
/config.toml
|
1850
Cargo.lock
generated
Normal file
1850
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
Normal file
31
Cargo.toml
Normal 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
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
5
config.example.toml
Normal 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
17
src/base64_sestring.rs
Normal 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
18
src/config.rs
Normal 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
15
src/ffxiv.rs
Normal 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
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
573
src/ffxiv/duties.rs
Normal 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
45
src/ffxiv/jobs.rs
Normal 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
41
src/ffxiv/roulettes.rs
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
806
src/ffxiv/territory_names.rs
Normal file
806
src/ffxiv/territory_names.rs
Normal 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
75
src/ffxiv/worlds.rs
Normal 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
375
src/listing.rs
Normal 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
33
src/listing_container.rs
Normal 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
54
src/main.rs
Normal 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
21
src/sestring_ext.rs
Normal 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
10
src/template/listings.rs
Normal 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
1
src/template/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod listings;
|
89
src/test.rs
Normal file
89
src/test.rs
Normal 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
231
src/web.rs
Normal 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
60
templates/_frame.html
Normal 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
330
templates/listings.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user