Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various changes following testing. Added support for files larger than 4GB. #7

Merged
merged 3 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 148 additions & 96 deletions SavegameToolkit/ArkSavegame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SavegameToolkit.Arrays;
Expand Down Expand Up @@ -45,9 +46,9 @@ public class ArkSavegame : GameObjectContainerMixin, IConversionSupport
[JsonProperty(Order = 7)]
public override List<GameObject> Objects { get; } = new List<GameObject>();

private int hibernationOffset;
public List<Tuple<long, long>> StoredDataOffsets = new List<Tuple<long, long>>();

private int storedOffset;
private int hibernationOffset;

private int nameTableOffset;

Expand All @@ -64,7 +65,7 @@ public class ArkSavegame : GameObjectContainerMixin, IConversionSupport
// Ark version 349.10 introduced two new unknown hibernation entries, the save version remained 9, though. Saving these is not supported.
private int hibernationV9_34910Unknown1;
private int hibernationV9_34910Unknown2;

private int hibernationV9Check1;
private int hibernationUnknown1;

private int hibernationUnknown2;
Expand All @@ -80,6 +81,11 @@ public class ArkSavegame : GameObjectContainerMixin, IConversionSupport
public bool HasUnknownData { get; set; }

private HashSet<string> nameTableForWriteBinary;
private int hibernationV9Check2;
private int hibernationV9Check3;
private int hibernationV9Check4;
private int hibernationV9Check5;
private int hibernationV9Check6;

#region readBinary

Expand Down Expand Up @@ -133,19 +139,13 @@ private void readBinaryHeader(ArkArchive archive)

if (SaveVersion > 10)
{
storedOffset = (int)archive.ReadLong();
var storedDataSize = archive.ReadLong();

var v11Unknown1 = archive.ReadLong(); //file size or some other pointer
var v11Unknown2 = archive.ReadLong(); //0
var v11Unknown3 = archive.ReadLong(); //file size or some other pointer
var v11Unknown4 = archive.ReadLong(); //0
var v11Unknown5 = archive.ReadLong(); //file size or some other pointer
var v11Unknown6 = archive.ReadLong(); //0
}
else
{
storedOffset = 0;

//read in stored data file offset and sizes
for (int x = 0; x < 4; x++)
{
var storedOffset = new Tuple<long, long>(archive.ReadLong(), archive.ReadLong());
StoredDataOffsets.Add(storedOffset);
}
}

hibernationOffset = archive.ReadInt();
Expand Down Expand Up @@ -359,7 +359,6 @@ private void readBinaryHibernation(ArkArchive archive, ReadingOptions options)

archive.Position = hibernationOffset;

/*
if (SaveVersion > 7)
{
hibernationV8Unknown1 = archive.ReadInt();
Expand All @@ -375,24 +374,32 @@ private void readBinaryHibernation(ArkArchive archive, ReadingOptions options)
archive.DebugMessage("non-zero unknown V9 fields, expecting duplicated set per 349.10");
hibernationV9_34910Unknown1 = archive.ReadInt();
hibernationV9_34910Unknown2 = archive.ReadInt();
if (!(hibernationV8Unknown1 == archive.ReadInt()
&& hibernationV8Unknown2 == archive.ReadInt()
&& hibernationV8Unknown3 == archive.ReadInt()
&& hibernationV8Unknown4 == archive.ReadInt()
&& hibernationV9_34910Unknown1 == archive.ReadInt()
&& hibernationV9_34910Unknown2 == archive.ReadInt()))
{
throw new NotSupportedException("349.10 workaround for duplicate unknown hibernation bytes failed");
}
}
*/

while (archive.Position < nameTableOffset)
{
//regardless of anything before the hibernated class count always seems to follow immediately after this byte in all save formats I have worked on.
if (archive.ReadSByte() == -52)

hibernationV9Check1 = archive.ReadInt();
hibernationV9Check2 = archive.ReadInt();
hibernationV9Check3 = archive.ReadInt();
hibernationV9Check4 = archive.ReadInt();
hibernationV9Check5 = archive.ReadInt();
hibernationV9Check6 = archive.ReadInt();

if (!(hibernationV8Unknown1 == hibernationV9Check1
&& hibernationV8Unknown2 == hibernationV9Check2
&& hibernationV8Unknown3 == hibernationV9Check3
&& hibernationV8Unknown4 == hibernationV9Check4
&& hibernationV9_34910Unknown1 == hibernationV9Check5
&& hibernationV9_34910Unknown2 == hibernationV9Check6))
{
break;
if (SaveVersion > 10)
{
//TODO:// more data reads - is it always 112 bytes?
archive.SkipBytes(112);

}
else
{
throw new NotSupportedException("349.10 workaround for duplicate unknown hibernation bytes failed");
}
}
}

Expand All @@ -402,10 +409,9 @@ private void readBinaryHibernation(ArkArchive archive, ReadingOptions options)
return;
}

/*

hibernationUnknown1 = archive.ReadInt();
hibernationUnknown2 = archive.ReadInt();
*/

int hibernatedClassesCount = archive.ReadInt();

Expand Down Expand Up @@ -445,103 +451,149 @@ private void readBinaryStoredObjects(ArkArchive archive, ReadingOptions options)
{
if (!options.CryopodCreatures) return;

var inventoryContainers = Objects.Where(x => x.GetPropertyValue<ObjectReference>("MyInventoryComponent") != null).ToList();
var inventoryContainers = Objects.AsParallel().Where(x => x.GetPropertyValue<ObjectReference>("MyInventoryComponent") != null).ToList();

var validStored = Objects
var validStored = Objects.AsParallel()
.Where(o =>
(o.ClassName.Name.Contains("Cryopod") || o.ClassString.Contains("SoulTrap_") || o.ClassString.Contains("Vivarium_"))
&& o.HasAnyProperty("CustomItemDatas")
(o.ClassString.Contains("Cryopod") || o.ClassString.Contains("SoulTrap_") || o.ClassString.Contains("Vivarium_"))
&& o.GetTypedProperty<PropertyArray>("CustomItemDatas") != null
)
.ToArray();
.ToList();

foreach (var o in validStored)
List<CryoStoreData> storedOffsets = new List<CryoStoreData>();

validStored.ForEach(x =>
{
if (!(o.Properties.First(p => p.NameString == "CustomItemDatas") is PropertyArray customData)) continue;
if (!(x.Properties.First(p => p.NameString == "CustomItemDatas") is PropertyArray customData)) return;

long cryoDataOffset = 0;
int dataFile = 1;

if (
archive.SaveVersion > 10
&& customData.Value is ArkArrayStruct redirectors
&& redirectors.All(x => x is StructCustomItemDataRef)
)
&& redirectors.All(r => r is StructCustomItemDataRef)
)
{
// cryopods use the first redirector, soulball use the second
// cryopods use the first redirector, soulballs use the second
var redirectorIndex = o.ClassName.Name.Contains("Cryopod") ? 0 : 1;
cryoDataOffset = ((StructCustomItemDataRef)redirectors[redirectorIndex]).Position;
dataFile = ((StructCustomItemDataRef)redirectors[redirectorIndex]).StoreDataIndex;
}

var creatureDataOffset = cryoDataOffset + storedOffset;
archive.Position = creatureDataOffset;

ArkCryoStore cryoStore = new ArkCryoStore(archive);
if (!cryoStore.Objects.Any()) continue;
cryoStore.LoadProperties(archive);
storedOffsets.Add(new CryoStoreData() { ParentObject = x, Offset = cryoDataOffset, StoreDataIndex = dataFile });

var dinoComponent = cryoStore.Objects[0];
var dinoCharacterStatusComponent = cryoStore.Objects[1];
var dinoInventoryComponent = cryoStore.Objects[2];
});
validStored = null;

//re-map and add properties as appropriate
if (dinoComponent == null) continue;
//sort and read bytes into memory between this and next offset
var sortedOffsets = storedOffsets.OrderBy(x => x.StoreDataIndex).ThenBy(x => x.Offset).ToList();
for (int x = 0; x < sortedOffsets.Count; x++)
{
var storedObject = sortedOffsets[x];
int storedOffsetIndex = storedObject.StoreDataIndex;
var offsetData = StoredDataOffsets[storedOffsetIndex];

dinoComponent.IsInCryo = true;
var storedOffset = offsetData.Item1 + storedObject.Offset;
long nextOffset = offsetData.Item1 + offsetData.Item2;
if (x < sortedOffsets.Count - 1)
{
var nextItem = sortedOffsets[x + 1];
if (nextItem.StoreDataIndex == storedObject.StoreDataIndex)
{
nextOffset = nextItem.Offset + offsetData.Item1;
}
}

// the tribe name is stored in `TamerString`, non-cryoed creatures have the property `TribeName` for that.
if (dinoComponent.GetPropertyValue<string>("TribeName")?.Length == 0 && dinoComponent.GetPropertyValue<string>("TamerString")?.Length > 0)
dinoComponent.Properties.Add(new PropertyString("TribeName", dinoComponent.GetPropertyValue<string>("TamerString")));
archive.Position = storedOffset;
int bytesToRead = (int)(nextOffset - storedOffset);
storedObject.Data = archive.ReadBytes(bytesToRead);
}
storedOffsets = new List<CryoStoreData>();

//get parent of cryopod owner inventory
var podParentRef = o.GetPropertyValue<ObjectReference>("OwnerInventory");
if (podParentRef != null)
//load the cryo creature data in parallel to speed up read
Parallel.ForEach(sortedOffsets, o =>
{
if (o.Data.Length == 0) return;
using (MemoryStream ms = new MemoryStream(o.Data))
{
var podParent = inventoryContainers.FirstOrDefault(x => x.GetPropertyValue<ObjectReference>("MyInventoryComponent")?.ObjectId == podParentRef.ObjectId);

//determine if we need to re-team the podded animal
if (podParent != null)
using (ArkArchive storedArchive = new ArkArchive(ms))
{
dinoComponent.Parent = podParent;
ArkCryoStore cryoStore = new ArkCryoStore(storedArchive);
if (!cryoStore.Objects.Any()) return;

cryoStore.LoadProperties(storedArchive);


var dinoComponent = cryoStore.Objects[0];
var dinoCharacterStatusComponent = cryoStore.Objects[1];
var dinoInventoryComponent = cryoStore.Objects[2];

//re-map and add properties as appropriate
if (dinoComponent == null) return;

dinoComponent.IsInCryo = true;

int obTeam = dinoComponent.GetPropertyValue<int>("TargetingTeam");
int containerTeam = podParent.GetPropertyValue<int>("TargetingTeam");
if (obTeam != containerTeam)
// the tribe name is stored in `TamerString`, non-cryoed creatures have the property `TribeName` for that.
if (dinoComponent.GetPropertyValue<string>("TribeName")?.Length == 0 && dinoComponent.GetPropertyValue<string>("TamerString")?.Length > 0)
dinoComponent.Properties.Add(new PropertyString("TribeName", dinoComponent.GetPropertyValue<string>("TamerString")));

//get parent of cryopod owner inventory
var podParentRef = o.ParentObject.GetPropertyValue<ObjectReference>("OwnerInventory");
if (podParentRef != null)
{
var propertyIndex = dinoComponent.Properties.FindIndex(i => i.NameString == "TargetingTeam");
if (propertyIndex != -1)
var podParent = inventoryContainers.FirstOrDefault(x => x.GetPropertyValue<ObjectReference>("MyInventoryComponent")?.ObjectId == podParentRef.ObjectId);

//determine if we need to re-team the podded animal
if (podParent != null)
{
dinoComponent.Properties.RemoveAt(propertyIndex);
}
dinoComponent.Properties.Add(new PropertyInt("TargetingTeam", containerTeam));
dinoComponent.Parent = podParent;

int obTeam = dinoComponent.GetPropertyValue<int>("TargetingTeam");
int containerTeam = podParent.GetPropertyValue<int>("TargetingTeam");
if (obTeam != containerTeam)
{
var propertyIndex = dinoComponent.Properties.FindIndex(i => i.NameString == "TargetingTeam");
if (propertyIndex != -1)
{
dinoComponent.Properties.RemoveAt(propertyIndex);
}
dinoComponent.Properties.Add(new PropertyInt("TargetingTeam", containerTeam));

if (dinoComponent.HasAnyProperty("TamingTeamID"))
{
dinoComponent.Properties.RemoveAt(dinoComponent.Properties.FindIndex(i => i.NameString == "TamingTeamID"));
dinoComponent.Properties.Add(new PropertyInt("TamingTeamID", containerTeam));
}

if (dinoComponent.HasAnyProperty("TamingTeamID"))
{
dinoComponent.Properties.RemoveAt(dinoComponent.Properties.FindIndex(i => i.NameString == "TamingTeamID"));
dinoComponent.Properties.Add(new PropertyInt("TamingTeamID", containerTeam));
}

}
}
}
}
}

if (dinoCharacterStatusComponent != null)
{
addObject(dinoCharacterStatusComponent, true);
if (dinoCharacterStatusComponent != null)
{
addObject(dinoCharacterStatusComponent, false);

var statusComponentRef = dinoComponent.GetTypedProperty<PropertyObject>("MyCharacterStatusComponent");
statusComponentRef.Value.ObjectId = dinoCharacterStatusComponent.Id;
var statusComponentRef = dinoComponent.GetTypedProperty<PropertyObject>("MyCharacterStatusComponent");
statusComponentRef.Value.ObjectId = dinoCharacterStatusComponent.Id;

}
}

if (dinoInventoryComponent != null)
{
addObject(dinoInventoryComponent, true);
if (dinoInventoryComponent != null)
{
addObject(dinoInventoryComponent, false);

var inventoryComponentRef = dinoComponent.GetTypedProperty<PropertyObject>("MyInventoryComponent");
inventoryComponentRef.Value.ObjectId = dinoInventoryComponent.Id;
}

var inventoryComponentRef = dinoComponent.GetTypedProperty<PropertyObject>("MyInventoryComponent");
inventoryComponentRef.Value.ObjectId = dinoInventoryComponent.Id;
addObject(dinoComponent, false);

}
}
});

addObject(dinoComponent, true);
}
}


Expand Down
11 changes: 7 additions & 4 deletions SavegameToolkit/Structs/StructCustomItemDataRef.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ namespace SavegameToolkit.Structs {
public class StructCustomItemDataRef : StructBase {

[JsonProperty(Order = 0)]
public short Unknown0 { get; private set; }
public byte Unknown0 { get; private set; }
[JsonProperty(Order = 1)]
public long Position { get; private set; }
public byte StoreDataIndex{ get; private set; }
[JsonProperty(Order = 2)]
public ObjectReference[] ObjectRefs { get; private set; }
public long Position { get; private set; }
[JsonProperty(Order = 3)]
public ObjectReference[] ObjectRefs { get; private set; }
[JsonProperty(Order = 4)]
public ObjectReference[] ClassRefs { get; private set; }

public override void Init(ArkArchive archive)
{
// The first unknown field may be two fields - perhaps format version and archive index
Unknown0 = archive.ReadShort();
Unknown0 = archive.ReadByte();
StoreDataIndex = archive.ReadByte();
Position = archive.ReadLong();
ObjectRefs = new ObjectReference[archive.ReadInt()];
for (int index = 0; index < ObjectRefs.Length; index++)
Expand Down
16 changes: 16 additions & 0 deletions SavegameToolkit/Types/CryoStoreData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace SavegameToolkit.Types
{
public class CryoStoreData
{

public GameObject ParentObject { get; set; }
public int StoreDataIndex { get; set; } = 1;
public long Offset { get; set; }
public byte[] Data { get; set; }

}
}