Skip to content

Commit a3deaa1

Browse files
committed
chore: wip gvas walker (header skip works, struct skip unfinished, reverted parser to heuristic)
1 parent bac1367 commit a3deaa1

33 files changed

Lines changed: 210 additions & 11 deletions
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Sequential GVAS property stream walker.
2+
// Walks top-level properties by reading their headers.
3+
// Skips struct values by scanning forward for the next valid property name.
4+
5+
using System.Collections.Generic;
6+
using System.Text;
7+
8+
namespace NotAlterra.Gvas;
9+
10+
public record GvasProperty(string Name, string Type, uint Size, uint ArrayIndex, object? Value);
11+
12+
public static class PropertyWalker
13+
{
14+
public static List<GvasProperty> WalkAll(byte[] data)
15+
{
16+
var props = new List<GvasProperty>();
17+
18+
int gvasStart = IndexOfMagic(data, "GVAS");
19+
if (gvasStart < 0) return props;
20+
21+
int offset = gvasStart + 12; // past "GVAS" + FileVersion + PackageVersion
22+
int headerEnd = FindSaveGameClassName(data, gvasStart);
23+
if (headerEnd <= offset) return props;
24+
offset = headerEnd;
25+
26+
while (offset < data.Length)
27+
{
28+
var (propName, nameEnd) = BinaryReader.ReadFName(data, offset);
29+
if (propName == null || nameEnd <= offset) break;
30+
31+
if (propName == "None")
32+
{ offset = nameEnd; break; }
33+
34+
offset = nameEnd;
35+
var (typeName, typeEnd) = BinaryReader.ReadFName(data, offset);
36+
if (typeName == null || typeEnd <= offset) break;
37+
offset = typeEnd;
38+
39+
uint propSize = BinaryReader.ReadU32(data, offset) ?? 0;
40+
offset += 4;
41+
uint arrayIndex = BinaryReader.ReadU32(data, offset) ?? 0;
42+
offset += 4;
43+
44+
object? value = null;
45+
int consumed = (int)propSize;
46+
47+
switch (typeName)
48+
{
49+
case "IntProperty":
50+
if (propSize >= 4) value = BinaryReader.ReadU32(data, offset);
51+
break;
52+
case "FloatProperty":
53+
if (propSize >= 4)
54+
{
55+
var bits = BinaryReader.ReadU32(data, offset);
56+
if (bits.HasValue) value = BitConverter.ToSingle(BitConverter.GetBytes(bits.Value), 0);
57+
}
58+
break;
59+
case "DoubleProperty":
60+
value = BinaryReader.ReadF64(data, offset);
61+
break;
62+
case "BoolProperty":
63+
if (propSize >= 1) value = data[offset] != 0;
64+
break;
65+
case "StrProperty":
66+
case "TextProperty":
67+
{ var (s, _) = BinaryReader.ReadFString(data, offset); value = s; }
68+
break;
69+
case "NameProperty":
70+
{ var (s, _) = BinaryReader.ReadFName(data, offset); value = s; }
71+
break;
72+
case "StructProperty":
73+
// Skip struct value by name + data; find end by scanning for next property
74+
consumed = SkipStruct(data, offset, propSize);
75+
value = consumed >= 0 ? "(struct)" : null;
76+
if (consumed < 0) consumed = (int)propSize;
77+
break;
78+
case "ByteProperty":
79+
{
80+
var (enumType, enumEnd) = BinaryReader.ReadFName(data, offset);
81+
if (enumType != null && enumEnd > offset && enumEnd < offset + propSize)
82+
value = data[enumEnd];
83+
else if (propSize >= 1) value = data[offset];
84+
}
85+
break;
86+
case "EnumProperty":
87+
{
88+
var (innerType, innerEnd) = BinaryReader.ReadFName(data, offset);
89+
if (innerType != null)
90+
{
91+
var (enumValue, valEnd) = BinaryReader.ReadFName(data, innerEnd);
92+
if (enumValue != null) value = $"{innerType}.{enumValue}";
93+
}
94+
}
95+
break;
96+
default:
97+
value = $"({typeName}:{propSize}b)";
98+
break;
99+
}
100+
101+
if (consumed < 0) consumed = 0;
102+
if (offset + consumed > data.Length) consumed = data.Length - offset;
103+
104+
props.Add(new GvasProperty(propName, typeName, propSize, arrayIndex, value));
105+
offset += consumed;
106+
}
107+
108+
return props;
109+
}
110+
111+
// Skip a struct value: read struct_type_name FName + its data.
112+
// Returns the number of bytes consumed from 'offset'.
113+
private static int SkipStruct(byte[] data, int offset, uint propSize)
114+
{
115+
int start = offset;
116+
var (sname, snameEnd) = BinaryReader.ReadFName(data, offset);
117+
if (sname == null || snameEnd <= offset) return (int)propSize;
118+
119+
// After struct name, scan for the end of struct data.
120+
// The struct value is not a GVAS property stream — it's binary C++ data.
121+
// We need to find where it ends by looking for the next top-level property.
122+
// Strategy: scan forward for a valid FName that looks like a property name.
123+
int end = FindNextProperty(data, snameEnd, start + 100000);
124+
if (end > snameEnd)
125+
return end - start;
126+
127+
// Fallback: use propSize (might be wrong but better than infinite loop)
128+
return (int)propSize;
129+
}
130+
131+
// Scan forward for the start of the next top-level property.
132+
// Looks for known property names with valid FName length prefixes.
133+
private static int FindNextProperty(byte[] data, int searchStart, int searchEnd)
134+
{
135+
searchEnd = Math.Min(searchEnd, data.Length);
136+
string[] knownProps = {
137+
"SlotName", "DisplayName", "GameMode", "LevelName", "bIsMultiplayerSave",
138+
"bWasMultiplayerSave", "BuildNumber", "BuildBranch", "SavesCount",
139+
"LatestVersion", "DataVersion", "EngineVersion", "MTime",
140+
"CreatedAt", "LastModified", "None"
141+
};
142+
143+
for (int i = searchStart; i <= searchEnd - 10; i++)
144+
{
145+
foreach (var name in knownProps)
146+
{
147+
if (i + name.Length > data.Length) continue;
148+
bool match = true;
149+
for (int j = 0; j < name.Length; j++)
150+
if (data[i + j] != name[j]) { match = false; break; }
151+
if (!match) continue;
152+
153+
// Verify FName length prefix
154+
int prefixLen = BitConverter.ToInt32(data, i - 4);
155+
if (prefixLen != name.Length + 1) continue;
156+
if (data[i + name.Length] != 0) continue;
157+
158+
return i - 4; // Return start of this property
159+
}
160+
}
161+
return -1;
162+
}
163+
164+
private static int FindSaveGameClassName(byte[] data, int gvasStart)
165+
{
166+
int searchEnd = Math.Min(data.Length, gvasStart + 4096);
167+
for (int i = gvasStart + 12; i <= searchEnd - 10; i++)
168+
{
169+
if (data[i] != '/' || data[i + 1] != 'S' || data[i + 2] != 'c') continue;
170+
int rawLen = BitConverter.ToInt32(data, i - 4);
171+
if (rawLen <= 0 || rawLen > 500) continue;
172+
if (i - 4 + rawLen > data.Length) continue;
173+
if (data[i + rawLen - 1] != 0) continue;
174+
175+
int propStart = i + rawLen;
176+
while (propStart < data.Length && data[propStart] == 0) propStart++;
177+
return propStart;
178+
}
179+
return -1;
180+
}
181+
182+
private static int IndexOfMagic(byte[] data, string magic)
183+
{
184+
var m = Encoding.UTF8.GetBytes(magic);
185+
int max = Math.Min(data.Length, 512);
186+
for (int i = 0; i <= max - 4; i++)
187+
if (data[i] == m[0] && data[i + 1] == m[1] && data[i + 2] == m[2] && data[i + 3] == m[3])
188+
return i;
189+
return -1;
190+
}
191+
192+
public static Dictionary<string, object?> WalkToDict(byte[] data)
193+
{
194+
var dict = new Dictionary<string, object?>();
195+
foreach (var prop in WalkAll(data))
196+
dict[prop.Name] = prop.Value;
197+
return dict;
198+
}
199+
}
5.5 KB
Binary file not shown.
2.48 KB
Binary file not shown.
5 KB
Binary file not shown.
1.8 KB
Binary file not shown.

src/NotAlterra.Core/obj/Debug/net9.0/NotAlterra.Core.AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[assembly: System.Reflection.AssemblyCompanyAttribute("NotAlterra.Core")]
1414
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
1515
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
16-
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+fd44ebe37ae5ba2997cecbe953e2d95b7648988f")]
16+
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+bac1367da5502ffde83c3210a3f2388aba99ba04")]
1717
[assembly: System.Reflection.AssemblyProductAttribute("NotAlterra.Core")]
1818
[assembly: System.Reflection.AssemblyTitleAttribute("NotAlterra.Core")]
1919
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d78f45794a73eb060926f260ed227adb49b76cc9d70d93ea173d32281c9def70
1+
c658dfbbc28fec56b92137e62821ba763ce3c0cea4198a90c9d2c533edd55120
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
e93ae937777897ab34673a11b4c7c0e6463cfd023f71dccce1fb3ecb8d03d928
1+
b1b7d7466aad50e0d2e8bf362fb944400823deb0a470a947367c97e48ddfe2c4
5.5 KB
Binary file not shown.
2.48 KB
Binary file not shown.

0 commit comments

Comments
 (0)