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

View File

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

View File

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

View File

@ -24,6 +24,15 @@ internal sealed class UiUtils
{
if (_questFunctions.IsQuestAccepted(elementId))
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))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_questFunctions.IsQuestUnobtainable(elementId))