"ecommons": {
"type": "Project"
},
- "gatheringpaths": {
- "type": "Project",
- "dependencies": {
- "Questionable.Model": "[1.0.0, )"
- }
- },
"questionable.model": {
"type": "Project",
"dependencies": {
--- /dev/null
+{
+ "$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
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1012299,
+ "Position": {
+ "X": -16.586609,
+ "Y": 206.49942,
+ "Z": 42.98462
+ },
+ "TerritoryId": 478,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1019615,
+ "Position": {
+ "X": -71.763245,
+ "Y": 206.50021,
+ "Z": 32.638916
+ },
+ "StopDistance": 5,
+ "TerritoryId": 478,
+ "InteractionType": "CompleteQuest"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1012299,
- "Position": {
- "X": -16.586609,
- "Y": 206.49942,
- "Z": 42.98462
- },
- "TerritoryId": 478,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1019615,
- "Position": {
- "X": -71.763245,
- "Y": 206.50021,
- "Z": 32.638916
- },
- "StopDistance": 5,
- "TerritoryId": 478,
- "InteractionType": "CompleteQuest"
- }
- ]
- }
- ]
-}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ ]
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ ]
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ ]
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038500,
+ "Position": {
+ "X": -357.83936,
+ "Y": 21.84602,
+ "Z": -91.32526
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038500,
+ "Position": {
+ "X": -357.83936,
+ "Y": 21.84602,
+ "Z": -91.32526
+ },
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest"
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038504,
+ "Position": {
+ "X": -357.62573,
+ "Y": 21.64856,
+ "Z": -95.99457
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 1,
+ "Steps": [
+ {
+ "DataId": 1038505,
+ "Position": {
+ "X": -376.45538,
+ "Y": 18.999998,
+ "Z": 37.00305
+ },
+ "TerritoryId": 962,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 2,
+ "Steps": [
+ {
+ "DataId": 1038503,
+ "Position": {
+ "X": -367.0863,
+ "Y": 21.84602,
+ "Z": -101.701416
+ },
+ "TerritoryId": 962,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "StopDistance": 7,
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest",
+ "NextQuestId": 4154
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Studium"
+ ],
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35600,
+ "ItemCount": 6,
+ "Collectability": 600
+ }
+ ],
+ "NextQuestId": 4155
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Studium"
+ ],
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35601,
+ "ItemCount": 6,
+ "Collectability": 600
+ }
+ ],
+ "NextQuestId": 4156
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Studium"
+ ],
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35602,
+ "ItemCount": 6,
+ "Collectability": 600
+ }
+ ],
+ "NextQuestId": 4157
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 1,
+ "Steps": [
+ {
+ "DataId": 2011719,
+ "Position": {
+ "X": -102.1897,
+ "Y": -7.0039062,
+ "Z": -59.92218
+ },
+ "TerritoryId": 628,
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Kugane",
+ "AethernetShortcut": [
+ "[Kugane] Aetheryte Plaza",
+ "[Kugane] Shiokaze Hostelry"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 2,
+ "Steps": [
+ {
+ "DataId": 1038506,
+ "Position": {
+ "X": -142.2904,
+ "Y": -4.7500057,
+ "Z": 199.6643
+ },
+ "TerritoryId": 628,
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Kugane] Kogane Dori Markets",
+ "[Kugane] Pier #1"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 3,
+ "Steps": [
+ {
+ "DataId": 1038507,
+ "Position": {
+ "X": 52.750366,
+ "Y": 8.02,
+ "Z": 152.81909
+ },
+ "TerritoryId": 628,
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Kugane] Pier #1",
+ "[Kugane] Thavnairian Consulate"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 4,
+ "Steps": [
+ {
+ "DataId": 2011720,
+ "Position": {
+ "X": 129.5033,
+ "Y": 24.979004,
+ "Z": 15.701477
+ },
+ "TerritoryId": 628,
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Kugane] Thavnairian Consulate",
+ "[Kugane] The Ruby Bazaar"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 5,
+ "Steps": [
+ {
+ "DataId": 1038508,
+ "Position": {
+ "X": 114.70203,
+ "Y": 12,
+ "Z": 35.965454
+ },
+ "TerritoryId": 628,
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Kugane",
+ "AethernetShortcut": [
+ "[Kugane] Aetheryte Plaza",
+ "[Kugane] The Ruby Bazaar"
+ ],
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35847,
+ "ItemCount": 1
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 6,
+ "Steps": [
+ {
+ "DataId": 1038510,
+ "Position": {
+ "X": -92.24078,
+ "Y": 11.799999,
+ "Z": -156.05408
+ },
+ "TerritoryId": 628,
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Kugane] The Ruby Bazaar",
+ "[Kugane] Bokairo Inn"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Studium"
+ ],
+ "NextQuestId": 4158
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Studium"
+ ],
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35603,
+ "ItemCount": 6,
+ "Collectability": 600
+ }
+ ],
+ "NextQuestId": 4159
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "TerritoryId": 962,
+ "InteractionType": "AcceptQuest"
+ }
+ ]
+ },
+ {
+ "Sequence": 1,
+ "Steps": [
+ {
+ "DataId": 1038503,
+ "Position": {
+ "X": -367.0863,
+ "Y": 21.84602,
+ "Z": -101.701416
+ },
+ "TerritoryId": 962,
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Studium"
+ ],
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35848,
+ "ItemCount": 1
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 2,
+ "Steps": [
+ {
+ "DataId": 1038512,
+ "Position": {
+ "X": -73.47223,
+ "Y": -16.147001,
+ "Z": 191.27173
+ },
+ "TerritoryId": 962,
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Old Sharlayan] The Studium",
+ "[Old Sharlayan] Scholar's Harbor"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 3,
+ "Steps": [
+ {
+ "DataId": 1038503,
+ "Position": {
+ "X": -367.0863,
+ "Y": 21.84602,
+ "Z": -101.701416
+ },
+ "TerritoryId": 962,
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Scholar's Harbor",
+ "[Old Sharlayan] The Studium"
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038501,
+ "Position": {
+ "X": -367.3305,
+ "Y": 21.846018,
+ "Z": -102.983154
+ },
+ "StopDistance": 7,
+ "TerritoryId": 962,
+ "InteractionType": "CompleteQuest"
+ }
+ ]
+ }
+ ]
+}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038500,
- "Position": {
- "X": -357.83936,
- "Y": 21.84602,
- "Z": -91.32526
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038500,
- "Position": {
- "X": -357.83936,
- "Y": 21.84602,
- "Z": -91.32526
- },
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest"
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038504,
- "Position": {
- "X": -357.62573,
- "Y": 21.64856,
- "Z": -95.99457
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 1,
- "Steps": [
- {
- "DataId": 1038505,
- "Position": {
- "X": -376.45538,
- "Y": 18.999998,
- "Z": 37.00305
- },
- "TerritoryId": 962,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 2,
- "Steps": [
- {
- "DataId": 1038503,
- "Position": {
- "X": -367.0863,
- "Y": 21.84602,
- "Z": -101.701416
- },
- "TerritoryId": 962,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "StopDistance": 7,
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest",
- "NextQuestId": 4154
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest",
- "AetheryteShortcut": "Old Sharlayan",
- "AethernetShortcut": [
- "[Old Sharlayan] Aetheryte Plaza",
- "[Old Sharlayan] The Studium"
- ],
- "RequiredGatheredItems": [
- {
- "ItemId": 35600,
- "ItemCount": 6,
- "Collectability": 600
- }
- ],
- "NextQuestId": 4155
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest",
- "AetheryteShortcut": "Old Sharlayan",
- "AethernetShortcut": [
- "[Old Sharlayan] Aetheryte Plaza",
- "[Old Sharlayan] The Studium"
- ],
- "RequiredGatheredItems": [
- {
- "ItemId": 35601,
- "ItemCount": 6,
- "Collectability": 600
- }
- ],
- "NextQuestId": 4156
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest",
- "AetheryteShortcut": "Old Sharlayan",
- "AethernetShortcut": [
- "[Old Sharlayan] Aetheryte Plaza",
- "[Old Sharlayan] The Studium"
- ],
- "RequiredGatheredItems": [
- {
- "ItemId": 35602,
- "ItemCount": 6,
- "Collectability": 600
- }
- ],
- "NextQuestId": 4157
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 1,
- "Steps": [
- {
- "DataId": 2011719,
- "Position": {
- "X": -102.1897,
- "Y": -7.0039062,
- "Z": -59.92218
- },
- "TerritoryId": 628,
- "InteractionType": "Interact",
- "AetheryteShortcut": "Kugane",
- "AethernetShortcut": [
- "[Kugane] Aetheryte Plaza",
- "[Kugane] Shiokaze Hostelry"
- ]
- }
- ]
- },
- {
- "Sequence": 2,
- "Steps": [
- {
- "DataId": 1038506,
- "Position": {
- "X": -142.2904,
- "Y": -4.7500057,
- "Z": 199.6643
- },
- "TerritoryId": 628,
- "InteractionType": "Interact",
- "AethernetShortcut": [
- "[Kugane] Kogane Dori Markets",
- "[Kugane] Pier #1"
- ]
- }
- ]
- },
- {
- "Sequence": 3,
- "Steps": [
- {
- "DataId": 1038507,
- "Position": {
- "X": 52.750366,
- "Y": 8.02,
- "Z": 152.81909
- },
- "TerritoryId": 628,
- "InteractionType": "Interact",
- "AethernetShortcut": [
- "[Kugane] Pier #1",
- "[Kugane] Thavnairian Consulate"
- ]
- }
- ]
- },
- {
- "Sequence": 4,
- "Steps": [
- {
- "DataId": 2011720,
- "Position": {
- "X": 129.5033,
- "Y": 24.979004,
- "Z": 15.701477
- },
- "TerritoryId": 628,
- "InteractionType": "Interact",
- "AethernetShortcut": [
- "[Kugane] Thavnairian Consulate",
- "[Kugane] The Ruby Bazaar"
- ]
- }
- ]
- },
- {
- "Sequence": 5,
- "Steps": [
- {
- "DataId": 1038508,
- "Position": {
- "X": 114.70203,
- "Y": 12,
- "Z": 35.965454
- },
- "TerritoryId": 628,
- "InteractionType": "Interact",
- "AetheryteShortcut": "Kugane",
- "AethernetShortcut": [
- "[Kugane] Aetheryte Plaza",
- "[Kugane] The Ruby Bazaar"
- ],
- "RequiredGatheredItems": [
- {
- "ItemId": 35847,
- "ItemCount": 1
- }
- ]
- }
- ]
- },
- {
- "Sequence": 6,
- "Steps": [
- {
- "DataId": 1038510,
- "Position": {
- "X": -92.24078,
- "Y": 11.799999,
- "Z": -156.05408
- },
- "TerritoryId": 628,
- "InteractionType": "Interact",
- "AethernetShortcut": [
- "[Kugane] The Ruby Bazaar",
- "[Kugane] Bokairo Inn"
- ]
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest",
- "AetheryteShortcut": "Old Sharlayan",
- "AethernetShortcut": [
- "[Old Sharlayan] Aetheryte Plaza",
- "[Old Sharlayan] The Studium"
- ],
- "NextQuestId": 4158
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest",
- "AetheryteShortcut": "Old Sharlayan",
- "AethernetShortcut": [
- "[Old Sharlayan] Aetheryte Plaza",
- "[Old Sharlayan] The Studium"
- ],
- "RequiredGatheredItems": [
- {
- "ItemId": 35603,
- "ItemCount": 6,
- "Collectability": 600
- }
- ],
- "NextQuestId": 4159
- }
- ]
- }
- ]
-}
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "TerritoryId": 962,
- "InteractionType": "AcceptQuest"
- }
- ]
- },
- {
- "Sequence": 1,
- "Steps": [
- {
- "DataId": 1038503,
- "Position": {
- "X": -367.0863,
- "Y": 21.84602,
- "Z": -101.701416
- },
- "TerritoryId": 962,
- "InteractionType": "Interact",
- "AetheryteShortcut": "Old Sharlayan",
- "AethernetShortcut": [
- "[Old Sharlayan] Aetheryte Plaza",
- "[Old Sharlayan] The Studium"
- ],
- "RequiredGatheredItems": [
- {
- "ItemId": 35848,
- "ItemCount": 1
- }
- ]
- }
- ]
- },
- {
- "Sequence": 2,
- "Steps": [
- {
- "DataId": 1038512,
- "Position": {
- "X": -73.47223,
- "Y": -16.147001,
- "Z": 191.27173
- },
- "TerritoryId": 962,
- "InteractionType": "Interact",
- "AethernetShortcut": [
- "[Old Sharlayan] The Studium",
- "[Old Sharlayan] Scholar's Harbor"
- ]
- }
- ]
- },
- {
- "Sequence": 3,
- "Steps": [
- {
- "DataId": 1038503,
- "Position": {
- "X": -367.0863,
- "Y": 21.84602,
- "Z": -101.701416
- },
- "TerritoryId": 962,
- "InteractionType": "Interact",
- "AethernetShortcut": [
- "[Old Sharlayan] Scholar's Harbor",
- "[Old Sharlayan] The Studium"
- ]
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038501,
- "Position": {
- "X": -367.3305,
- "Y": 21.846018,
- "Z": -102.983154
- },
- "StopDistance": 7,
- "TerritoryId": 962,
- "InteractionType": "CompleteQuest"
- }
- ]
- }
- ]
-}
<AdditionalFiles Include="6.x - Endwalker\**\*.json" />
<AdditionalFiles Include="7.x - Dawntrail\**\*.json" />
</ItemGroup>
-
- <ItemGroup>
- <Folder Include="7.x - Dawntrail\Custom Deliveries\" />
- </ItemGroup>
</Project>
"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."
}
"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."
}
IshgardTribunal = 86,
IshgardLastVigil = 87,
IshgardGatesOfJudgement = 88,
+ IshgardFirmament = 100001,
Idyllshire = 75,
IdyllshireWest = 90,
{ EAetheryteLocation.IshgardTribunal, "[Ishgard] The Tribunal" },
{ EAetheryteLocation.IshgardLastVigil, "[Ishgard] The Last Vigil" },
{ EAetheryteLocation.IshgardGatesOfJudgement, "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)" },
+ { EAetheryteLocation.IshgardFirmament, "[Ishgard] Firmament" },
{ EAetheryteLocation.Idyllshire, "[Idyllshire] Aetheryte Plaza" },
{ EAetheryteLocation.IdyllshireWest, "[Idyllshire] West Idyllshire" },
namespace Questionable.Model.Questing.Converter;
-public class ElementIdConverter : JsonConverter<ElementId>
+public sealed class ElementIdConverter : JsonConverter<ElementId>
{
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)
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 sealed class QuestId : ElementId
-{
- public QuestId(ushort value)
- : base(value)
+ 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(ushort value) : ElementId(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 "L" + Value.ToString(CultureInfo.InvariantCulture);
+ return "S" + Value.ToString(CultureInfo.InvariantCulture);
}
}
"[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)",
case "start":
_questWindow.IsOpen = true;
- _questController.ExecuteNextStep(true);
+ _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
break;
case "stop":
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
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}).");
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}).");
--- /dev/null
+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<ContextMenuController> _logger;
+
+ public ContextMenuController(
+ IContextMenu contextMenu,
+ QuestController questController,
+ GatheringData gatheringData,
+ QuestRegistry questRegistry,
+ QuestData questData,
+ IGameGui gameGui,
+ IChatGui chatGui,
+ IClientState clientState,
+ ILogger<ContextMenuController> 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;
+ }
+}
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;
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;
/// </summary>
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;
private QuestProgress? _startedQuest;
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
- private bool _automatic;
+ private EAutomationType _automationType;
/// <summary>
/// Some combat encounters finish relatively early (i.e. they're done as part of progressing the quest, but not
_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;
}
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)
{
CurrentQuest.SetStep(0);
}
- ExecuteNextStep(true);
+ ExecuteNextStep(_automationType);
return;
}
// 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;
}
}
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
{
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))
{
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;
}
CurrentQuest.SetStep(255);
}
- if (shouldContinue && _automatic)
- ExecuteNextStep(true);
+ if (shouldContinue && _automationType != EAutomationType.Manual)
+ ExecuteNextStep(_automationType);
}
private void ClearTasksInternal()
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;
}
}
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
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
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");
(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;
}
}
_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);
return false;
var (currentQuest, type) = details.Value;
- if (type != CurrentQuestType.Normal)
+ if (type != ECurrentQuestType.Normal)
return false;
QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
DateTime StartedAt,
int PointMenuCounter = 0);
- public enum CurrentQuestType
+ public enum ECurrentQuestType
{
Normal,
Next,
Simulated,
}
+
+ public enum EAutomationType
+ {
+ Manual,
+ Automatic,
+ CurrentQuestOnly,
+ }
}
{
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);
Quest quest = new Quest
{
- QuestElementId = questId,
+ Id = questId,
Root = questNode.Deserialize<QuestRoot>()!,
Info = _questData.GetQuestInfo(questId),
ReadOnly = false,
};
- _quests[quest.QuestElementId] = quest;
+ _quests[quest.Id] = quest;
}
private void LoadFromDirectory(DirectoryInfo directory, LogLevel logLevel = LogLevel.Information)
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(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 IsKnownQuest(ElementId questId) => _quests.ContainsKey(questId);
- 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);
}
if (step.NextQuestId == null)
return null;
- if (step.NextQuestId == quest.QuestElementId)
+ if (step.NextQuestId == quest.Id)
return null;
return serviceProvider.GetRequiredService<SetQuest>()
- .With(step.NextQuestId, quest.QuestElementId);
+ .With(step.NextQuestId, quest.Id);
}
}
ArgumentNullException.ThrowIfNull(step.ItemId);
yield return serviceProvider.GetRequiredService<UseItem.UseOnObject>()
- .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;
bool isLastStep = sequence.Steps.Last() == step;
return serviceProvider.GetRequiredService<HandleCombat>()
- .With(quest.QuestElementId, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
+ .With(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
step.CompletionQuestVariablesFlags, step.ComplexCombatData);
}
}
yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>();
yield return serviceProvider.GetRequiredService<DoInteract>()
- .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)
}
var task = serviceProvider.GetRequiredService<Use>()
- .With(quest.QuestElementId, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+ .With(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return
[
unmount, task,
ITask task;
if (step.DataId != null)
task = serviceProvider.GetRequiredService<UseOnGround>()
- .With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+ .With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
else
{
ArgumentNullException.ThrowIfNull(step.Position);
task = serviceProvider.GetRequiredService<UseOnPosition>()
- .With(quest.QuestElementId, step.Position.Value, step.ItemId.Value,
+ .With(quest.Id, step.Position.Value, step.ItemId.Value,
step.CompletionQuestVariablesFlags);
}
else if (step.DataId != null)
{
var task = serviceProvider.GetRequiredService<UseOnObject>()
- .With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+ .With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return [unmount, task];
}
else
{
var task = serviceProvider.GetRequiredService<Use>()
- .With(quest.QuestElementId, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+ .With(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return [unmount, task];
}
}
if (!AssemblyGatheringLocationLoader.GetLocations()
.TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot))
- throw new TaskException("No path found for gathering point");
+ throw new TaskException($"No path found for gathering point {gatheringPointId}");
if (HasRequiredItems(requiredGatheredItems))
continue;
return null;
return serviceProvider.GetRequiredService<CheckSkip>()
- .With(step, skipConditions ?? new(), quest.QuestElementId);
+ .With(step, skipConditions ?? new(), quest.Id);
}
}
if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
- .With((QuestId)quest.QuestElementId, step);
+ .With((QuestId)quest.Id, step);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, Next(quest, sequence)];
}
case EInteractionType.AcceptQuest:
{
var accept = serviceProvider.GetRequiredService<WaitQuestAccepted>()
- .With(step.PickUpQuestId ?? quest.QuestElementId);
+ .With(step.PickUpQuestId ?? quest.Id);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.PickUpQuestId != null)
return [accept, delay, Next(quest, sequence)];
case EInteractionType.CompleteQuest:
{
var complete = serviceProvider.GetRequiredService<WaitQuestCompleted>()
- .With(step.TurnInQuestId ?? quest.QuestElementId);
+ .With(step.TurnInQuestId ?? quest.Id);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.TurnInQuestId != null)
return [complete, delay, Next(quest, sequence)];
private static NextStep Next(Quest quest, QuestSequence sequence)
{
- return new NextStep(quest.QuestElementId, sequence.Sequence);
+ return new NextStep(quest.Id, sequence.Sequence);
}
}
aethernetGroups[(EAetheryteLocation)aetheryte.RowId] = aetheryte.AethernetGroup;
}
+ aethernetNames[EAetheryteLocation.IshgardFirmament] = "Firmament";
+ territoryIds[EAetheryteLocation.IshgardFirmament] = 886;
+ aethernetGroups[EAetheryteLocation.IshgardFirmament] = aethernetGroups[EAetheryteLocation.Ishgard];
+
AethernetNames = aethernetNames.AsReadOnly();
TerritoryIds = territoryIds.AsReadOnly();
AethernetGroups = aethernetGroups.AsReadOnly();
{ EAetheryteLocation.GridaniaAirship, new(24.86354f, -19.000002f, 96f) },
{ EAetheryteLocation.UldahAirship, new(-16.954851f, 82.999985f, -9.421141f) },
{ EAetheryteLocation.KuganeAirship, new(-55.72525f, 79.10602f, 46.23109f) },
+ { EAetheryteLocation.IshgardFirmament, new(9.92315f, -15.2f, 173.5059f) },
}.AsReadOnly();
public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; }
public bool IsCityAetheryte(EAetheryteLocation aetheryte)
{
+ if (aetheryte == EAetheryteLocation.IshgardFirmament)
+ return true;
+
var territoryId = TerritoryIds[aetheryte];
return TownTerritoryIds.Contains(territoryId);
}
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.Logging;
namespace Questionable.Data;
internal sealed class GatheringData
{
- private readonly Dictionary<uint, uint> _gatheringItemToItem;
private readonly Dictionary<uint, ushort> _minerGatheringPoints = [];
private readonly Dictionary<uint, ushort> _botanistGatheringPoints = [];
+ private readonly Dictionary<uint, ushort> _itemIdToCollectability;
+ private readonly Dictionary<uint, uint> _npcForCustomDeliveries;
public GatheringData(IDataManager dataManager)
{
- _gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
+ Dictionary<uint, uint> gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
.Where(x => x.RowId != 0 && x.Item != 0)
.ToDictionary(x => x.RowId, x => (uint)x.Item);
{
foreach (var gatheringItemId in gatheringPointBase.Item.Where(x => x != 0))
{
- if (_gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId))
+ if (gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId))
{
if (gatheringPointBase.GatheringType.Row is 0 or 1)
_minerGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId;
}
}
}
- }
+ _itemIdToCollectability = dataManager.GetExcelSheet<SatisfactionSupply>()!
+ .Where(x => x.RowId > 0)
+ .Where(x => x.Slot is 2)
+ .Select(x => new
+ {
+ ItemId = x.Item.Row,
+ Collectability = x.CollectabilityHigh,
+ })
+ .Distinct()
+ .ToDictionary(x => x.ItemId, x => x.Collectability);
+
+ _npcForCustomDeliveries = dataManager.GetExcelSheet<SatisfactionNpc>()!
+ .Where(x => x.RowId > 0)
+ .SelectMany(x => dataManager.GetExcelSheet<SatisfactionSupply>()!
+ .Where(y => y.RowId == x.SupplyIndex.Last())
+ .Select(y => new
+ {
+ ItemId = y.Item.Row,
+ NpcId = x.Npc.Row
+ }))
+ .Where(x => x.ItemId > 0)
+ .Distinct()
+ .ToDictionary(x => x.ItemId, x => x.NpcId);
+ }
public bool TryGetGatheringPointId(uint itemId, uint classJobId, out ushort gatheringPointId)
{
return false;
}
}
+
+ public ushort GetRecommendedCollectability(uint itemId)
+ => _itemIdToCollectability.GetValueOrDefault(itemId);
+
+ public bool TryGetCustomDeliveryNpc(uint itemId, out uint npcId)
+ => _npcForCustomDeliveries.TryGetValue(itemId, out npcId);
}
var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1,
new uint[] { 108, 109 }.Concat(limsaStart.Quest.Select(x => x.Row))
.Where(x => x != 0)
- .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
+ .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1,
new uint[] { 85, 123, 124 }.Concat(gridaniaStart.Quest.Select(x => x.Row))
.Where(x => x != 0)
- .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
+ .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1,
new uint[] { 568, 569, 570 }.Concat(uldahStart.Quest.Select(x => x.Row))
.Where(x => x != 0)
- .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
+ .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]);
genres.Single(x => x.Id == 1)
using System.Collections.Immutable;
using System.Linq;
using Dalamud.Plugin.Services;
+using Lumina.Excel.GeneratedSheets;
using Questionable.Model;
using Questionable.Model.Questing;
using Quest = Lumina.Excel.GeneratedSheets.Quest;
internal sealed class QuestData
{
- private readonly ImmutableDictionary<QuestId, QuestInfo> _quests;
+ private readonly Dictionary<ElementId, IQuestInfo> _quests;
public QuestData(IDataManager dataManager)
{
- _quests = dataManager.GetExcelSheet<Quest>()!
- .Where(x => x.RowId > 0)
- .Where(x => x.IssuerLocation.Row > 0)
- .Where(x => x.Festival.Row == 0)
- .Select(x => new QuestInfo(x))
- .ToImmutableDictionary(x => x.QuestId, x => x);
+ List<IQuestInfo> quests =
+ [
+ ..dataManager.GetExcelSheet<Quest>()!
+ .Where(x => x.RowId > 0)
+ .Where(x => x.IssuerLocation.Row > 0)
+ .Where(x => x.Festival.Row == 0)
+ .Select(x => new QuestInfo(x)),
+ ..dataManager.GetExcelSheet<SatisfactionNpc>()!
+ .Where(x => x.RowId > 0)
+ .Select(x => new SatisfactionSupplyInfo(x))
+ ];
+ _quests = quests.ToDictionary(x => x.QuestId, x => x);
}
- public QuestInfo GetQuestInfo(ElementId elementId)
+ public IQuestInfo GetQuestInfo(ElementId elementId)
{
- if (elementId is QuestId questId)
- return GetQuestInfo(questId);
-
- throw new ArgumentException("Invalid id", nameof(elementId));
- }
-
- public QuestInfo GetQuestInfo(QuestId questId)
- {
- return _quests[questId] ?? throw new ArgumentOutOfRangeException(nameof(questId));
+ return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId));
}
- public List<QuestInfo> GetAllByIssuerDataId(uint targetId)
+ public List<IQuestInfo> GetAllByIssuerDataId(uint targetId)
{
return _quests.Values
.Where(x => x.IssuerDataId == targetId)
public List<QuestInfo> GetAllByJournalGenre(uint journalGenre)
{
return _quests.Values
+ .Where(x => x is QuestInfo)
+ .Cast<QuestInfo>()
.Where(x => x.JournalGenre == journalGenre)
.OrderBy(x => x.SortKey)
.ThenBy(x => x.QuestId)
public bool Teleport(EAetheryteLocation aetheryteLocation)
{
+ if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
+ {
+ // TODO does this even work on non-EN clients?
+ return _aethernetTeleport.InvokeFunc("Firmament");
+ }
+
if (!_aetheryteData.AethernetNames.TryGetValue(aetheryteLocation, out string? name))
return false;
{
if (elementId is QuestId questId)
return IsReadyToAcceptQuest(questId);
- return false;
+ else if (elementId is SatisfactionSupplyNpcId)
+ return true;
+ else
+ throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsReadyToAcceptQuest(QuestId questId)
{
if (elementId is QuestId questId)
return IsQuestAccepted(questId);
- return false;
+ else if (elementId is SatisfactionSupplyNpcId)
+ return false;
+ else
+ throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsQuestAccepted(QuestId questId)
{
if (elementId is QuestId questId)
return IsQuestComplete(questId);
- return false;
+ else if (elementId is SatisfactionSupplyNpcId)
+ return false;
+ else
+ throw new ArgumentOutOfRangeException(nameof(elementId));
}
[SuppressMessage("Performance", "CA1822")]
{
if (elementId is QuestId questId)
return IsQuestLocked(questId, extraCompletedQuest);
- return false;
+ else if (elementId is SatisfactionSupplyNpcId)
+ return false;
+ else
+ throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
{
- var questInfo = _questData.GetQuestInfo(questId);
+ var questInfo = (QuestInfo) _questData.GetQuestInfo(questId);
if (questInfo.QuestLocks.Count > 0)
{
var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
}
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
- => IsAetheryteUnlocked((uint)aetheryteLocation, out _);
+ {
+ if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
+ return IsQuestComplete(new QuestId(3672));
+ return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
+ }
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
{
if (excelSheetName == null)
{
var questRow =
- _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestElementId.Value +
+ _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.Id.Value +
0x10000);
if (questRow == null)
{
- _logger.LogError("Could not find quest row for {QuestId}", currentQuest.QuestElementId);
+ _logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id);
return null;
}
- excelSheetName = $"quest/{(currentQuest.QuestElementId.Value / 100):000}/{questRow.Id}";
+ excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
}
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
--- /dev/null
+using System;
+using Dalamud.Game.Text;
+using Questionable.Model.Questing;
+
+namespace Questionable.Model;
+
+public interface IQuestInfo
+{
+ public ElementId QuestId { get; }
+ public string Name { get; }
+ public uint IssuerDataId { get; }
+ public bool IsRepeatable { get; }
+ public ushort Level { get; }
+ public EBeastTribe BeastTribe { get; }
+ public bool IsMainScenarioQuest { get; }
+
+ public string SimplifiedName => Name
+ .Replace(".", "", StringComparison.Ordinal)
+ .TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' ');
+}
internal sealed class Quest
{
- public required ElementId QuestElementId { get; init; }
+ public required ElementId Id { get; init; }
public required QuestRoot Root { get; init; }
- public required QuestInfo Info { get; init; }
+ public required IQuestInfo Info { get; init; }
public required bool ReadOnly { get; init; }
public QuestSequence? FindSequence(byte currentSequence)
namespace Questionable.Model;
-internal sealed class QuestInfo
+internal sealed class QuestInfo : IQuestInfo
{
public QuestInfo(ExcelQuest quest)
{
}
- public QuestId QuestId { get; }
+ public ElementId QuestId { get; }
public string Name { get; }
public ushort Level { get; }
public uint IssuerDataId { get; }
public GrandCompany GrandCompany { get; }
public EBeastTribe BeastTribe { get; }
- public string SimplifiedName => Name
- .Replace(".", "", StringComparison.Ordinal)
- .TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' ');
-
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)]
public enum QuestJoin : byte
{
--- /dev/null
+using Lumina.Excel.GeneratedSheets;
+using Questionable.Model.Questing;
+
+namespace Questionable.Model;
+
+internal sealed class SatisfactionSupplyInfo : IQuestInfo
+{
+ public SatisfactionSupplyInfo(SatisfactionNpc npc)
+ {
+ QuestId = new SatisfactionSupplyNpcId((ushort)npc.RowId);
+ Name = npc.Npc.Value!.Singular;
+ IssuerDataId = npc.Npc.Row;
+ Level = npc.LevelUnlock;
+ }
+
+ public ElementId QuestId { get; }
+ public string Name { get; }
+ public uint IssuerDataId { get; }
+ public bool IsRepeatable => true;
+ public ushort Level { get; }
+ public EBeastTribe BeastTribe => EBeastTribe.None;
+ public bool IsMainScenarioQuest => false;
+}
IChatGui chatGui,
ICommandManager commandManager,
IAddonLifecycle addonLifecycle,
- IKeyState keyState)
+ IKeyState keyState,
+ IContextMenu contextMenu)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState);
+ serviceCollection.AddSingleton(contextMenu);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
_serviceProvider = serviceCollection.BuildServiceProvider();
_serviceProvider.GetRequiredService<QuestRegistry>().Reload();
_serviceProvider.GetRequiredService<CommandHandler>();
+ _serviceProvider.GetRequiredService<ContextMenuController>();
_serviceProvider.GetRequiredService<DalamudInitializer>();
}
serviceCollection.AddSingleton<NavigationShortcutController>();
serviceCollection.AddSingleton<CombatController>();
serviceCollection.AddSingleton<GatheringController>();
+ serviceCollection.AddSingleton<ContextMenuController>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
return quest.AllSteps()
- .Select(x => Validate(quest.QuestElementId, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut))
+ .Select(x => Validate(quest.Id, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut))
.Where(x => x != null)
.Cast<ValidationIssue>();
}
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = 0,
Step = null,
Type = EIssueType.MissingSequence0,
yield break;
}
- if (quest.Info.CompletesInstantly)
+ if (quest.Info is QuestInfo { CompletesInstantly: true })
{
foreach (var sequence in sequences)
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)sequence.Sequence,
Step = null,
Type = EIssueType.InstantQuestWithMultipleSteps,
};
}
}
- else
+ else if (quest.Info is QuestInfo)
{
int maxSequence = sequences.Select(x => x.Sequence)
.Where(x => x != 255)
{
return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)sequenceNo,
Step = null,
Type = EIssueType.MissingSequence,
{
return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)sequenceNo,
Step = null,
Type = EIssueType.DuplicateSequence,
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)sequence.Sequence,
Step = i,
Type = EIssueType.DuplicateCompletionFlags,
{
_questSchema ??= JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
- if (_questNodes.TryGetValue(quest.QuestElementId, out JsonNode? questNode))
+ if (_questNodes.TryGetValue(quest.Id, out JsonNode? questNode))
{
var evaluationResult = _questSchema.Evaluate(questNode, new EvaluationOptions
{
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.InvalidJsonSchema,
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
- foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.QuestElementId))
+ foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.Id))
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)invalidNextQuest.Sequence.Sequence,
Step = invalidNextQuest.StepId,
Type = EIssueType.InvalidNextQuestId,
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.QuestDisabled,
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
+ if (quest.Id is SatisfactionSupplyNpcId)
+ yield break;
+
var questAccepts = FindQuestStepsWithInteractionType(quest, EInteractionType.AcceptQuest)
.Where(x => x.Step.PickUpQuestId == null)
.ToList();
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)accept.Sequence.Sequence,
Step = accept.StepId,
Type = EIssueType.UnexpectedAcceptQuestStep,
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = 0,
Step = null,
Type = EIssueType.MissingQuestAccept,
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = (byte)complete.Sequence.Sequence,
Step = complete.StepId,
Type = EIssueType.UnexpectedCompleteQuestStep,
{
yield return new ValidationIssue
{
- QuestId = quest.QuestElementId,
+ QuestId = quest.Id,
Sequence = 255,
Step = null,
Type = EIssueType.MissingQuestComplete,
QuestStep? step = sequence.FindStep(i);
if (step != null && TryGetPosition(step, out Vector3? position))
{
- DrawStep($"{quest.QuestElementId} / {sequence.Sequence} / {i}", step, position.Value, 0xFFFFFFFF);
+ DrawStep($"{quest.Id} / {sequence.Sequence} / {i}", step, position.Value, 0xFFFFFFFF);
}
}
}
if (ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
{
- _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString(), commandInfo);
+ _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo);
}
if (ImGui.IsItemHovered())
{
var currentQuestDetails = _questController.CurrentQuestDetails;
QuestController.QuestProgress? currentQuest = currentQuestDetails?.Progress;
- QuestController.CurrentQuestType? currentQuestType = currentQuestDetails?.Type;
+ QuestController.ECurrentQuestType? currentQuestType = currentQuestDetails?.Type;
if (currentQuest != null)
{
DrawQuestNames(currentQuest, currentQuestType);
}
private void DrawQuestNames(QuestController.QuestProgress currentQuest,
- QuestController.CurrentQuestType? currentQuestType)
+ QuestController.ECurrentQuestType? currentQuestType)
{
- if (currentQuestType == QuestController.CurrentQuestType.Simulated)
+ if (currentQuestType == QuestController.ECurrentQuestType.Simulated)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextUnformatted(
private QuestWork? DrawQuestWork(QuestController.QuestProgress currentQuest)
{
- if (currentQuest.Quest.QuestElementId is not QuestId questId)
+ if (currentQuest.Quest.Id is not QuestId questId)
return null;
var questWork = _gameFunctions.GetQuestEx(questId);
{
using var disabled = ImRaii.Disabled();
- if (currentQuest.Quest.QuestElementId == _questController.NextQuest?.Quest.QuestElementId)
+ if (currentQuest.Quest.Id == _questController.NextQuest?.Quest.Id)
ImGui.TextUnformatted("(Next quest in story line not accepted)");
else
ImGui.TextUnformatted("(Not accepted)");
if (questWork == null)
_questController.SetNextQuest(currentQuest.Quest);
- _questController.ExecuteNextStep(true);
+ _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
}
ImGui.SameLine();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step"))
{
- _questController.ExecuteNextStep(false);
+ _questController.ExecuteNextStep(QuestController.EAutomationType.Manual);
}
ImGui.EndDisabled();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
{
_movementController.Stop();
- _questController.Skip(currentQuest.Quest.QuestElementId, currentQuest.Sequence);
+ _questController.Skip(currentQuest.Quest.Id, currentQuest.Sequence);
}
if (colored)
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Atlas))
_commandManager.DispatchCommand("/questinfo",
- currentQuest.Quest.QuestElementId.ToString() ?? string.Empty, commandInfo);
+ currentQuest.Quest.Id.ToString() ?? string.Empty, commandInfo);
}
bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Model;
+using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
_uiUtils = uiUtils;
}
+ public void Draw(IQuestInfo quest)
+ {
+ if (quest is QuestInfo questInfo)
+ Draw(questInfo);
+ }
+
public void Draw(QuestInfo quest)
{
using var tooltip = ImRaii.Tooltip();
_uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
- if (counter <= 2 || icon != FontAwesomeIcon.Check)
- DrawQuestUnlocks(qInfo, counter + 1);
+ if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
+ DrawQuestUnlocks(qstInfo, counter + 1);
}
}
ImGui.Unindent();
}
- private static string FormatQuestUnlockName(QuestInfo questInfo)
+ private static string FormatQuestUnlockName(IQuestInfo questInfo)
{
if (questInfo.IsMainScenarioQuest)
return $"{questInfo.Name} ({questInfo.QuestId}, MSQ)";
private readonly UiUtils _uiUtils;
private readonly QuestTooltipComponent _questTooltipComponent;
- private List<QuestInfo> _quests = [];
- private List<QuestInfo> _offeredQuests = [];
+ private List<IQuestInfo> _quests = [];
+ private List<IQuestInfo> _offeredQuests = [];
private bool _onlyAvailableQuests = true;
public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions,
_quests = _questRegistry.AllQuests
.Where(x => x.FindSequence(0)?.FindStep(0)?.TerritoryId == territoryId)
- .Select(x => _questData.GetQuestInfo(x.QuestElementId))
+ .Select(x => _questData.GetQuestInfo(x.Id))
.ToList();
foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers)
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, actionIconSize);
ImGui.TableHeadersRow();
- foreach (QuestInfo quest in (_offeredQuests.Count != 0 && _onlyAvailableQuests) ? _offeredQuests : _quests)
+ foreach (IQuestInfo quest in (_offeredQuests.Count != 0 && _onlyAvailableQuests) ? _offeredQuests : _quests)
{
ImGui.TableNextRow();
- string questId = quest.QuestId.ToString();
+ string questId = quest.QuestId.ToString() ?? string.Empty;
bool isKnownQuest = _questRegistry.TryGetQuest(quest.QuestId, out var knownQuest);
if (ImGui.TableNextColumn())
if (startNextQuest)
{
_questController.SetNextQuest(knownQuest);
- _questController.ExecuteNextStep(true);
+ _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
}
ImGui.SameLine();
}
}
- private void CopyToClipboard(QuestInfo quest, bool suffix)
+ private void CopyToClipboard(IQuestInfo quest, bool suffix)
{
string fileName = $"{quest.QuestId}_{quest.SimplifiedName}{(suffix ? ".json" : "")}";
ImGui.SetClipboardText(fileName);
public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId questElementId)
{
if (_gameFunctions.IsQuestAccepted(questElementId))
- return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active");
+ return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_gameFunctions.IsQuestLocked(questElementId))
return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
else
- return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Available");
+ return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");
}
public static (Vector4 color, FontAwesomeIcon icon) GetInstanceStyle(ushort instanceId)
if (UIState.IsInstanceContentCompleted(instanceId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
else if (UIState.IsInstanceContentUnlocked(instanceId))
- return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight);
+ return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running);
else
return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times);
}