Support non-english clients

This commit is contained in:
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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net7.0-windows</TargetFramework>
<Version>2.3</Version> <Version>2.4</Version>
<LangVersion>11.0</LangVersion> <LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -165,26 +165,6 @@ partial class DeliverooPlugin
return false; 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() private unsafe void CloseGcExchange()
{ {
if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) && 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;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Math; using FFXIVClientStructs.FFXIV.Common.Math;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@ -51,10 +50,13 @@ partial class DeliverooPlugin
internal unsafe byte GetGrandCompanyRank() => PlayerState.Instance()->GetGrandCompanyRank(); internal unsafe byte GetGrandCompanyRank() => PlayerState.Instance()->GetGrandCompanyRank();
private float GetDistanceToNpc(int npcId, out GameObject? o) private float GetDistanceToNpc(int npcId, out GameObject? o)
{
try
{ {
foreach (var obj in _objectTable) foreach (var obj in _objectTable)
{ {
if (obj.ObjectKind == ObjectKind.EventNpc && obj is Character c) // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (obj != null && obj.ObjectKind == ObjectKind.EventNpc && obj is Character c)
{ {
if (GetNpcId(obj) == npcId) if (GetNpcId(obj) == npcId)
{ {
@ -63,6 +65,11 @@ partial class DeliverooPlugin
} }
} }
} }
}
catch (Exception)
{
// ignore
}
o = null; o = null;
return float.MaxValue; return float.MaxValue;
@ -196,32 +203,4 @@ partial class DeliverooPlugin
{ {
return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; 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; CurrentStage = Stage.OpenGcSupply;
} }
private void OpenGcSupply()
{
if (SelectSelectString(0))
CurrentStage = Stage.SelectExpertDeliveryTab;
}
private unsafe void SelectExpertDeliveryTab() private unsafe void SelectExpertDeliveryTab()
{ {
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply); var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
@ -147,9 +141,6 @@ partial class DeliverooPlugin
private unsafe void TurnInSelectedItem() private unsafe void TurnInSelectedItem()
{ {
if (SelectSelectYesno(0, s => s == "Do you really want to trade a high-quality item?"))
return;
if (TryGetAddonByName<AddonGrandCompanySupplyReward>("GrandCompanySupplyReward", if (TryGetAddonByName<AddonGrandCompanySupplyReward>("GrandCompanySupplyReward",
out var addonSupplyReward) && IsAddonReady(&addonSupplyReward->AtkUnitBase)) 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() private ItemFilterType ResolveSelectedSupplyFilter()
{ {
if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true }) if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })

View File

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