Initial import of IC.Rebuild

This commit is contained in:
2026-10-13 22:55:05 +08:00
commit 16d61f7a5e
87 changed files with 17054 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
<ItemGroup><Reference Include="Lumina;Lumina.Excel"><HintPath>$(APPDATA)\XIVLauncher\addon\Hooks\dev\%(Identity).dll</HintPath></Reference></ItemGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
using Lumina;
using System.Text.Json;
if(args.Length is <2 or >3) throw new ArgumentException("ActionAudit sqpack-path name-fragment|--movement [output-json]");
using var game=new GameData(args[0]);
var rows=new List<object>();
var timelines=new HashSet<uint>();
var ids=args[1].StartsWith("--ids:") ? args[1][6..].Split(',').Select(uint.Parse).ToHashSet() : null;
foreach(var action in game.GetExcelSheet<Lumina.Excel.Sheets.Action>() ?? throw new InvalidDataException("Action sheet unavailable"))
if(ids!=null ? ids.Contains(action.RowId) : args[1]=="--movement" ? action.IsPlayerAction && action.AffectsPosition : action.Name.ToString().Contains(args[1],StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{action.RowId} {action.Name}: AffectsPosition={action.AffectsPosition}, Hostile={action.CanTargetHostile}, AnimationEnd={action.AnimationEnd.RowId}");
rows.Add(new { ActionId=action.RowId,Name=action.Name.ToString(),Timeline=action.AnimationEnd.RowId,PvP=action.IsPvP,PlayerAction=action.IsPlayerAction,AffectsPosition=action.AffectsPosition });
if(action.AnimationEnd.RowId is >0 and <=ushort.MaxValue) timelines.Add(action.AnimationEnd.RowId);
}
Console.WriteLine($"Skills={rows.Count}, Timelines={timelines.Count}");
if(args.Length==3) File.WriteAllText(args[2],JsonSerializer.Serialize(rows,new JsonSerializerOptions { WriteIndented=true }));
+4
View File
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
<ItemGroup><Compile Include="../../Core/**/*.cs" Link="Core/%(Filename)%(Extension)" /></ItemGroup>
</Project>
+62
View File
@@ -0,0 +1,62 @@
using System.Security.Cryptography;
using System.Text.Json;
using ICRebuild.Core;
if(args.Length!=3) throw new ArgumentException("CaptureAudit capture.jsonl opcodes.json report.json");
var packets=PacketFiles.Import(args[0]);
var versions=packets.Select(p=>p.GameVersion).Distinct().ToArray();
if(versions.Length!=1 || packets.Length==0) throw new InvalidDataException("Expected one nonempty capture version");
var map=new OpcodeMap(); map.Load(File.ReadAllText(args[1]),versions[0],"local cache");
string? Name(PacketRecord p)=>map.Resolve(p.GameVersion,p.Direction,p.Opcode);
var supported=new HashSet<string>{
"UP_PingReq","DOWN_PingRsp","UP_UpdatePositionHandler","UP_UpdatePositionInstance","UP_EventStart",
"UP_ActionSend","UP_ActionSendPos","UP_ClientTrigger",
"UP_EventFinish","UP_EventFinish4","UP_EventFinish8","UP_EventFinish16","UP_EventFinish32","UP_EventFinish64","UP_EventFinish128","UP_EventFinish255",
"UP_EventAction","UP_EventAction4","UP_EventAction8","UP_EventAction16","UP_EventAction32","UP_EventAction64","UP_EventAction128","UP_EventAction255",
"DOWN_EffectResult","DOWN_UpdateHate","DOWN_UpdateHater","DOWN_EffectResultBasic","DOWN_JobGauge","DOWN_UpdateClassInfo","DOWN_FirstAttack",
"DOWN_ActorControl","DOWN_ActorControlSelf","DOWN_ActorControlTarget","DOWN_Effect","DOWN_StatusEffectList","DOWN_ActorDelete","DOWN_ObjectSpawn","DOWN_NpcSpawn"
};
var parsed=packets.Select(p=>(Packet:p,Name:Name(p),Result:p.PayloadComplete ? PacketParser.Parse(Name(p),p.Payload,p.GameVersion) : new ParseResult(false,"incomplete"))).ToArray();
var correlations=PacketCorrelator.Ping(packets,map);
var actionCorrelations=PacketCorrelator.Actions(packets,map);
// Replay owned sample data through the manager and recorder; no native calls or sockets.
using var manager=new NetworkPipeline(versions[0]);
var replayBuffer=new PacketBuffer { Paused=false };
var recorder=new PacketRecorder(replayBuffer);
using var up=manager.Subscribe(NetworkPhase.BeforeSend,recorder.Record);
using var down=manager.Subscribe(NetworkPhase.Received,recorder.Record);
long returned=0;
using var post=manager.Subscribe(NetworkPhase.AfterSend,_=>returned++);
foreach(var p in packets)
{
CaptureRead Read(Func<int,bool> accepts)=>accepts(p.Opcode) ? new(new(p.Opcode,p.Target,p.Payload,p.PayloadComplete,p.CaptureIssue)) : new(null);
if(p.Direction==PacketDirection.Up) manager.Send(()=>1,Read); else manager.Receive(()=>1,Read);
}
var replayed=replayBuffer.Snapshot();
var replayMatches=replayed.Length==packets.Length && replayed.Zip(packets).All(pair=>
pair.First.Direction==pair.Second.Direction && pair.First.Opcode==pair.Second.Opcode &&
pair.First.GameVersion==pair.Second.GameVersion && pair.First.Target==pair.Second.Target &&
pair.First.PayloadComplete==pair.Second.PayloadComplete && pair.First.CaptureIssue==pair.Second.CaptureIssue &&
pair.First.Payload.SequenceEqual(pair.Second.Payload));
if(!replayMatches || manager.ObserverErrors!=0 || returned!=packets.Count(p=>p.Direction==PacketDirection.Up))
throw new InvalidDataException("Manager replay changed sample data or missed send completion observations");
var report=new {
Source=Path.GetFileName(args[0]),SHA256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(args[0]))),
MapSHA256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(args[1]))),MapSource=map.Snapshot.Source,Version=versions[0],
Records=packets.Length,Up=packets.Count(p=>p.Direction==PacketDirection.Up),Down=packets.Count(p=>p.Direction==PacketDirection.Down),
ManagerReplay=new {Matches=replayMatches,Records=replayed.Length,SendReturns=returned,manager.ObserverErrors,manager.ReadErrors},
FirstTime=packets.Min(p=>p.Time),LastTime=packets.Max(p=>p.Time),FirstSequence=packets.First().Sequence,LastSequence=packets.Last().Sequence,
SequenceContinuous=packets.Zip(packets.Skip(1)).All(p=>p.Second.Sequence==p.First.Sequence+1),
MonotonicOrder=packets.Zip(packets.Skip(1)).All(p=>p.Second.MonotonicTicks>=p.First.MonotonicTicks),
Complete=packets.Count(p=>p.PayloadComplete),Incomplete=packets.Count(p=>!p.PayloadComplete),CaptureIssues=packets.Count(p=>p.CaptureIssue!=null),
PayloadBytes=packets.Sum(p=>p.Payload.Length),MinPayload=packets.Min(p=>p.Payload.Length),MaxPayload=packets.Max(p=>p.Payload.Length),
EmptyPayloads=packets.Count(p=>p.Payload.Length==0),Known=packets.Count(p=>Name(p)!=null),Unknown=packets.Count(p=>Name(p)==null),
Parsed=parsed.Count(p=>p.Result.Parsed),SupportedButRejected=parsed.Count(p=>p.Name!=null && supported.Contains(p.Name) && !p.Result.Parsed),
Groups=parsed.GroupBy(p=>new{p.Packet.Direction,p.Packet.Opcode,p.Name,Length=p.Packet.Payload.Length,p.Packet.PayloadComplete,p.Result.Parsed})
.OrderByDescending(g=>g.Count()).Select(g=>new{g.Key,Count=g.Count(),Rejection=g.Key.Name!=null && supported.Contains(g.Key.Name) && !g.Key.Parsed ? g.First().Result.Summary : null}),
PingPairs=correlations.Select(p=>new{p.Request,p.Response,Milliseconds=p.Elapsed.TotalMilliseconds}),
ActionPairs=actionCorrelations.Select(p=>new{p.Request,p.Response,Milliseconds=p.Elapsed.TotalMilliseconds}),
Limitations="No raw payload, coordinates or actor IDs included. Successful parsing does not verify all fields. Header-read failures absent from export require UI counters."
};
File.WriteAllText(args[2],JsonSerializer.Serialize(report,new JsonSerializerOptions{WriteIndented=true}));
Console.WriteLine($"Records={report.Records}; up={report.Up}; down={report.Down}; complete={report.Complete}; known={report.Known}; parsed={report.Parsed}; rejected={report.SupportedButRejected}; pingPairs={correlations.Length}; actionPairs={actionCorrelations.Length}");
+82
View File
@@ -0,0 +1,82 @@
param(
[Parameter(Mandatory)][string]$AssemblyPath,
[Parameter(Mandatory)][string]$OutputPath,
[string]$CecilPath = (Join-Path $env:APPDATA 'XIVLauncher\addon\Hooks\dev\Mono.Cecil.dll')
)
$ErrorActionPreference = 'Stop'
# Read the target as data with Cecil. Never load or execute the target assembly.
Add-Type -Path $CecilPath
$icInput = (Resolve-Path -LiteralPath $AssemblyPath).Path
$icAssembly = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($icInput)
try {
$icSelected = $icAssembly.MainModule.Types | Where-Object {
$_.FullName -match '^StatusEffectList(Entry)?$|^SamplePlugin\.Hook\.(ActorRadiusHook|NoActionMoveHook|YMove|StatusCheck)$'
}
$icTypes = foreach ($icType in $icSelected) {
$icMethods = foreach ($icMethod in $icType.Methods) {
$icState = 'NoBody'
$icError = $null
$icCodeSize = $null
$icInstructions = $null
if ($icMethod.HasBody) {
try {
$icBody = $icMethod.get_Body()
$icCodeSize = $icBody.CodeSize
$icInstructions = @($icBody.Instructions | ForEach-Object { $_.ToString() })
# Parsing success alone is not validation of a protected method body.
$icState = 'ParsedUnvalidated'
$icMissingOperands = @($icBody.Instructions | Where-Object {
$_.OpCode.OperandType.ToString() -ne 'InlineNone' -and $null -eq $_.Operand
})
if ($icCodeSize -gt 1048576 -or $icInstructions.Count -eq 0 -or
$icBody.Instructions[0].OpCode.Name -eq 'ldelem.i1' -or $icMissingOperands.Count -gt 0) {
$icState = 'SuspiciousBody'
$icInstructions = $null
}
} catch {
$icState = 'ReadError'
$icError = $_.Exception.GetBaseException().Message
}
}
[ordered]@{
Signature = $icMethod.FullName
Token = $icMethod.MetadataToken.ToUInt32()
RVA = ('0x{0:X}' -f $icMethod.RVA)
BodyState = $icState
DeclaredCodeSize = $icCodeSize
Error = $icError
UnvalidatedIL = $icInstructions
}
}
[ordered]@{
Name = $icType.FullName
Attributes = $icType.Attributes.ToString()
DeclaredSize = $icType.ClassSize
PackingSize = $icType.PackingSize
Fields = @($icType.Fields | ForEach-Object {
[ordered]@{Name=$_.Name; Type=$_.FieldType.FullName; Offset=$_.Offset}
})
NestedTypes = @($icType.NestedTypes | ForEach-Object {
[ordered]@{
Name=$_.FullName
DeclaredSize=$_.ClassSize
DelegateInvoke=@($_.Methods | Where-Object Name -eq 'Invoke' | ForEach-Object { $_.FullName })
}
})
Methods = @($icMethods)
}
}
$icReport = [ordered]@{
Source = $icInput
SHA256 = (Get-FileHash -LiteralPath $icInput -Algorithm SHA256).Hash
Assembly = $icAssembly.Name.FullName
Runtime = $icAssembly.MainModule.RuntimeVersion
References = @($icAssembly.MainModule.AssemblyReferences | ForEach-Object {
[ordered]@{Name=$_.Name; Version=$_.Version.ToString()}
})
Types = @($icTypes)
Limitation = 'Metadata only. Method-body parsing is not proof of executable logic. Target assembly was not executed.'
}
$icReport | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding utf8
"Saved metadata audit: $OutputPath"
} finally { $icAssembly.Dispose() }
+73
View File
@@ -0,0 +1,73 @@
param(
[Parameter(Mandatory)][string]$FirstPath,
[Parameter(Mandatory)][string]$SecondPath,
[Parameter(Mandatory)][string]$OutputPath,
[string]$CecilPath = (Join-Path $env:APPDATA 'XIVLauncher\addon\Hooks\dev\Mono.Cecil.dll')
)
$ErrorActionPreference = 'Stop'
Add-Type -Path $CecilPath
function Get-IcTypes($Types) {
foreach ($icType in $Types) {
$icType
Get-IcTypes $icType.NestedTypes
}
}
function Get-IcMetadata([string]$Path) {
$icFile = Get-Item -LiteralPath $Path
$icAsm = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($icFile.FullName)
try {
$icAllTypes = @(Get-IcTypes $icAsm.MainModule.Types)
$icNamedTypes = @($icAllTypes | Where-Object { $_.FullName -match '^[\x20-\x7E]+$' })
$icShapes = [ordered]@{}
foreach ($icType in $icNamedTypes) {
$icShapes[$icType.FullName] = @(
"TYPE $($icType.Attributes) SIZE $($icType.ClassSize) PACK $($icType.PackingSize) BASE $($icType.BaseType.FullName)"
$icType.Interfaces | ForEach-Object { "INTERFACE $($_.InterfaceType.FullName)" }
$icType.Fields | ForEach-Object { "FIELD $($_.FullName) OFFSET $($_.Offset) FLAGS $($_.Attributes)" }
$icType.Methods | ForEach-Object { "METHOD $($_.FullName) FLAGS $($_.Attributes) IMPL $($_.ImplAttributes)" }
) | Sort-Object
}
[ordered]@{
File=$icFile.Name
Size=$icFile.Length
SHA256=(Get-FileHash -LiteralPath $icFile.FullName).Hash
Assembly=$icAsm.Name.FullName
MVID=$icAsm.MainModule.Mvid.ToString()
Architecture=$icAsm.MainModule.Architecture.ToString()
Attributes=@($icAsm.CustomAttributes | Where-Object {
$_.AttributeType.Name -match '^TargetFrameworkAttribute$|^Assembly(File|Informational)VersionAttribute$'
} | ForEach-Object {
[ordered]@{Type=$_.AttributeType.FullName; Values=@($_.ConstructorArguments | ForEach-Object { [string]$_.Value })}
})
References=@($icAsm.MainModule.AssemblyReferences | ForEach-Object { "$($_.Name) $($_.Version)" })
Resources=@($icAsm.MainModule.Resources | ForEach-Object { "$($_.ResourceType) $($_.Name)" })
TopLevelTypeCount=$icAsm.MainModule.Types.Count
TotalTypeCount=$icAllTypes.Count
AsciiNamedTypeCount=$icNamedTypes.Count
TypeShapes=$icShapes
}
} finally { $icAsm.Dispose() }
}
$icFirst = Get-IcMetadata $FirstPath
$icSecond = Get-IcMetadata $SecondPath
$icOnlyFirst = @($icFirst.TypeShapes.Keys | Where-Object { !$icSecond.TypeShapes.Contains($_) })
$icOnlySecond = @($icSecond.TypeShapes.Keys | Where-Object { !$icFirst.TypeShapes.Contains($_) })
$icChanges = foreach ($icName in $icFirst.TypeShapes.Keys) {
if (!$icSecond.TypeShapes.Contains($icName)) { continue }
$icDiff = @(Compare-Object $icFirst.TypeShapes[$icName] $icSecond.TypeShapes[$icName])
if ($icDiff.Count -gt 0) {
[ordered]@{Type=$icName; Differences=@($icDiff | ForEach-Object {
[ordered]@{OnlyIn=if ($_.SideIndicator -eq '=>') {'Second'} else {'First'}; Declaration=$_.InputObject}
})}
}
}
$icOutput = [ordered]@{
First=$icFirst
Second=$icSecond
AsciiTypesOnlyInFirst=$icOnlyFirst
AsciiTypesOnlyInSecond=$icOnlySecond
CommonTypeMetadataChanges=@($icChanges)
Limitation='Static declarations only; equal metadata does not establish equal method bodies or behavior. No target assembly was executed.'
}
$icOutput | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding utf8
"Saved comparison: $OutputPath"
+4
View File
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
<ItemGroup><Reference Include="Iced"><HintPath>$(APPDATA)\XIVLauncher\addon\Hooks\dev\Iced.dll</HintPath></Reference></ItemGroup>
</Project>
+125
View File
@@ -0,0 +1,125 @@
using System.Reflection.PortableExecutable;
using Iced.Intel;
using System.Buffers.Binary;
// Read-only on-disk signature audit. Does not open a process or modify the executable.
if(args.Length<2) throw new ArgumentException("NativeAudit executable signature|rva:HEX [instruction-count]");
using var input=File.OpenRead(args[0]);
using var pe=new PEReader(input);
var section=pe.PEHeaders.SectionHeaders.Single(x=>x.Name==".text");
var bytes=pe.GetSectionData(section.VirtualAddress).GetContent().ToArray();
if(args[1].StartsWith("xrefstr:") || args[1].StartsWith("xrefs:"))
{
int target;
if(args[1].StartsWith("xrefstr:"))
{
var image=File.ReadAllBytes(args[0]);
var offset=image.AsSpan().IndexOf(System.Text.Encoding.UTF8.GetBytes(args[1][8..]+"\0"));
if(offset<0) throw new InvalidDataException("String not found");
var dataSection=pe.PEHeaders.SectionHeaders.Single(s=>offset>=s.PointerToRawData && offset<s.PointerToRawData+s.SizeOfRawData);
target=offset-dataSection.PointerToRawData+dataSection.VirtualAddress;
}
else target=Convert.ToInt32(args[1][6..].Split('-')[0],16);
var endTarget=args[1].StartsWith("xrefs:") && args[1].Contains('-') ? Convert.ToInt32(args[1].Split('-')[1],16) : target;
var directory=pe.PEHeaders.PEHeader!.ExceptionTableDirectory;
var functions=pe.GetSectionData(directory.RelativeVirtualAddress).GetContent(0,directory.Size).ToArray();
Console.WriteLine($"XREF TARGET RVA=0x{target:X}");
bool IsStringPointer(ulong pointer)
{
if(pointer<pe.PEHeaders.PEHeader!.ImageBase) return false;
var rva=(long)(pointer-pe.PEHeaders.PEHeader.ImageBase);
var data=pe.PEHeaders.SectionHeaders.FirstOrDefault(s=>rva>=s.VirtualAddress && rva<s.VirtualAddress+s.SizeOfRawData);
if(data.SizeOfRawData==0) return false;
var content=pe.GetSectionData((int)rva).GetContent(0,Math.Min(128,data.VirtualAddress+data.SizeOfRawData-(int)rva)).ToArray();
var end=Array.IndexOf(content,(byte)0);
return end>=3 && content.Take(end).All(b=>b>=32 && b<127);
}
foreach(var dataSection in pe.PEHeaders.SectionHeaders.Where(s=>s.Name!=".text" && s.SizeOfRawData>0))
{
var raw=pe.GetSectionData(dataSection.VirtualAddress).GetContent().ToArray();
for(int at=0;at+8<=raw.Length;at+=8)
if(BinaryPrimitives.ReadUInt64LittleEndian(raw.AsSpan(at))==pe.PEHeaders.PEHeader!.ImageBase+(uint)target)
{
Console.WriteLine($"DATA REF RVA=0x{dataSection.VirtualAddress+at:X}");
var before=at;
while(before>=8 && at-before<8192 && IsStringPointer(BinaryPrimitives.ReadUInt64LittleEndian(raw.AsSpan(before-8)))) before-=8;
Console.WriteLine($"STRING TABLE candidate RVA=0x{dataSection.VirtualAddress+before:X}, preceding={(at-before)/8}");
}
}
for(int i=0;i+7<=bytes.Length;i++)
{
var callTarget=section.VirtualAddress+i+5+BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(i+1));
var ripTarget=section.VirtualAddress+i+7+BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(i+3));
bool call=(bytes[i]==0xE8 || bytes[i]==0xE9) && callTarget>=target && callTarget<=endTarget;
bool rip=(bytes[i] is 0x48 or 0x4C) && bytes[i+1] is 0x8D or 0x8B && (bytes[i+2]&0xC7)==5 && ripTarget>=target && ripTarget<=endTarget;
var rawTarget=BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(i));
bool immediate=rawTarget>=target && rawTarget<=endTarget;
if(!call && !rip && !immediate) continue;
var rva=section.VirtualAddress+i;
bool described=false;
for(int at=0;at+12<=functions.Length;at+=12)
{
int begin=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at)),end=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at+4));
if(rva>=begin && rva<end) { Console.WriteLine($"REF RVA=0x{rva:X}, FUNCTION=0x{begin:X}..0x{end:X}, {(call ? "CALL/JMP" : rip ? "RIP" : "IMM/CANDIDATE")} target=0x{(call ? callTarget : rip ? ripTarget : rawTarget):X}"); described=true; break; }
}
if(!described) Console.WriteLine($"REF RVA=0x{rva:X}, FUNCTION=leaf/unknown, {(call ? "CALL/JMP" : rip ? "RIP" : "IMM/CANDIDATE")} target=0x{(call ? callTarget : rip ? ripTarget : rawTarget):X}");
}
return;
}
var containing=args[1].StartsWith("func:",StringComparison.OrdinalIgnoreCase);
var direct=containing || args[1].StartsWith("rva:",StringComparison.OrdinalIgnoreCase);
var requested=direct ? Convert.ToInt32(args[1][(containing ? 5 : 4)..],16) : 0;
if(containing)
{
var directory=pe.PEHeaders.PEHeader!.ExceptionTableDirectory;
var functions=pe.GetSectionData(directory.RelativeVirtualAddress).GetContent(0,directory.Size).ToArray();
bool found=false;
for(int at=0;at+12<=functions.Length;at+=12)
{
int begin=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at)),end=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at+4));
if(requested>=begin && requested<end)
{
Console.WriteLine($"CONTAINING RVA=0x{begin:X}..0x{end:X}");
var unwind=BinaryPrimitives.ReadInt32LittleEndian(functions.AsSpan(at+8));
for(int depth=0;depth<16;depth++)
{
var header=pe.GetSectionData(unwind).GetContent(0,4).ToArray();
if(((header[0]>>3)&4)==0) break;
var chainAt=(4+header[2]*2+3)&~3;
var chain=pe.GetSectionData(unwind+chainAt).GetContent(0,12).ToArray();
begin=BinaryPrimitives.ReadInt32LittleEndian(chain); end=BinaryPrimitives.ReadInt32LittleEndian(chain.AsSpan(4));
unwind=BinaryPrimitives.ReadInt32LittleEndian(chain.AsSpan(8));
Console.WriteLine($"CHAINED ROOT RVA=0x{begin:X}..0x{end:X}");
}
requested=begin;found=true;break;
}
}
if(!found) throw new InvalidDataException("No unwind entry for requested RVA");
}
var start=direct ? requested-section.VirtualAddress : 0;
if(start<0 || start>=bytes.Length) throw new ArgumentException("RVA outside .text");
var pattern=direct ? "" : args[1].Replace(" ","");
if(pattern.Length%2!=0) throw new ArgumentException("Odd signature length");
var values=Enumerable.Range(0,pattern.Length/2).Select(i=>pattern.Substring(i*2,2)).ToArray();
var count=args.Length>2 ? int.Parse(args[2]) : 100;
int matches=0;
for(int i=start;i<=(direct ? start : bytes.Length-values.Length);i++)
{
bool match=true;
for(int j=0;j<values.Length;j++)
if(values[j]!="??" && bytes[i+j]!=Convert.ToByte(values[j],16)) { match=false; break; }
if(!match) continue;
matches++;
var decoder=Decoder.Create(64,new ByteArrayCodeReader(bytes,i,Math.Min(8192,bytes.Length-i)));
decoder.IP=pe.PEHeaders.PEHeader!.ImageBase+(uint)section.VirtualAddress+(uint)i;
Console.WriteLine($"MATCH RVA=0x{section.VirtualAddress+i:X}");
Console.WriteLine("PREFIX "+Convert.ToHexString(bytes.AsSpan(i,Math.Min(64,bytes.Length-i))));
var formatter=new IntelFormatter(); var output=new StringOutput();
for(int n=0;n<count;n++)
{
decoder.Decode(out var instruction); formatter.Format(instruction,output);
Console.WriteLine($"{instruction.IP:X16} {output.ToStringAndReset()}");
if(instruction.FlowControl==FlowControl.Return) break;
}
}
Console.WriteLine($"Matches: {matches}");
+38
View File
@@ -0,0 +1,38 @@
using Lumina;
using ICRebuild.Core;
using System.Text.Json;
if(args.Length!=2) throw new ArgumentException("StatusAudit sqpack-directory output-json");
using var game=new GameData(args[0]);
var sheet=game.GetExcelSheet<Lumina.Excel.Sheets.Status>() ?? throw new InvalidDataException("Status sheet unavailable");
var result=new List<object>();
var root=game.GetFile<Lumina.Data.Files.Excel.ExcelListFile>("exd/root.exl");
if(root!=null)
foreach(var entry in root.ExdMap.Where(x=>x.Value==0x14A)) Console.WriteLine($"Native movement-check sheet 0x14A = {entry.Key}");
foreach(var (group,id) in StatusCatalog.Entries())
{
var row=sheet.GetRowOrDefault(id);
if(row==null) { Console.WriteLine($"MISSING {group} {id}"); result.Add(new{Group=group,Id=id,Missing=true}); continue; }
var s=row.Value;
Console.WriteLine($"{group} {id} {s.Name}: movement={s.LockMovement} control={s.LockControl} actions={s.LockActions}");
result.Add(new {Group=group,Id=id,Name=s.Name.ToString(),Description=s.Description.ToString(),s.LockMovement,s.LockControl,s.LockActions,s.Flags,s.Flag2,s.ParamEffect,s.ParamModifier});
}
foreach(var s in sheet)
{
var name=s.Name.ToString();
if((name=="Heavy" || name=="Leaden") && !StatusCatalog.SlowCandidates.Contains(s.RowId))
{
Console.WriteLine($"UNLISTED_SLOW {s.RowId} {name}: {s.Description}");
result.Add(new{Group="UnlistedSlowNameSearch",Id=s.RowId,Name=name,Description=s.Description.ToString()});
}
if(!new[]{"charm","fear","hysteria","seduc","terror"}.Any(x=>name.Contains(x,StringComparison.OrdinalIgnoreCase))) continue;
Console.WriteLine($"CONTROL {s.RowId} {name}: movement={s.LockMovement} control={s.LockControl} actions={s.LockActions}");
result.Add(new {Group="ControlNameSearch",Id=s.RowId,Name=name,Description=s.Description.ToString(),s.LockMovement,s.LockControl,s.LockActions,s.Flags,s.Flag2,s.ParamEffect,s.ParamModifier});
}
var territories=game.GetExcelSheet<Lumina.Excel.Sheets.TerritoryType>();
foreach(uint id in new uint[]{1108,1290})
{
var territory=territories?.GetRowOrDefault(id);
if(territory.HasValue) Console.WriteLine($"TERRITORY {id}: use={territory.Value.TerritoryIntendedUse.RowId}, name={territory.Value.PlaceName.Value.Name}");
}
File.WriteAllText(args[1],JsonSerializer.Serialize(result,new JsonSerializerOptions{WriteIndented=true}));
+4
View File
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
<ItemGroup><Reference Include="Lumina;Lumina.Excel"><HintPath>$(APPDATA)\XIVLauncher\addon\Hooks\dev\%(Identity).dll</HintPath></Reference><Compile Include="../../Core/StatusCatalog.cs" Link="StatusCatalog.cs" /></ItemGroup>
</Project>