using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Runtime.InteropServices; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Client.Game; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Functions; internal sealed class AlliedSocietyQuestFunctions { private sealed class NpcData { public required uint IssuerDataId { get; init; } public required List AllQuests { get; init; } = new List(); } private record struct Rng(uint S0, uint S1 = 0u, uint S2 = 0u, uint S3 = 0u) { public int Next(int range) { uint s = S3; uint s2 = Transform(S0, S1); uint s3 = S1; uint s4 = S2; S0 = s; S1 = s2; S2 = s3; S3 = s4; return (int)(S1 % range); } private static uint Transform(uint s0, uint s1) { uint num = s0 ^ (s0 << 11); return s1 ^ num ^ ((num ^ (s1 >> 11)) >> 8); } } private readonly ILogger _logger; private readonly QuestData _questData; private readonly Dictionary> _questsByAlliedSociety = new Dictionary>(); private readonly Dictionary<(uint NpcDataId, byte Seed, bool OutranksAll, bool RankedUp), List> _dailyQuests = new Dictionary<(uint, byte, bool, bool), List>(); private readonly ImmutableDictionary _reputationRequiredByRank; private readonly ImmutableDictionary _questReputationRewards; private readonly ImmutableDictionary _maxRankByAlliedSociety; private readonly ImmutableDictionary<(EAlliedSociety, byte), QuestId> _rankUpQuests; public AlliedSocietyQuestFunctions(QuestData questData, IDataManager dataManager, ILogger logger) { _logger = logger; _questData = questData; _reputationRequiredByRank = (from x in dataManager.GetExcelSheet() where x.RowId != 0 select x).ToImmutableDictionary((BeastReputationRank x) => x.RowId, (BeastReputationRank x) => x.RequiredReputation); _questReputationRewards = (from x in dataManager.GetExcelSheet() where x.RowId != 0 && x.BeastTribe.RowId != 0 && x.BeastReputationValue > 0 select x).ToImmutableDictionary((Lumina.Excel.Sheets.Quest x) => x.RowId, (Lumina.Excel.Sheets.Quest x) => x.BeastReputationValue); _maxRankByAlliedSociety = (from x in dataManager.GetExcelSheet() where x.RowId != 0 && !x.Name.IsEmpty && Enum.IsDefined(typeof(EAlliedSociety), (byte)x.RowId) select x).ToImmutableDictionary((BeastTribe x) => (EAlliedSociety)x.RowId, (BeastTribe x) => x.MaxRank); Dictionary<(EAlliedSociety, byte), QuestId> dictionary = new Dictionary<(EAlliedSociety, byte), QuestId>(); foreach (EAlliedSociety item in from x in Enum.GetValues() where x != EAlliedSociety.None select x) { foreach (QuestInfo item2 in from x in questData.GetAllByAlliedSociety(item) where !x.IsRepeatable select x) { byte b = (byte)item2.AlliedSocietyRank; if (b > 0) { dictionary[(item, b)] = (QuestId)item2.QuestId; } } } _rankUpQuests = dictionary.ToImmutableDictionary(); foreach (EAlliedSociety item3 in from x in Enum.GetValues() where x != EAlliedSociety.None select x) { foreach (KeyValuePair> item4 in (from x in questData.GetAllByAlliedSociety(item3) where x.IsRepeatable group x by x.IssuerDataId).ToDictionary((IGrouping x) => x.Key, (IGrouping x) => (from y in x orderby y.AlliedSocietyQuestGroup == 3, y.QuestId select y).ToList())) { item4.Deconstruct(out var key, out var value); uint issuerDataId = key; List allQuests = value; NpcData npcData = new NpcData { IssuerDataId = issuerDataId, AllQuests = allQuests }; if (_questsByAlliedSociety.TryGetValue(item3, out List value2)) { value2.Add(npcData); continue; } Dictionary> questsByAlliedSociety = _questsByAlliedSociety; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); CollectionsMarshal.AsSpan(list)[0] = npcData; questsByAlliedSociety[item3] = list; } } } public unsafe List GetAvailableAlliedSocietyQuests(EAlliedSociety alliedSociety) { if (alliedSociety == EAlliedSociety.None) { return new List(); } byte rank = QuestManager.Instance()->BeastReputation[(int)(alliedSociety - 1)].Rank; byte currentRank = (byte)(rank & 0x7F); if (currentRank == 0) { return new List(); } bool flag = (rank & 0x80) != 0; byte dailyQuestSeed = QuestManager.Instance()->DailyQuestSeed; List list = new List(); if (!_questsByAlliedSociety.TryGetValue(alliedSociety, out List value)) { return list; } foreach (NpcData item in value) { bool flag2 = item.AllQuests.All((QuestInfo x) => currentRank > x.AlliedSocietyRank); (uint, byte, bool, bool) key = (item.IssuerDataId, dailyQuestSeed, flag2, flag); if (_dailyQuests.TryGetValue(key, out List value2)) { list.AddRange(value2); continue; } List list2 = CalculateAvailableQuests(item.AllQuests, dailyQuestSeed, flag2, currentRank, flag); _logger.LogDebug("Available for {Tribe} (Seed: {Seed}, Issuer: {IssuerId}): {Quests}", alliedSociety, dailyQuestSeed, item.IssuerDataId, string.Join(", ", list2)); _dailyQuests[key] = list2; list.AddRange(list2); } return list; } public unsafe (byte CurrentRank, ushort CurrentValue, ushort MaxValue, bool IsMaxRank, bool IsAtCapacity)? GetReputationStatus(EAlliedSociety alliedSociety) { BeastReputationWork beastReputationWork = QuestManager.Instance()->BeastReputation[(int)(alliedSociety - 1)]; byte b = (byte)(beastReputationWork.Rank & 0x7F); if (b == 0) { return null; } ushort value = beastReputationWork.Value; ushort valueOrDefault = _reputationRequiredByRank.GetValueOrDefault(b, ushort.MaxValue); byte valueOrDefault2 = _maxRankByAlliedSociety.GetValueOrDefault(alliedSociety, 8); bool item = b >= valueOrDefault2; bool item2 = value >= valueOrDefault; return (b, value, valueOrDefault, item, item2); } public QuestId? GetRankUpQuest(EAlliedSociety alliedSociety, byte currentRank) { return _rankUpQuests.GetValueOrDefault((alliedSociety, currentRank)); } public ushort GetQuestReputationReward(QuestId questId) { return _questReputationRewards.GetValueOrDefault(questId.Value, 0); } public bool WouldOvercapReputation(QuestId questId) { if (!_questData.TryGetQuestInfo(questId, out IQuestInfo questInfo) || !(questInfo is QuestInfo questInfo2)) { return false; } if (questInfo2.AlliedSociety == EAlliedSociety.None) { return false; } (byte, ushort, ushort, bool, bool)? reputationStatus = GetReputationStatus(questInfo2.AlliedSociety); if (!reputationStatus.HasValue) { return false; } (byte, ushort, ushort, bool, bool) value = reputationStatus.Value; ushort item = value.Item2; ushort item2 = value.Item3; if (value.Item4) { return false; } ushort questReputationReward = GetQuestReputationReward(questId); return item + questReputationReward > item2; } private static List CalculateAvailableQuests(List allQuests, byte seed, bool outranksAll, byte currentRank, bool rankedUp) { List list = allQuests.Where((QuestInfo q) => IsEligible(q, currentRank, rankedUp)).ToList(); List list2 = new List(); if (list.Count == 0) { return new List(); } Rng rng = new Rng(seed); if (outranksAll) { int num = 0; for (int num2 = Math.Min(list.Count, 3); num < num2; num++) { int num3 = rng.Next(list.Count); while (list2.Contains(list[num3])) { num3 = (num3 + 1) % list.Count; } list2.Add(list[num3]); } } else { int num4 = list.FindIndex((QuestInfo q) => q.AlliedSocietyQuestGroup == 3); if (num4 >= 0) { list2.Add(list[num4 + rng.Next(list.Count - num4)]); } else { num4 = list.Count; } int num5 = list2.Count; for (int num6 = Math.Min(num4, 3); num5 < num6; num5++) { int num7 = rng.Next(num4); while (list2.Contains(list[num7])) { num7 = (num7 + 1) % num4; } list2.Add(list[num7]); } } return list2.Select((QuestInfo x) => (QuestId)x.QuestId).ToList(); } private static bool IsEligible(QuestInfo questInfo, byte currentRank, bool rankedUp) { if (!rankedUp) { return questInfo.AlliedSocietyRank <= currentRank; } return questInfo.AlliedSocietyRank == currentRank; } }