Add experimental PurchaseItem step
This commit is contained in:
parent
3b6dc2e03b
commit
09f8cb9078
2
LLib
2
LLib
@ -1 +1 @@
|
||||
Subproject commit 43c3dba112c202e2d0ff1a6909020c2b83e20dc3
|
||||
Subproject commit e6e3a1f29715e2af4976dd7338ed2f09ae82c99c
|
@ -127,6 +127,8 @@ internal static class QuestStepExtensions
|
||||
.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(),
|
||||
|
@ -43,7 +43,14 @@
|
||||
"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",
|
||||
|
@ -119,6 +119,7 @@
|
||||
"Combat",
|
||||
"UseItem",
|
||||
"EquipItem",
|
||||
"PurchaseItem",
|
||||
"EquipRecommended",
|
||||
"Say",
|
||||
"Emote",
|
||||
@ -707,6 +708,44 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
|
@ -16,6 +16,7 @@ public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>
|
||||
{ EInteractionType.Combat, "Combat" },
|
||||
{ EInteractionType.UseItem, "UseItem" },
|
||||
{ EInteractionType.EquipItem, "EquipItem" },
|
||||
{ EInteractionType.PurchaseItem, "PurchaseItem" },
|
||||
{ EInteractionType.EquipRecommended, "EquipRecommended" },
|
||||
{ EInteractionType.Say, "Say" },
|
||||
{ EInteractionType.Emote, "Emote" },
|
||||
|
@ -15,6 +15,7 @@ public enum EInteractionType
|
||||
Combat,
|
||||
UseItem,
|
||||
EquipItem,
|
||||
PurchaseItem,
|
||||
EquipRecommended,
|
||||
Say,
|
||||
Emote,
|
||||
|
12
Questionable.Model/Questing/PurchaseMenu.cs
Normal file
12
Questionable.Model/Questing/PurchaseMenu.cs
Normal 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; }
|
||||
}
|
@ -78,6 +78,7 @@ public sealed class QuestStep
|
||||
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))]
|
||||
|
@ -46,6 +46,7 @@ internal sealed class InteractionUiController : IDisposable
|
||||
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;
|
||||
|
||||
@ -67,6 +68,7 @@ internal sealed class InteractionUiController : IDisposable
|
||||
ITargetManager targetManager,
|
||||
IPluginLog pluginLog,
|
||||
IClientState clientState,
|
||||
ShopController shopController,
|
||||
ILogger<InteractionUiController> logger)
|
||||
{
|
||||
_addonLifecycle = addonLifecycle;
|
||||
@ -83,6 +85,7 @@ internal sealed class InteractionUiController : IDisposable
|
||||
_gameGui = gameGui;
|
||||
_targetManager = targetManager;
|
||||
_clientState = clientState;
|
||||
_shopController = shopController;
|
||||
_logger = logger;
|
||||
|
||||
_returnRegex = _dataManager.GetExcelSheet<Addon>()!.GetRow(196)!.GetRegex(addon => addon.Text, pluginLog)!;
|
||||
@ -334,7 +337,17 @@ internal sealed class InteractionUiController : IDisposable
|
||||
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
|
||||
@ -516,6 +529,13 @@ internal sealed class InteractionUiController : IDisposable
|
||||
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 &&
|
||||
|
156
Questionable/Controller/GameUi/ShopController.cs
Normal file
156
Questionable/Controller/GameUi/ShopController.cs
Normal file
@ -0,0 +1,156 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -35,6 +35,11 @@ internal static class Interact
|
||||
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)
|
||||
@ -51,7 +56,8 @@ internal static class Interact
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +153,8 @@ internal static class Interact
|
||||
}
|
||||
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])
|
||||
|
22
Questionable/Controller/Steps/Interactions/PurchaseItem.cs
Normal file
22
Questionable/Controller/Steps/Interactions/PurchaseItem.cs
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ namespace Questionable.Controller.Steps;
|
||||
internal interface ITaskExecutor
|
||||
{
|
||||
ITask CurrentTask { get; }
|
||||
public InteractionProgressContext? ProgressContext { get; }
|
||||
|
||||
Type GetTaskType();
|
||||
|
||||
@ -19,7 +20,7 @@ internal abstract class TaskExecutor<T> : ITaskExecutor
|
||||
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()
|
||||
|
@ -91,6 +91,11 @@ internal sealed class ExcelFunctions
|
||||
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);
|
||||
|
@ -224,6 +224,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceCollection.AddSingleton<CombatController>();
|
||||
serviceCollection.AddSingleton<GatheringController>();
|
||||
serviceCollection.AddSingleton<ContextMenuController>();
|
||||
serviceCollection.AddSingleton<ShopController>();
|
||||
|
||||
serviceCollection.AddSingleton<CraftworksSupplyController>();
|
||||
serviceCollection.AddSingleton<CreditsController>();
|
||||
@ -284,6 +285,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceProvider.GetRequiredService<CreditsController>();
|
||||
serviceProvider.GetRequiredService<HelpUiController>();
|
||||
serviceProvider.GetRequiredService<LeveUiController>();
|
||||
serviceProvider.GetRequiredService<ShopController>();
|
||||
serviceProvider.GetRequiredService<QuestionableIpc>();
|
||||
serviceProvider.GetRequiredService<DalamudInitializer>();
|
||||
serviceProvider.GetRequiredService<AutoSnipeHandler>().Enable();
|
||||
|
@ -29,6 +29,7 @@ namespace Questionable.Windows.QuestComponents;
|
||||
|
||||
internal sealed class CreationUtilsComponent
|
||||
{
|
||||
private readonly QuestController _questController;
|
||||
private readonly MovementController _movementController;
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
@ -43,6 +44,7 @@ internal sealed class CreationUtilsComponent
|
||||
private readonly ILogger<CreationUtilsComponent> _logger;
|
||||
|
||||
public CreationUtilsComponent(
|
||||
QuestController questController,
|
||||
MovementController movementController,
|
||||
GameFunctions gameFunctions,
|
||||
QuestFunctions questFunctions,
|
||||
@ -56,6 +58,7 @@ internal sealed class CreationUtilsComponent
|
||||
Configuration configuration,
|
||||
ILogger<CreationUtilsComponent> logger)
|
||||
{
|
||||
_questController = questController;
|
||||
_movementController = movementController;
|
||||
_gameFunctions = gameFunctions;
|
||||
_questFunctions = questFunctions;
|
||||
@ -154,13 +157,14 @@ internal sealed class CreationUtilsComponent
|
||||
}
|
||||
#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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user