Show available daily quests in journal

This commit is contained in:
Liza 2024-12-08 18:13:22 +01:00
parent ba0cd60dae
commit 70c47e20fb
Signed by: liza
GPG Key ID: 8DD6D21C03BB0848
6 changed files with 183 additions and 2 deletions

View File

@ -235,6 +235,16 @@ internal sealed class QuestData
.ToList(); .ToList();
} }
public List<QuestInfo> GetAllByAlliedSociety(EAlliedSociety alliedSociety)
{
return _quests.Values
.Where(x => x is QuestInfo)
.Cast<QuestInfo>()
.Where(x => x.AlliedSociety == alliedSociety)
.OrderBy(x => x.QuestId)
.ToList();
}
public List<QuestInfo> GetClassJobQuests(EClassJob classJob) public List<QuestInfo> GetClassJobQuests(EClassJob classJob)
{ {
List<uint> chapterIds = classJob switch List<uint> chapterIds = classJob switch

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Functions;
internal sealed class AlliedSocietyQuestFunctions
{
private readonly ILogger<AlliedSocietyQuestFunctions> _logger;
private readonly Dictionary<EAlliedSociety, List<NpcData>> _questsByAlliedSociety = [];
private readonly Dictionary<(uint NpcDataId, byte Seed, bool OutranksAll), List<QuestId>> _dailyQuests = [];
public AlliedSocietyQuestFunctions(QuestData questData, ILogger<AlliedSocietyQuestFunctions> logger)
{
_logger = logger;
foreach (var alliedSociety in Enum.GetValues<EAlliedSociety>().Where(x => x != EAlliedSociety.None))
{
var allQuests = questData.GetAllByAlliedSociety(alliedSociety);
var questsByIssuer = allQuests
.Where(x => x.IsRepeatable)
.GroupBy(x => x.IssuerDataId)
.ToDictionary(x => x.Key,
x => x.OrderBy(y => y.AlliedSocietyQuestGroup == 3).ThenBy(y => y.QuestId).ToList());
foreach ((uint issuerDataId, List<QuestInfo> quests) in questsByIssuer)
{
var npcData = new NpcData { IssuerDataId = issuerDataId, AllQuests = quests };
if (_questsByAlliedSociety.TryGetValue(alliedSociety, out List<NpcData>? existingNpcs))
existingNpcs.Add(npcData);
else
_questsByAlliedSociety[alliedSociety] = [npcData];
}
}
}
public unsafe List<QuestId> GetAvailableAlliedSocietyQuests(EAlliedSociety alliedSociety)
{
byte rankData = QuestManager.Instance()->BeastReputation[(int)alliedSociety - 1].Rank;
byte currentRank = (byte)(rankData & 0x7F);
if (currentRank == 0)
return [];
bool rankedUp = (rankData & 0x80) != 0;
byte seed = 183;
List<QuestId> result = [];
foreach (NpcData npcData in _questsByAlliedSociety[alliedSociety])
{
bool outranksAll = npcData.AllQuests.All(x => currentRank > x.AlliedSocietyRank);
var key = (NpcDataId: npcData.IssuerDataId, seed, outranksAll);
if (_dailyQuests.TryGetValue(key, out List<QuestId>? questIds))
result.AddRange(questIds);
else
{
var quests = CalculateAvailableQuests(npcData.AllQuests, seed, outranksAll, currentRank, rankedUp);
_logger.LogInformation("Available for {Tribe} (Issuer: {IssuerId}: {Quests}", alliedSociety, npcData.IssuerDataId, string.Join(", ", quests));
_dailyQuests[key] = quests;
result.AddRange(quests);
}
}
return result;
}
private static List<QuestId> CalculateAvailableQuests(List<QuestInfo> allQuests, byte seed, bool outranksAll,
byte currentRank, bool rankedUp)
{
List<QuestInfo> eligible = [.. allQuests.Where(q => IsEligible(q, currentRank, rankedUp))];
List<QuestInfo> available = [];
if (eligible.Count == 0)
return [];
var rng = new Rng(seed);
if (outranksAll)
{
for (int i = 0, cnt = Math.Min(eligible.Count, 3); i < cnt; ++i)
{
var index = rng.Next(eligible.Count);
while (available.Contains(eligible[index]))
index = (index + 1) % eligible.Count;
available.Add(eligible[index]);
}
}
else
{
var firstExclusive = eligible.FindIndex(q => q.AlliedSocietyQuestGroup == 3);
if (firstExclusive >= 0)
available.Add(eligible[firstExclusive + rng.Next(eligible.Count - firstExclusive)]);
else
firstExclusive = eligible.Count;
for (int i = available.Count, cnt = Math.Min(firstExclusive, 3); i < cnt; ++i)
{
var index = rng.Next(firstExclusive);
while (available.Contains(eligible[index]))
index = (index + 1) % firstExclusive;
available.Add(eligible[index]);
}
}
return available.Select(x => (QuestId)x.QuestId).ToList();
}
private static bool IsEligible(QuestInfo questInfo, byte currentRank, bool rankedUp)
{
return rankedUp ? questInfo.AlliedSocietyRank == currentRank : questInfo.AlliedSocietyRank <= currentRank;
}
private sealed class NpcData
{
public required uint IssuerDataId { get; init; }
public required List<QuestInfo> AllQuests { get; init; } = [];
}
private record struct Rng(uint S0, uint S1 = 0, uint S2 = 0, uint S3 = 0)
{
public int Next(int range)
{
(S0, S1, S2, S3) = (S3, Transform(S0, S1), S1, S2);
return (int)(S1 % range);
}
// returns new value for s1
private static uint Transform(uint s0, uint s1)
{
var temp = s0 ^ (s0 << 11);
return s1 ^ temp ^ ((temp ^ (s1 >> 11)) >> 8);
}
}
}

View File

@ -28,6 +28,7 @@ internal sealed unsafe class QuestFunctions
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly AetheryteFunctions _aetheryteFunctions; private readonly AetheryteFunctions _aetheryteFunctions;
private readonly AlliedSocietyQuestFunctions _alliedSocietyQuestFunctions;
private readonly AlliedSocietyData _alliedSocietyData; private readonly AlliedSocietyData _alliedSocietyData;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
@ -38,6 +39,7 @@ internal sealed unsafe class QuestFunctions
QuestRegistry questRegistry, QuestRegistry questRegistry,
QuestData questData, QuestData questData,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
AlliedSocietyQuestFunctions alliedSocietyQuestFunctions,
AlliedSocietyData alliedSocietyData, AlliedSocietyData alliedSocietyData,
Configuration configuration, Configuration configuration,
IDataManager dataManager, IDataManager dataManager,
@ -47,6 +49,7 @@ internal sealed unsafe class QuestFunctions
_questRegistry = questRegistry; _questRegistry = questRegistry;
_questData = questData; _questData = questData;
_aetheryteFunctions = aetheryteFunctions; _aetheryteFunctions = aetheryteFunctions;
_alliedSocietyQuestFunctions = alliedSocietyQuestFunctions;
_alliedSocietyData = alliedSocietyData; _alliedSocietyData = alliedSocietyData;
_configuration = configuration; _configuration = configuration;
_dataManager = dataManager; _dataManager = dataManager;
@ -447,9 +450,12 @@ internal sealed unsafe class QuestFunctions
if (IsQuestAccepted(questId)) if (IsQuestAccepted(questId))
return false; return false;
if (quest.Info.AlliedSociety != EAlliedSociety.None)
{
if (QuestManager.Instance()->IsDailyQuestCompleted(questId.Value)) if (QuestManager.Instance()->IsDailyQuestCompleted(questId.Value))
return false; return false;
} }
}
else else
{ {
if (IsQuestAcceptedOrComplete(questId)) if (IsQuestAcceptedOrComplete(questId))
@ -546,6 +552,9 @@ internal sealed unsafe class QuestFunctions
if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany()) if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany())
return true; return true;
if (questInfo.AlliedSociety != EAlliedSociety.None)
return !IsDailyAlliedSocietyQuestAndAvailableToday(questId);
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
} }
@ -569,6 +578,21 @@ internal sealed unsafe class QuestFunctions
return !HasCompletedPreviousQuests(questInfo, null); return !HasCompletedPreviousQuests(questInfo, null);
} }
public bool IsDailyAlliedSocietyQuest(QuestId questId)
{
var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
return questInfo.AlliedSociety != EAlliedSociety.None && questInfo.IsRepeatable;
}
public bool IsDailyAlliedSocietyQuestAndAvailableToday(QuestId questId)
{
if (!IsDailyAlliedSocietyQuest(questId))
return false;
var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
return _alliedSocietyQuestFunctions.GetAvailableAlliedSocietyQuests(questInfo.AlliedSociety).Contains(questId);
}
public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null) public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null)
{ {
if (elementId is QuestId questId) if (elementId is QuestId questId)

View File

@ -60,6 +60,8 @@ internal sealed class QuestInfo : IQuestInfo
PreviousInstanceContentJoin = (EQuestJoin)quest.InstanceContentJoin; PreviousInstanceContentJoin = (EQuestJoin)quest.InstanceContentJoin;
GrandCompany = (GrandCompany)quest.GrandCompany.RowId; GrandCompany = (GrandCompany)quest.GrandCompany.RowId;
AlliedSociety = (EAlliedSociety)quest.BeastTribe.RowId; AlliedSociety = (EAlliedSociety)quest.BeastTribe.RowId;
AlliedSocietyQuestGroup = quest.Unknown11;
AlliedSocietyRank = (int)quest.BeastReputationRank.RowId;
ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.ValueNullable!); ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.ValueNullable!);
IsSeasonalEvent = quest.Festival.RowId != 0; IsSeasonalEvent = quest.Festival.RowId != 0;
NewGamePlusChapter = newGamePlusChapter; NewGamePlusChapter = newGamePlusChapter;
@ -85,6 +87,8 @@ internal sealed class QuestInfo : IQuestInfo
public bool CompletesInstantly { get; } public bool CompletesInstantly { get; }
public GrandCompany GrandCompany { get; } public GrandCompany GrandCompany { get; }
public EAlliedSociety AlliedSociety { get; } public EAlliedSociety AlliedSociety { get; }
public byte AlliedSocietyQuestGroup { get; }
public int AlliedSocietyRank { get; }
public IReadOnlyList<EClassJob> ClassJobs { get; } public IReadOnlyList<EClassJob> ClassJobs { get; }
public bool IsSeasonalEvent { get; } public bool IsSeasonalEvent { get; }
public uint NewGamePlusChapter { get; } public uint NewGamePlusChapter { get; }

View File

@ -111,6 +111,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<GameFunctions>(); serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>(); serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<QuestFunctions>(); serviceCollection.AddSingleton<QuestFunctions>();
serviceCollection.AddSingleton<AlliedSocietyQuestFunctions>();
serviceCollection.AddSingleton<DalamudReflector>(); serviceCollection.AddSingleton<DalamudReflector>();
serviceCollection.AddSingleton<AetherCurrentData>(); serviceCollection.AddSingleton<AetherCurrentData>();

View File

@ -24,6 +24,15 @@ internal sealed class UiUtils
{ {
if (_questFunctions.IsQuestAccepted(elementId)) if (_questFunctions.IsQuestAccepted(elementId))
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active"); return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
else if (elementId is QuestId questId && _questFunctions.IsDailyAlliedSocietyQuestAndAvailableToday(questId))
{
if (!_questFunctions.IsReadyToAcceptQuest(questId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_questFunctions.IsQuestComplete(questId))
return (ImGuiColors.ParsedBlue, FontAwesomeIcon.Running, "Available (Complete)");
else
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");
}
else if (_questFunctions.IsQuestAcceptedOrComplete(elementId)) else if (_questFunctions.IsQuestAcceptedOrComplete(elementId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_questFunctions.IsQuestUnobtainable(elementId)) else if (_questFunctions.IsQuestUnobtainable(elementId))