diff --git a/GatheringPathRenderer/packages.lock.json b/GatheringPathRenderer/packages.lock.json
index c7a267af..269c43d7 100644
--- a/GatheringPathRenderer/packages.lock.json
+++ b/GatheringPathRenderer/packages.lock.json
@@ -92,12 +92,6 @@
"ecommons": {
"type": "Project"
},
- "gatheringpaths": {
- "type": "Project",
- "dependencies": {
- "Questionable.Model": "[1.0.0, )"
- }
- },
"questionable.model": {
"type": "Project",
"dependencies": {
diff --git a/GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json b/GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json
new file mode 100644
index 00000000..23fae294
--- /dev/null
+++ b/GatheringPaths/4.x - Stormblood/Yanxia/731_Yuzuka Manor_BTN.json
@@ -0,0 +1,115 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
+ "Author": "liza",
+ "TerritoryId": 614,
+ "AetheryteShortcut": "Yanxia - Namai",
+ "Groups": [
+ {
+ "Nodes": [
+ {
+ "DataId": 33334,
+ "Locations": [
+ {
+ "Position": {
+ "X": -222.386,
+ "Y": 23.28162,
+ "Z": 425.76
+ }
+ },
+ {
+ "Position": {
+ "X": -209.1725,
+ "Y": 22.35068,
+ "Z": 425.5524
+ }
+ }
+ ]
+ },
+ {
+ "DataId": 33333,
+ "Locations": [
+ {
+ "Position": {
+ "X": -219.9592,
+ "Y": 22.78741,
+ "Z": 431.5036
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 33335,
+ "Locations": [
+ {
+ "Position": {
+ "X": -349.8553,
+ "Y": 33.90925,
+ "Z": 452.5893
+ },
+ "MinimumAngle": -90,
+ "MaximumAngle": 90
+ }
+ ]
+ },
+ {
+ "DataId": 33336,
+ "Locations": [
+ {
+ "Position": {
+ "X": -361.5062,
+ "Y": 33.49068,
+ "Z": 453.4639
+ }
+ },
+ {
+ "Position": {
+ "X": -359.826,
+ "Y": 35.47207,
+ "Z": 442.164
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Nodes": [
+ {
+ "DataId": 33331,
+ "Locations": [
+ {
+ "Position": {
+ "X": -231.3864,
+ "Y": 17.74118,
+ "Z": 511.0694
+ }
+ }
+ ]
+ },
+ {
+ "DataId": 33332,
+ "Locations": [
+ {
+ "Position": {
+ "X": -219.0789,
+ "Y": 18.05494,
+ "Z": 525.418
+ }
+ },
+ {
+ "Position": {
+ "X": -220.9139,
+ "Y": 17.97838,
+ "Z": 514.0063
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/3.x - Heavensward/Unlocks/Custom Deliveries/1551_Arms Wide Open.json b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/1551_Arms Wide Open.json
similarity index 100%
rename from QuestPaths/3.x - Heavensward/Unlocks/Custom Deliveries/1551_Arms Wide Open.json
rename to QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/1551_Arms Wide Open.json
diff --git a/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json
new file mode 100644
index 00000000..e909b81d
--- /dev/null
+++ b/QuestPaths/3.x - Heavensward/Custom Deliveries/Zhloe/S1_Zhloe Aliapoh.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "Position": {
+ "X": -71.31451,
+ "Y": 206.56206,
+ "Z": 29.3684
+ },
+ "TerritoryId": 478,
+ "InteractionType": "WalkTo",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Idyllshire"
+ },
+ {
+ "DataId": 1019615,
+ "Position": {
+ "X": -71.763245,
+ "Y": 206.50021,
+ "Z": 32.638916
+ },
+ "StopDistance": 5,
+ "TerritoryId": 478,
+ "InteractionType": "Interact"
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json
new file mode 100644
index 00000000..07c9f828
--- /dev/null
+++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/Adkiragh/S4_Adkiragh.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1018393,
+ "Position": {
+ "X": -60.380005,
+ "Y": 206.50021,
+ "Z": 26.16919
+ },
+ "TerritoryId": 478,
+ "InteractionType": "Interact",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Idyllshire"
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json
new file mode 100644
index 00000000..edcd6dd8
--- /dev/null
+++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/Kurenai/S3_Kurenai.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1025878,
+ "Position": {
+ "X": 343.984,
+ "Y": -120.32947,
+ "Z": -306.0197
+ },
+ "TerritoryId": 613,
+ "InteractionType": "Interact",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Ruby Sea - Tamamizu"
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json b/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json
new file mode 100644
index 00000000..435169ec
--- /dev/null
+++ b/QuestPaths/4.x - Stormblood/Custom Deliveries/M'naago/S2_M'naago.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1020337,
+ "Position": {
+ "X": 171.31299,
+ "Y": 13.02367,
+ "Z": -89.951965
+ },
+ "TerritoryId": 635,
+ "InteractionType": "Interact",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Rhalgr's Reach"
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json
new file mode 100644
index 00000000..0e73e7d0
--- /dev/null
+++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Charlemend/S7_Charlemend.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1035211,
+ "Position": {
+ "X": -116.96039,
+ "Y": 0,
+ "Z": -133.95898
+ },
+ "TerritoryId": 886,
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Ishgard",
+ "AethernetShortcut": [
+ "[Ishgard] Aetheryte Plaza",
+ "[Ishgard] Firmament"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json
new file mode 100644
index 00000000..2e9c047b
--- /dev/null
+++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Ehll Tou/S6_Ehll Tou.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1033543,
+ "Position": {
+ "X": 113.38977,
+ "Y": -20,
+ "Z": -0.96136475
+ },
+ "TerritoryId": 886,
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Ishgard",
+ "AethernetShortcut": [
+ "[Ishgard] Aetheryte Plaza",
+ "[Ishgard] Firmament"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json
new file mode 100644
index 00000000..9e279e15
--- /dev/null
+++ b/QuestPaths/5.x - Shadowbringers/Custom Deliveries/Kai-Shirr/S5_Kai-Shirr.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1031801,
+ "Position": {
+ "X": 52.8114,
+ "Y": 83.001076,
+ "Z": -65.38495
+ },
+ "TerritoryId": 820,
+ "InteractionType": "Interact",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Eulmore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json
new file mode 100644
index 00000000..cc256d9f
--- /dev/null
+++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Ameliance/S8_Ameliance.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1042241,
+ "Position": {
+ "X": 222.85791,
+ "Y": 24.942732,
+ "Z": -197.77222
+ },
+ "TerritoryId": 962,
+ "InteractionType": "Interact",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Leveilleur Estate"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json
new file mode 100644
index 00000000..7cdc3904
--- /dev/null
+++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Anden/S9_Anden.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1044547,
+ "Position": {
+ "X": -241.68768,
+ "Y": 51.058994,
+ "Z": 620.8744
+ },
+ "TerritoryId": 816,
+ "InteractionType": "Interact",
+ "RequiredGatheredItems": [],
+ "AetheryteShortcut": "Il Mheg - Lydha Lran",
+ "Fly": true
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json
new file mode 100644
index 00000000..3d9a80be
--- /dev/null
+++ b/QuestPaths/6.x - Endwalker/Custom Deliveries/Margrat/S10_Margrat.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "Position": {
+ "X": -44.066154,
+ "Y": -29.530005,
+ "Z": -55.85129
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo",
+ "AetheryteShortcut": "Labyrinthos - Sharlayan Hamlet",
+ "RequiredGatheredItems": [],
+ "Fly": true
+ },
+ {
+ "DataId": 1046073,
+ "Position": {
+ "X": -53.635498,
+ "Y": -29.497286,
+ "Z": -65.14081
+ },
+ "TerritoryId": 956,
+ "InteractionType": "Interact"
+ }
+ ]
+ }
+ ]
+}
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/4473_The Faculty.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/4473_The Faculty.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/4473_The Faculty.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/4473_The Faculty.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4153_Cultured Pursuits.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4153_Cultured Pursuits.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4154_Cooking Up a Culture.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4154_Cooking Up a Culture.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4155_The Culture of Ceruleum.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4155_The Culture of Ceruleum.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4156_The Culture of Carrots.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4156_The Culture of Carrots.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4157_Hinageshi in Hingashi.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4157_Hinageshi in Hingashi.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4158_The Culture of the Past.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4158_The Culture of the Past.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4158_The Culture of the Past.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4158_The Culture of the Past.json
diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4159_The Culture of Love.json b/QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4159_The Culture of Love.json
similarity index 100%
rename from QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4159_The Culture of Love.json
rename to QuestPaths/6.x - Endwalker/Custom Deliveries/Studium/MIN, BTN/4159_The Culture of Love.json
diff --git a/QuestPaths/QuestPaths.csproj b/QuestPaths/QuestPaths.csproj
index 5d91a016..1e0389ef 100644
--- a/QuestPaths/QuestPaths.csproj
+++ b/QuestPaths/QuestPaths.csproj
@@ -40,8 +40,4 @@
-
-
-
-
diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json
index 9a031631..e7d7ee2b 100644
--- a/QuestPaths/quest-v1.json
+++ b/QuestPaths/quest-v1.json
@@ -1041,7 +1041,8 @@
"PickUpQuestId": {
"type": [
"null",
- "number"
+ "number",
+ "string"
],
"description": "Determines the quest which should be accepted. If empty/null, accepts the quest corresponding to the file name."
}
@@ -1061,14 +1062,16 @@
"TurnInQuestId": {
"type": [
"null",
- "number"
+ "number",
+ "string"
],
"description": "Determines the quest which should be turned in. If empty/null, turns in the quest corresponding to the file name."
},
"NextQuestId": {
"type": [
"null",
- "number"
+ "number",
+ "string"
],
"description": "For quest chains (e.g. DT healer role quests) Which quest to do next, given that you meet the required level."
}
diff --git a/Questionable.Model/Common/EAetheryteLocation.cs b/Questionable.Model/Common/EAetheryteLocation.cs
index a00f7b51..3313d4df 100644
--- a/Questionable.Model/Common/EAetheryteLocation.cs
+++ b/Questionable.Model/Common/EAetheryteLocation.cs
@@ -85,6 +85,7 @@ public enum EAetheryteLocation
IshgardTribunal = 86,
IshgardLastVigil = 87,
IshgardGatesOfJudgement = 88,
+ IshgardFirmament = 100001,
Idyllshire = 75,
IdyllshireWest = 90,
diff --git a/Questionable.Model/Questing/Converter/AethernetShardConverter.cs b/Questionable.Model/Questing/Converter/AethernetShardConverter.cs
index bd07bc39..b1560c7c 100644
--- a/Questionable.Model/Questing/Converter/AethernetShardConverter.cs
+++ b/Questionable.Model/Questing/Converter/AethernetShardConverter.cs
@@ -56,6 +56,7 @@ public sealed class AethernetShardConverter() : EnumConverter
+public sealed class ElementIdConverter : JsonConverter
{
public override ElementId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- uint value = reader.GetUInt32();
- return ElementId.From(value);
+ if (reader.TokenType == JsonTokenType.Number)
+ return new QuestId(reader.GetUInt16());
+ else
+ return ElementId.FromString(reader.GetString() ?? throw new JsonException());
}
public override void Write(Utf8JsonWriter writer, ElementId value, JsonSerializerOptions options)
diff --git a/Questionable.Model/Questing/ElementId.cs b/Questionable.Model/Questing/ElementId.cs
index e4b3c71c..396c3524 100644
--- a/Questionable.Model/Questing/ElementId.cs
+++ b/Questionable.Model/Questing/ElementId.cs
@@ -50,37 +50,51 @@ public abstract class ElementId : IComparable, IEquatable
return !Equals(left, right);
}
- public static ElementId From(uint value)
+ public static ElementId FromString(string value)
{
- if (value >= 100_000 && value < 200_000)
- return new LeveId((ushort)(value - 100_000));
+ if (value.StartsWith("L"))
+ return new LeveId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
+ else if (value.StartsWith("S"))
+ return new SatisfactionSupplyNpcId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
else
- return new QuestId((ushort)value);
+ return new QuestId(ushort.Parse(value, CultureInfo.InvariantCulture));
+ }
+
+ public static bool TryFromString(string value, out ElementId? elementId)
+ {
+ try
+ {
+ elementId = FromString(value);
+ return true;
+ }
+ catch (Exception)
+ {
+ elementId = null;
+ return false;
+ }
}
}
-public sealed class QuestId : ElementId
+public sealed class QuestId(ushort value) : ElementId(value)
{
- public QuestId(ushort value)
- : base(value)
- {
- }
-
public override string ToString()
{
return Value.ToString(CultureInfo.InvariantCulture);
}
}
-public sealed class LeveId : ElementId
+public sealed class LeveId(ushort value) : ElementId(value)
{
- public LeveId(ushort value)
- : base(value)
- {
- }
-
public override string ToString()
{
return "L" + Value.ToString(CultureInfo.InvariantCulture);
}
}
+
+public sealed class SatisfactionSupplyNpcId(ushort value) : ElementId(value)
+{
+ public override string ToString()
+ {
+ return "S" + Value.ToString(CultureInfo.InvariantCulture);
+ }
+}
diff --git a/Questionable.Model/common-schema.json b/Questionable.Model/common-schema.json
index 6017ecb7..01d1ef6c 100644
--- a/Questionable.Model/common-schema.json
+++ b/Questionable.Model/common-schema.json
@@ -161,6 +161,7 @@
"[Ishgard] The Tribunal",
"[Ishgard] The Last Vigil",
"[Ishgard] The Gates of Judgement (Coerthas Central Highlands)",
+ "[Ishgard] Firmament",
"[Idyllshire] Aetheryte Plaza",
"[Idyllshire] West Idyllshire",
"[Idyllshire] Prologue Gate (Western Hinterlands)",
diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs
index 5e88cf85..142d3263 100644
--- a/Questionable/Controller/CommandHandler.cs
+++ b/Questionable/Controller/CommandHandler.cs
@@ -77,7 +77,7 @@ internal sealed class CommandHandler : IDisposable
case "start":
_questWindow.IsOpen = true;
- _questController.ExecuteNextStep(true);
+ _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
break;
case "stop":
@@ -128,11 +128,11 @@ internal sealed class CommandHandler : IDisposable
return;
}
- if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId))
+ if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
{
- if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest))
+ if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{
- _debugOverlay.HighlightedQuest = quest.QuestElementId;
+ _debugOverlay.HighlightedQuest = quest.Id;
_chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name}).");
}
else
@@ -147,11 +147,11 @@ internal sealed class CommandHandler : IDisposable
private void SetNextQuest(string[] arguments)
{
- if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId))
+ if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
{
- if (_gameFunctions.IsQuestLocked(ElementId.From(questId)))
+ if (_gameFunctions.IsQuestLocked(questId))
_chatGui.PrintError($"[Questionable] Quest {questId} is locked.");
- else if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest))
+ else if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{
_questController.SetNextQuest(quest);
_chatGui.Print($"[Questionable] Set next quest to {questId} ({quest.Info.Name}).");
@@ -170,9 +170,9 @@ internal sealed class CommandHandler : IDisposable
private void SetSimulatedQuest(string[] arguments)
{
- if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId))
+ if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
{
- if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest))
+ if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{
_questController.SimulateQuest(quest);
_chatGui.Print($"[Questionable] Simulating quest {questId} ({quest.Info.Name}).");
diff --git a/Questionable/Controller/ContextMenuController.cs b/Questionable/Controller/ContextMenuController.cs
new file mode 100644
index 00000000..13c5e03f
--- /dev/null
+++ b/Questionable/Controller/ContextMenuController.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Linq;
+using Dalamud.Game.Gui.ContextMenu;
+using Dalamud.Game.Text;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using Microsoft.Extensions.Logging;
+using Questionable.Data;
+using Questionable.Model;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller;
+
+internal sealed class ContextMenuController : IDisposable
+{
+ private readonly IContextMenu _contextMenu;
+ private readonly QuestController _questController;
+ private readonly GatheringData _gatheringData;
+ private readonly QuestRegistry _questRegistry;
+ private readonly QuestData _questData;
+ private readonly IGameGui _gameGui;
+ private readonly IChatGui _chatGui;
+ private readonly IClientState _clientState;
+ private readonly ILogger _logger;
+
+ public ContextMenuController(
+ IContextMenu contextMenu,
+ QuestController questController,
+ GatheringData gatheringData,
+ QuestRegistry questRegistry,
+ QuestData questData,
+ IGameGui gameGui,
+ IChatGui chatGui,
+ IClientState clientState,
+ ILogger logger)
+ {
+ _contextMenu = contextMenu;
+ _questController = questController;
+ _gatheringData = gatheringData;
+ _questRegistry = questRegistry;
+ _questData = questData;
+ _gameGui = gameGui;
+ _chatGui = chatGui;
+ _clientState = clientState;
+ _logger = logger;
+
+ _contextMenu.OnMenuOpened += MenuOpened;
+ }
+
+ private void MenuOpened(IMenuOpenedArgs args)
+ {
+ uint itemId = (uint) _gameGui.HoveredItem;
+ if (itemId == 0)
+ return;
+
+ if (itemId > 1_000_000)
+ itemId -= 1_000_000;
+
+ if (itemId >= 500_000)
+ itemId -= 500_000;
+
+ if (!_gatheringData.TryGetGatheringPointId(itemId, _clientState.LocalPlayer!.ClassJob.Id, out _))
+ {
+ _logger.LogInformation("No gathering point found for current job.");
+ return;
+ }
+
+ if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
+ {
+ ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
+ int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
+ if (collectability == 0)
+ return;
+
+ args.AddMenuItem(new MenuItem
+ {
+ Prefix = SeIconChar.Hyadelyn,
+ PrefixColor = 52,
+ Name = "Gather with Questionable",
+ OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability),
+ });
+ }
+ }
+
+ private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability)
+ {
+ var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single(x => x is SatisfactionSupplyInfo);
+ if (_questRegistry.TryGetQuest(info.QuestId, out Quest? quest))
+ {
+ var step = quest.FindSequence(0)!.FindStep(0)!;
+ step.RequiredGatheredItems =
+ [
+ new GatheredItem
+ {
+ ItemId = itemId,
+ ItemCount = quantity,
+ Collectability = collectability
+ }
+ ];
+ _questController.SetNextQuest(quest);
+ _questController.ExecuteNextStep(QuestController.EAutomationType.CurrentQuestOnly);
+ }
+ else
+ _chatGui.PrintError($"No associated quest ({info.QuestId}).", "Questionable");
+ }
+
+ public void Dispose()
+ {
+ _contextMenu.OnMenuOpened -= MenuOpened;
+ }
+}
diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs
index e7df2213..7f8a6223 100644
--- a/Questionable/Controller/GameUiController.cs
+++ b/Questionable/Controller/GameUiController.cs
@@ -600,7 +600,7 @@ internal sealed class GameUiController : IDisposable
private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
{
- if (_questController.StartedQuest?.Quest.QuestElementId.Value == 4526)
+ if (_questController.StartedQuest?.Quest.Id.Value == 4526)
{
_logger.LogInformation("Closing Unending Codex");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
@@ -610,7 +610,7 @@ internal sealed class GameUiController : IDisposable
private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
{
- if (_questController.StartedQuest?.Quest.QuestElementId.Value == 245)
+ if (_questController.StartedQuest?.Quest.Id.Value == 245)
{
_logger.LogInformation("Closing ContentsTutorial");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
@@ -623,7 +623,7 @@ internal sealed class GameUiController : IDisposable
///
private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
{
- if (_questController.StartedQuest?.Quest.QuestElementId.Value == 245)
+ if (_questController.StartedQuest?.Quest.Id.Value == 245)
{
_logger.LogInformation("Closing MultipleHelpWindow");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs
index dcd3b8b0..c953b851 100644
--- a/Questionable/Controller/QuestController.cs
+++ b/Questionable/Controller/QuestController.cs
@@ -33,7 +33,7 @@ internal sealed class QuestController : MiniTaskController
private QuestProgress? _startedQuest;
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
- private bool _automatic;
+ private EAutomationType _automationType;
///
/// Some combat encounters finish relatively early (i.e. they're done as part of progressing the quest, but not
@@ -71,16 +71,16 @@ internal sealed class QuestController : MiniTaskController
_taskFactories = taskFactories.ToList().AsReadOnly();
}
- public (QuestProgress Progress, CurrentQuestType Type)? CurrentQuestDetails
+ public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails
{
get
{
if (_simulatedQuest != null)
- return (_simulatedQuest, CurrentQuestType.Simulated);
- else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.QuestElementId))
- return (_nextQuest, CurrentQuestType.Next);
+ return (_simulatedQuest, ECurrentQuestType.Simulated);
+ else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
+ return (_nextQuest, ECurrentQuestType.Next);
else if (_startedQuest != null)
- return (_startedQuest, CurrentQuestType.Normal);
+ return (_startedQuest, ECurrentQuestType.Normal);
else
return null;
}
@@ -153,10 +153,11 @@ internal sealed class QuestController : MiniTaskController
if (CurrentQuest != null && CurrentQuest.Quest.Root.TerritoryBlacklist.Contains(_clientState.TerritoryType))
return;
- if (_automatic && ((_currentTask == null && _taskQueue.Count == 0) ||
- _currentTask is WaitAtEnd.WaitQuestAccepted)
- && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 }
- && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
+ if (_automationType == EAutomationType.Automatic &&
+ ((_currentTask == null && _taskQueue.Count == 0) ||
+ _currentTask is WaitAtEnd.WaitQuestAccepted)
+ && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 }
+ && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
{
lock (_progressLock)
{
@@ -164,7 +165,7 @@ internal sealed class QuestController : MiniTaskController
CurrentQuest.SetStep(0);
}
- ExecuteNextStep(true);
+ ExecuteNextStep(_automationType);
return;
}
@@ -182,13 +183,14 @@ internal sealed class QuestController : MiniTaskController
// if the quest is accepted, we no longer track it
bool canUseNextQuest;
if (_nextQuest.Quest.Info.IsRepeatable)
- canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.QuestElementId);
+ canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.Id);
else
- canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.QuestElementId);
+ canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id);
if (!canUseNextQuest)
{
- _logger.LogInformation("Next quest {QuestId} accepted or completed", _nextQuest.Quest.QuestElementId);
+ _logger.LogInformation("Next quest {QuestId} accepted or completed",
+ _nextQuest.Quest.Id);
_nextQuest = null;
}
}
@@ -200,12 +202,15 @@ internal sealed class QuestController : MiniTaskController
currentSequence = _simulatedQuest.Sequence;
questToRun = _simulatedQuest;
}
- else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.QuestElementId))
+ else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
{
questToRun = _nextQuest;
currentSequence = _nextQuest.Sequence; // by definition, this should always be 0
- if (_nextQuest.Step == 0 && _currentTask == null && _taskQueue.Count == 0 && _automatic)
- ExecuteNextStep(true);
+ if (_nextQuest.Step == 0 &&
+ _currentTask == null &&
+ _taskQueue.Count == 0 &&
+ _automationType == EAutomationType.Automatic)
+ ExecuteNextStep(_automationType);
}
else
{
@@ -221,7 +226,7 @@ internal sealed class QuestController : MiniTaskController
questToRun = null;
}
- else if (_startedQuest == null || _startedQuest.Quest.QuestElementId != currentQuestId)
+ else if (_startedQuest == null || _startedQuest.Quest.Id != currentQuestId)
{
if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
{
@@ -341,11 +346,11 @@ internal sealed class QuestController : MiniTaskController
return;
}
- if (questId != null && CurrentQuest.Quest.QuestElementId != questId)
+ if (questId != null && CurrentQuest.Quest.Id != questId)
{
_logger.LogWarning(
"Ignoring 'increase step count' for different quest (expected {ExpectedQuestId}, but we are at {CurrentQuestId}",
- questId, CurrentQuest.Quest.QuestElementId);
+ questId, CurrentQuest.Quest.Id);
return;
}
@@ -363,8 +368,8 @@ internal sealed class QuestController : MiniTaskController
CurrentQuest.SetStep(255);
}
- if (shouldContinue && _automatic)
- ExecuteNextStep(true);
+ if (shouldContinue && _automationType != EAutomationType.Manual)
+ ExecuteNextStep(_automationType);
}
private void ClearTasksInternal()
@@ -387,17 +392,17 @@ internal sealed class QuestController : MiniTaskController
ClearTasksInternal();
// reset task queue
- if (continueIfAutomatic && _automatic)
+ if (continueIfAutomatic && _automationType == EAutomationType.Automatic)
{
if (CurrentQuest?.Step is >= 0 and < 255)
- ExecuteNextStep(true);
+ ExecuteNextStep(_automationType);
else
_logger.LogInformation("Couldn't execute next step during Stop() call");
}
- else if (_automatic)
+ else if (_automationType != EAutomationType.Manual)
{
_logger.LogInformation("Stopping automatic questing");
- _automatic = false;
+ _automationType = EAutomationType.Manual;
_nextQuest = null;
}
}
@@ -406,7 +411,7 @@ internal sealed class QuestController : MiniTaskController
public void SimulateQuest(Quest? quest)
{
- _logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestElementId);
+ _logger.LogInformation("SimulateQuest: {QuestId}", quest?.Id);
if (quest != null)
_simulatedQuest = new QuestProgress(quest);
else
@@ -415,7 +420,7 @@ internal sealed class QuestController : MiniTaskController
public void SetNextQuest(Quest? quest)
{
- _logger.LogInformation("NextQuest: {QuestId}", quest?.QuestElementId);
+ _logger.LogInformation("NextQuest: {QuestId}", quest?.Id);
if (quest != null)
_nextQuest = new QuestProgress(quest);
else
@@ -441,10 +446,10 @@ internal sealed class QuestController : MiniTaskController
IncreaseStepCount(task.QuestElementId, task.Sequence, true);
}
- public void ExecuteNextStep(bool automatic)
+ public void ExecuteNextStep(EAutomationType automatic)
{
ClearTasksInternal();
- _automatic = automatic;
+ _automationType = automatic;
if (TryPickPriorityQuest())
_logger.LogInformation("Using priority quest over current quest");
@@ -452,8 +457,21 @@ internal sealed class QuestController : MiniTaskController
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
{
- _logger.LogWarning("Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]",
- CurrentQuest?.Quest.QuestElementId, CurrentQuest?.Sequence, CurrentQuest?.Step);
+ if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId &&
+ CurrentQuestDetails?.Progress.Sequence == 0 &&
+ CurrentQuestDetails?.Progress.Step == 255 &&
+ CurrentQuestDetails?.Type == ECurrentQuestType.Next)
+ {
+ _logger.LogInformation("Completed delivery quest");
+ SetNextQuest(null);
+ }
+ else
+ {
+ _logger.LogWarning(
+ "Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]",
+ CurrentQuest?.Quest.Id, CurrentQuest?.Sequence, CurrentQuest?.Step);
+ }
+
return;
}
@@ -488,7 +506,7 @@ internal sealed class QuestController : MiniTaskController
}
_logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}",
- CurrentQuest.Quest.QuestElementId, seq.Sequence, seq.Steps.IndexOf(step),
+ CurrentQuest.Quest.Id, seq.Sequence, seq.Steps.IndexOf(step),
string.Join(", ", newTasks.Select(x => x.ToString())));
foreach (var task in newTasks)
_taskQueue.Enqueue(task);
@@ -587,7 +605,7 @@ internal sealed class QuestController : MiniTaskController
return false;
var (currentQuest, type) = details.Value;
- if (type != CurrentQuestType.Normal)
+ if (type != ECurrentQuestType.Normal)
return false;
QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
@@ -628,10 +646,17 @@ internal sealed class QuestController : MiniTaskController
DateTime StartedAt,
int PointMenuCounter = 0);
- public enum CurrentQuestType
+ public enum ECurrentQuestType
{
Normal,
Next,
Simulated,
}
+
+ public enum EAutomationType
+ {
+ Manual,
+ Automatic,
+ CurrentQuestOnly,
+ }
}
diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs
index 67cd658e..58b93f3f 100644
--- a/Questionable/Controller/QuestRegistry.cs
+++ b/Questionable/Controller/QuestRegistry.cs
@@ -91,12 +91,12 @@ internal sealed class QuestRegistry
{
Quest quest = new()
{
- QuestElementId = new QuestId(questId),
+ Id = new QuestId(questId),
Root = questRoot,
Info = _questData.GetQuestInfo(new QuestId(questId)),
ReadOnly = true,
};
- _quests[quest.QuestElementId] = quest;
+ _quests[quest.Id] = quest;
}
_logger.LogInformation("Loaded {Count} quests from assembly", _quests.Count);
@@ -145,12 +145,12 @@ internal sealed class QuestRegistry
Quest quest = new Quest
{
- QuestElementId = questId,
+ Id = questId,
Root = questNode.Deserialize()!,
Info = _questData.GetQuestInfo(questId),
ReadOnly = false,
};
- _quests[quest.QuestElementId] = quest;
+ _quests[quest.Id] = quest;
}
private void LoadFromDirectory(DirectoryInfo directory, LogLevel logLevel = LogLevel.Information)
@@ -188,30 +188,11 @@ internal sealed class QuestRegistry
return null;
string[] parts = name.Split('_', 2);
- return ElementId.From(uint.Parse(parts[0], CultureInfo.InvariantCulture));
+ return ElementId.FromString(parts[0]);
}
- public bool IsKnownQuest(ElementId elementId)
- {
- if (elementId is QuestId questId)
- return IsKnownQuest(questId);
- else
- return false;
- }
+ public bool IsKnownQuest(ElementId questId) => _quests.ContainsKey(questId);
- public bool IsKnownQuest(QuestId questId) => _quests.ContainsKey(questId);
-
- public bool TryGetQuest(ElementId elementId, [NotNullWhen(true)] out Quest? quest)
- {
- if (elementId is QuestId questId)
- return TryGetQuest(questId, out quest);
- else
- {
- quest = null;
- return false;
- }
- }
-
- public bool TryGetQuest(QuestId questId, [NotNullWhen(true)] out Quest? quest)
+ public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest)
=> _quests.TryGetValue(questId, out quest);
}
diff --git a/Questionable/Controller/Steps/Common/NextQuest.cs b/Questionable/Controller/Steps/Common/NextQuest.cs
index ecc521b3..2afc6ecb 100644
--- a/Questionable/Controller/Steps/Common/NextQuest.cs
+++ b/Questionable/Controller/Steps/Common/NextQuest.cs
@@ -18,11 +18,11 @@ internal static class NextQuest
if (step.NextQuestId == null)
return null;
- if (step.NextQuestId == quest.QuestElementId)
+ if (step.NextQuestId == quest.Id)
return null;
return serviceProvider.GetRequiredService()
- .With(step.NextQuestId, quest.QuestElementId);
+ .With(step.NextQuestId, quest.Id);
}
}
diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs
index d6a3f52a..d23608f0 100644
--- a/Questionable/Controller/Steps/Interactions/Combat.cs
+++ b/Questionable/Controller/Steps/Interactions/Combat.cs
@@ -47,7 +47,7 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.ItemId);
yield return serviceProvider.GetRequiredService()
- .With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags,
+ .With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags,
true);
yield return CreateTask(quest, sequence, step);
break;
@@ -73,7 +73,7 @@ internal static class Combat
bool isLastStep = sequence.Steps.Last() == step;
return serviceProvider.GetRequiredService()
- .With(quest.QuestElementId, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
+ .With(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
step.CompletionQuestVariablesFlags, step.ComplexCombatData);
}
}
diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs
index df27d3c2..869a1fcf 100644
--- a/Questionable/Controller/Steps/Interactions/Interact.cs
+++ b/Questionable/Controller/Steps/Interactions/Interact.cs
@@ -33,7 +33,8 @@ internal static class Interact
yield return serviceProvider.GetRequiredService();
yield return serviceProvider.GetRequiredService()
- .With(step.DataId.Value, step.TargetTerritoryId != null);
+ .With(step.DataId.Value,
+ step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId);
}
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs
index ea46939c..cf866414 100644
--- a/Questionable/Controller/Steps/Interactions/UseItem.cs
+++ b/Questionable/Controller/Steps/Interactions/UseItem.cs
@@ -48,7 +48,7 @@ internal static class UseItem
}
var task = serviceProvider.GetRequiredService