Support non-english clients

pull/3/head v2.4
Liza 2023-10-08 17:21:43 +02:00
parent 3e68dd6e79
commit 500e25b508
Signed by: liza
GPG Key ID: 7199F8D727D55F67
8 changed files with 266 additions and 110 deletions

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Version>2.3</Version>
<Version>2.4</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -165,26 +165,6 @@ partial class DeliverooPlugin
return false;
}
private void ConfirmReward()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
return;
}
if (SelectSelectYesno(0, s => s.StartsWith("Exchange ")))
{
var nextItem = GetNextItemToPurchase(item);
if (nextItem != null && GetCurrentSealCount() >= _configuration.ReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.SelectRewardTier;
else
CurrentStage = Stage.CloseGcExchange;
_continueAt = DateTime.Now.AddSeconds(0.5);
}
}
private unsafe void CloseGcExchange()
{
if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&

View File

@ -9,7 +9,6 @@ using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Math;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -52,17 +51,25 @@ partial class DeliverooPlugin
private float GetDistanceToNpc(int npcId, out GameObject? o)
{
foreach (var obj in _objectTable)
try
{
if (obj.ObjectKind == ObjectKind.EventNpc && obj is Character c)
foreach (var obj in _objectTable)
{
if (GetNpcId(obj) == npcId)
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (obj != null && obj.ObjectKind == ObjectKind.EventNpc && obj is Character c)
{
o = obj;
return Vector3.Distance(_clientState.LocalPlayer!.Position, c.Position);
if (GetNpcId(obj) == npcId)
{
o = obj;
return Vector3.Distance(_clientState.LocalPlayer!.Position, c.Position);
}
}
}
}
catch (Exception)
{
// ignore
}
o = null;
return float.MaxValue;
@ -196,32 +203,4 @@ partial class DeliverooPlugin
{
return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded;
}
private unsafe bool SelectSelectString(int choice)
{
if (TryGetAddonByName<AddonSelectString>("SelectString", out var addonSelectString) &&
IsAddonReady(&addonSelectString->AtkUnitBase))
{
addonSelectString->AtkUnitBase.FireCallbackInt(choice);
return true;
}
return false;
}
private unsafe bool SelectSelectYesno(int choice, Predicate<string> predicate)
{
if (TryGetAddonByName<AddonSelectYesno>("SelectYesno", out var addonSelectYesno) &&
IsAddonReady(&addonSelectYesno->AtkUnitBase) &&
predicate(MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString()))
{
_pluginLog.Information(
$"Selecting choice={choice} for '{MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText)}'");
addonSelectYesno->AtkUnitBase.FireCallbackInt(choice);
return true;
}
return false;
}
}

View File

@ -0,0 +1,100 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Deliveroo;
partial class DeliverooPlugin
{
private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
{
_pluginLog.Verbose("SelectString post-setup");
string desiredText;
Action followUp;
if (CurrentStage == Stage.OpenGcSupply)
{
desiredText = _gameStrings.UndertakeSupplyAndProvisioningMission;
followUp = OpenGcSupplyFollowUp;
}
else if (CurrentStage == Stage.CloseGcSupply)
{
desiredText = _gameStrings.ClosePersonnelOfficerTalk;
followUp = CloseGcSupplyFollowUp;
}
else if (CurrentStage == Stage.CloseGcSupplyThenStop)
{
desiredText = _gameStrings.ClosePersonnelOfficerTalk;
followUp = CloseGcSupplyThenCloseFollowUp;
}
else
return;
_pluginLog.Verbose($"Looking for '{desiredText}' in prompt");
AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount;
for (int i = 0; i < entries; ++i)
{
var textPointer = addonSelectString->PopupMenu.PopupMenu.EntryNames[i];
if (textPointer == null)
continue;
var text = MemoryHelper.ReadSeStringNullTerminated((nint)textPointer).ToString();
_pluginLog.Verbose($" Choice {i} → {text}");
if (text == desiredText)
{
_pluginLog.Information($"Selecting choice {i} ({text})");
addonSelectString->AtkUnitBase.FireCallbackInt(i);
followUp();
return;
}
}
_pluginLog.Verbose($"Text '{desiredText}' was not found in prompt.");
}
private void OpenGcSupplyFollowUp()
{
CurrentStage = Stage.SelectExpertDeliveryTab;
}
private void CloseGcSupplyFollowUp()
{
if (GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else
{
// you can occasionally get a 'not enough seals' warning lol
_continueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}
private void CloseGcSupplyThenCloseFollowUp()
{
if (GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else if (GetCurrentSealCount() <=
_configuration.ReservedSealCount + GetNextItemToPurchase()!.SealCost)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else
{
_continueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Deliveroo;
partial class DeliverooPlugin
{
private unsafe void SelectYesNoPostSetup(AddonEvent type, AddonArgs args)
{
_pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", "");
_pluginLog.Verbose($"YesNo prompt: '{text}'");
if (CurrentStage == Stage.ConfirmReward &&
_gameStrings.ExchangeItems.IsMatch(text))
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
addonSelectYesNo->AtkUnitBase.FireCallbackInt(1);
CurrentStage = Stage.CloseGcExchange;
return;
}
_pluginLog.Information($"Selecting 'yes' ({text})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
var nextItem = GetNextItemToPurchase(item);
if (nextItem != null && GetCurrentSealCount() >= _configuration.ReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.SelectRewardTier;
else
CurrentStage = Stage.CloseGcExchange;
_continueAt = DateTime.Now.AddSeconds(0.5);
}
else if (CurrentStage == Stage.TurnInSelected &&
_gameStrings.TradeHighQualityItem == text)
{
_pluginLog.Information($"Selecting 'yes' ({text})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
}
}
}

View File

@ -20,12 +20,6 @@ partial class DeliverooPlugin
CurrentStage = Stage.OpenGcSupply;
}
private void OpenGcSupply()
{
if (SelectSelectString(0))
CurrentStage = Stage.SelectExpertDeliveryTab;
}
private unsafe void SelectExpertDeliveryTab()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
@ -147,9 +141,6 @@ partial class DeliverooPlugin
private unsafe void TurnInSelectedItem()
{
if (SelectSelectYesno(0, s => s == "Do you really want to trade a high-quality item?"))
return;
if (TryGetAddonByName<AddonGrandCompanySupplyReward>("GrandCompanySupplyReward",
out var addonSupplyReward) && IsAddonReady(&addonSupplyReward->AtkUnitBase))
{
@ -201,47 +192,6 @@ partial class DeliverooPlugin
}
}
private void CloseGcSupply()
{
if (SelectSelectString(3))
{
if (GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else
{
// you can occasionally get a 'not enough seals' warning lol
_continueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}
}
private void CloseGcSupplyThenStop()
{
if (SelectSelectString(3))
{
if (GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else if (GetCurrentSealCount() <=
_configuration.ReservedSealCount + GetNextItemToPurchase()!.SealCost)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else
{
_continueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}
}
private ItemFilterType ResolveSelectedSupplyFilter()
{
if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
@ -31,10 +32,12 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
private readonly ICondition _condition;
private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly Configuration _configuration;
private readonly GameStrings _gameStrings;
private readonly ExternalPluginHandler _externalPluginHandler;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
@ -50,7 +53,8 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
public DeliverooPlugin(DalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
IFramework framework, IClientState clientState, IObjectTable objectTable, ITargetManager targetManager,
IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog)
IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog,
IAddonLifecycle addonLifecycle)
{
_pluginInterface = pluginInterface;
_chatGui = chatGui;
@ -62,7 +66,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_condition = condition;
_commandManager = commandManager;
_pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
_gameStrings = new GameStrings(dataManager, _pluginLog);
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _framework, _pluginLog);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_gcRewardsCache = new GcRewardsCache(dataManager);
@ -88,6 +94,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (_configuration.AddVentureIfNoItemToPurchaseSelected())
_pluginInterface.SavePluginConfig(_configuration);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
}
internal CharacterConfiguration? CharacterConfiguration { get; set; }
@ -215,7 +224,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.OpenGcSupply:
OpenGcSupply();
// see SelectStringPostSetup
break;
case Stage.SelectExpertDeliveryTab:
@ -243,11 +252,11 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.CloseGcSupply:
CloseGcSupply();
// see SelectStringPostSetup
break;
case Stage.CloseGcSupplyThenStop:
CloseGcSupplyThenStop();
// see SelectStringPostSetup
break;
case Stage.TargetQuartermaster:
@ -267,7 +276,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.ConfirmReward:
ConfirmReward();
// see SelectYesNoPostSetup
break;
case Stage.CloseGcExchange:
@ -292,6 +301,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_commandManager.RemoveHandler("/deliveroo");
_clientState.Logout -= Logout;
_clientState.Login -= Login;

View File

@ -0,0 +1,88 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
using Lumina.Text;
using Lumina.Text.Payloads;
namespace Deliveroo.GameData;
internal sealed class GameStrings
{
private readonly IDataManager _dataManager;
private readonly IPluginLog _pluginLog;
public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{
_dataManager = dataManager;
_pluginLog = pluginLog;
UndertakeSupplyAndProvisioningMission =
GetDialogue<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_002");
ClosePersonnelOfficerTalk =
GetDialogue<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_004");
ExchangeItems = GetRegex<Addon>(4928, addon => addon.Text)
?? throw new Exception($"Unable to resolve {nameof(ExchangeItems)}");
TradeHighQualityItem = GetString<Addon>(102434, addon => addon.Text)
?? throw new Exception($"Unable to resolve {nameof(TradeHighQualityItem)}");
}
public string UndertakeSupplyAndProvisioningMission { get; }
public string ClosePersonnelOfficerTalk { get; }
public Regex ExchangeItems { get; }
public string TradeHighQualityItem { get; }
private string GetDialogue<T>(string key)
where T : QuestDialogueText
{
string result = _dataManager.GetExcelSheet<T>()!
.Single(x => x.Key == key)
.Value
.ToString();
_pluginLog.Verbose($"{typeof(T).Name}.{key} => {result}");
return result;
}
private SeString? GetSeString<T>(uint rowId, Func<T, SeString?> mapper)
where T : ExcelRow
{
var row = _dataManager.GetExcelSheet<T>()?.GetRow(rowId);
if (row == null)
return null;
return mapper(row);
}
private string? GetString<T>(uint rowId, Func<T, SeString?> mapper)
where T : ExcelRow
{
string? text = GetSeString(rowId, mapper)?.ToString();
_pluginLog.Verbose($"{typeof(T).Name}.{rowId} => {text}");
return text;
}
private Regex? GetRegex<T>(uint rowId, Func<T, SeString?> mapper)
where T : ExcelRow
{
SeString? text = GetSeString(rowId, mapper);
string regex = string.Join("", text.Payloads.Select(payload =>
{
if (payload is TextPayload)
return Regex.Escape(payload.RawString);
else
return ".*";
}));
_pluginLog.Verbose($"{typeof(T).Name}.{rowId} => /{regex}/");
return new Regex(regex);
}
[Sheet("custom/000/ComDefGrandCompanyOfficer_00073")]
private class ComDefGrandCompanyOfficer : QuestDialogueText
{
}
}