punish v6.8.18.0

This commit is contained in:
alydev 2025-10-09 07:47:19 +10:00
commit 060278c1b7
317 changed files with 554155 additions and 0 deletions

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>GatheringPaths</AssemblyName>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<TargetFramework>netcoreapp9.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<None Remove="Questionable.GatheringPaths.GatheringLocationSchema" />
<EmbeddedResource Include="Questionable.GatheringPaths.GatheringLocationSchema" LogicalName="Questionable.GatheringPaths.GatheringLocationSchema" />
</ItemGroup>
<ItemGroup>
<Reference Include="Questionable.Model">
<HintPath>..\..\Questionable.Model.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,115 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/gatheringlocation-v1.json",
"title": "Gathering Location V1",
"description": "A series of gathering locationsk",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"const": "https://qstxiv.github.io/schema/gatheringlocation-v1.json"
},
"Author": {
"description": "Author of the gathering location data",
"type": [
"string",
"array"
],
"items": {
"type": "string"
}
},
"Steps": {
"type": "array",
"items": {
"$ref": "https://qstxiv.github.io/schema/quest-v1.json#/$defs/Step"
},
"minItems": 1
},
"FlyBetweenNodes": {
"description": "If nodes are close enough together, flying makes no sense due to the pathfinding overhead",
"type": "boolean",
"default": true
},
"ExtraQuestItems": {
"description": "Some quests (such as Ixal) add quest items to gathering nodes, but there's no clear connection between the item and the node in the sheets",
"type": "array",
"items": {
"type": "integer",
"minimum": 2000000
}
},
"Groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Nodes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DataId": {
"type": "number",
"minimum": 30000,
"maximum": 50000
},
"Fly": {
"type": "boolean"
},
"Locations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
},
"MinimumAngle": {
"type": "number",
"minimum": -360,
"maximum": 360
},
"MaximumAngle": {
"type": "number",
"minimum": -360,
"maximum": 360
},
"MinimumDistance": {
"type": "number",
"minimum": 0
},
"MaximumDistance": {
"type": "number",
"exclusiveMinimum": 0
}
},
"required": [
"Position"
],
"additionalProperties": false
}
}
},
"required": [
"DataId"
],
"additionalProperties": false
}
}
},
"required": [
"Nodes"
],
"additionalProperties": false
}
}
},
"required": [
"$schema",
"Author",
"Steps",
"Groups"
],
"additionalProperties": false
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
namespace LLib.GameData;
public enum EClassJob : uint
{
Adventurer,
Gladiator,
Pugilist,
Marauder,
Lancer,
Archer,
Conjurer,
Thaumaturge,
Carpenter,
Blacksmith,
Armorer,
Goldsmith,
Leatherworker,
Weaver,
Alchemist,
Culinarian,
Miner,
Botanist,
Fisher,
Paladin,
Monk,
Warrior,
Dragoon,
Bard,
WhiteMage,
BlackMage,
Arcanist,
Summoner,
Scholar,
Rogue,
Ninja,
Machinist,
DarkKnight,
Astrologian,
Samurai,
RedMage,
BlueMage,
Gunbreaker,
Dancer,
Reaper,
Sage,
Viper,
Pictomancer
}

View file

@ -0,0 +1,188 @@
using System;
using System.Linq;
namespace LLib.GameData;
public static class EClassJobExtensions
{
public static bool IsClass(this EClassJob classJob)
{
bool flag;
switch (classJob)
{
case EClassJob.Gladiator:
case EClassJob.Pugilist:
case EClassJob.Marauder:
case EClassJob.Lancer:
case EClassJob.Archer:
case EClassJob.Conjurer:
case EClassJob.Thaumaturge:
case EClassJob.Arcanist:
case EClassJob.Rogue:
flag = true;
break;
default:
flag = false;
break;
}
if (!flag && !classJob.IsCrafter())
{
return classJob.IsGatherer();
}
return true;
}
public static bool HasBaseClass(this EClassJob classJob)
{
return (from x in Enum.GetValues<EClassJob>()
where x.IsClass()
select x).Any((EClassJob x) => x.AsJob() == classJob);
}
public static EClassJob AsJob(this EClassJob classJob)
{
return classJob switch
{
EClassJob.Gladiator => EClassJob.Paladin,
EClassJob.Marauder => EClassJob.Warrior,
EClassJob.Pugilist => EClassJob.Monk,
EClassJob.Lancer => EClassJob.Dragoon,
EClassJob.Rogue => EClassJob.Ninja,
EClassJob.Archer => EClassJob.Bard,
EClassJob.Conjurer => EClassJob.WhiteMage,
EClassJob.Thaumaturge => EClassJob.BlackMage,
EClassJob.Arcanist => EClassJob.Summoner,
_ => classJob,
};
}
public static bool IsTank(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Gladiator:
case EClassJob.Marauder:
case EClassJob.Paladin:
case EClassJob.Warrior:
case EClassJob.DarkKnight:
case EClassJob.Gunbreaker:
return true;
default:
return false;
}
}
public static bool IsHealer(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Conjurer:
case EClassJob.WhiteMage:
case EClassJob.Scholar:
case EClassJob.Astrologian:
case EClassJob.Sage:
return true;
default:
return false;
}
}
public static bool IsMelee(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Pugilist:
case EClassJob.Lancer:
case EClassJob.Monk:
case EClassJob.Dragoon:
case EClassJob.Rogue:
case EClassJob.Ninja:
case EClassJob.Samurai:
case EClassJob.Reaper:
case EClassJob.Viper:
return true;
default:
return false;
}
}
public static bool IsPhysicalRanged(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Archer:
case EClassJob.Bard:
case EClassJob.Machinist:
case EClassJob.Dancer:
return true;
default:
return false;
}
}
public static bool IsCaster(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Thaumaturge:
case EClassJob.BlackMage:
case EClassJob.Arcanist:
case EClassJob.Summoner:
case EClassJob.RedMage:
case EClassJob.BlueMage:
case EClassJob.Pictomancer:
return true;
default:
return false;
}
}
public static bool DealsPhysicalDamage(this EClassJob classJob)
{
if (!classJob.IsTank() && !classJob.IsMelee())
{
return classJob.IsPhysicalRanged();
}
return true;
}
public static bool DealsMagicDamage(this EClassJob classJob)
{
if (!classJob.IsHealer())
{
return classJob.IsCaster();
}
return true;
}
public static bool IsCrafter(this EClassJob classJob)
{
if (classJob >= EClassJob.Carpenter)
{
return classJob <= EClassJob.Culinarian;
}
return false;
}
public static bool IsGatherer(this EClassJob classJob)
{
if (classJob >= EClassJob.Miner)
{
return classJob <= EClassJob.Fisher;
}
return false;
}
public static string ToFriendlyString(this EClassJob classJob)
{
return classJob switch
{
EClassJob.WhiteMage => "White Mage",
EClassJob.BlackMage => "Black Mage",
EClassJob.DarkKnight => "Dark Knight",
EClassJob.RedMage => "Red Mage",
EClassJob.BlueMage => "Blue Mage",
_ => classJob.ToString(),
};
}
}

View file

@ -0,0 +1,60 @@
namespace LLib.GameData;
public enum ETerritoryIntendedUse : byte
{
CityArea = 0,
OpenWorld = 1,
Inn = 2,
Dungeon = 3,
VariantDungeon = 4,
Gaol = 5,
StartingArea = 6,
QuestArea = 7,
AllianceRaid = 8,
QuestBattle = 9,
Trial = 10,
QuestArea2 = 12,
ResidentialArea = 13,
HousingInstances = 14,
QuestArea3 = 15,
Raid = 16,
Raid2 = 17,
Frontline = 18,
ChocoboSquare = 20,
RestorationEvent = 21,
Sanctum = 22,
GoldSaucer = 23,
LordOfVerminion = 25,
Diadem = 26,
HallOfTheNovice = 27,
CrystallineConflict = 28,
QuestBattle2 = 29,
Barracks = 30,
DeepDungeon = 31,
SeasonalEvent = 32,
TreasureMapDuty = 33,
SeasonalEventDuty = 34,
Battlehall = 35,
CrystallineConflict2 = 37,
Diadem2 = 38,
RivalWings = 39,
Unknown1 = 40,
Eureka = 41,
SeasonalEvent2 = 43,
LeapOfFaith = 44,
MaskedCarnivale = 45,
OceanFishing = 46,
Diadem3 = 47,
Bozja = 48,
IslandSanctuary = 49,
Battlehall2 = 50,
Battlehall3 = 51,
LargeScaleRaid = 52,
LargeScaleSavageRaid = 53,
QuestArea4 = 54,
TribalInstance = 56,
CriterionDuty = 57,
CriterionSavageDuty = 58,
Blunderville = 59,
CosmicExploration = 60
}

View file

@ -0,0 +1,53 @@
using System;
using System.Linq;
using Dalamud.Game.NativeWrapper;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace LLib.GameUI;
public static class LAddon
{
private const int UnitListCount = 18;
public unsafe static AtkUnitBase* GetAddonById(uint id)
{
AtkUnitList* ptr = &AtkStage.Instance()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList;
for (int i = 0; i < 18; i++)
{
AtkUnitList* ptr2 = ptr + i;
foreach (int item in Enumerable.Range(0, Math.Min(ptr2->Count, ptr2->Entries.Length)))
{
AtkUnitBase* value = ptr2->Entries[item].Value;
if (value != null && value->Id == id)
{
return value;
}
}
}
return null;
}
public unsafe static bool TryGetAddonByName<T>(this IGameGui gameGui, string addonName, out T* addonPtr) where T : unmanaged
{
ArgumentNullException.ThrowIfNull(gameGui, "gameGui");
ArgumentException.ThrowIfNullOrEmpty(addonName, "addonName");
AtkUnitBasePtr addonByName = gameGui.GetAddonByName(addonName);
if (!addonByName.IsNull)
{
addonPtr = (T*)addonByName.Address;
return true;
}
addonPtr = null;
return false;
}
public unsafe static bool IsAddonReady(AtkUnitBase* addon)
{
if (addon->IsVisible)
{
return addon->UldManager.LoadedState == AtkLoadState.Loaded;
}
return false;
}
}

View file

@ -0,0 +1,21 @@
using System;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace LLib.GameUI;
public static class LAtkValue
{
public unsafe static string? ReadAtkString(this AtkValue atkValue)
{
if (atkValue.Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Undefined)
{
return null;
}
if (atkValue.String.HasValue)
{
return MemoryHelper.ReadSeStringNullTerminated(new IntPtr((byte*)atkValue.String)).WithCertainMacroCodeReplacements();
}
return null;
}
}

View file

@ -0,0 +1,16 @@
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Text.ReadOnly;
namespace LLib.GameUI;
public static class SeStringExtensions
{
public static string WithCertainMacroCodeReplacements(this SeString? str)
{
if (str == null)
{
return string.Empty;
}
return new ReadOnlySeString(str.Encode()).WithCertainMacroCodeReplacements();
}
}

View file

@ -0,0 +1,28 @@
namespace LLib.Gear;
public enum EBaseParam : byte
{
None = 0,
Strength = 1,
Dexterity = 2,
Vitality = 3,
Intelligence = 4,
Mind = 5,
Piety = 6,
GP = 10,
CP = 11,
DamagePhys = 12,
DamageMag = 13,
DefensePhys = 21,
DefenseMag = 24,
Tenacity = 19,
Crit = 27,
DirectHit = 22,
Determination = 44,
SpellSpeed = 46,
SkillSpeed = 45,
Craftsmanship = 70,
Control = 71,
Gathering = 72,
Perception = 73
}

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace LLib.Gear;
public sealed record EquipmentStats(Dictionary<EBaseParam, StatInfo> Stats, byte MateriaCount)
{
private sealed class KeyValuePairComparer : IEqualityComparer<KeyValuePair<EBaseParam, StatInfo>>
{
public bool Equals(KeyValuePair<EBaseParam, StatInfo> x, KeyValuePair<EBaseParam, StatInfo> y)
{
if (x.Key == y.Key)
{
return object.Equals(x.Value, y.Value);
}
return false;
}
public int GetHashCode(KeyValuePair<EBaseParam, StatInfo> obj)
{
return HashCode.Combine((int)obj.Key, obj.Value);
}
}
public short Get(EBaseParam param)
{
return (short)(GetEquipment(param) + GetMateria(param));
}
public short GetEquipment(EBaseParam param)
{
Stats.TryGetValue(param, out StatInfo value);
return value?.EquipmentValue ?? 0;
}
public short GetMateria(EBaseParam param)
{
Stats.TryGetValue(param, out StatInfo value);
return value?.MateriaValue ?? 0;
}
public bool IsOvercapped(EBaseParam param)
{
Stats.TryGetValue(param, out StatInfo value);
return value?.Overcapped ?? false;
}
public bool Has(EBaseParam substat)
{
return Stats.ContainsKey(substat);
}
public bool HasMateria()
{
return Stats.Values.Any((StatInfo x) => x.MateriaValue > 0);
}
public bool Equals(EquipmentStats? other)
{
if (other != null && MateriaCount == other.MateriaCount)
{
return Stats.SequenceEqual(other.Stats, new KeyValuePairComparer());
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(MateriaCount, Stats);
}
}

View file

@ -0,0 +1,37 @@
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace LLib.Gear;
[Sheet("BaseParam")]
public readonly struct ExtendedBaseParam : IExcelRow<ExtendedBaseParam>
{
private const int ParamCount = 23;
public uint RowId => _003Crow_003EP;
public BaseParam BaseParam => new BaseParam(_003Cpage_003EP, _003Coffset_003EP, _003Crow_003EP);
public unsafe Collection<ushort> EquipSlotCategoryPct => new Collection<ushort>(_003Cpage_003EP, _003Coffset_003EP, _003Coffset_003EP, (delegate*<ExcelPage, uint, uint, uint, ushort>)(&EquipSlotCategoryPctCtor), 23);
public ExtendedBaseParam(ExcelPage page, uint offset, uint row)
{
_003Cpage_003EP = page;
_003Coffset_003EP = offset;
_003Crow_003EP = row;
}
private static ushort EquipSlotCategoryPctCtor(ExcelPage page, uint parentOffset, uint offset, uint i)
{
if (i != 0)
{
return page.ReadUInt16(offset + 8 + (i - 1) * 2);
}
return 0;
}
public static ExtendedBaseParam Create(ExcelPage page, uint offset, uint row)
{
return new ExtendedBaseParam(page, offset, row);
}
}

View file

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace LLib.Gear;
public sealed class GearStatsCalculator
{
private sealed record MateriaInfo(EBaseParam BaseParam, Collection<short> Values, bool HasItem);
private const uint EternityRingItemId = 8575u;
private static readonly uint[] CanHaveOffhand = new uint[14]
{
2u, 6u, 8u, 12u, 14u, 16u, 18u, 20u, 22u, 24u,
26u, 28u, 30u, 32u
};
private readonly ExcelSheet<Item> _itemSheet;
private readonly Dictionary<(uint ItemLevel, EBaseParam BaseParam), ushort> _itemLevelStatCaps = new Dictionary<(uint, EBaseParam), ushort>();
private readonly Dictionary<(EBaseParam BaseParam, int EquipSlotCategory), ushort> _equipSlotCategoryPct;
private readonly Dictionary<uint, MateriaInfo> _materiaStats;
public GearStatsCalculator(IDataManager? dataManager)
: this(dataManager?.GetExcelSheet<ItemLevel>() ?? throw new ArgumentNullException("dataManager"), dataManager.GetExcelSheet<ExtendedBaseParam>(), dataManager.GetExcelSheet<Materia>(), dataManager.GetExcelSheet<Item>())
{
}
public GearStatsCalculator(ExcelSheet<ItemLevel> itemLevelSheet, ExcelSheet<ExtendedBaseParam> baseParamSheet, ExcelSheet<Materia> materiaSheet, ExcelSheet<Item> itemSheet)
{
ArgumentNullException.ThrowIfNull(itemLevelSheet, "itemLevelSheet");
ArgumentNullException.ThrowIfNull(baseParamSheet, "baseParamSheet");
ArgumentNullException.ThrowIfNull(materiaSheet, "materiaSheet");
ArgumentNullException.ThrowIfNull(itemSheet, "itemSheet");
_itemSheet = itemSheet;
foreach (ItemLevel item in itemLevelSheet)
{
_itemLevelStatCaps[(item.RowId, EBaseParam.Strength)] = item.Strength;
_itemLevelStatCaps[(item.RowId, EBaseParam.Dexterity)] = item.Dexterity;
_itemLevelStatCaps[(item.RowId, EBaseParam.Vitality)] = item.Vitality;
_itemLevelStatCaps[(item.RowId, EBaseParam.Intelligence)] = item.Intelligence;
_itemLevelStatCaps[(item.RowId, EBaseParam.Mind)] = item.Mind;
_itemLevelStatCaps[(item.RowId, EBaseParam.Piety)] = item.Piety;
_itemLevelStatCaps[(item.RowId, EBaseParam.GP)] = item.GP;
_itemLevelStatCaps[(item.RowId, EBaseParam.CP)] = item.CP;
_itemLevelStatCaps[(item.RowId, EBaseParam.DamagePhys)] = item.PhysicalDamage;
_itemLevelStatCaps[(item.RowId, EBaseParam.DamageMag)] = item.MagicalDamage;
_itemLevelStatCaps[(item.RowId, EBaseParam.DefensePhys)] = item.Defense;
_itemLevelStatCaps[(item.RowId, EBaseParam.DefenseMag)] = item.MagicDefense;
_itemLevelStatCaps[(item.RowId, EBaseParam.Tenacity)] = item.Tenacity;
_itemLevelStatCaps[(item.RowId, EBaseParam.Crit)] = item.CriticalHit;
_itemLevelStatCaps[(item.RowId, EBaseParam.DirectHit)] = item.DirectHitRate;
_itemLevelStatCaps[(item.RowId, EBaseParam.Determination)] = item.Determination;
_itemLevelStatCaps[(item.RowId, EBaseParam.SpellSpeed)] = item.SpellSpeed;
_itemLevelStatCaps[(item.RowId, EBaseParam.SkillSpeed)] = item.SkillSpeed;
_itemLevelStatCaps[(item.RowId, EBaseParam.Gathering)] = item.Gathering;
_itemLevelStatCaps[(item.RowId, EBaseParam.Perception)] = item.Perception;
_itemLevelStatCaps[(item.RowId, EBaseParam.Craftsmanship)] = item.Craftsmanship;
_itemLevelStatCaps[(item.RowId, EBaseParam.Control)] = item.Control;
}
_equipSlotCategoryPct = baseParamSheet.SelectMany((ExtendedBaseParam x) => from y in Enumerable.Range(0, x.EquipSlotCategoryPct.Count)
select ((EBaseParam)x.RowId, y: y, x.EquipSlotCategoryPct[y])).ToDictionary(((EBaseParam, int y, ushort) x) => (x.Item1, x.y), ((EBaseParam, int y, ushort) x) => x.Item3);
_materiaStats = materiaSheet.Where((Materia x) => x.RowId != 0 && x.BaseParam.RowId != 0).ToDictionary((Materia x) => x.RowId, (Materia x) => new MateriaInfo((EBaseParam)x.BaseParam.RowId, x.Value, x.Item[0].RowId != 0));
}
public unsafe EquipmentStats CalculateGearStats(InventoryItem* item)
{
List<(uint, byte)> list = new List<(uint, byte)>();
byte b = 0;
if (item->ItemId != 8575)
{
for (int i = 0; i < 5; i++)
{
ushort num = item->Materia[i];
if (num != 0)
{
b++;
list.Add((num, item->MateriaGrades[i]));
}
}
}
return CalculateGearStats(_itemSheet.GetRow(item->ItemId), item->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality), list)with
{
MateriaCount = b
};
}
public EquipmentStats CalculateGearStats(Item item, bool highQuality, IReadOnlyList<(uint MateriaId, byte Grade)> materias)
{
ArgumentNullException.ThrowIfNull(materias, "materias");
Dictionary<EBaseParam, StatInfo> dictionary = new Dictionary<EBaseParam, StatInfo>();
for (int i = 0; i < item.BaseParam.Count; i++)
{
AddEquipmentStat(dictionary, item.BaseParam[i], item.BaseParamValue[i]);
}
if (highQuality)
{
for (int j = 0; j < item.BaseParamSpecial.Count; j++)
{
AddEquipmentStat(dictionary, item.BaseParamSpecial[j], item.BaseParamValueSpecial[j]);
}
}
foreach (var materia in materias)
{
if (_materiaStats.TryGetValue(materia.MateriaId, out MateriaInfo value))
{
AddMateriaStat(item, dictionary, value, materia.Grade);
}
}
return new EquipmentStats(dictionary, 0);
}
private static void AddEquipmentStat(Dictionary<EBaseParam, StatInfo> result, RowRef<BaseParam> baseParam, short value)
{
if (baseParam.RowId != 0)
{
if (result.TryGetValue((EBaseParam)baseParam.RowId, out StatInfo value2))
{
result[(EBaseParam)baseParam.RowId] = value2 with
{
EquipmentValue = (short)(value2.EquipmentValue + value)
};
}
else
{
result[(EBaseParam)baseParam.RowId] = new StatInfo(value, 0, Overcapped: false);
}
}
}
private void AddMateriaStat(Item item, Dictionary<EBaseParam, StatInfo> result, MateriaInfo materiaInfo, short grade)
{
if (!result.TryGetValue(materiaInfo.BaseParam, out StatInfo value))
{
value = (result[materiaInfo.BaseParam] = new StatInfo(0, 0, Overcapped: false));
}
if (materiaInfo.HasItem)
{
short num = (short)(GetMaximumStatValue(item, materiaInfo.BaseParam) - value.EquipmentValue);
if (value.MateriaValue + materiaInfo.Values[grade] > num)
{
result[materiaInfo.BaseParam] = value with
{
MateriaValue = num,
Overcapped = true
};
}
else
{
result[materiaInfo.BaseParam] = value with
{
MateriaValue = (short)(value.MateriaValue + materiaInfo.Values[grade])
};
}
}
else
{
result[materiaInfo.BaseParam] = value with
{
MateriaValue = (short)(value.MateriaValue + materiaInfo.Values[grade])
};
}
}
public short GetMaximumStatValue(Item item, EBaseParam baseParamValue)
{
if (_itemLevelStatCaps.TryGetValue((item.LevelItem.RowId, baseParamValue), out var value))
{
return (short)Math.Round((float)(value * _equipSlotCategoryPct[(baseParamValue, (int)item.EquipSlotCategory.RowId)]) / 1000f, MidpointRounding.AwayFromZero);
}
return 0;
}
public unsafe short CalculateAverageItemLevel(InventoryContainer* container)
{
uint num = 0u;
int num2 = 12;
for (int i = 0; i < 13; i++)
{
if (i == 5)
{
continue;
}
InventoryItem* inventorySlot = container->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
continue;
}
Item? rowOrDefault = _itemSheet.GetRowOrDefault(inventorySlot->ItemId);
if (!rowOrDefault.HasValue)
{
continue;
}
if (rowOrDefault.Value.ItemUICategory.RowId == 105)
{
if (i == 0)
{
num2--;
}
num2--;
continue;
}
if (i == 0 && !CanHaveOffhand.Contains(rowOrDefault.Value.ItemUICategory.RowId))
{
num += rowOrDefault.Value.LevelItem.RowId;
i++;
}
num += rowOrDefault.Value.LevelItem.RowId;
}
return (short)(num / num2);
}
}

View file

@ -0,0 +1,6 @@
namespace LLib.Gear;
public sealed record StatInfo(short EquipmentValue, short MateriaValue, bool Overcapped)
{
public short TotalValue => (short)(EquipmentValue + MateriaValue);
}

View file

@ -0,0 +1,14 @@
namespace LLib.ImGui;
public interface IPersistableWindowConfig
{
WindowConfig? WindowConfig { get; }
void SaveWindowConfig();
}
public interface IPersistableWindowConfig<out T> : IPersistableWindowConfig where T : WindowConfig
{
new T? WindowConfig { get; }
WindowConfig? IPersistableWindowConfig.WindowConfig => WindowConfig;
}

188
LLib/LLib.ImGui/LWindow.cs Normal file
View file

@ -0,0 +1,188 @@
using System.Runtime.CompilerServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Windowing;
namespace LLib.ImGui;
public abstract class LWindow : Window
{
private bool _initializedConfig;
private bool _wasCollapsedLastFrame;
protected bool ClickedHeaderLastFrame { get; private set; }
protected bool ClickedHeaderCurrentFrame { get; private set; }
protected bool UncollapseNextFrame { get; set; }
public bool IsOpenAndUncollapsed
{
get
{
if (base.IsOpen)
{
return !_wasCollapsedLastFrame;
}
return false;
}
set
{
base.IsOpen = value;
UncollapseNextFrame = value;
}
}
protected bool IsPinned
{
get
{
return InternalIsPinned(this);
}
set
{
InternalIsPinned(this) = value;
}
}
protected bool IsClickthrough
{
get
{
return InternalIsClickthrough(this);
}
set
{
InternalIsClickthrough(this) = value;
}
}
protected int? Alpha
{
get
{
return (int?)(100000f * InternalAlpha(this));
}
set
{
InternalAlpha(this) = (float?)value / 100000f;
}
}
protected LWindow(string windowName, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false)
: base(windowName, flags, forceMainWindow)
{
}
private void LoadWindowConfig()
{
if (!(this is IPersistableWindowConfig persistableWindowConfig))
{
return;
}
WindowConfig windowConfig = persistableWindowConfig.WindowConfig;
if (windowConfig != null)
{
if (base.AllowPinning)
{
IsPinned = windowConfig.IsPinned;
}
if (base.AllowClickthrough)
{
IsClickthrough = windowConfig.IsClickthrough;
}
Alpha = windowConfig.Alpha;
}
_initializedConfig = true;
}
private void UpdateWindowConfig()
{
if (!(this is IPersistableWindowConfig persistableWindowConfig) || Dalamud.Bindings.ImGui.ImGui.IsAnyMouseDown())
{
return;
}
WindowConfig windowConfig = persistableWindowConfig.WindowConfig;
if (windowConfig != null)
{
bool flag = false;
if (base.AllowPinning && windowConfig.IsPinned != IsPinned)
{
windowConfig.IsPinned = IsPinned;
flag = true;
}
if (base.AllowClickthrough && windowConfig.IsClickthrough != IsClickthrough)
{
windowConfig.IsClickthrough = IsClickthrough;
flag = true;
}
if (windowConfig.Alpha != Alpha)
{
windowConfig.Alpha = Alpha;
flag = true;
}
if (flag)
{
persistableWindowConfig.SaveWindowConfig();
}
}
}
public void ToggleOrUncollapse()
{
IsOpenAndUncollapsed = !IsOpenAndUncollapsed;
}
public override void OnOpen()
{
UncollapseNextFrame = true;
base.OnOpen();
}
public override void Update()
{
_wasCollapsedLastFrame = true;
}
public override void PreDraw()
{
if (!_initializedConfig)
{
LoadWindowConfig();
}
if (UncollapseNextFrame)
{
Dalamud.Bindings.ImGui.ImGui.SetNextWindowCollapsed(collapsed: false);
UncollapseNextFrame = false;
}
base.PreDraw();
ClickedHeaderLastFrame = ClickedHeaderCurrentFrame;
ClickedHeaderCurrentFrame = false;
}
public sealed override void Draw()
{
_wasCollapsedLastFrame = false;
DrawContent();
}
public abstract void DrawContent();
public override void PostDraw()
{
base.PostDraw();
if (_initializedConfig)
{
UpdateWindowConfig();
}
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "internalIsPinned")]
private static extern ref bool InternalIsPinned(Window @this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "internalIsClickthrough")]
private static extern ref bool InternalIsClickthrough(Window @this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "internalAlpha")]
private static extern ref float? InternalAlpha(Window @this);
}

View file

@ -0,0 +1,10 @@
namespace LLib.ImGui;
public class WindowConfig
{
public bool IsPinned { get; set; }
public bool IsClickthrough { get; set; }
public int? Alpha { get; set; }
}

View file

@ -0,0 +1,14 @@
namespace LLib.Shop.Model;
public sealed class ItemForSale
{
public required int Position { get; init; }
public required uint ItemId { get; init; }
public required string? ItemName { get; init; }
public required uint Price { get; init; }
public required uint OwnedItems { get; init; }
}

View file

@ -0,0 +1,24 @@
using System;
namespace LLib.Shop.Model;
public sealed class PurchaseState
{
public int DesiredItems { get; }
public int OwnedItems { get; set; }
public int ItemsLeftToBuy => Math.Max(0, DesiredItems - OwnedItems);
public bool IsComplete => ItemsLeftToBuy == 0;
public bool IsAwaitingYesNo { get; set; }
public DateTime NextStep { get; set; } = DateTime.MinValue;
public PurchaseState(int desiredItems, int ownedItems)
{
DesiredItems = desiredItems;
OwnedItems = ownedItems;
}
}

View file

@ -0,0 +1,23 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace LLib.Shop;
public interface IShopWindow
{
bool IsEnabled { get; }
bool IsOpen { get; set; }
Vector2? Position { get; set; }
int GetCurrencyCount();
unsafe void UpdateShopStock(AtkUnitBase* addon);
unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow);
void SaveExternalPluginState();
void RestoreExternalPluginState();
}

View file

@ -0,0 +1,275 @@
using System;
using System.Numerics;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using LLib.Shop.Model;
namespace LLib.Shop;
public class RegularShopBase
{
private readonly IShopWindow _parentWindow;
private readonly string _addonName;
private readonly IPluginLog _pluginLog;
private readonly IGameGui _gameGui;
private readonly IAddonLifecycle _addonLifecycle;
public ItemForSale? ItemForSale { get; set; }
public PurchaseState? PurchaseState { get; private set; }
public bool AutoBuyEnabled => PurchaseState != null;
public bool IsAwaitingYesNo
{
get
{
return PurchaseState?.IsAwaitingYesNo ?? false;
}
set
{
PurchaseState.IsAwaitingYesNo = value;
}
}
public RegularShopBase(IShopWindow parentWindow, string addonName, IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle)
{
_parentWindow = parentWindow;
_addonName = addonName;
_pluginLog = pluginLog;
_gameGui = gameGui;
_addonLifecycle = addonLifecycle;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
}
private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args)
{
if (!_parentWindow.IsEnabled)
{
ItemForSale = null;
_parentWindow.IsOpen = false;
return;
}
_parentWindow.UpdateShopStock((AtkUnitBase*)args.Addon.Address);
PostUpdateShopStock();
if (ItemForSale != null)
{
_parentWindow.IsOpen = true;
}
}
private void ShopPreFinalize(AddonEvent type, AddonArgs args)
{
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
_parentWindow.IsOpen = false;
}
private unsafe void ShopPostUpdate(AddonEvent type, AddonArgs args)
{
if (!_parentWindow.IsEnabled)
{
ItemForSale = null;
_parentWindow.IsOpen = false;
return;
}
_parentWindow.UpdateShopStock((AtkUnitBase*)args.Addon.Address);
PostUpdateShopStock();
if (ItemForSale != null)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
short num = 0;
short num2 = 0;
address->GetPosition(&num, &num2);
short num3 = 0;
short num4 = 0;
address->GetSize(&num3, &num4, scaled: true);
num += num3;
Vector2? position = _parentWindow.Position;
if (position.HasValue)
{
Vector2 valueOrDefault = position.GetValueOrDefault();
if ((short)valueOrDefault.X != num || (short)valueOrDefault.Y != num2)
{
_parentWindow.Position = new Vector2(num, num2);
}
}
_parentWindow.IsOpen = true;
}
else
{
_parentWindow.IsOpen = false;
}
}
private void PostUpdateShopStock()
{
if (ItemForSale != null && PurchaseState != null)
{
int ownedItems = (int)ItemForSale.OwnedItems;
if (PurchaseState.OwnedItems != ownedItems)
{
PurchaseState.OwnedItems = ownedItems;
PurchaseState.NextStep = DateTime.Now.AddSeconds(0.25);
}
}
}
public unsafe int GetItemCount(uint itemId)
{
return InventoryManager.Instance()->GetInventoryItemCount(itemId, isHq: false, checkEquipped: false, checkArmory: false, 0);
}
public int GetMaxItemsToPurchase()
{
if (ItemForSale == null)
{
return 0;
}
return (int)(_parentWindow.GetCurrencyCount() / ItemForSale.Price);
}
public void CancelAutoPurchase()
{
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
}
public void StartAutoPurchase(int toPurchase)
{
PurchaseState = new PurchaseState((int)ItemForSale.OwnedItems + toPurchase, (int)ItemForSale.OwnedItems);
_parentWindow.SaveExternalPluginState();
}
public unsafe void HandleNextPurchaseStep()
{
if (ItemForSale == null || PurchaseState == null)
{
return;
}
int num = DetermineMaxStackSize(ItemForSale.ItemId);
if (num == 0 && !HasFreeInventorySlot())
{
_pluginLog.Warning("No free inventory slots, can't buy more " + ItemForSale.ItemName);
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
}
else if (!PurchaseState.IsComplete)
{
if (PurchaseState.NextStep <= DateTime.Now && _gameGui.TryGetAddonByName<AtkUnitBase>(_addonName, out var addonPtr))
{
int num2 = Math.Min(PurchaseState.ItemsLeftToBuy, num);
_pluginLog.Information($"Buying {num2}x {ItemForSale.ItemName}");
_parentWindow.TriggerPurchase(addonPtr, num2);
PurchaseState.NextStep = DateTime.MaxValue;
PurchaseState.IsAwaitingYesNo = true;
}
}
else
{
_pluginLog.Information($"Stopping item purchase (desired = {PurchaseState.DesiredItems}, owned = {PurchaseState.OwnedItems})");
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
}
public bool HasFreeInventorySlot()
{
return CountFreeInventorySlots() > 0;
}
public unsafe int CountFreeInventorySlots()
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
{
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
for (int i = 0; i < inventoryContainer->Size; i++)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
num++;
}
}
}
return num;
}
private unsafe int DetermineMaxStackSize(uint itemId)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
{
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
for (int i = 0; i < inventoryContainer->Size; i++)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
return 99;
}
if (inventorySlot->ItemId == itemId)
{
num += 999 - inventorySlot->Quantity;
if (num >= 99)
{
break;
}
}
}
}
return Math.Min(99, num);
}
public unsafe int CountInventorySlotsWithCondition(uint itemId, Predicate<int> predicate)
{
ArgumentNullException.ThrowIfNull(predicate, "predicate");
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
{
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
for (int i = 0; i < inventoryContainer->Size; i++)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot != null && inventorySlot->ItemId != 0 && inventorySlot->ItemId == itemId && predicate(inventorySlot->Quantity))
{
num++;
}
}
}
return num;
}
}

35
LLib/LLib.csproj Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>LLib</AssemblyName>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<TargetFramework>netcoreapp9.0</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup />
<ItemGroup />
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
</Reference>
<Reference Include="Lumina">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
</Reference>
<Reference Include="Dalamud.Bindings.ImGui">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Dalamud.Bindings.ImGui.dll</HintPath>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
</Reference>
<Reference Include="InteropGenerator.Runtime">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\InteropGenerator.Runtime.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace LLib;
public sealed class DalamudReflector : IDisposable
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IFramework _framework;
private readonly IPluginLog _pluginLog;
private readonly Dictionary<string, IDalamudPlugin> _pluginCache = new Dictionary<string, IDalamudPlugin>();
private bool _pluginsChanged;
public DalamudReflector(IDalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_framework = framework;
_pluginLog = pluginLog;
object pluginManager = GetPluginManager();
pluginManager.GetType().GetEvent("OnInstalledPluginsChanged").AddEventHandler(pluginManager, new Action(OnInstalledPluginsChanged));
_framework.Update += FrameworkUpdate;
}
public void Dispose()
{
_framework.Update -= FrameworkUpdate;
object pluginManager = GetPluginManager();
pluginManager.GetType().GetEvent("OnInstalledPluginsChanged").RemoveEventHandler(pluginManager, new Action(OnInstalledPluginsChanged));
}
private void FrameworkUpdate(IFramework framework)
{
if (_pluginsChanged)
{
_pluginsChanged = false;
_pluginCache.Clear();
}
}
private object GetPluginManager()
{
return _pluginInterface.GetType().Assembly.GetType("Dalamud.Service`1", throwOnError: true).MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", throwOnError: true)).GetMethod("Get")
.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null);
}
public bool TryGetDalamudPlugin(string internalName, [MaybeNullWhen(false)] out IDalamudPlugin instance, bool suppressErrors = false, bool ignoreCache = false)
{
if (!ignoreCache && _pluginCache.TryGetValue(internalName, out instance))
{
return true;
}
try
{
object pluginManager = GetPluginManager();
foreach (object item in (IList)pluginManager.GetType().GetProperty("InstalledPlugins").GetValue(pluginManager))
{
if ((string)item.GetType().GetProperty("Name").GetValue(item) == internalName)
{
IDalamudPlugin dalamudPlugin = (IDalamudPlugin)((item.GetType().Name == "LocalDevPlugin") ? item.GetType().BaseType : item.GetType()).GetField("instance", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(item);
if (dalamudPlugin != null)
{
instance = dalamudPlugin;
_pluginCache[internalName] = dalamudPlugin;
return true;
}
if (!suppressErrors)
{
_pluginLog.Warning("[DalamudReflector] Found requested plugin " + internalName + " but it was null");
}
}
}
instance = null;
return false;
}
catch (Exception ex)
{
if (!suppressErrors)
{
_pluginLog.Error(ex, "Can't find " + internalName + " plugin: " + ex.Message);
}
instance = null;
return false;
}
}
private void OnInstalledPluginsChanged()
{
_pluginLog.Verbose("Installed plugins changed event fired");
_pluginsChanged = true;
}
}

View file

@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
namespace LLib;
public static class DataManagerExtensions
{
public static ReadOnlySeString? GetSeString<T>(this IDataManager dataManager, string key) where T : struct, IQuestDialogueText, IExcelRow<T>
{
ArgumentNullException.ThrowIfNull(dataManager, "dataManager");
return dataManager.GetExcelSheet<T>().Cast<T?>().SingleOrDefault((T? x) => x.Value.Key == key)?.Value;
}
public static string? GetString<T>(this IDataManager dataManager, string key, IPluginLog? pluginLog) where T : struct, IQuestDialogueText, IExcelRow<T>
{
string text = dataManager.GetSeString<T>(key)?.WithCertainMacroCodeReplacements();
pluginLog?.Verbose($"{typeof(T).Name}.{key} => {text}");
return text;
}
public static Regex? GetRegex<T>(this IDataManager dataManager, string key, IPluginLog? pluginLog) where T : struct, IQuestDialogueText, IExcelRow<T>
{
ReadOnlySeString? seString = dataManager.GetSeString<T>(key);
if (!seString.HasValue)
{
return null;
}
string text = string.Join("", seString.Select((ReadOnlySePayload payload) => (payload.Type == ReadOnlySePayloadType.Text) ? Regex.Escape(payload.ToString()) : "(.*)"));
pluginLog?.Verbose($"{typeof(T).Name}.{key} => /{text}/");
return new Regex(text);
}
public static ReadOnlySeString? GetSeString<T>(this IDataManager dataManager, uint rowId, Func<T, ReadOnlySeString?> mapper) where T : struct, IExcelRow<T>
{
ArgumentNullException.ThrowIfNull(dataManager, "dataManager");
ArgumentNullException.ThrowIfNull(mapper, "mapper");
T? rowOrDefault = dataManager.GetExcelSheet<T>().GetRowOrDefault(rowId);
if (!rowOrDefault.HasValue)
{
return null;
}
return mapper(rowOrDefault.Value);
}
public static string? GetString<T>(this IDataManager dataManager, uint rowId, Func<T, ReadOnlySeString?> mapper, IPluginLog? pluginLog = null) where T : struct, IExcelRow<T>
{
string text = dataManager.GetSeString(rowId, mapper)?.WithCertainMacroCodeReplacements();
pluginLog?.Verbose($"{typeof(T).Name}.{rowId} => {text}");
return text;
}
public static Regex? GetRegex<T>(this IDataManager dataManager, uint rowId, Func<T, ReadOnlySeString?> mapper, IPluginLog? pluginLog = null) where T : struct, IExcelRow<T>
{
ReadOnlySeString? seString = dataManager.GetSeString(rowId, mapper);
if (!seString.HasValue)
{
return null;
}
Regex regex = seString.ToRegex();
pluginLog?.Verbose($"{typeof(T).Name}.{rowId} => /{regex}/");
return regex;
}
public static Regex? GetRegex<T>(this T excelRow, Func<T, ReadOnlySeString?> mapper, IPluginLog? pluginLog) where T : struct, IExcelRow<T>
{
ArgumentNullException.ThrowIfNull(mapper, "mapper");
ReadOnlySeString? text = mapper(excelRow);
if (!text.HasValue)
{
return null;
}
Regex regex = text.ToRegex();
pluginLog?.Verbose($"{typeof(T).Name}.regex => /{regex}/");
return regex;
}
public static Regex ToRegex(this ReadOnlySeString? text)
{
ArgumentNullException.ThrowIfNull(text, "text");
return new Regex(string.Join("", text.Value.Select((ReadOnlySePayload payload) => (payload.Type == ReadOnlySePayloadType.Text) ? Regex.Escape(payload.ToString()) : "(.*)")));
}
public static string WithCertainMacroCodeReplacements(this ReadOnlySeString text)
{
return string.Join("", text.Select((ReadOnlySePayload payload) => payload.Type switch
{
ReadOnlySePayloadType.Text => payload.ToString(),
ReadOnlySePayloadType.Macro => payload.MacroCode switch
{
MacroCode.NewLine => "",
MacroCode.NonBreakingSpace => " ",
MacroCode.Hyphen => "-",
MacroCode.SoftHyphen => "",
_ => payload.ToString(),
},
_ => payload.ToString(),
}));
}
}

View file

@ -0,0 +1,10 @@
using Lumina.Text.ReadOnly;
namespace LLib;
public interface IQuestDialogueText
{
ReadOnlySeString Key { get; }
ReadOnlySeString Value { get; }
}

View file

@ -0,0 +1,26 @@
using Lumina.Excel;
using Lumina.Text.ReadOnly;
namespace LLib;
[Sheet("PleaseSpecifyTheSheetExplicitly")]
public readonly struct QuestDialogueText : IQuestDialogueText, IExcelRow<QuestDialogueText>
{
public uint RowId => _003Crow_003EP;
public ReadOnlySeString Key => _003Cpage_003EP.ReadString(_003Coffset_003EP, _003Coffset_003EP);
public ReadOnlySeString Value => _003Cpage_003EP.ReadString(_003Coffset_003EP + 4, _003Coffset_003EP);
public QuestDialogueText(ExcelPage page, uint offset, uint row)
{
_003Cpage_003EP = page;
_003Coffset_003EP = offset;
_003Crow_003EP = row;
}
static QuestDialogueText IExcelRow<QuestDialogueText>.Create(ExcelPage page, uint offset, uint row)
{
return new QuestDialogueText(page, offset, row);
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>QuestPaths</AssemblyName>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<TargetFramework>netcoreapp9.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<None Remove="Questionable.QuestPaths.QuestSchema" />
<EmbeddedResource Include="Questionable.QuestPaths.QuestSchema" LogicalName="Questionable.QuestPaths.QuestSchema" />
</ItemGroup>
<ItemGroup>
<Reference Include="Questionable.Model">
<HintPath>..\..\Questionable.Model.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,448 @@
using System.Collections.Generic;
namespace Questionable.Model.Common.Converter;
public sealed class AetheryteConverter : EnumConverter<EAetheryteLocation>
{
private static readonly Dictionary<EAetheryteLocation, string> Values = new Dictionary<EAetheryteLocation, string>
{
{
EAetheryteLocation.Gridania,
"Gridania"
},
{
EAetheryteLocation.CentralShroudBentbranchMeadows,
"Central Shroud - Bentbranch Meadows"
},
{
EAetheryteLocation.EastShroudHawthorneHut,
"East Shroud - Hawthorne Hut"
},
{
EAetheryteLocation.SouthShroudQuarrymill,
"South Shroud - Quarrymill"
},
{
EAetheryteLocation.SouthShroudCampTranquil,
"South Shroud - Camp Tranquil"
},
{
EAetheryteLocation.NorthShroudFallgourdFloat,
"North Shroud - Fallgourd Float"
},
{
EAetheryteLocation.Uldah,
"Ul'dah"
},
{
EAetheryteLocation.WesternThanalanHorizon,
"Western Thanalan - Horizon"
},
{
EAetheryteLocation.CentralThanalanBlackBrushStation,
"Central Thanalan - Black Brush Station"
},
{
EAetheryteLocation.EasternThanalanCampDrybone,
"Eastern Thanalan - Camp Drybone"
},
{
EAetheryteLocation.SouthernThanalanLittleAlaMhigo,
"Southern Thanalan - Little Ala Mhigo"
},
{
EAetheryteLocation.SouthernThanalanForgottenSprings,
"Southern Thanalan - Forgotten Springs"
},
{
EAetheryteLocation.NorthernThanalanCampBluefog,
"Northern Thanalan - Camp Bluefog"
},
{
EAetheryteLocation.NorthernThanalanCeruleumProcessingPlant,
"Northern Thanalan - Ceruleum Processing Plant"
},
{
EAetheryteLocation.Limsa,
"Limsa Lominsa"
},
{
EAetheryteLocation.MiddleLaNosceaSummerfordFarms,
"Middle La Noscea - Summerford Farms"
},
{
EAetheryteLocation.LowerLaNosceaMorabyDrydocks,
"Lower La Noscea - Moraby Drydocks"
},
{
EAetheryteLocation.EasternLaNosceaCostaDelSol,
"Eastern La Noscea - Costa Del Sol"
},
{
EAetheryteLocation.EasternLaNosceaWineport,
"Eastern La Noscea - Wineport"
},
{
EAetheryteLocation.WesternLaNosceaSwiftperch,
"Western La Noscea - Swiftperch"
},
{
EAetheryteLocation.WesternLaNosceaAleport,
"Western La Noscea - Aleport"
},
{
EAetheryteLocation.UpperLaNosceaCampBronzeLake,
"Upper La Noscea - Camp Bronze Lake"
},
{
EAetheryteLocation.OuterLaNosceaCampOverlook,
"Outer La Noscea - Camp Overlook"
},
{
EAetheryteLocation.CoerthasCentralHighlandsCampDragonhead,
"Coerthas Central Highlands - Camp Dragonhead"
},
{
EAetheryteLocation.MorDhona,
"Mor Dhona"
},
{
EAetheryteLocation.WolvesDenPier,
"Wolves' Den Pier"
},
{
EAetheryteLocation.GoldSaucer,
"Gold Saucer"
},
{
EAetheryteLocation.Ishgard,
"Ishgard"
},
{
EAetheryteLocation.Idyllshire,
"Idyllshire"
},
{
EAetheryteLocation.CoerthasWesternHighlandsFalconsNest,
"Coerthas Western Highlands - Falcon's Nest"
},
{
EAetheryteLocation.SeaOfCloudsCampCloudtop,
"The Sea of Clouds - Camp Cloudtop"
},
{
EAetheryteLocation.SeaOfCloudsOkZundu,
"The Sea of Clouds - Ok' Zundu"
},
{
EAetheryteLocation.AzysLlaHelix,
"Azys Lla - Helix"
},
{
EAetheryteLocation.DravanianForelandsTailfeather,
"The Dravanian Forelands - Tailfeather"
},
{
EAetheryteLocation.DravanianForelandsAnyxTrine,
"The Dravanian Forelands - Anyx Trine"
},
{
EAetheryteLocation.ChurningMistsMoghome,
"The Churning Mists - Moghome"
},
{
EAetheryteLocation.ChurningMistsZenith,
"The Churning Mists - Zenith"
},
{
EAetheryteLocation.RhalgrsReach,
"Rhalgr's Reach"
},
{
EAetheryteLocation.FringesCastrumOriens,
"Fringes - Castrum Oriens"
},
{
EAetheryteLocation.FringesPeeringStones,
"Fringes - Peering Stones"
},
{
EAetheryteLocation.PeaksAlaGannha,
"Peaks - Ala Gannha"
},
{
EAetheryteLocation.PeaksAlaGhiri,
"Peaks - Ala Ghiri"
},
{
EAetheryteLocation.LochsPortaPraetoria,
"Lochs - Porta Praetoria"
},
{
EAetheryteLocation.LochsAlaMhiganQuarter,
"Lochs - Ala Mhigan Quarter"
},
{
EAetheryteLocation.Kugane,
"Kugane"
},
{
EAetheryteLocation.RubySeaTamamizu,
"Ruby Sea - Tamamizu"
},
{
EAetheryteLocation.RubySeaOnokoro,
"Ruby Sea - Onokoro"
},
{
EAetheryteLocation.YanxiaNamai,
"Yanxia - Namai"
},
{
EAetheryteLocation.YanxiaHouseOfTheFierce,
"Yanxia - House of the Fierce"
},
{
EAetheryteLocation.AzimSteppeReunion,
"Azim Steppe - Reunion"
},
{
EAetheryteLocation.AzimSteppeDawnThrone,
"Azim Steppe - Dawn Throne"
},
{
EAetheryteLocation.AzimSteppeDhoroIloh,
"Azim Steppe - Dhoro Iloh"
},
{
EAetheryteLocation.DomanEnclave,
"Doman Enclave"
},
{
EAetheryteLocation.Crystarium,
"Crystarium"
},
{
EAetheryteLocation.Eulmore,
"Eulmore"
},
{
EAetheryteLocation.LakelandFortJobb,
"Lakeland - Fort Jobb"
},
{
EAetheryteLocation.LakelandOstallImperative,
"Lakeland - Ostall Imperative"
},
{
EAetheryteLocation.KholusiaStilltide,
"Kholusia - Stilltide"
},
{
EAetheryteLocation.KholusiaWright,
"Kholusia - Wright"
},
{
EAetheryteLocation.KholusiaTomra,
"Kholusia - Tomra"
},
{
EAetheryteLocation.AmhAraengMordSouq,
"Amh Araeng - Mord Souq"
},
{
EAetheryteLocation.AmhAraengInnAtJourneysHead,
"Amh Araeng - Inn at Journey's Head"
},
{
EAetheryteLocation.AmhAraengTwine,
"Amh Araeng - Twine"
},
{
EAetheryteLocation.RaktikaSlitherbough,
"Rak'tika - Slitherbough"
},
{
EAetheryteLocation.RaktikaFanow,
"Rak'tika - Fanow"
},
{
EAetheryteLocation.IlMhegLydhaLran,
"Il Mheg - Lydha Lran"
},
{
EAetheryteLocation.IlMhegPlaEnni,
"Il Mheg - Pla Enni"
},
{
EAetheryteLocation.IlMhegWolekdorf,
"Il Mheg - Wolekdorf"
},
{
EAetheryteLocation.TempestOndoCups,
"Tempest - Ondo Cups"
},
{
EAetheryteLocation.TempestMacarensesAngle,
"Tempest - Macarenses Angle"
},
{
EAetheryteLocation.OldSharlayan,
"Old Sharlayan"
},
{
EAetheryteLocation.RadzAtHan,
"Radz-at-Han"
},
{
EAetheryteLocation.LabyrinthosArcheion,
"Labyrinthos - Archeion"
},
{
EAetheryteLocation.LabyrinthosSharlayanHamlet,
"Labyrinthos - Sharlayan Hamlet"
},
{
EAetheryteLocation.LabyrinthosAporia,
"Labyrinthos - Aporia"
},
{
EAetheryteLocation.ThavnairYedlihmad,
"Thavnair - Yedlihmad"
},
{
EAetheryteLocation.ThavnairGreatWork,
"Thavnair - Great Work"
},
{
EAetheryteLocation.ThavnairPalakasStand,
"Thavnair - Palaka's Stand"
},
{
EAetheryteLocation.GarlemaldCampBrokenGlass,
"Garlemald - Camp Broken Glass"
},
{
EAetheryteLocation.GarlemaldTertium,
"Garlemald - Tertium"
},
{
EAetheryteLocation.MareLamentorumSinusLacrimarum,
"Mare Lamentorum - Sinus Lacrimarum"
},
{
EAetheryteLocation.MareLamentorumBestwaysBurrow,
"Mare Lamentorum - Bestways Burrow"
},
{
EAetheryteLocation.ElpisAnagnorisis,
"Elpis - Anagnorisis"
},
{
EAetheryteLocation.ElpisTwelveWonders,
"Elpis - Twelve Wonders"
},
{
EAetheryteLocation.ElpisPoietenOikos,
"Elpis - Poieten Oikos"
},
{
EAetheryteLocation.UltimaThuleReahTahra,
"Ultima Thule - Reah Tahra"
},
{
EAetheryteLocation.UltimaThuleAbodeOfTheEa,
"Ultima Thule - Abode of the Ea"
},
{
EAetheryteLocation.UltimaThuleBaseOmicron,
"Ultima Thule - Base Omicron"
},
{
EAetheryteLocation.Tuliyollal,
"Tuliyollal"
},
{
EAetheryteLocation.SolutionNine,
"Solution Nine"
},
{
EAetheryteLocation.UrqopachaWachunpelo,
"Urqopacha - Wachunpelo"
},
{
EAetheryteLocation.UrqopachaWorlarsEcho,
"Urqopacha - Worlar's Echo"
},
{
EAetheryteLocation.KozamaukaOkHanu,
"Kozama'uka - Ok'hanu"
},
{
EAetheryteLocation.KozamaukaManyFires,
"Kozama'uka - Many Fires"
},
{
EAetheryteLocation.KozamaukaEarthenshire,
"Kozama'uka - Earthenshire"
},
{
EAetheryteLocation.KozamaukaDockPoga,
"Kozama'uka - Dock Poga"
},
{
EAetheryteLocation.YakTelIqBraax,
"Yak T'el - Iq Br'aax"
},
{
EAetheryteLocation.YakTelMamook,
"Yak T'el - Mamook"
},
{
EAetheryteLocation.ShaaloaniHhusatahwi,
"Shaaloani - Hhusatahwi"
},
{
EAetheryteLocation.ShaaloaniShesheneweziSprings,
"Shaaloani - Sheshenewezi Springs"
},
{
EAetheryteLocation.ShaaloaniMehwahhetsoan,
"Shaaloani - Mehwahhetsoan"
},
{
EAetheryteLocation.HeritageFoundYyasulaniStation,
"Heritage Found - Yyasulani Station"
},
{
EAetheryteLocation.HeritageFoundTheOutskirts,
"Heritage Found - The Outskirts"
},
{
EAetheryteLocation.HeritageFoundElectropeStrike,
"Heritage Found - Electrope Strike"
},
{
EAetheryteLocation.LivingMemoryLeynodeMnemo,
"Living Memory - Leynode Mnemo"
},
{
EAetheryteLocation.LivingMemoryLeynodePyro,
"Living Memory - Leynode Pyro"
},
{
EAetheryteLocation.LivingMemoryLeynodeAero,
"Living Memory - Leynode Aero"
}
};
public AetheryteConverter()
: base((IReadOnlyDictionary<EAetheryteLocation, string>)Values)
{
}
public static bool IsLargeAetheryte(EAetheryteLocation aetheryte)
{
return Values.ContainsKey(aetheryte);
}
}

View file

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Common.Converter;
public abstract class EnumConverter<T> : JsonConverter<T> where T : Enum
{
private readonly ReadOnlyDictionary<T, string> _enumToString;
private readonly ReadOnlyDictionary<string, T> _stringToEnum;
protected EnumConverter(IReadOnlyDictionary<T, string> values)
{
_enumToString = ((values is IDictionary<T, string> dictionary) ? new ReadOnlyDictionary<T, string>(dictionary) : new ReadOnlyDictionary<T, string>(values.ToDictionary<KeyValuePair<T, string>, T, string>((KeyValuePair<T, string> x) => x.Key, (KeyValuePair<T, string> x) => x.Value)));
_stringToEnum = new ReadOnlyDictionary<string, T>(_enumToString.ToDictionary<KeyValuePair<T, string>, string, T>((KeyValuePair<T, string> x) => x.Value, (KeyValuePair<T, string> x) => x.Key));
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
string text = reader.GetString();
if (text == null)
{
throw new JsonException();
}
if (!_stringToEnum.TryGetValue(text, out var value))
{
throw new JsonException();
}
return value;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(_enumToString[value]);
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Common.Converter;
public sealed class StringListOrValueConverter : JsonConverter<List<string>>
{
public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
return new List<string>(1) { reader.GetString() };
}
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
reader.Read();
List<string> list = new List<string>();
while (reader.TokenType != JsonTokenType.EndArray)
{
list.Add(reader.GetString());
reader.Read();
}
return list;
}
public override void Write(Utf8JsonWriter writer, List<string>? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (value.Count == 1)
{
writer.WriteStringValue(value[0]);
return;
}
writer.WriteStartArray();
foreach (string item in value)
{
writer.WriteStringValue(item);
}
writer.WriteEndArray();
}
}

View file

@ -0,0 +1,61 @@
using System;
using System.Numerics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Common.Converter;
public sealed class VectorConverter : JsonConverter<Vector3>
{
public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
Vector3 result = default(Vector3);
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
{
string text = reader.GetString();
if (text == null || !reader.Read())
{
throw new JsonException();
}
switch (text)
{
case "X":
result.X = reader.GetSingle();
break;
case "Y":
result.Y = reader.GetSingle();
break;
case "Z":
result.Z = reader.GetSingle();
break;
default:
throw new JsonException();
}
break;
}
case JsonTokenType.EndObject:
return result;
default:
throw new JsonException();
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, Vector3 value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber("X", value.X);
writer.WriteNumber("Y", value.Y);
writer.WriteNumber("Z", value.Z);
writer.WriteEndObject();
}
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Common;
[JsonConverter(typeof(AethernetShortcutConverter))]
public sealed class AethernetShortcut
{
public EAetheryteLocation From { get; set; }
public EAetheryteLocation To { get; set; }
}

View file

@ -0,0 +1,246 @@
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Common;
[JsonConverter(typeof(AetheryteConverter))]
public enum EAetheryteLocation
{
None = 0,
Gridania = 2,
GridaniaArcher = 25,
GridaniaLeatherworker = 26,
GridaniaLancer = 27,
GridaniaConjurer = 28,
GridaniaBotanist = 29,
GridaniaAmphitheatre = 30,
GridaniaBlueBadgerGate = 31,
GridaniaYellowSerpentGate = 32,
GridaniaWhiteWolfGate = 54,
GridaniaAirship = 94,
CentralShroudBentbranchMeadows = 3,
EastShroudHawthorneHut = 4,
SouthShroudQuarrymill = 5,
SouthShroudCampTranquil = 6,
NorthShroudFallgourdFloat = 7,
Uldah = 9,
UldahAdventurers = 33,
UldahThaumaturge = 34,
UldahGladiator = 35,
UldahMiner = 36,
UldahAlchemist = 37,
UldahWeaver = 47,
UldahGoldsmith = 50,
UldahChamberOfRule = 51,
UldahAirship = 95,
UldahGateOfTheSultana = 38,
UldahGateOfNald = 39,
UldahGateOfThal = 40,
UldahSapphireAvenue = 125,
WesternThanalanHorizon = 17,
EasternThanalanCampDrybone = 18,
SouthernThanalanLittleAlaMhigo = 19,
SouthernThanalanForgottenSprings = 20,
NorthernThanalanCampBluefog = 21,
NorthernThanalanCeruleumProcessingPlant = 22,
CentralThanalanBlackBrushStation = 53,
Limsa = 8,
LimsaAftcastle = 41,
LimsaCulinarian = 42,
LimsaArcanist = 43,
LimsaFisher = 44,
LimsaMarauder = 48,
LimsaHawkersAlley = 49,
LimsaZephyrGate = 45,
LimsaTempestGate = 46,
LimsaAirship = 93,
LowerLaNosceaMorabyDrydocks = 10,
EasternLaNosceaCostaDelSol = 11,
EasternLaNosceaWineport = 12,
WesternLaNosceaSwiftperch = 13,
WesternLaNosceaAleport = 14,
UpperLaNosceaCampBronzeLake = 15,
OuterLaNosceaCampOverlook = 16,
MiddleLaNosceaSummerfordFarms = 52,
CoerthasCentralHighlandsCampDragonhead = 23,
MorDhona = 24,
WolvesDenPier = 55,
GoldSaucer = 62,
GoldSaucerEntranceCardSquares = 63,
GoldSaucerWonderSquareEast = 64,
GoldSaucerWonderSquareWest = 65,
GoldSaucerEventSquare = 66,
GoldSaucerCactpotBoard = 67,
GoldSaucerRoundSquare = 68,
GoldSaucerChocoboSquare = 69,
GoldSaucerMinionSquare = 89,
Ishgard = 70,
IshgardForgottenKnight = 80,
IshgardSkysteelManufactory = 81,
IshgardBrume = 82,
IshgardAthenaeumAstrologicum = 83,
IshgardJeweledCrozier = 84,
IshgardSaintReymanaudsCathedral = 85,
IshgardTribunal = 86,
IshgardLastVigil = 87,
IshgardGatesOfJudgement = 88,
IshgardFirmament = 100001,
FirmamentMendicantsCourt = 2011373,
FirmamentMattock = 2011374,
FirmamentNewNest = 2011384,
FirmanentSaintRoellesDais = 2011385,
FirmamentFeatherfall = 2011386,
FirmamentHoarfrostHall = 2011387,
FirmamentWesternRisensongQuarter = 2011388,
FIrmamentEasternRisensongQuarter = 2011389,
Idyllshire = 75,
IdyllshireWest = 90,
IdyllshirePrologueGate = 91,
IdyllshireEpilogueGate = 92,
CoerthasWesternHighlandsFalconsNest = 71,
SeaOfCloudsCampCloudtop = 72,
SeaOfCloudsOkZundu = 73,
AzysLlaHelix = 74,
DravanianForelandsTailfeather = 76,
DravanianForelandsAnyxTrine = 77,
ChurningMistsMoghome = 78,
ChurningMistsZenith = 79,
RhalgrsReach = 104,
RhalgrsReachWest = 121,
RhalgrsReachNorthEast = 122,
RhalgrsReachFringesGate = 123,
RhalgrsReachPeaksGate = 124,
Kugane = 111,
KuganeShiokazeHostelry = 112,
KuganePier1 = 113,
KuganeThavnairianConsulate = 114,
KuganeMarkets = 115,
KuganeBokairoInn = 116,
KuganeRubyBazaar = 117,
KuganeSekiseigumiBarracks = 118,
KuganeRakuzaDistrict = 119,
KuganeRubyPrice = 120,
KuganeAirship = 126,
FringesCastrumOriens = 98,
FringesPeeringStones = 99,
PeaksAlaGannha = 100,
PeaksAlaGhiri = 101,
LochsPortaPraetoria = 102,
LochsAlaMhiganQuarter = 103,
RubySeaTamamizu = 105,
RubySeaOnokoro = 106,
YanxiaNamai = 107,
YanxiaHouseOfTheFierce = 108,
AzimSteppeReunion = 109,
AzimSteppeDawnThrone = 110,
AzimSteppeDhoroIloh = 128,
DomanEnclave = 127,
DomanEnclaveNorthern = 129,
DomanEnclaveSouthern = 130,
DomanEnclaveOneRiver = 131,
DomanEnclaveDocks = 162,
DomanEnclaveGangos = 163,
Crystarium = 133,
CrystariumMarkets = 149,
CrystariumTemenosRookery = 150,
CrystariumDossalGate = 151,
CrystariumPendants = 152,
CrystariumAmaroLaunch = 153,
CrystariumCrystallineMean = 154,
CrystariumCabinetOfCuriosity = 155,
CrystariumTessellation = 156,
Eulmore = 134,
EulmoreMainstay = 157,
EulmoreNightsoilPots = 158,
EulmoreGloryGate = 159,
EulmoreSoutheastDerelict = 135,
EulmorePathToGlory = 160,
LakelandFortJobb = 132,
LakelandOstallImperative = 136,
KholusiaStilltide = 137,
KholusiaWright = 138,
KholusiaTomra = 139,
AmhAraengMordSouq = 140,
AmhAraengInnAtJourneysHead = 161,
AmhAraengTwine = 141,
RaktikaSlitherbough = 142,
RaktikaFanow = 143,
IlMhegLydhaLran = 144,
IlMhegPlaEnni = 145,
IlMhegWolekdorf = 146,
TempestOndoCups = 147,
TempestMacarensesAngle = 148,
OldSharlayan = 182,
OldSharlayanStudium = 184,
OldSharlayanBaldesionAnnex = 185,
OldSharlayanRostra = 186,
OldSharlayanLeveilleurEstate = 187,
OldSharlayanJourneysEnd = 188,
OldSharlayanScholarsHarbor = 189,
OldSharlayanHallOfArtifice = 190,
RadzAtHan = 183,
RadzAtHanMeghaduta = 191,
RadzAtHanRuveydahFibers = 192,
RadzAtHanAirship = 193,
RadzAtHanAlzadaalsPeace = 194,
RadzAtHanHallOfTheRadiantHost = 195,
RadzAtHanMehrydesMeyhane = 196,
RadzAtHanKama = 198,
RadzAtHanHighCrucible = 199,
RadzAtHanGateOfFirstSight = 197,
LabyrinthosArcheion = 166,
LabyrinthosSharlayanHamlet = 167,
LabyrinthosAporia = 168,
ThavnairYedlihmad = 169,
ThavnairGreatWork = 170,
ThavnairPalakasStand = 171,
GarlemaldCampBrokenGlass = 172,
GarlemaldTertium = 173,
MareLamentorumSinusLacrimarum = 174,
MareLamentorumBestwaysBurrow = 175,
ElpisAnagnorisis = 176,
ElpisTwelveWonders = 177,
ElpisPoietenOikos = 178,
UltimaThuleReahTahra = 179,
UltimaThuleAbodeOfTheEa = 180,
UltimaThuleBaseOmicron = 181,
Tuliyollal = 216,
TuliyollalDirigibleLanding = 218,
TuliyollalTheResplendentQuarter = 219,
TuliyollalTheForardCabins = 220,
TuliyollalBaysideBevyMarketplace = 221,
TuliyollalVollokShoonsa = 222,
TuliyollalWachumeqimeqi = 223,
TuliyollalBrightploomPost = 224,
TuliyollalArchOfTheDawnUrqopacha = 225,
TuliyollalArchOfTheDawnKozamauka = 226,
TuliyollalIhuykatumu = 227,
TuliyollalDirigibleLandingYakTel = 228,
TuliyollalXakTuralSkygate = 229,
SolutionNine = 217,
SolutionNineInformationCenter = 230,
SolutionNineTrueVue = 231,
SolutionNineNeonStein = 232,
SolutionNineTheArcadion = 233,
SolutionNineResolution = 234,
SolutionNineNexusArcade = 235,
SolutionNineResidentialSector = 236,
SolutionNineScanningPortNine = 237,
UrqopachaWachunpelo = 200,
UrqopachaWorlarsEcho = 201,
KozamaukaOkHanu = 202,
KozamaukaManyFires = 203,
KozamaukaEarthenshire = 204,
KozamaukaDockPoga = 238,
YakTelIqBraax = 205,
YakTelMamook = 206,
ShaaloaniHhusatahwi = 207,
ShaaloaniShesheneweziSprings = 208,
ShaaloaniMehwahhetsoan = 209,
HeritageFoundYyasulaniStation = 210,
HeritageFoundTheOutskirts = 211,
HeritageFoundElectropeStrike = 212,
LivingMemoryLeynodeMnemo = 213,
LivingMemoryLeynodePyro = 214,
LivingMemoryLeynodeAero = 215
}

View file

@ -0,0 +1,13 @@
namespace Questionable.Model.Common;
public static class EAetheryteLocationExtensions
{
public static bool IsFirmamentAetheryte(this EAetheryteLocation aetheryteLocation)
{
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament || (uint)(aetheryteLocation - 2011373) <= 1u || (uint)(aetheryteLocation - 2011384) <= 5u)
{
return true;
}
return false;
}
}

View file

@ -0,0 +1,151 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-aethernetshard.json",
"type": "string",
"enum": [
"[Gridania] Aetheryte Plaza",
"[Gridania] Archers' Guild",
"[Gridania] Leatherworkers' Guild & Shaded Bower",
"[Gridania] Lancers' Guild",
"[Gridania] Conjurers' Guild",
"[Gridania] Botanists' Guild",
"[Gridania] Mih Khetto's Amphitheatre",
"[Gridania] Blue Badger Gate (Central Shroud)",
"[Gridania] Yellow Serpent Gate (North Shroud)",
"[Gridania] White Wolf Gate (Central Shroud)",
"[Gridania] Airship Landing",
"[Ul'dah] Aetheryte Plaza",
"[Ul'dah] Adventurers' Guild",
"[Ul'dah] Thaumaturges' Guild",
"[Ul'dah] Gladiators' Guild",
"[Ul'dah] Miners' Guild",
"[Ul'dah] Weavers' Guild",
"[Ul'dah] Goldsmiths' Guild",
"[Ul'dah] Sapphire Avenue Exchange",
"[Ul'dah] Alchemists' Guild",
"[Ul'dah] Gate of the Sultana (Western Thanalan)",
"[Ul'dah] Gate of Nald (Central Thanalan)",
"[Ul'dah] Gate of Thal (Central Thanalan)",
"[Ul'dah] The Chamber of Rule",
"[Ul'dah] Airship Landing",
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanists' Guild",
"[Limsa Lominsa] Fishermen's Guild",
"[Limsa Lominsa] Hawkers' Alley",
"[Limsa Lominsa] The Aftcastle",
"[Limsa Lominsa] Culinarians' Guild",
"[Limsa Lominsa] Marauders' Guild",
"[Limsa Lominsa] Zephyr Gate (Middle La Noscea)",
"[Limsa Lominsa] Tempest Gate (Lower La Noscea)",
"[Limsa Lominsa] Airship Landing",
"[Gold Saucer] Aetheryte Plaza",
"[Gold Saucer] Entrance & Card Squares",
"[Gold Saucer] Wonder Square East",
"[Gold Saucer] Wonder Square West",
"[Gold Saucer] Event Square",
"[Gold Saucer] Cactpot Board",
"[Gold Saucer] Round Square",
"[Gold Saucer] Chocobo Square",
"[Gold Saucer] Minion Square",
"[Ishgard] Aetheryte Plaza",
"[Ishgard] The Forgotten Knight",
"[Ishgard] Skysteel Manufactory",
"[Ishgard] The Brume",
"[Ishgard] Athenaeum Astrologicum",
"[Ishgard] The Jeweled Crozier",
"[Ishgard] Saint Reymanaud's Cathedral",
"[Ishgard] The Tribunal",
"[Ishgard] The Last Vigil",
"[Ishgard] The Gates of Judgement (Coerthas Central Highlands)",
"[Ishgard] Firmament",
"[Firmament] The Mendicant's Court",
"[Firmament] The Mattock",
"[Firmament] The New Nest",
"[Firmament] Saint Roelle's Dais",
"[Firmament] Featherfall",
"[Firmament] Hoarfrost Hall",
"[Firmament] Western Risensong Quarter",
"[Firmament] Eastern Risensong Quarter",
"[Idyllshire] Aetheryte Plaza",
"[Idyllshire] West Idyllshire",
"[Idyllshire] Prologue Gate (Western Hinterlands)",
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)",
"[Rhalgr's Reach] Aetheryte Plaza",
"[Rhalgr's Reach] Western Rhalgr's Reach",
"[Rhalgr's Reach] Northeastern Rhalgr's Reach",
"[Rhalgr's Reach] Fringes Gate",
"[Rhalgr's Reach] Peaks Gate",
"[Kugane] Aetheryte Plaza",
"[Kugane] Shiokaze Hostelry",
"[Kugane] Pier #1",
"[Kugane] Thavnairian Consulate",
"[Kugane] Kogane Dori Markets",
"[Kugane] Bokairo Inn",
"[Kugane] The Ruby Bazaar",
"[Kugane] Sekiseigumi Barracks",
"[Kugane] Rakuza District",
"[Kugane] The Ruby Price",
"[Kugane] Airship Landing",
"[Doman Enclave] Aetheryte Plaza",
"[Doman Enclave] The Northern Enclave",
"[Doman Enclave] The Southern Enclave",
"[Doman Enclave] Ferry Docks",
"[Doman Enclave] The One River",
"[Doman Enclave] Gangos",
"[Crystarium] Aetheryte Plaza",
"[Crystarium] Musica Universalis Markets",
"[Crystarium] Temenos Rookery",
"[Crystarium] The Dossal Gate",
"[Crystarium] The Pendants",
"[Crystarium] The Amaro Launch",
"[Crystarium] The Crystalline Mean",
"[Crystarium] The Cabinet of Curiosity",
"[Crystarium] Tessellation (Lakeland)",
"[Eulmore] Aetheryte Plaza",
"[Eulmore] Southeast Derelicts",
"[Eulmore] Nightsoil Pots",
"[Eulmore] The Glory Gate",
"[Eulmore] The Mainstay",
"[Eulmore] The Path to Glory (Kholusia)",
"[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Studium",
"[Old Sharlayan] The Baldesion Annex",
"[Old Sharlayan] The Rostra",
"[Old Sharlayan] The Leveilleur Estate",
"[Old Sharlayan] Journey's End",
"[Old Sharlayan] Scholar's Harbor",
"[Old Sharlayan] The Hall of Artifice (Labyrinthos)",
"[Radz-at-Han] Aetheryte Plaza",
"[Radz-at-Han] Meghaduta",
"[Radz-at-Han] Ruveydah Fibers",
"[Radz-at-Han] Airship Landing",
"[Radz-at-Han] Alzadaal's Peace",
"[Radz-at-Han] Hall of the Radiant Host",
"[Radz-at-Han] Mehryde's Meyhane",
"[Radz-at-Han] Kama",
"[Radz-at-Han] The High Crucible of Al-Kimiya",
"[Radz-at-Han] The Gate of First Sight (Thavnair)",
"[Tuliyollal] Aetheryte Plaza",
"[Tuliyollal] Dirigible Landing",
"[Tuliyollal] The Resplendent Quarter",
"[Tuliyollal] The For'ard Cabins",
"[Tuliyollal] Bayside Bevy Marketplace",
"[Tuliyollal] Vollok Shoonsa",
"[Tuliyollal] Wachumeqimeqi",
"[Tuliyollal] Brightploom Post",
"[Tuliyollal] Arch of the Dawn (Urqopacha)",
"[Tuliyollal] Arch of the Dawn (Kozama'uka)",
"[Tuliyollal] Ihuykatumu (Kozama'uka)",
"[Tuliyollal] Dirigible Landing (Yak T'el)",
"[Tuliyollal] Xak Tural Skygate (Shaaloani)",
"[Solution Nine] Aetheryte Plaza",
"[Solution Nine] Information Center",
"[Solution Nine] True Vue",
"[Solution Nine] Neon Stein",
"[Solution Nine] The Arcadion",
"[Solution Nine] Resolution",
"[Solution Nine] Nexus Arcade",
"[Solution Nine] Residential Sector",
"[Solution Nine] Scanning Port Nine (Heritage Found)"
]
}

View file

@ -0,0 +1,114 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-aetheryte.json",
"type": "string",
"enum": [
"Gridania",
"Central Shroud - Bentbranch Meadows",
"East Shroud - Hawthorne Hut",
"South Shroud - Quarrymill",
"South Shroud - Camp Tranquil",
"North Shroud - Fallgourd Float",
"Ul'dah",
"Western Thanalan - Horizon",
"Central Thanalan - Black Brush Station",
"Eastern Thanalan - Camp Drybone",
"Southern Thanalan - Little Ala Mhigo",
"Southern Thanalan - Forgotten Springs",
"Northern Thanalan - Camp Bluefog",
"Northern Thanalan - Ceruleum Processing Plant",
"Limsa Lominsa",
"Middle La Noscea - Summerford Farms",
"Lower La Noscea - Moraby Drydocks",
"Eastern La Noscea - Costa Del Sol",
"Eastern La Noscea - Wineport",
"Western La Noscea - Swiftperch",
"Western La Noscea - Aleport",
"Upper La Noscea - Camp Bronze Lake",
"Outer La Noscea - Camp Overlook",
"Coerthas Central Highlands - Camp Dragonhead",
"Mor Dhona",
"Wolves' Den Pier",
"Gold Saucer",
"Ishgard",
"Idyllshire",
"Coerthas Western Highlands - Falcon's Nest",
"The Sea of Clouds - Camp Cloudtop",
"The Sea of Clouds - Ok' Zundu",
"Azys Lla - Helix",
"The Dravanian Forelands - Tailfeather",
"The Dravanian Forelands - Anyx Trine",
"The Churning Mists - Moghome",
"The Churning Mists - Zenith",
"Rhalgr's Reach",
"Fringes - Castrum Oriens",
"Fringes - Peering Stones",
"Peaks - Ala Gannha",
"Peaks - Ala Ghiri",
"Lochs - Porta Praetoria",
"Lochs - Ala Mhigan Quarter",
"Kugane",
"Ruby Sea - Tamamizu",
"Ruby Sea - Onokoro",
"Yanxia - Namai",
"Yanxia - House of the Fierce",
"Azim Steppe - Reunion",
"Azim Steppe - Dawn Throne",
"Azim Steppe - Dhoro Iloh",
"Doman Enclave",
"Crystarium",
"Eulmore",
"Lakeland - Fort Jobb",
"Lakeland - Ostall Imperative",
"Kholusia - Stilltide",
"Kholusia - Wright",
"Kholusia - Tomra",
"Amh Araeng - Mord Souq",
"Amh Araeng - Inn at Journey's Head",
"Amh Araeng - Twine",
"Rak'tika - Slitherbough",
"Rak'tika - Fanow",
"Il Mheg - Lydha Lran",
"Il Mheg - Pla Enni",
"Il Mheg - Wolekdorf",
"Tempest - Ondo Cups",
"Tempest - Macarenses Angle",
"Old Sharlayan",
"Radz-at-Han",
"Labyrinthos - Archeion",
"Labyrinthos - Sharlayan Hamlet",
"Labyrinthos - Aporia",
"Thavnair - Yedlihmad",
"Thavnair - Great Work",
"Thavnair - Palaka's Stand",
"Garlemald - Camp Broken Glass",
"Garlemald - Tertium",
"Mare Lamentorum - Sinus Lacrimarum",
"Mare Lamentorum - Bestways Burrow",
"Elpis - Anagnorisis",
"Elpis - Twelve Wonders",
"Elpis - Poieten Oikos",
"Ultima Thule - Reah Tahra",
"Ultima Thule - Abode of the Ea",
"Ultima Thule - Base Omicron",
"Tuliyollal",
"Solution Nine",
"Urqopacha - Wachunpelo",
"Urqopacha - Worlar's Echo",
"Kozama'uka - Ok'hanu",
"Kozama'uka - Many Fires",
"Kozama'uka - Earthenshire",
"Kozama'uka - Dock Poga",
"Yak T'el - Iq Br'aax",
"Yak T'el - Mamook",
"Shaaloani - Hhusatahwi",
"Shaaloani - Sheshenewezi Springs",
"Shaaloani - Mehwahhetsoan",
"Heritage Found - Yyasulani Station",
"Heritage Found - The Outskirts",
"Heritage Found - Electrope Strike",
"Living Memory - Leynode Mnemo",
"Living Memory - Leynode Pyro",
"Living Memory - Leynode Aero"
]
}

View file

@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-classjob.json",
"type": "string",
"enum": [
"Gladiator",
"Pugilist",
"Marauder",
"Lancer",
"Archer",
"Conjurer",
"Thaumaturge",
"Carpenter",
"Blacksmith",
"Armorer",
"Goldsmith",
"Leatherworker",
"Weaver",
"Alchemist",
"Culinarian",
"Miner",
"Botanist",
"Fisher",
"Paladin",
"Monk",
"Warrior",
"Dragoon",
"Bard",
"White Mage",
"Black Mage",
"Arcanist",
"Summoner",
"Scholar",
"Rogue",
"Ninja",
"Machinist",
"Dark Knight",
"Astrologian",
"Samurai",
"Red Mage",
"Blue Mage",
"Gunbreaker",
"Dancer",
"Reaper",
"Sage",
"Viper",
"Pictomancer",
"DoW",
"DoM",
"DoH",
"DoL",
"ConfiguredCombatJob",
"QuestStartJob"
]
}

View file

@ -0,0 +1,56 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-completionflags.json",
"type": "array",
"description": "Quest Variables that dictate whether or not this step is skipped: null is don't check, positive values need to be set, negative values need to be unset",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"High": {
"type": [
"number",
"null"
],
"minimum": 0,
"maximum": 15
},
"Low": {
"type": [
"number",
"null"
],
"minimum": 0,
"maximum": 15
},
"Mode": {
"type": "string",
"enum": [
"Bitwise",
"Exact"
]
}
}
},
{
"type": "number",
"enum": [
1,
2,
4,
8,
16,
32,
64,
128
]
},
{
"type": "null"
}
]
},
"minItems": 6,
"maxItems": 6
}

View file

@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-vector3.json",
"type": "object",
"description": "Position in the world",
"properties": {
"X": {
"type": "number"
},
"Y": {
"type": "number"
},
"Z": {
"type": "number"
}
},
"required": [
"X",
"Y",
"Z"
]
}

View file

@ -0,0 +1,42 @@
using System;
using System.Numerics;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Gathering;
public sealed class GatheringLocation
{
[JsonIgnore]
public Guid InternalId { get; } = Guid.NewGuid();
[JsonConverter(typeof(VectorConverter))]
public Vector3 Position { get; set; }
public int? MinimumAngle { get; set; }
public int? MaximumAngle { get; set; }
public float? MinimumDistance { get; set; }
public float? MaximumDistance { get; set; }
public bool IsCone()
{
if (MinimumAngle.HasValue)
{
return MaximumAngle.HasValue;
}
return false;
}
public float CalculateMinimumDistance()
{
return MinimumDistance ?? 1f;
}
public float CalculateMaximumDistance()
{
return MaximumDistance ?? 3f;
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Questionable.Model.Gathering;
public sealed class GatheringNode
{
public uint DataId { get; set; }
public bool? Fly { get; set; }
public List<GatheringLocation> Locations { get; set; } = new List<GatheringLocation>();
}

View file

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Questionable.Model.Gathering;
public sealed class GatheringNodeGroup
{
public List<GatheringNode> Nodes { get; set; } = new List<GatheringNode>();
}

View file

@ -0,0 +1,77 @@
using System;
using System.Globalization;
namespace Questionable.Model.Gathering;
public class GatheringPointId : IComparable<GatheringPointId>, IEquatable<GatheringPointId>
{
public ushort Value { get; }
public GatheringPointId(ushort value)
{
Value = value;
}
public int CompareTo(GatheringPointId? other)
{
if ((object)this == other)
{
return 0;
}
if ((object)other == null)
{
return 1;
}
return Value.CompareTo(other.Value);
}
public bool Equals(GatheringPointId? other)
{
if ((object)other == null)
{
return false;
}
if ((object)this == other)
{
return true;
}
return Value == other.Value;
}
public override bool Equals(object? obj)
{
if (obj == null)
{
return false;
}
if (this == obj)
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((GatheringPointId)obj);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(GatheringPointId? left, GatheringPointId? right)
{
return object.Equals(left, right);
}
public static bool operator !=(GatheringPointId? left, GatheringPointId? right)
{
return !object.Equals(left, right);
}
public static GatheringPointId FromString(string value)
{
return new GatheringPointId(ushort.Parse(value, CultureInfo.InvariantCulture));
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
using Questionable.Model.Questing;
namespace Questionable.Model.Gathering;
public sealed class GatheringRoot
{
[JsonConverter(typeof(StringListOrValueConverter))]
public List<string> Author { get; set; } = new List<string>();
public List<QuestStep> Steps { get; set; } = new List<QuestStep>();
public bool? FlyBetweenNodes { get; set; }
public List<uint> ExtraQuestItems { get; set; } = new List<uint>();
public List<GatheringNodeGroup> Groups { get; set; } = new List<GatheringNodeGroup>();
}

View file

@ -0,0 +1,200 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class ActionConverter : EnumConverter<EAction>
{
private static readonly Dictionary<EAction, string> Values = new Dictionary<EAction, string>
{
{
EAction.DutyAction1,
"Duty Action I"
},
{
EAction.DutyAction2,
"Duty Action II"
},
{
EAction.HeavySwing,
"Heavy Swing"
},
{
EAction.Bootshine,
"Bootshine"
},
{
EAction.TwinSnakes,
"Twin Snakes"
},
{
EAction.Demolish,
"Demolish"
},
{
EAction.DragonKick,
"Dragon Kick"
},
{
EAction.HeavyShot,
"Heavy Shot"
},
{
EAction.Cure,
"Cure"
},
{
EAction.Cure2,
"Cure II"
},
{
EAction.Eukrasia,
"Eukrasia"
},
{
EAction.Diagnosis,
"Diagnosis"
},
{
EAction.EukrasianDiagnosis,
"Eukrasian Diagnosis"
},
{
EAction.Esuna,
"Esuna"
},
{
EAction.Physick,
"Physick"
},
{
EAction.AspectedBenefic,
"Aspected Benefic"
},
{
EAction.FormShift,
"Form Shift"
},
{
EAction.FieryBreath,
"Fiery Breath"
},
{
EAction.BuffetSanuwa,
"Buffet (Sanuwa)"
},
{
EAction.BuffetGriffin,
"Buffet (Griffin)"
},
{
EAction.Trample,
"Trample"
},
{
EAction.Fumigate,
"Fumigate"
},
{
EAction.Roar,
"Roar"
},
{
EAction.Seed,
"Seed"
},
{
EAction.Inhale,
"Inhale"
},
{
EAction.SiphonSnout,
"Siphon Snout"
},
{
EAction.PeculiarLight,
"Peculiar Light"
},
{
EAction.Cannonfire,
"Cannonfire"
},
{
EAction.RedGulal,
"Red Gulal"
},
{
EAction.YellowGulal,
"Yellow Gulal"
},
{
EAction.BlueGulal,
"Blue Gulal"
},
{
EAction.ElectrixFlux,
"Electric Flux"
},
{
EAction.HopStep,
"Hop-step"
},
{
EAction.Hide,
"Hide"
},
{
EAction.FumaShuriken,
"Fuma Shuriken"
},
{
EAction.Katon,
"Katon"
},
{
EAction.Raiton,
"Raiton"
},
{
EAction.SlugShot,
"Slug Shot"
},
{
EAction.BosomBrook,
"Bosom Brook"
},
{
EAction.Souleater,
"Souleater"
},
{
EAction.Fire3,
"Fire III"
},
{
EAction.Adloquium,
"Adloquium"
},
{
EAction.WaterCannon,
"Water Cannon"
},
{
EAction.Wasshoi,
"Wasshoi"
},
{
EAction.ShroudedLuminescence,
"Shrouded Luminescence"
},
{
EAction.BigSneeze,
"Big Sneeze"
}
};
public ActionConverter()
: base((IReadOnlyDictionary<EAction, string>)Values)
{
}
}

View file

@ -0,0 +1,593 @@
using System.Collections.Generic;
using Questionable.Model.Common;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class AethernetShardConverter : EnumConverter<EAetheryteLocation>
{
public static readonly Dictionary<EAetheryteLocation, string> Values = new Dictionary<EAetheryteLocation, string>
{
{
EAetheryteLocation.Gridania,
"[Gridania] Aetheryte Plaza"
},
{
EAetheryteLocation.GridaniaArcher,
"[Gridania] Archers' Guild"
},
{
EAetheryteLocation.GridaniaLeatherworker,
"[Gridania] Leatherworkers' Guild & Shaded Bower"
},
{
EAetheryteLocation.GridaniaLancer,
"[Gridania] Lancers' Guild"
},
{
EAetheryteLocation.GridaniaConjurer,
"[Gridania] Conjurers' Guild"
},
{
EAetheryteLocation.GridaniaBotanist,
"[Gridania] Botanists' Guild"
},
{
EAetheryteLocation.GridaniaAmphitheatre,
"[Gridania] Mih Khetto's Amphitheatre"
},
{
EAetheryteLocation.GridaniaBlueBadgerGate,
"[Gridania] Blue Badger Gate (Central Shroud)"
},
{
EAetheryteLocation.GridaniaYellowSerpentGate,
"[Gridania] Yellow Serpent Gate (North Shroud)"
},
{
EAetheryteLocation.GridaniaWhiteWolfGate,
"[Gridania] White Wolf Gate (Central Shroud)"
},
{
EAetheryteLocation.GridaniaAirship,
"[Gridania] Airship Landing"
},
{
EAetheryteLocation.Uldah,
"[Ul'dah] Aetheryte Plaza"
},
{
EAetheryteLocation.UldahAdventurers,
"[Ul'dah] Adventurers' Guild"
},
{
EAetheryteLocation.UldahThaumaturge,
"[Ul'dah] Thaumaturges' Guild"
},
{
EAetheryteLocation.UldahGladiator,
"[Ul'dah] Gladiators' Guild"
},
{
EAetheryteLocation.UldahMiner,
"[Ul'dah] Miners' Guild"
},
{
EAetheryteLocation.UldahWeaver,
"[Ul'dah] Weavers' Guild"
},
{
EAetheryteLocation.UldahGoldsmith,
"[Ul'dah] Goldsmiths' Guild"
},
{
EAetheryteLocation.UldahSapphireAvenue,
"[Ul'dah] Sapphire Avenue Exchange"
},
{
EAetheryteLocation.UldahAlchemist,
"[Ul'dah] Alchemists' Guild"
},
{
EAetheryteLocation.UldahChamberOfRule,
"[Ul'dah] The Chamber of Rule"
},
{
EAetheryteLocation.UldahGateOfTheSultana,
"[Ul'dah] Gate of the Sultana (Western Thanalan)"
},
{
EAetheryteLocation.UldahGateOfNald,
"[Ul'dah] Gate of Nald (Central Thanalan)"
},
{
EAetheryteLocation.UldahGateOfThal,
"[Ul'dah] Gate of Thal (Central Thanalan)"
},
{
EAetheryteLocation.UldahAirship,
"[Ul'dah] Airship Landing"
},
{
EAetheryteLocation.Limsa,
"[Limsa Lominsa] Aetheryte Plaza"
},
{
EAetheryteLocation.LimsaArcanist,
"[Limsa Lominsa] Arcanists' Guild"
},
{
EAetheryteLocation.LimsaFisher,
"[Limsa Lominsa] Fishermen's Guild"
},
{
EAetheryteLocation.LimsaHawkersAlley,
"[Limsa Lominsa] Hawkers' Alley"
},
{
EAetheryteLocation.LimsaAftcastle,
"[Limsa Lominsa] The Aftcastle"
},
{
EAetheryteLocation.LimsaCulinarian,
"[Limsa Lominsa] Culinarians' Guild"
},
{
EAetheryteLocation.LimsaMarauder,
"[Limsa Lominsa] Marauders' Guild"
},
{
EAetheryteLocation.LimsaZephyrGate,
"[Limsa Lominsa] Zephyr Gate (Middle La Noscea)"
},
{
EAetheryteLocation.LimsaTempestGate,
"[Limsa Lominsa] Tempest Gate (Lower La Noscea)"
},
{
EAetheryteLocation.LimsaAirship,
"[Limsa Lominsa] Airship Landing"
},
{
EAetheryteLocation.GoldSaucer,
"[Gold Saucer] Aetheryte Plaza"
},
{
EAetheryteLocation.GoldSaucerEntranceCardSquares,
"[Gold Saucer] Entrance & Card Squares"
},
{
EAetheryteLocation.GoldSaucerWonderSquareEast,
"[Gold Saucer] Wonder Square East"
},
{
EAetheryteLocation.GoldSaucerWonderSquareWest,
"[Gold Saucer] Wonder Square West"
},
{
EAetheryteLocation.GoldSaucerEventSquare,
"[Gold Saucer] Event Square"
},
{
EAetheryteLocation.GoldSaucerCactpotBoard,
"[Gold Saucer] Cactpot Board"
},
{
EAetheryteLocation.GoldSaucerRoundSquare,
"[Gold Saucer] Round Square"
},
{
EAetheryteLocation.GoldSaucerChocoboSquare,
"[Gold Saucer] Chocobo Square"
},
{
EAetheryteLocation.GoldSaucerMinionSquare,
"[Gold Saucer] Minion Square"
},
{
EAetheryteLocation.Ishgard,
"[Ishgard] Aetheryte Plaza"
},
{
EAetheryteLocation.IshgardForgottenKnight,
"[Ishgard] The Forgotten Knight"
},
{
EAetheryteLocation.IshgardSkysteelManufactory,
"[Ishgard] Skysteel Manufactory"
},
{
EAetheryteLocation.IshgardBrume,
"[Ishgard] The Brume"
},
{
EAetheryteLocation.IshgardAthenaeumAstrologicum,
"[Ishgard] Athenaeum Astrologicum"
},
{
EAetheryteLocation.IshgardJeweledCrozier,
"[Ishgard] The Jeweled Crozier"
},
{
EAetheryteLocation.IshgardSaintReymanaudsCathedral,
"[Ishgard] Saint Reymanaud's Cathedral"
},
{
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.FirmamentMendicantsCourt,
"[Firmament] The Mendicant's Court"
},
{
EAetheryteLocation.FirmamentMattock,
"[Firmament] The Mattock"
},
{
EAetheryteLocation.FirmamentNewNest,
"[Firmament] The New Nest"
},
{
EAetheryteLocation.FirmanentSaintRoellesDais,
"[Firmament] Saint Roelle's Dais"
},
{
EAetheryteLocation.FirmamentFeatherfall,
"[Firmament] Featherfall"
},
{
EAetheryteLocation.FirmamentHoarfrostHall,
"[Firmament] Hoarfrost Hall"
},
{
EAetheryteLocation.FirmamentWesternRisensongQuarter,
"[Firmament] Western Risensong Quarter"
},
{
EAetheryteLocation.FIrmamentEasternRisensongQuarter,
"[Firmament] Eastern Risensong Quarter"
},
{
EAetheryteLocation.Idyllshire,
"[Idyllshire] Aetheryte Plaza"
},
{
EAetheryteLocation.IdyllshireWest,
"[Idyllshire] West Idyllshire"
},
{
EAetheryteLocation.IdyllshirePrologueGate,
"[Idyllshire] Prologue Gate (Western Hinterlands)"
},
{
EAetheryteLocation.IdyllshireEpilogueGate,
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
},
{
EAetheryteLocation.RhalgrsReach,
"[Rhalgr's Reach] Aetheryte Plaza"
},
{
EAetheryteLocation.RhalgrsReachWest,
"[Rhalgr's Reach] Western Rhalgr's Reach"
},
{
EAetheryteLocation.RhalgrsReachNorthEast,
"[Rhalgr's Reach] Northeastern Rhalgr's Reach"
},
{
EAetheryteLocation.RhalgrsReachFringesGate,
"[Rhalgr's Reach] Fringes Gate"
},
{
EAetheryteLocation.RhalgrsReachPeaksGate,
"[Rhalgr's Reach] Peaks Gate"
},
{
EAetheryteLocation.Kugane,
"[Kugane] Aetheryte Plaza"
},
{
EAetheryteLocation.KuganeShiokazeHostelry,
"[Kugane] Shiokaze Hostelry"
},
{
EAetheryteLocation.KuganePier1,
"[Kugane] Pier #1"
},
{
EAetheryteLocation.KuganeThavnairianConsulate,
"[Kugane] Thavnairian Consulate"
},
{
EAetheryteLocation.KuganeMarkets,
"[Kugane] Kogane Dori Markets"
},
{
EAetheryteLocation.KuganeBokairoInn,
"[Kugane] Bokairo Inn"
},
{
EAetheryteLocation.KuganeRubyBazaar,
"[Kugane] The Ruby Bazaar"
},
{
EAetheryteLocation.KuganeSekiseigumiBarracks,
"[Kugane] Sekiseigumi Barracks"
},
{
EAetheryteLocation.KuganeRakuzaDistrict,
"[Kugane] Rakuza District"
},
{
EAetheryteLocation.KuganeRubyPrice,
"[Kugane] The Ruby Price"
},
{
EAetheryteLocation.KuganeAirship,
"[Kugane] Airship Landing"
},
{
EAetheryteLocation.DomanEnclave,
"[Doman Enclave] Aetheryte Plaza"
},
{
EAetheryteLocation.DomanEnclaveNorthern,
"[Doman Enclave] The Northern Enclave"
},
{
EAetheryteLocation.DomanEnclaveSouthern,
"[Doman Enclave] The Southern Enclave"
},
{
EAetheryteLocation.DomanEnclaveDocks,
"[Doman Enclave] Ferry Docks"
},
{
EAetheryteLocation.DomanEnclaveOneRiver,
"[Doman Enclave] The One River"
},
{
EAetheryteLocation.DomanEnclaveGangos,
"[Doman Enclave] Gangos"
},
{
EAetheryteLocation.Crystarium,
"[Crystarium] Aetheryte Plaza"
},
{
EAetheryteLocation.CrystariumMarkets,
"[Crystarium] Musica Universalis Markets"
},
{
EAetheryteLocation.CrystariumTemenosRookery,
"[Crystarium] Temenos Rookery"
},
{
EAetheryteLocation.CrystariumDossalGate,
"[Crystarium] The Dossal Gate"
},
{
EAetheryteLocation.CrystariumPendants,
"[Crystarium] The Pendants"
},
{
EAetheryteLocation.CrystariumAmaroLaunch,
"[Crystarium] The Amaro Launch"
},
{
EAetheryteLocation.CrystariumCrystallineMean,
"[Crystarium] The Crystalline Mean"
},
{
EAetheryteLocation.CrystariumCabinetOfCuriosity,
"[Crystarium] The Cabinet of Curiosity"
},
{
EAetheryteLocation.CrystariumTessellation,
"[Crystarium] Tessellation (Lakeland)"
},
{
EAetheryteLocation.Eulmore,
"[Eulmore] Aetheryte Plaza"
},
{
EAetheryteLocation.EulmoreSoutheastDerelict,
"[Eulmore] Southeast Derelicts"
},
{
EAetheryteLocation.EulmoreNightsoilPots,
"[Eulmore] Nightsoil Pots"
},
{
EAetheryteLocation.EulmoreGloryGate,
"[Eulmore] The Glory Gate"
},
{
EAetheryteLocation.EulmoreMainstay,
"[Eulmore] The Mainstay"
},
{
EAetheryteLocation.EulmorePathToGlory,
"[Eulmore] The Path to Glory (Kholusia)"
},
{
EAetheryteLocation.OldSharlayan,
"[Old Sharlayan] Aetheryte Plaza"
},
{
EAetheryteLocation.OldSharlayanStudium,
"[Old Sharlayan] The Studium"
},
{
EAetheryteLocation.OldSharlayanBaldesionAnnex,
"[Old Sharlayan] The Baldesion Annex"
},
{
EAetheryteLocation.OldSharlayanRostra,
"[Old Sharlayan] The Rostra"
},
{
EAetheryteLocation.OldSharlayanLeveilleurEstate,
"[Old Sharlayan] The Leveilleur Estate"
},
{
EAetheryteLocation.OldSharlayanJourneysEnd,
"[Old Sharlayan] Journey's End"
},
{
EAetheryteLocation.OldSharlayanScholarsHarbor,
"[Old Sharlayan] Scholar's Harbor"
},
{
EAetheryteLocation.OldSharlayanHallOfArtifice,
"[Old Sharlayan] The Hall of Artifice (Labyrinthos)"
},
{
EAetheryteLocation.RadzAtHan,
"[Radz-at-Han] Aetheryte Plaza"
},
{
EAetheryteLocation.RadzAtHanMeghaduta,
"[Radz-at-Han] Meghaduta"
},
{
EAetheryteLocation.RadzAtHanRuveydahFibers,
"[Radz-at-Han] Ruveydah Fibers"
},
{
EAetheryteLocation.RadzAtHanAirship,
"[Radz-at-Han] Airship Landing"
},
{
EAetheryteLocation.RadzAtHanAlzadaalsPeace,
"[Radz-at-Han] Alzadaal's Peace"
},
{
EAetheryteLocation.RadzAtHanHallOfTheRadiantHost,
"[Radz-at-Han] Hall of the Radiant Host"
},
{
EAetheryteLocation.RadzAtHanMehrydesMeyhane,
"[Radz-at-Han] Mehryde's Meyhane"
},
{
EAetheryteLocation.RadzAtHanKama,
"[Radz-at-Han] Kama"
},
{
EAetheryteLocation.RadzAtHanHighCrucible,
"[Radz-at-Han] The High Crucible of Al-Kimiya"
},
{
EAetheryteLocation.RadzAtHanGateOfFirstSight,
"[Radz-at-Han] The Gate of First Sight (Thavnair)"
},
{
EAetheryteLocation.Tuliyollal,
"[Tuliyollal] Aetheryte Plaza"
},
{
EAetheryteLocation.TuliyollalDirigibleLanding,
"[Tuliyollal] Dirigible Landing"
},
{
EAetheryteLocation.TuliyollalTheResplendentQuarter,
"[Tuliyollal] The Resplendent Quarter"
},
{
EAetheryteLocation.TuliyollalTheForardCabins,
"[Tuliyollal] The For'ard Cabins"
},
{
EAetheryteLocation.TuliyollalBaysideBevyMarketplace,
"[Tuliyollal] Bayside Bevy Marketplace"
},
{
EAetheryteLocation.TuliyollalVollokShoonsa,
"[Tuliyollal] Vollok Shoonsa"
},
{
EAetheryteLocation.TuliyollalWachumeqimeqi,
"[Tuliyollal] Wachumeqimeqi"
},
{
EAetheryteLocation.TuliyollalBrightploomPost,
"[Tuliyollal] Brightploom Post"
},
{
EAetheryteLocation.TuliyollalArchOfTheDawnUrqopacha,
"[Tuliyollal] Arch of the Dawn (Urqopacha)"
},
{
EAetheryteLocation.TuliyollalArchOfTheDawnKozamauka,
"[Tuliyollal] Arch of the Dawn (Kozama'uka)"
},
{
EAetheryteLocation.TuliyollalIhuykatumu,
"[Tuliyollal] Ihuykatumu (Kozama'uka)"
},
{
EAetheryteLocation.TuliyollalDirigibleLandingYakTel,
"[Tuliyollal] Dirigible Landing (Yak T'el)"
},
{
EAetheryteLocation.TuliyollalXakTuralSkygate,
"[Tuliyollal] Xak Tural Skygate (Shaaloani)"
},
{
EAetheryteLocation.SolutionNine,
"[Solution Nine] Aetheryte Plaza"
},
{
EAetheryteLocation.SolutionNineInformationCenter,
"[Solution Nine] Information Center"
},
{
EAetheryteLocation.SolutionNineTrueVue,
"[Solution Nine] True Vue"
},
{
EAetheryteLocation.SolutionNineNeonStein,
"[Solution Nine] Neon Stein"
},
{
EAetheryteLocation.SolutionNineTheArcadion,
"[Solution Nine] The Arcadion"
},
{
EAetheryteLocation.SolutionNineResolution,
"[Solution Nine] Resolution"
},
{
EAetheryteLocation.SolutionNineNexusArcade,
"[Solution Nine] Nexus Arcade"
},
{
EAetheryteLocation.SolutionNineResidentialSector,
"[Solution Nine] Residential Sector"
},
{
EAetheryteLocation.SolutionNineScanningPortNine,
"[Solution Nine] Scanning Port Nine (Heritage Found)"
}
};
public AethernetShardConverter()
: base((IReadOnlyDictionary<EAetheryteLocation, string>)Values)
{
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Questionable.Model.Common;
namespace Questionable.Model.Questing.Converter;
public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
{
private static readonly Dictionary<EAetheryteLocation, string> EnumToString = AethernetShardConverter.Values;
private static readonly Dictionary<string, EAetheryteLocation> StringToEnum = EnumToString.ToDictionary<KeyValuePair<EAetheryteLocation, string>, string, EAetheryteLocation>((KeyValuePair<EAetheryteLocation, string> x) => x.Value, (KeyValuePair<EAetheryteLocation, string> x) => x.Key);
public override AethernetShortcut Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
string key = reader.GetString() ?? throw new JsonException();
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
string key2 = reader.GetString() ?? throw new JsonException();
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
{
throw new JsonException();
}
AethernetShortcut aethernetShortcut = new AethernetShortcut();
if (!StringToEnum.TryGetValue(key, out var value))
{
throw new JsonException();
}
aethernetShortcut.From = value;
if (!StringToEnum.TryGetValue(key2, out var value2))
{
throw new JsonException();
}
aethernetShortcut.To = value2;
return aethernetShortcut;
}
public override void Write(Utf8JsonWriter writer, AethernetShortcut value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteStringValue(EnumToString[value.From]);
writer.WriteStringValue(EnumToString[value.To]);
writer.WriteEndArray();
}
}

View file

@ -0,0 +1,28 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class CombatItemUseConditionConverter : EnumConverter<ECombatItemUseCondition>
{
private static readonly Dictionary<ECombatItemUseCondition, string> Values = new Dictionary<ECombatItemUseCondition, string>
{
{
ECombatItemUseCondition.Incapacitated,
"Incapacitated"
},
{
ECombatItemUseCondition.HealthPercent,
"Health%"
},
{
ECombatItemUseCondition.MissingStatus,
"MissingStatus"
}
};
public CombatItemUseConditionConverter()
: base((IReadOnlyDictionary<ECombatItemUseCondition, string>)Values)
{
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class DialogueChoiceTypeConverter : EnumConverter<EDialogChoiceType>
{
private static readonly Dictionary<EDialogChoiceType, string> Values = new Dictionary<EDialogChoiceType, string>
{
{
EDialogChoiceType.YesNo,
"YesNo"
},
{
EDialogChoiceType.List,
"List"
}
};
public DialogueChoiceTypeConverter()
: base((IReadOnlyDictionary<EDialogChoiceType, string>)Values)
{
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Questing.Converter;
public sealed class ElementIdConverter : JsonConverter<ElementId>
{
public override ElementId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return new QuestId(reader.GetUInt16());
}
return ElementId.FromString(reader.GetString() ?? throw new JsonException());
}
public override void Write(Utf8JsonWriter writer, ElementId value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Questing.Converter;
public sealed class ElementIdListConverter : JsonConverter<List<ElementId>>
{
public override List<ElementId> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
reader.Read();
List<ElementId> list = new List<ElementId>();
while (reader.TokenType != JsonTokenType.EndArray)
{
if (reader.TokenType == JsonTokenType.Number)
{
list.Add(new QuestId(reader.GetUInt16()));
}
else
{
list.Add(ElementId.FromString(reader.GetString() ?? throw new JsonException()));
}
reader.Read();
}
return list;
}
public override void Write(Utf8JsonWriter writer, List<ElementId> value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class EnemySpawnTypeConverter : EnumConverter<EEnemySpawnType>
{
private static readonly Dictionary<EEnemySpawnType, string> Values = new Dictionary<EEnemySpawnType, string>
{
{
EEnemySpawnType.AfterInteraction,
"AfterInteraction"
},
{
EEnemySpawnType.AfterItemUse,
"AfterItemUse"
},
{
EEnemySpawnType.AfterAction,
"AfterAction"
},
{
EEnemySpawnType.AfterEmote,
"AfterEmote"
},
{
EEnemySpawnType.AutoOnEnterArea,
"AutoOnEnterArea"
},
{
EEnemySpawnType.OverworldEnemies,
"OverworldEnemies"
},
{
EEnemySpawnType.FateEnemies,
"FateEnemies"
},
{
EEnemySpawnType.FinishCombatIfAny,
"FinishCombatIfAny"
}
};
public EnemySpawnTypeConverter()
: base((IReadOnlyDictionary<EEnemySpawnType, string>)Values)
{
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Questing.Converter;
public sealed class ExcelRefConverter : JsonConverter<ExcelRef>
{
public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => ExcelRef.FromKey(reader.GetString()),
JsonTokenType.Number => ExcelRef.FromRowId(reader.GetUInt32()),
_ => null,
};
}
public override void Write(Utf8JsonWriter writer, ExcelRef? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (value.Type == ExcelRef.EType.Key)
{
writer.WriteStringValue(value.AsKey());
return;
}
if (value.Type == ExcelRef.EType.RowId)
{
writer.WriteNumberValue(value.AsRowId());
return;
}
throw new JsonException();
}
}

View file

@ -0,0 +1,212 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
internal sealed class ExtendedClassJobConverter : EnumConverter<EExtendedClassJob>
{
private static readonly Dictionary<EExtendedClassJob, string> Values = new Dictionary<EExtendedClassJob, string>
{
{
EExtendedClassJob.None,
"None"
},
{
EExtendedClassJob.Gladiator,
"Gladiator"
},
{
EExtendedClassJob.Pugilist,
"Pugilist"
},
{
EExtendedClassJob.Marauder,
"Marauder"
},
{
EExtendedClassJob.Lancer,
"Lancer"
},
{
EExtendedClassJob.Archer,
"Archer"
},
{
EExtendedClassJob.Conjurer,
"Conjurer"
},
{
EExtendedClassJob.Thaumaturge,
"Thaumaturge"
},
{
EExtendedClassJob.Carpenter,
"Carpenter"
},
{
EExtendedClassJob.Blacksmith,
"Blacksmith"
},
{
EExtendedClassJob.Armorer,
"Armorer"
},
{
EExtendedClassJob.Goldsmith,
"Goldsmith"
},
{
EExtendedClassJob.Leatherworker,
"Leatherworker"
},
{
EExtendedClassJob.Weaver,
"Weaver"
},
{
EExtendedClassJob.Alchemist,
"Alchemist"
},
{
EExtendedClassJob.Culinarian,
"Culinarian"
},
{
EExtendedClassJob.Miner,
"Miner"
},
{
EExtendedClassJob.Botanist,
"Botanist"
},
{
EExtendedClassJob.Fisher,
"Fisher"
},
{
EExtendedClassJob.Paladin,
"Paladin"
},
{
EExtendedClassJob.Monk,
"Monk"
},
{
EExtendedClassJob.Warrior,
"Warrior"
},
{
EExtendedClassJob.Dragoon,
"Dragoon"
},
{
EExtendedClassJob.Bard,
"Bard"
},
{
EExtendedClassJob.WhiteMage,
"White Mage"
},
{
EExtendedClassJob.BlackMage,
"Black Mage"
},
{
EExtendedClassJob.Arcanist,
"Arcanist"
},
{
EExtendedClassJob.Summoner,
"Summoner"
},
{
EExtendedClassJob.Scholar,
"Scholar"
},
{
EExtendedClassJob.Rogue,
"Rogue"
},
{
EExtendedClassJob.Ninja,
"Ninja"
},
{
EExtendedClassJob.Machinist,
"Machinist"
},
{
EExtendedClassJob.DarkKnight,
"Dark Knight"
},
{
EExtendedClassJob.Astrologian,
"Astrologian"
},
{
EExtendedClassJob.Samurai,
"Samurai"
},
{
EExtendedClassJob.RedMage,
"Red Mage"
},
{
EExtendedClassJob.BlueMage,
"Blue Mage"
},
{
EExtendedClassJob.Gunbreaker,
"Gunbreaker"
},
{
EExtendedClassJob.Dancer,
"Dancer"
},
{
EExtendedClassJob.Reaper,
"Reaper"
},
{
EExtendedClassJob.Sage,
"Sage"
},
{
EExtendedClassJob.Viper,
"Viper"
},
{
EExtendedClassJob.Pictomancer,
"Pictomancer"
},
{
EExtendedClassJob.DoW,
"DoW"
},
{
EExtendedClassJob.DoM,
"DoM"
},
{
EExtendedClassJob.DoH,
"DoH"
},
{
EExtendedClassJob.DoL,
"DoL"
},
{
EExtendedClassJob.ConfiguredCombatJob,
"ConfiguredCombatJob"
},
{
EExtendedClassJob.QuestStartJob,
"QuestStartJob"
}
};
public ExtendedClassJobConverter()
: base((IReadOnlyDictionary<EExtendedClassJob, string>)Values)
{
}
}

View file

@ -0,0 +1,136 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class InteractionTypeConverter : EnumConverter<EInteractionType>
{
private static readonly Dictionary<EInteractionType, string> Values = new Dictionary<EInteractionType, string>
{
{
EInteractionType.None,
"None"
},
{
EInteractionType.Interact,
"Interact"
},
{
EInteractionType.WalkTo,
"WalkTo"
},
{
EInteractionType.AttuneAethernetShard,
"AttuneAethernetShard"
},
{
EInteractionType.AttuneAetheryte,
"AttuneAetheryte"
},
{
EInteractionType.RegisterFreeOrFavoredAetheryte,
"RegisterFreeOrFavoredAetheryte"
},
{
EInteractionType.AttuneAetherCurrent,
"AttuneAetherCurrent"
},
{
EInteractionType.Combat,
"Combat"
},
{
EInteractionType.UseItem,
"UseItem"
},
{
EInteractionType.EquipItem,
"EquipItem"
},
{
EInteractionType.PurchaseItem,
"PurchaseItem"
},
{
EInteractionType.EquipRecommended,
"EquipRecommended"
},
{
EInteractionType.Say,
"Say"
},
{
EInteractionType.Emote,
"Emote"
},
{
EInteractionType.Action,
"Action"
},
{
EInteractionType.StatusOff,
"StatusOff"
},
{
EInteractionType.WaitForObjectAtPosition,
"WaitForNpcAtPosition"
},
{
EInteractionType.WaitForManualProgress,
"WaitForManualProgress"
},
{
EInteractionType.Duty,
"Duty"
},
{
EInteractionType.SinglePlayerDuty,
"SinglePlayerDuty"
},
{
EInteractionType.Jump,
"Jump"
},
{
EInteractionType.Dive,
"Dive"
},
{
EInteractionType.Craft,
"Craft"
},
{
EInteractionType.Gather,
"Gather"
},
{
EInteractionType.Snipe,
"Snipe"
},
{
EInteractionType.SwitchClass,
"SwitchClass"
},
{
EInteractionType.UnlockTaxiStand,
"UnlockTaxiStand"
},
{
EInteractionType.Instruction,
"Instruction"
},
{
EInteractionType.AcceptQuest,
"AcceptQuest"
},
{
EInteractionType.CompleteQuest,
"CompleteQuest"
}
};
public InteractionTypeConverter()
: base((IReadOnlyDictionary<EInteractionType, string>)Values)
{
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class JumpTypeConverter : EnumConverter<EJumpType>
{
private static readonly Dictionary<EJumpType, string> Values = new Dictionary<EJumpType, string>
{
{
EJumpType.SingleJump,
"SingleJump"
},
{
EJumpType.RepeatedJumps,
"RepeatedJumps"
}
};
public JumpTypeConverter()
: base((IReadOnlyDictionary<EJumpType, string>)Values)
{
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class LockedSkipConditionConverter : EnumConverter<ELockedSkipCondition>
{
private static readonly Dictionary<ELockedSkipCondition, string> Values = new Dictionary<ELockedSkipCondition, string>
{
{
ELockedSkipCondition.Locked,
"Locked"
},
{
ELockedSkipCondition.Unlocked,
"Unlocked"
}
};
public LockedSkipConditionConverter()
: base((IReadOnlyDictionary<ELockedSkipCondition, string>)Values)
{
}
}

View file

@ -0,0 +1,62 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.Questing.Converter;
public sealed class QuestWorkConfigConverter : JsonConverter<QuestWorkValue>
{
public override QuestWorkValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return new QuestWorkValue(reader.GetByte());
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
byte? high = null;
byte? low = null;
EQuestWorkMode mode = EQuestWorkMode.Bitwise;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
{
string text = reader.GetString();
if (text == null || !reader.Read())
{
throw new JsonException();
}
switch (text)
{
case "High":
high = reader.GetByte();
break;
case "Low":
low = reader.GetByte();
break;
case "Mode":
mode = new QuestWorkModeConverter().Read(ref reader, typeof(EQuestWorkMode), options);
break;
default:
throw new JsonException();
}
break;
}
case JsonTokenType.EndObject:
return new QuestWorkValue(high, low, mode);
default:
throw new JsonException();
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class QuestWorkModeConverter : EnumConverter<EQuestWorkMode>
{
private static readonly Dictionary<EQuestWorkMode, string> Values = new Dictionary<EQuestWorkMode, string>
{
{
EQuestWorkMode.Bitwise,
"Bitwise"
},
{
EQuestWorkMode.Exact,
"Exact"
}
};
public QuestWorkModeConverter()
: base((IReadOnlyDictionary<EQuestWorkMode, string>)Values)
{
}
}

View file

@ -0,0 +1,40 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class SkipConditionConverter : EnumConverter<EExtraSkipCondition>
{
private static readonly Dictionary<EExtraSkipCondition, string> Values = new Dictionary<EExtraSkipCondition, string>
{
{
EExtraSkipCondition.WakingSandsMainArea,
"WakingSandsMainArea"
},
{
EExtraSkipCondition.WakingSandsSolar,
"WakingSandsSolar"
},
{
EExtraSkipCondition.RisingStonesSolar,
"RisingStonesSolar"
},
{
EExtraSkipCondition.RoguesGuild,
"RoguesGuild"
},
{
EExtraSkipCondition.NotRoguesGuild,
"NotRoguesGuild"
},
{
EExtraSkipCondition.DockStorehouse,
"DockStorehouse"
}
};
public SkipConditionConverter()
: base((IReadOnlyDictionary<EExtraSkipCondition, string>)Values)
{
}
}

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class StatusConverter : EnumConverter<EStatus>
{
private static readonly Dictionary<EStatus, string> Values = new Dictionary<EStatus, string> {
{
EStatus.Hidden,
"Hidden"
} };
public StatusConverter()
: base((IReadOnlyDictionary<EStatus, string>)Values)
{
}
}

View file

@ -0,0 +1,16 @@
using System.Globalization;
namespace Questionable.Model.Questing;
public sealed class AetherCurrentId : ElementId
{
public AetherCurrentId(ushort value)
: base(value)
{
}
public override string ToString()
{
return "C" + base.Value.ToString(CultureInfo.InvariantCulture);
}
}

View file

@ -0,0 +1,16 @@
using System.Globalization;
namespace Questionable.Model.Questing;
public sealed class AethernetId : ElementId
{
public AethernetId(ushort value)
: base(value)
{
}
public override string ToString()
{
return "N" + base.Value.ToString(CultureInfo.InvariantCulture);
}
}

View file

@ -0,0 +1,13 @@
namespace Questionable.Model.Questing;
public sealed class AlliedSocietyDailyId(byte alliedSociety, byte rank = 0) : ElementId((ushort)(alliedSociety * 10 + rank))
{
public byte AlliedSociety { get; } = alliedSociety;
public byte Rank { get; } = rank;
public override string ToString()
{
return "A" + AlliedSociety + "x" + Rank;
}
}

View file

@ -0,0 +1,8 @@
namespace Questionable.Model.Questing;
public sealed class ChatMessage
{
public string? ExcelSheet { get; set; }
public string Key { get; set; }
}

View file

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
public sealed class CombatItemUse
{
public uint ItemId { get; set; }
[JsonConverter(typeof(CombatItemUseConditionConverter))]
public ECombatItemUseCondition Condition { get; set; }
public int Value { get; set; }
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace Questionable.Model.Questing;
public sealed class ComplexCombatData
{
public uint DataId { get; set; }
public uint? NameId { get; set; }
public uint? MinimumKillCount { get; set; }
public uint? RewardItemId { get; set; }
public int? RewardItemCount { get; set; }
public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue>();
public bool IgnoreQuestMarker { get; set; }
}

View file

@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
public sealed class DialogueChoice
{
[JsonConverter(typeof(DialogueChoiceTypeConverter))]
public EDialogChoiceType Type { get; set; }
public string? ExcelSheet { get; set; }
[JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Prompt { get; set; }
public bool Yes { get; set; } = true;
[JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Answer { get; set; }
public bool PromptIsRegularExpression { get; set; }
public bool AnswerIsRegularExpression { get; set; }
public uint? DataId { get; set; }
public string? SpecialCondition { get; set; }
}

View file

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Questionable.Model.Questing;
public class DutyOptions
{
public bool Enabled { get; set; }
public uint ContentFinderConditionId { get; set; }
public bool LowPriority { get; set; }
public List<string> Notes { get; set; } = new List<string>();
}

View file

@ -0,0 +1,80 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(ActionConverter))]
public enum EAction
{
DutyAction1 = 65562,
DutyAction2 = 65563,
HeavySwing = 31,
Bootshine = 53,
TwinSnakes = 61,
Demolish = 66,
DragonKick = 74,
HeavyShot = 97,
Cure = 120,
Cure2 = 135,
Eukrasia = 24290,
Diagnosis = 24284,
EukrasianDiagnosis = 24291,
Esuna = 7568,
Physick = 190,
AspectedBenefic = 3595,
FormShift = 4262,
FieryBreath = 1764,
BuffetSanuwa = 4931,
BuffetGriffin = 4583,
Trample = 4585,
Fumigate = 5872,
Roar = 6293,
Seed = 6294,
MagitekPulse = 8624,
MagitekThunder = 8625,
Inhale = 10013,
SiphonSnout = 18187,
PeculiarLight = 20030,
Cannonfire = 20121,
RedGulal = 29382,
YellowGulal = 29383,
BlueGulal = 29384,
ElectrixFlux = 29718,
HopStep = 31116,
Hide = 2245,
Ten = 2259,
Ninjutsu = 2260,
Chi = 2261,
Jin = 2263,
FumaShuriken = 2265,
Katon = 2266,
Raiton = 2267,
RabbitMedium = 2272,
SlugShot = 2868,
BosomBrook = 37173,
Souleater = 3632,
Fire3 = 152,
Adloquium = 185,
WaterCannon = 11385,
Wasshoi = 11499,
ShroudedLuminescence = 39505,
BigSneeze = 1765,
Prospect = 227,
CollectMiner = 240,
LuckOfTheMountaineer = 4081,
ScourMiner = 22182,
MeticulousMiner = 22184,
ScrutinyMiner = 22185,
Triangulate = 210,
CollectBotanist = 815,
LuckOfThePioneer = 4095,
ScourBotanist = 22186,
MeticulousBotanist = 22188,
ScrutinyBotanist = 22189,
SharpVision1 = 235,
SharpVision2 = 237,
SharpVision3 = 295,
FieldMastery1 = 218,
FieldMastery2 = 220,
FieldMastery3 = 294
}

View file

@ -0,0 +1,34 @@
namespace Questionable.Model.Questing;
public static class EActionExtensions
{
public static bool RequiresMount(this EAction action)
{
switch (action)
{
case EAction.FieryBreath:
case EAction.BigSneeze:
case EAction.BuffetGriffin:
case EAction.Trample:
case EAction.BuffetSanuwa:
case EAction.Fumigate:
case EAction.Roar:
case EAction.Seed:
case EAction.Inhale:
case EAction.Wasshoi:
case EAction.SiphonSnout:
case EAction.PeculiarLight:
case EAction.Cannonfire:
case EAction.RedGulal:
case EAction.YellowGulal:
case EAction.BlueGulal:
case EAction.ElectrixFlux:
case EAction.HopStep:
case EAction.BosomBrook:
case EAction.ShroudedLuminescence:
return true;
default:
return false;
}
}
}

View file

@ -0,0 +1,9 @@
namespace Questionable.Model.Questing;
public enum ECombatItemUseCondition
{
None,
Incapacitated,
HealthPercent,
MissingStatus
}

View file

@ -0,0 +1,8 @@
namespace Questionable.Model.Questing;
public enum EDialogChoiceType
{
None,
YesNo,
List
}

View file

@ -0,0 +1,275 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(EmoteConverter))]
public enum EEmote
{
None = 0,
Surprised = 1,
Angry = 2,
Furious = 3,
Blush = 4,
Bow = 5,
Cheer = 6,
Clap = 7,
Beckon = 8,
Comfort = 9,
Cry = 10,
Dance = 11,
Doubt = 12,
Doze = 13,
Fume = 14,
Goodbye = 15,
Wave = 16,
Huh = 17,
Joy = 18,
Kneel = 19,
Chuckle = 20,
Laugh = 21,
Lookout = 22,
Me = 23,
No = 24,
Deny = 25,
Panic = 26,
Point = 27,
Poke = 28,
Congratulate = 29,
Psych = 30,
Salute = 31,
Shocked = 32,
Shrug = 33,
Rally = 34,
Soothe = 35,
Stagger = 36,
Stretch = 37,
Sulk = 38,
Think = 39,
Upset = 40,
Welcome = 41,
Yes = 42,
ThumbsUp = 43,
ExamineSelf = 44,
Pose = 45,
BlowKiss = 46,
Grovel = 47,
Happy = 48,
Disappointed = 49,
Lounge = 50,
GroundSit = 52,
AirQuotes = 54,
GcSalute = 55,
Pray = 58,
ImperialSalute = 59,
Visor = 60,
Megaflare = 62,
CrimsonLotus = 63,
Charmed = 64,
CheerOn = 65,
CheerWave = 66,
CheerJump = 67,
StraightFace = 68,
Smile = 69,
Grin = 70,
Smirk = 71,
Taunt = 72,
ShutEyes = 73,
Sad = 74,
Scared = 75,
Amazed = 76,
Ouch = 77,
Annoyed = 78,
Alert = 79,
Worried = 80,
BigGrin = 81,
Reflect = 82,
Furrow = 83,
Scoff = 84,
Throw = 85,
ChangePose = 90,
StepDance = 101,
HarvestDance = 102,
BallDance = 103,
MandervilleDance = 104,
Pet = 105,
HandOver = 106,
BombDance = 109,
Hurray = 110,
Slap = 111,
Hug = 112,
Embrace = 113,
Hildibrand = 114,
FistBump = 115,
ThavDance = 118,
GoldDance = 119,
SundropDance = 120,
BattleStance = 121,
VictoryPose = 122,
Backflip = 123,
EasternGreeting = 124,
Eureka = 125,
MogDance = 126,
Haurchefant = 127,
EasternStretch = 128,
EasternDance = 129,
RangerPose1R = 130,
RangerPose2R = 131,
RangerPose3R = 132,
Wink = 133,
RangerPose1L = 134,
RangerPose2L = 135,
RangerPose3L = 136,
Facepalm = 137,
Zantetsuken = 138,
Flex = 139,
Respect = 140,
Sneer = 141,
PrettyPlease = 142,
PlayDead = 143,
IceHeart = 144,
MoonLift = 145,
Dote = 146,
Spectacles = 148,
Songbird = 149,
WaterFloat = 150,
WaterFlip = 151,
PuckerUp = 152,
PowerUp = 153,
EasternBow = 154,
Squats = 155,
PushUps = 156,
SitUps = 157,
BreathControl = 158,
Converse = 159,
Concentrate = 160,
Disturbed = 161,
Simper = 162,
Beam = 163,
Attention = 164,
AtEase = 165,
Box = 166,
RitualPrayer = 167,
Tremble = 169,
Winded = 170,
Aback = 171,
Greeting = 172,
BoxStep = 173,
SideStep = 174,
Ultima = 175,
YolDance = 176,
Splash = 178,
Sweat = 180,
Shiver = 181,
Elucidate = 182,
Ponder = 183,
LeftWink = 184,
GetFantasy = 185,
PopotoStep = 186,
Hum = 187,
Confirm = 188,
Scheme = 189,
Endure = 190,
Tomestone = 191,
HeelToe = 192,
GoobbueDouble = 193,
Gratuity = 194,
FistPump = 195,
Reprimand = 196,
Sabotender = 197,
MandervilleMambo = 198,
LaliHo = 199,
SimulationM = 200,
SimulationF = 201,
Toast = 202,
Lean = 203,
Headache = 204,
Snap = 205,
BreakFast = 206,
Read = 207,
Insist = 208,
Consider = 209,
Wasshoi = 210,
FlowerShower = 211,
FlameDance = 212,
HighFive = 213,
Guard = 214,
Malevolence = 215,
BeesKnees = 216,
LaliHop = 217,
EatRiceBall = 220,
EatApple = 221,
WringHands = 222,
Sweep = 223,
PaintBlack = 224,
PaintRed = 225,
PaintYellow = 226,
PaintBlue = 227,
FakeSmile = 228,
Pantomime = 229,
Vexed = 230,
Shush = 231,
EatPizza = 232,
ClutchHead = 233,
EatChocolate = 234,
EatEgg = 235,
Content = 236,
Sheathe = 237,
Draw = 238,
Tea = 239,
Determined = 240,
ShowRight = 241,
ShowLeft = 242,
Deride = 245,
Wow = 246,
EatPumpkinCookie = 247,
Spirit = 248,
MagicTrick = 249,
LittleLadiesDance = 250,
Linkpearl = 251,
EarWiggle = 252,
Frighten = 256,
AdventOfLight = 257,
JumpForJoy1 = 258,
JumpForJoy2 = 259,
JumpForJoy3 = 260,
JumpForJoy4 = 261,
JumpForJoy5 = 262,
HandToHeart = 263,
CheerOnBright = 264,
CheerWaveViolet = 265,
CheerJumpGreen = 266,
AllSaintsCharm = 267,
LopHop = 269,
Reference = 270,
EatChicken = 271,
Sundering = 272,
Slump = 273,
LoveHeart = 274,
HumbleTriumph = 275,
VictoryReveal = 276,
FryEgg = 277,
Uchiwasshoi = 278,
Attend = 279,
Water = 280,
ShakeDrink = 281,
Unbound = 282,
Bouquet = 283,
BlowBubbles = 284,
Ohokaliy = 285,
Visage = 286,
Photograph = 288,
Overreact = 291,
Twirl = 292,
Dazed = 293,
Rage = 294,
TomeScroll = 295,
Study = 296,
GridanianSip = 298,
UldahnSip = 299,
LominsanSip = 300,
GridanianGulp = 301,
UldahnGulp = 302,
LominsanGulp = 303,
Pen = 307
}

View file

@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(EnemySpawnTypeConverter))]
public enum EEnemySpawnType
{
None,
AfterInteraction,
AfterItemUse,
AfterAction,
AfterEmote,
AutoOnEnterArea,
OverworldEnemies,
FateEnemies,
FinishCombatIfAny,
QuestInterruption
}

View file

@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(ExtendedClassJobConverter))]
public enum EExtendedClassJob
{
None,
Gladiator,
Pugilist,
Marauder,
Lancer,
Archer,
Conjurer,
Thaumaturge,
Carpenter,
Blacksmith,
Armorer,
Goldsmith,
Leatherworker,
Weaver,
Alchemist,
Culinarian,
Miner,
Botanist,
Fisher,
Paladin,
Monk,
Warrior,
Dragoon,
Bard,
WhiteMage,
BlackMage,
Arcanist,
Summoner,
Scholar,
Rogue,
Ninja,
Machinist,
DarkKnight,
Astrologian,
Samurai,
RedMage,
BlueMage,
Gunbreaker,
Dancer,
Reaper,
Sage,
Viper,
Pictomancer,
DoW,
DoM,
DoH,
DoL,
ConfiguredCombatJob,
QuestStartJob
}

View file

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(SkipConditionConverter))]
public enum EExtraSkipCondition
{
None,
WakingSandsMainArea,
WakingSandsSolar,
RisingStonesSolar,
RoguesGuild,
NotRoguesGuild,
DockStorehouse
}

View file

@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(InteractionTypeConverter))]
public enum EInteractionType
{
None,
Interact,
WalkTo,
AttuneAethernetShard,
AttuneAetheryte,
RegisterFreeOrFavoredAetheryte,
AttuneAetherCurrent,
Combat,
UseItem,
EquipItem,
PurchaseItem,
EquipRecommended,
Say,
Emote,
Action,
StatusOff,
WaitForObjectAtPosition,
WaitForManualProgress,
Duty,
SinglePlayerDuty,
Jump,
Dive,
Craft,
Gather,
Snipe,
SwitchClass,
UnlockTaxiStand,
Instruction,
AcceptQuest,
CompleteQuest
}

View file

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(JumpTypeConverter))]
public enum EJumpType
{
SingleJump,
RepeatedJumps
}

View file

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(LockedSkipConditionConverter))]
public enum ELockedSkipCondition
{
Locked,
Unlocked
}

View file

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(QuestWorkModeConverter))]
public enum EQuestWorkMode
{
Bitwise,
Exact
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(StatusConverter))]
public enum EStatus : uint
{
Triangulate = 217u,
GatheringRateUp = 218u,
Prospect = 225u,
Hidden = 614u,
Eukrasia = 2606u,
Jog = 4209u
}

View file

@ -0,0 +1,123 @@
using System;
using System.Globalization;
namespace Questionable.Model.Questing;
public abstract class ElementId : IComparable<ElementId>, IEquatable<ElementId>
{
public ushort Value { get; }
protected ElementId(ushort value)
{
Value = value;
}
public int CompareTo(ElementId? other)
{
if ((object)this == other)
{
return 0;
}
if ((object)other == null)
{
return 1;
}
return Value.CompareTo(other.Value);
}
public bool Equals(ElementId? other)
{
if ((object)other == null)
{
return false;
}
if ((object)this == other)
{
return true;
}
if (other.GetType() != GetType())
{
return false;
}
return Value == other.Value;
}
public override bool Equals(object? obj)
{
if (obj == null)
{
return false;
}
if (this == obj)
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((ElementId)obj);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(ElementId? left, ElementId? right)
{
return object.Equals(left, right);
}
public static bool operator !=(ElementId? left, ElementId? right)
{
return !object.Equals(left, right);
}
public static ElementId FromString(string value)
{
if (value.StartsWith("S"))
{
return new SatisfactionSupplyNpcId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
}
if (value.StartsWith("U"))
{
return new UnlockLinkId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
}
if (value.StartsWith("N"))
{
return new AethernetId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
}
if (value.StartsWith("C"))
{
return new AetherCurrentId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
}
if (value.StartsWith("A"))
{
value = value.Substring(1);
string[] array = value.Split(new char[1] { 'x' });
if (array.Length == 2)
{
return new AlliedSocietyDailyId(byte.Parse(array[0], CultureInfo.InvariantCulture), byte.Parse(array[1], CultureInfo.InvariantCulture));
}
return new AlliedSocietyDailyId(byte.Parse(value, CultureInfo.InvariantCulture), 0);
}
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 abstract override string ToString();
}

View file

@ -0,0 +1,83 @@
using System;
namespace Questionable.Model.Questing;
public class ExcelRef
{
public enum EType
{
None,
Key,
RowId,
RawString
}
private readonly string? _stringValue;
private readonly uint? _rowIdValue;
public EType Type { get; }
public ExcelRef(string value)
{
_stringValue = value;
_rowIdValue = null;
Type = EType.Key;
}
public ExcelRef(uint value)
{
_stringValue = null;
_rowIdValue = value;
Type = EType.RowId;
}
private ExcelRef(string? stringValue, uint? rowIdValue, EType type)
{
_stringValue = stringValue;
_rowIdValue = rowIdValue;
Type = type;
}
public static ExcelRef FromKey(string value)
{
return new ExcelRef(value, null, EType.Key);
}
public static ExcelRef FromRowId(uint rowId)
{
return new ExcelRef(null, rowId, EType.RowId);
}
public static ExcelRef FromSheetValue(string value)
{
return new ExcelRef(value, null, EType.RawString);
}
public string AsKey()
{
if (Type != EType.Key)
{
throw new InvalidOperationException();
}
return _stringValue;
}
public uint AsRowId()
{
if (Type != EType.RowId)
{
throw new InvalidOperationException();
}
return _rowIdValue.Value;
}
public string AsRawString()
{
if (Type != EType.RawString)
{
throw new InvalidOperationException();
}
return _stringValue;
}
}

View file

@ -0,0 +1,12 @@
namespace Questionable.Model.Questing;
public sealed class GatheredItem
{
public uint ItemId { get; set; }
public uint AlternativeItemId { get; set; }
public int ItemCount { get; set; }
public ushort Collectability { get; set; }
}

View file

@ -0,0 +1,22 @@
using System.Numerics;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing;
public sealed class JumpDestination
{
[JsonConverter(typeof(VectorConverter))]
public Vector3 Position { get; set; }
public float? StopDistance { get; set; }
public float? DelaySeconds { get; set; }
public EJumpType Type { get; set; }
public float CalculateStopDistance()
{
return StopDistance ?? 1f;
}
}

View file

@ -0,0 +1,15 @@
using System.Numerics;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing;
public sealed class NearPositionCondition
{
[JsonConverter(typeof(VectorConverter))]
public Vector3 Position { get; set; }
public float MaximumDistance { get; set; }
public ushort TerritoryId { get; set; }
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
public sealed class PurchaseMenu
{
public string? ExcelSheet { get; set; }
[JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Key { get; set; }
}

View file

@ -0,0 +1,21 @@
using System.Globalization;
namespace Questionable.Model.Questing;
public sealed class QuestId : ElementId
{
public QuestId(ushort value)
: base(value)
{
}
public static QuestId FromRowId(uint rowId)
{
return new QuestId((ushort)(rowId & 0xFFFF));
}
public override string ToString()
{
return base.Value.ToString(CultureInfo.InvariantCulture);
}
}

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing;
public sealed class QuestRoot
{
[JsonConverter(typeof(StringListOrValueConverter))]
public List<string> Author { get; set; } = new List<string>();
public bool Disabled { get; set; }
public bool Interruptible { get; set; } = true;
public string? Comment { get; set; }
public List<QuestSequence> QuestSequence { get; set; } = new List<QuestSequence>();
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
namespace Questionable.Model.Questing;
public sealed class QuestSequence
{
public byte Sequence { get; set; }
public string? Comment { get; set; }
public List<QuestStep> Steps { get; set; } = new List<QuestStep>();
public QuestStep? FindStep(int step)
{
if (step < 0 || step >= Steps.Count)
{
return null;
}
return Steps[step];
}
public QuestStep? LastStep()
{
return Steps.LastOrDefault();
}
}

View file

@ -0,0 +1,172 @@
using System.Collections.Generic;
using System.Numerics;
using System.Text.Json.Serialization;
using Questionable.Model.Common;
using Questionable.Model.Common.Converter;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
public sealed class QuestStep
{
public const float DefaultStopDistance = 3f;
public const int VesperBayAetheryteTicket = 30362;
public uint? DataId { get; set; }
[JsonConverter(typeof(VectorConverter))]
public Vector3? Position { get; set; }
public float? StopDistance { get; set; }
public ushort TerritoryId { get; set; }
[JsonIgnore(/*Could not decode attribute arguments.*/)]
public EInteractionType InteractionType { get; set; }
public float? NpcWaitDistance { get; set; }
public ushort? TargetTerritoryId { get; set; }
public float? DelaySecondsAtStart { get; set; }
public uint? PickUpItemId { get; set; }
public bool Disabled { get; set; }
public bool DisableNavmesh { get; set; }
public bool? Mount { get; set; }
public bool? Fly { get; set; }
public bool? Land { get; set; }
public bool? Sprint { get; set; }
public bool? IgnoreDistanceToObject { get; set; }
public bool? RestartNavigationIfCancelled { get; set; }
public string? Comment { get; set; }
public EAetheryteLocation? Aetheryte { get; set; }
[JsonConverter(typeof(AethernetShardConverter))]
public EAetheryteLocation? AethernetShard { get; set; }
public EAetheryteLocation? AetheryteShortcut { get; set; }
public AethernetShortcut? AethernetShortcut { get; set; }
public uint? AetherCurrentId { get; set; }
public uint? ItemId { get; set; }
public bool? GroundTarget { get; set; }
public int? ItemCount { get; set; }
public EEmote? Emote { get; set; }
public ChatMessage? ChatMessage { get; set; }
public EAction? Action { get; set; }
public EStatus? Status { get; set; }
public EExtendedClassJob TargetClass { get; set; }
public byte? TaxiStandId { get; set; }
public EEnemySpawnType? EnemySpawnType { get; set; }
public List<uint> KillEnemyDataIds { get; set; } = new List<uint>();
public List<ComplexCombatData> ComplexCombatData { get; set; } = new List<ComplexCombatData>();
public CombatItemUse? CombatItemUse { get; set; }
public float? CombatDelaySecondsAtStart { get; set; }
public JumpDestination? JumpDestination { get; set; }
public DutyOptions? DutyOptions { get; set; }
public SinglePlayerDutyOptions? SinglePlayerDutyOptions { get; set; }
public byte SinglePlayerDutyIndex => SinglePlayerDutyOptions?.Index ?? 0;
public SkipConditions? SkipConditions { get; set; }
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new List<List<QuestWorkValue>>();
public List<EExtendedClassJob> RequiredCurrentJob { get; set; } = new List<EExtendedClassJob>();
public List<EExtendedClassJob> RequiredQuestAcceptedJob { get; set; } = new List<EExtendedClassJob>();
public List<GatheredItem> ItemsToGather { get; set; } = new List<GatheredItem>();
public List<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue>();
public List<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
public List<uint> PointMenuChoices { get; set; } = new List<uint>();
public PurchaseMenu? PurchaseMenu { get; set; }
[JsonConverter(typeof(ElementIdConverter))]
public ElementId? PickUpQuestId { get; set; }
[JsonConverter(typeof(ElementIdConverter))]
public ElementId? TurnInQuestId { get; set; }
[JsonConverter(typeof(ElementIdConverter))]
public ElementId? NextQuestId { get; set; }
[JsonConstructor]
public QuestStep()
{
}
public QuestStep(EInteractionType interactionType, uint? dataId, Vector3? position, ushort territoryId)
{
InteractionType = interactionType;
DataId = dataId;
Position = position;
TerritoryId = territoryId;
}
public float CalculateActualStopDistance()
{
float? stopDistance = StopDistance;
if (stopDistance.HasValue)
{
return stopDistance.GetValueOrDefault();
}
switch (InteractionType)
{
case EInteractionType.WalkTo:
return 0.25f;
case EInteractionType.AttuneAetheryte:
case EInteractionType.RegisterFreeOrFavoredAetheryte:
return 10f;
default:
return 3f;
}
}
public bool IsTeleportableForPriorityQuests()
{
if (AetheryteShortcut.HasValue)
{
return true;
}
if (InteractionType == EInteractionType.UseItem && ItemId == 30362)
{
return true;
}
return false;
}
}

View file

@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(QuestWorkConfigConverter))]
public sealed class QuestWorkValue(byte? high, byte? low, EQuestWorkMode mode)
{
public byte? High { get; set; } = high;
public byte? Low { get; set; } = low;
public EQuestWorkMode Mode { get; set; } = mode;
public QuestWorkValue(byte value)
: this((byte)(value >> 4), (byte)(value & 0xF), EQuestWorkMode.Bitwise)
{
}
public override string ToString()
{
if (High.HasValue && Low.HasValue)
{
return ((byte)(High << 4).Value + Low).ToString();
}
if (High.HasValue)
{
return High + "H";
}
if (Low.HasValue)
{
return Low + "L";
}
return "-";
}
}

View file

@ -0,0 +1,16 @@
using System.Globalization;
namespace Questionable.Model.Questing;
public sealed class SatisfactionSupplyNpcId : ElementId
{
public SatisfactionSupplyNpcId(ushort value)
: base(value)
{
}
public override string ToString()
{
return "S" + base.Value.ToString(CultureInfo.InvariantCulture);
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Questionable.Model.Questing;
public sealed class SinglePlayerDutyOptions
{
public bool Enabled { get; set; }
public List<string> Notes { get; set; } = new List<string>();
public byte Index { get; set; }
}

View file

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Questionable.Model.Common;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
public sealed class SkipAetheryteCondition
{
public bool Never { get; set; }
public bool InSameTerritory { get; set; }
public List<ushort> InTerritory { get; set; } = new List<ushort>();
[JsonConverter(typeof(ElementIdListConverter))]
public List<ElementId> QuestsAccepted { get; set; } = new List<ElementId>();
[JsonConverter(typeof(ElementIdListConverter))]
public List<ElementId> QuestsCompleted { get; set; } = new List<ElementId>();
public EAetheryteLocation? AetheryteLocked { get; set; }
public EAetheryteLocation? AetheryteUnlocked { get; set; }
public bool RequiredQuestVariablesNotMet { get; set; }
public NearPositionCondition? NearPosition { get; set; }
public NearPositionCondition? NotNearPosition { get; set; }
public EExtraSkipCondition? ExtraCondition { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace Questionable.Model.Questing;
public sealed class SkipConditions
{
public SkipStepConditions? StepIf { get; set; }
public SkipAetheryteCondition? AetheryteShortcutIf { get; set; }
public SkipAetheryteCondition? AethernetShortcutIf { get; set; }
}

Some files were not shown because too many files have changed in this diff Show more