-Subproject commit 43c3dba112c202e2d0ff1a6909020c2b83e20dc3
+Subproject commit e6e3a1f29715e2af4976dd7338ed2f09ae82c99c
.AsSyntaxNodeOrToken(),
AssignmentList(nameof(QuestStep.PointMenuChoices), step.PointMenuChoices)
.AsSyntaxNodeOrToken(),
+ Assignment(nameof(QuestStep.PurchaseMenu), step.PurchaseMenu, emptyStep.PurchaseMenu)
+ .AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.PickUpQuestId), step.PickUpQuestId,
emptyStep.PickUpQuestId)
.AsSyntaxNodeOrToken(),
"Z": 150.92688
},
"TerritoryId": 886,
- "InteractionType": "Instruction",
+ "InteractionType": "PurchaseItem",
+ "PurchaseMenu": {
+ "ExcelSheet": "GilShop",
+ "Key": 262151,
+ "$": "This isn't the correct shop id, but it's also unclear how you'd find out"
+ },
+ "ItemId": 5768,
+ "ItemCount": 2,
"Comment": "Buy cream yellow dye",
"AethernetShortcut": [
"[Firmament] The New Nest",
"Combat",
"UseItem",
"EquipItem",
+ "PurchaseItem",
"EquipRecommended",
"Say",
"Emote",
]
}
},
+ {
+ "if": {
+ "properties": {
+ "InteractionType": {
+ "const": "PurchaseItem"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "ItemCount": {
+ "type": "integer"
+ },
+ "PurchaseMenu": {
+ "type": "object",
+ "description": "The text to use with /say",
+ "properties": {
+ "ExcelSheet": {
+ "type": "string"
+ },
+ "Key": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ }
+ },
+ "required": [
+ "Key"
+ ]
+ }
+ },
+ "required": [
+ "ItemId",
+ "ItemCount"
+ ]
+ }
+ },
{
"if": {
"properties": {
{ EInteractionType.Combat, "Combat" },
{ EInteractionType.UseItem, "UseItem" },
{ EInteractionType.EquipItem, "EquipItem" },
+ { EInteractionType.PurchaseItem, "PurchaseItem" },
{ EInteractionType.EquipRecommended, "EquipRecommended" },
{ EInteractionType.Say, "Say" },
{ EInteractionType.Emote, "Emote" },
Combat,
UseItem,
EquipItem,
+ PurchaseItem,
EquipRecommended,
Say,
Emote,
--- /dev/null
+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; }
+}
public List<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = [];
public List<DialogueChoice> DialogueChoices { get; set; } = [];
public List<uint> PointMenuChoices { get; set; } = [];
+ public PurchaseMenu? PurchaseMenu { get; set; }
// TODO: Not implemented
[JsonConverter(typeof(ElementIdConverter))]
private readonly IGameGui _gameGui;
private readonly ITargetManager _targetManager;
private readonly IClientState _clientState;
+ private readonly ShopController _shopController;
private readonly ILogger<InteractionUiController> _logger;
private readonly Regex _returnRegex;
ITargetManager targetManager,
IPluginLog pluginLog,
IClientState clientState,
+ ShopController shopController,
ILogger<InteractionUiController> logger)
{
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_targetManager = targetManager;
_clientState = clientState;
+ _shopController = shopController;
_logger = logger;
_returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
if (step == null)
_logger.LogDebug("Ignoring current quest dialogue choices, no active step");
else
+ {
dialogueChoices.AddRange(step.DialogueChoices.Select(x => new DialogueChoiceInfo(quest, x)));
+ if (step.PurchaseMenu != null)
+ dialogueChoices.Add(new DialogueChoiceInfo(quest, new DialogueChoice
+ {
+ Type = EDialogChoiceType.List,
+ ExcelSheet = step.PurchaseMenu.ExcelSheet,
+ Prompt = null,
+ Answer = step.PurchaseMenu.Key,
+ }));
+ }
}
// add all travel dialogue choices
return;
_logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
+ if (_shopController.IsAutoBuyEnabled && _shopController.IsAwaitingYesNo)
+ {
+ addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+ _shopController.IsAwaitingYesNo = false;
+ return;
+ }
+
var director = UIState.Instance()->DirectorTodo.Director;
if (director != null &&
director->Info.EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
--- /dev/null
+using System;
+using System.Linq;
+using System.Numerics;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
+using LLib.Shop;
+using Microsoft.Extensions.Logging;
+using Questionable.Model.Questing;
+using Workshoppa.GameData.Shops;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
+
+namespace Questionable.Controller.GameUi;
+
+internal sealed class ShopController : IDisposable, IShopWindow
+{
+ private readonly QuestController _questController;
+ private readonly IGameGui _gameGui;
+ private readonly IFramework _framework;
+ private readonly RegularShopBase _shop;
+ private readonly ILogger<ShopController> _logger;
+
+ public ShopController(QuestController questController, IGameGui gameGui, IAddonLifecycle addonLifecycle,
+ IFramework framework, ILogger<ShopController> logger, IPluginLog pluginLog)
+ {
+ _questController = questController;
+ _gameGui = gameGui;
+ _framework = framework;
+ _shop = new RegularShopBase(this, "Shop", pluginLog, gameGui, addonLifecycle);
+ _logger = logger;
+
+ _framework.Update += FrameworkUpdate;
+ }
+
+ public bool IsEnabled => _questController.IsRunning;
+ public bool IsOpen { get; set; }
+ public bool IsAutoBuyEnabled => _shop.AutoBuyEnabled;
+
+ public bool IsAwaitingYesNo
+ {
+ get { return _shop.IsAwaitingYesNo; }
+ set { _shop.IsAwaitingYesNo = value; }
+ }
+
+ public Vector2? Position { get; set; } // actual implementation doesn't matter, not a real window
+
+ public void Dispose()
+ {
+ _framework.Update -= FrameworkUpdate;
+ _shop.Dispose();
+ }
+
+ private void FrameworkUpdate(IFramework framework)
+ {
+ if (IsOpen && _shop.ItemForSale != null)
+ {
+ if (_shop.PurchaseState != null)
+ {
+ _shop.HandleNextPurchaseStep();
+ }
+ else
+ {
+ var currentStep = FindCurrentStep();
+ if (currentStep == null || currentStep.InteractionType != EInteractionType.PurchaseItem)
+ return;
+
+ int missingItems = Math.Max(0,
+ currentStep.ItemCount.GetValueOrDefault() - (int)_shop.ItemForSale.OwnedItems);
+ int toPurchase = Math.Min(_shop.GetMaxItemsToPurchase(), missingItems);
+ if (toPurchase > 0)
+ {
+ _logger.LogDebug("Auto-buying {MissingItems} {ItemName}", missingItems, _shop.ItemForSale.ItemName);
+ _shop.StartAutoPurchase(missingItems);
+ _shop.HandleNextPurchaseStep();
+ }
+ else
+ _shop.CancelAutoPurchase();
+ }
+ }
+ }
+
+ public int GetCurrencyCount() => _shop.GetItemCount(1); // TODO: support other currencies
+
+ private QuestStep? FindCurrentStep()
+ {
+ var currentQuest = _questController.CurrentQuest;
+ QuestSequence? currentSequence = currentQuest?.Quest.FindSequence(currentQuest.Sequence);
+ return currentSequence?.FindStep(currentQuest?.Step ?? 0);
+ }
+
+ public unsafe void UpdateShopStock(AtkUnitBase* addon)
+ {
+ var currentStep = FindCurrentStep();
+ if (currentStep == null || currentStep.InteractionType != EInteractionType.PurchaseItem)
+ {
+ _shop.ItemForSale = null;
+ return;
+ }
+
+ if (addon->AtkValuesCount != 625)
+ {
+ _logger.LogError("Unexpected amount of atkvalues for Shop addon ({AtkValueCount})", addon->AtkValuesCount);
+ _shop.ItemForSale = null;
+ return;
+ }
+
+ var atkValues = addon->AtkValues;
+
+ // Check if on 'Current Stock' tab?
+ if (atkValues[0].UInt != 0)
+ {
+ _shop.ItemForSale = null;
+ return;
+ }
+
+ uint itemCount = atkValues[2].UInt;
+ if (itemCount == 0)
+ {
+ _shop.ItemForSale = null;
+ return;
+ }
+
+ _shop.ItemForSale = Enumerable.Range(0, (int)itemCount)
+ .Select(i => new ItemForSale
+ {
+ Position = i,
+ ItemName = atkValues[14 + i].ReadAtkString(),
+ Price = atkValues[75 + i].UInt,
+ OwnedItems = atkValues[136 + i].UInt,
+ ItemId = atkValues[441 + i].UInt,
+ })
+ .FirstOrDefault(x => x.ItemId == currentStep.ItemId);
+ }
+
+ public unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow)
+ {
+ var buyItem = stackalloc AtkValue[]
+ {
+ new() { Type = ValueType.Int, Int = 0 },
+ new() { Type = ValueType.Int, Int = _shop.ItemForSale!.Position },
+ new() { Type = ValueType.Int, Int = buyNow },
+ new() { Type = 0, Int = 0 }
+ };
+ addonShop->FireCallback(4, buyItem);
+ }
+
+ public void SaveExternalPluginState()
+ {
+ }
+
+ public unsafe void RestoreExternalPluginState()
+ {
+ if (_gameGui.TryGetAddonByName("Shop", out AtkUnitBase* addonShop))
+ addonShop->FireCallbackInt(-1);
+ }
+}
if (step.DataId == null)
yield break;
}
+ else if (step.InteractionType == EInteractionType.PurchaseItem)
+ {
+ if (step.DataId == null)
+ yield break;
+ }
else if (step.InteractionType == EInteractionType.Snipe)
{
if (!configuration.General.AutomaticallyCompleteSnipeTasks)
yield return new Task(step.DataId.Value, quest, step.InteractionType,
step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId ||
- step.SkipConditions is { StepIf.Never: true }, step.PickUpItemId, step.SkipConditions?.StepIf);
+ step.SkipConditions is { StepIf.Never: true } || step.InteractionType == EInteractionType.PurchaseItem,
+ step.PickUpItemId, step.SkipConditions?.StepIf);
}
}
}
else
{
- if (ProgressContext != null && (ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed))
+ if (ProgressContext != null && (ProgressContext.WasSuccessful() ||
+ _interactionState == EInteractionState.InteractionConfirmed))
return ETaskResult.TaskComplete;
if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
--- /dev/null
+using Questionable.Model;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller.Steps.Interactions;
+
+internal static class PurchaseItem
+{
+ internal sealed class Factory : SimpleTaskFactory
+ {
+ public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.PurchaseItem)
+ return null;
+ throw new System.NotImplementedException();
+ }
+ }
+
+ internal sealed class PurchaseRequest
+ {
+
+ }
+}
internal interface ITaskExecutor
{
ITask CurrentTask { get; }
+ public InteractionProgressContext? ProgressContext { get; }
Type GetTaskType();
where T : class, ITask
{
protected T Task { get; set; } = null!;
- protected InteractionProgressContext? ProgressContext { get; set; }
+ public InteractionProgressContext? ProgressContext { get; set; }
ITask ITaskExecutor.CurrentTask => Task;
public bool WasInterrupted()
var questRow = _dataManager.GetExcelSheet<EventPathMove>()!.GetRow(rowId);
return questRow?.Unknown10;
}
+ else if (excelSheet is "GilShop")
+ {
+ var questRow = _dataManager.GetExcelSheet<GilShop>()!.GetRow(rowId);
+ return questRow?.Name;
+ }
else if (excelSheet is "ContentTalk" or null)
{
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
serviceCollection.AddSingleton<CombatController>();
serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ContextMenuController>();
+ serviceCollection.AddSingleton<ShopController>();
serviceCollection.AddSingleton<CraftworksSupplyController>();
serviceCollection.AddSingleton<CreditsController>();
serviceProvider.GetRequiredService<CreditsController>();
serviceProvider.GetRequiredService<HelpUiController>();
serviceProvider.GetRequiredService<LeveUiController>();
+ serviceProvider.GetRequiredService<ShopController>();
serviceProvider.GetRequiredService<QuestionableIpc>();
serviceProvider.GetRequiredService<DalamudInitializer>();
serviceProvider.GetRequiredService<AutoSnipeHandler>().Enable();
internal sealed class CreationUtilsComponent
{
+ private readonly QuestController _questController;
private readonly MovementController _movementController;
private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly ILogger<CreationUtilsComponent> _logger;
public CreationUtilsComponent(
+ QuestController questController,
MovementController movementController,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
Configuration configuration,
ILogger<CreationUtilsComponent> logger)
{
+ _questController = questController;
_movementController = movementController;
_gameFunctions = gameFunctions;
_questFunctions = questFunctions;
}
#endif
-#if false
+#if true
unsafe
{
var actionManager = ActionManager.Instance();
ImGui.Text(
$"A1: {actionManager->CastActionId} ({actionManager->LastUsedActionSequence} → {actionManager->LastHandledActionSequence})");
ImGui.Text($"A2: {actionManager->CastTimeElapsed} / {actionManager->CastTimeTotal}");
+ ImGui.Text($"{_questController.TaskQueue.CurrentTaskExecutor?.ProgressContext}");
}
#endif