using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Event; using ICRebuild.Core; using System.Collections.Concurrent; namespace ICRebuild.Runtime; public sealed unsafe class GameContext(IObjectTable objects, IClientState client, ICondition condition, IDataManager data, ISigScanner scanner, IGameInteropProvider interop, ITargetManager targets) { public IObjectTable Objects { get; } = objects; public IClientState Client { get; } = client; public ICondition Conditions { get; } = condition; public IDataManager Data { get; } = data; public ITargetManager Targets { get; } = targets; public FrameStore Frames { get; } = new(); public string GameVersion { get; } = ReadGameVersion(); private readonly ConcurrentQueue<(int Id,string Error)> faults = new(); public void Fault(int id,Exception error) { if(faults.Count<32) faults.Enqueue((id,error.Message)); } public void DrainFaults(ModuleManager modules) { while(faults.TryDequeue(out var fault)) { modules.Disable(fault.Id); modules.Status[fault.Id].State=ModuleState.Failed; modules.Status[fault.Id].Detail=$"运行错误,已停用:{fault.Error}"; } } public bool Ready => Frames.Read().Ready; public nint LocalAddress => Frames.Read().LocalAddress; public bool IsPvP => Frames.Read().IsPvP; public bool IsSlowed => Frames.Read().IsSlowed; public DungeonSnapshot ReadDungeon() => Frames.Read().Dungeon; // Called exclusively from Framework.Update, never from a native hook or worker thread. public void Refresh(bool sampleDungeon) { try { var player=Objects.LocalPlayer; if(!Client.IsLoggedIn || player==null || Conditions[ConditionFlag.BetweenAreas] || Conditions[ConditionFlag.BetweenAreas51]) { Frames.Clear(); return; } var dungeon=sampleDungeon ? SampleDungeon() : DungeonSnapshot.Empty; var target=Targets.Target; Frames.Publish(new(true,player.Address,Client.IsPvP,player.StatusList.Any(s=>StatusCatalog.AuditedSlow.Contains(s.StatusId)),dungeon, (nint)FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance(),target?.Address ?? 0,target?.EntityId ?? 0, (nint)(&((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)player.Address)->StatusManager), player.EntityId,Conditions[ConditionFlag.DutyRecorderPlayback])); } catch { Frames.Clear(); throw; } } private DungeonSnapshot SampleDungeon() { if (!Conditions[ConditionFlag.InDeepDungeon]) return DungeonSnapshot.Empty; var framework = EventFramework.Instance(); if (framework == null) return DungeonSnapshot.Empty; var dd = framework->GetInstanceContentDeepDungeon(); return dd == null || dd->Floor == 0 ? DungeonSnapshot.Empty : new(true, Client.TerritoryType, dd->DeepDungeonId, dd->Floor); } public Hook Hook(ResourceScope scope, string signature, T detour) where T : Delegate { if (!scanner.TryScanText(signature, out var address) || address == 0) throw new InvalidOperationException("签名未匹配,此模块未适配当前客户端。"); return scope.Own(interop.HookFromAddress(address, detour)); } public nint Scan(string signature) { if (!scanner.TryScanText(signature, out var address) || address == 0) throw new InvalidOperationException("签名未匹配。"); return address; } public nint StaticAddress(string signature, int offset) => scanner.GetStaticAddressFromSig(signature, offset); public Hook HookAddress(ResourceScope scope,nint address,T detour) where T:Delegate { if(address==0) throw new InvalidOperationException("原生函数地址未解析"); return scope.Own(interop.HookFromAddress(address,detour)); } public SendSession ReadSendSession() { var frame = Frames.Read(); return frame.Ready && !frame.IsReplay ? new(true, Client.TerritoryType, frame.LocalAddress, frame.LocalEntityId) : SendSession.Empty; } public AdaptationReport EvaluateAdaptation(ModuleManager modules) { var offsetsValid = System.Runtime.InteropServices.Marshal.OffsetOf( nameof(FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara.StatusManager)).ToInt32() == StatusBlockPolicy.ManagerOffset && System.Runtime.InteropServices.Marshal.OffsetOf( nameof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject.EntityId)).ToInt32() == StatusBlockPolicy.EntityIdOffset; return AdaptationManifest.Evaluate(modules, GameVersion, sig => scanner.TryScanText(sig, out var addr) && addr != 0 ? 1 : 0, offsetsValid); } public static string ReadGameVersion() { try { var dir = Path.GetDirectoryName(Environment.ProcessPath); var file = Path.Combine(dir ?? "", "ffxivgame.ver"); var version = File.Exists(file) ? File.ReadAllText(file).Trim() : "unknown"; return version.Length is > 0 and < 128 ? version : "unknown"; } catch { return "unknown"; } } }