From bbd31a34fab8a0fe91916a39bd8b3375e898d926 Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 16:30:21 +0100 Subject: [PATCH 01/11] add u6 + u7 header version and save custom version --- .../Save/FSaveCustomVersion.cs | 24 +++++++++++++++++++ .../Save/SaveHeaderVersion.cs | 3 +++ 2 files changed, 27 insertions(+) diff --git a/SatisfactorySaveParser/Save/FSaveCustomVersion.cs b/SatisfactorySaveParser/Save/FSaveCustomVersion.cs index 170efb5..f588db9 100644 --- a/SatisfactorySaveParser/Save/FSaveCustomVersion.cs +++ b/SatisfactorySaveParser/Save/FSaveCustomVersion.cs @@ -89,7 +89,31 @@ public enum FSaveCustomVersion // 2021-09-21 Migrate FGTrain from native only to a blueprint class BP_Train. TrainBlueprintClassAdded, + + // 2021-12-03: Added sublevel streaming support + AddedSublevelStreaming, + + // 2022-08-10: Added additional track progression path to resource sink subsystem + AddedResourceSinkTrack, + // 2022-07-28: Added Coloring support to concrete pillars, in post load we check if the swatch if the default one, if so we swap it with concrete. + AddedColoringSupportToConcretePillars, + + // 2022-10-24: Readded since AddedColoringSupportToConcretePillars was merged to main + AddedResourceSinkTrack2, + + // 2022-10-18: Added Cached locations for wire locations for use in visualization in blueprint hologram (can't depend on connection components) + AddedCachedLocationsForWire, + + // 2022-11-17: Added migration of inventories from the old splitters and mergers to the new ones that have a smaller inventories. + ReworkedSplittersAndMergers, + + // 2022-11-23: Added new productivity monitor implementation. + ReworkedProductivityMonitor, + + // 2022-11-25: Nativized shopping list and added blueprint support. + NativizedShoppingList, + // ------------------------------------------------------ VersionPlusOne, LatestVersion = VersionPlusOne - 1 diff --git a/SatisfactorySaveParser/Save/SaveHeaderVersion.cs b/SatisfactorySaveParser/Save/SaveHeaderVersion.cs index 1e30851..5193f64 100644 --- a/SatisfactorySaveParser/Save/SaveHeaderVersion.cs +++ b/SatisfactorySaveParser/Save/SaveHeaderVersion.cs @@ -31,6 +31,9 @@ public enum SaveHeaderVersion // @2021-04-15 UE4.26 Engine Upgrade. FEditorObjectVersion Changes occurred UE426EngineUpdate, + + // @2022-01-06 Added GUID to identify saves, it is for analytics purposes. + AddedSaveIdentifier, // ---------- VersionPlusOne, From af314a8348b319b4735dc09c349bcc6d4a0b9a3c Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 16:33:25 +0100 Subject: [PATCH 02/11] parse/read save header field --- SatisfactorySaveParser/Save/FSaveHeader.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/SatisfactorySaveParser/Save/FSaveHeader.cs b/SatisfactorySaveParser/Save/FSaveHeader.cs index df8898b..3c6a834 100644 --- a/SatisfactorySaveParser/Save/FSaveHeader.cs +++ b/SatisfactorySaveParser/Save/FSaveHeader.cs @@ -67,6 +67,11 @@ public class FSaveHeader /// Was this save ever saved with mods enabled? /// public bool IsModdedSave { get; set; } + + /// + /// a unique identifier for this save, for analytics purposes + /// + public string SaveIdentifier { get; set; } public void Serialize(BinaryWriter writer) { @@ -92,6 +97,9 @@ public void Serialize(BinaryWriter writer) writer.WriteLengthPrefixedString(ModMetadata); writer.Write(IsModdedSave ? 1 : 0); } + + if (HeaderVersion >= SaveHeaderVersion.AddedSaveIdentifier) + writer.WriteLengthPrefixedString(SaveIdentifier); } public static FSaveHeader Parse(BinaryReader reader) @@ -136,6 +144,12 @@ public static FSaveHeader Parse(BinaryReader reader) header.IsModdedSave = reader.ReadInt32() > 0; log.Debug($"ModMetadata={header.ModMetadata}, IsModdedSave={header.IsModdedSave}"); } + + if (header.HeaderVersion >= SaveHeaderVersion.AddedSaveIdentifier) + { + header.SaveIdentifier = reader.ReadLengthPrefixedString(); + log.Debug($"SaveIdentifier={header.SaveIdentifier}"); + } return header; } From 03520e9ec1ecf927b5e9cb3a23109c07ee067fde Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 16:35:42 +0100 Subject: [PATCH 03/11] add savelevel object class --- .../SatisfactorySaveParser.csproj | 1 + SatisfactorySaveParser/SaveLevel.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 SatisfactorySaveParser/SaveLevel.cs diff --git a/SatisfactorySaveParser/SatisfactorySaveParser.csproj b/SatisfactorySaveParser/SatisfactorySaveParser.csproj index f560412..0462821 100644 --- a/SatisfactorySaveParser/SatisfactorySaveParser.csproj +++ b/SatisfactorySaveParser/SatisfactorySaveParser.csproj @@ -117,6 +117,7 @@ + diff --git a/SatisfactorySaveParser/SaveLevel.cs b/SatisfactorySaveParser/SaveLevel.cs new file mode 100644 index 0000000..a793062 --- /dev/null +++ b/SatisfactorySaveParser/SaveLevel.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using SatisfactorySaveParser.Structures; + +namespace SatisfactorySaveParser +{ + public class SaveLevel + { + /// + /// The name of the level + /// + public string Name; + + /// + /// a list of object instance names that belong in this level. + /// + public List ContainedObjectInstances = new List(); + + + /// + /// a list of references to collected objects, that belong in this level + /// + public List ContainedCollectablesInstances = new List(); + + public SaveLevel(string name) + { + Name = name; + } + } +} \ No newline at end of file From a2eebba2c9c0db3bf871a5b4cc465eb3c8815742 Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 16:38:05 +0100 Subject: [PATCH 04/11] SetProperty: fix writing 4 bytes that are always present. And add struct property as element, supports only vector structs so far. --- .../PropertyTypes/SetProperty.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/SatisfactorySaveParser/PropertyTypes/SetProperty.cs b/SatisfactorySaveParser/PropertyTypes/SetProperty.cs index e8120b5..f16a9e2 100644 --- a/SatisfactorySaveParser/PropertyTypes/SetProperty.cs +++ b/SatisfactorySaveParser/PropertyTypes/SetProperty.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using SatisfactorySaveParser.Structures; +using Vector = SatisfactorySaveParser.PropertyTypes.Structs.Vector; namespace SatisfactorySaveParser.PropertyTypes { @@ -42,7 +44,6 @@ public override void Serialize(BinaryWriter writer, int buildVersion, bool write { case IntProperty.TypeName: { - sizeExtra = 4; msWriter.Write(Elements.Count); foreach (var prop in Elements.Cast()) { @@ -69,12 +70,31 @@ public override void Serialize(BinaryWriter writer, int buildVersion, bool write } } break; + case StructProperty.TypeName: + { + + msWriter.Write(Elements.Count); + foreach (var prop in Elements.Cast()) + { + if (prop.Data is Vector vec) + { + msWriter.Write(vec.Data); + } + else + { + throw new NotSupportedException( + $"Writing Structs of type {prop.Type} is not yet supported."); + } + } + } + break; default: throw new NotImplementedException($"Serializing an array of {Type} is not yet supported."); } var bytes = ms.ToArray(); + sizeExtra += 4; writer.Write(bytes.Length + sizeExtra); writer.Write(Index); @@ -135,6 +155,22 @@ public static SetProperty Parse(string propertyName, int index, BinaryReader rea } } break; + case StructProperty.TypeName: + { + if (propertyName != "mRemovalLocations") + { + throw new NotImplementedException("Parsing a set of StructProperty other than mRemovalLocations is not yet supported"); + } + var locationsCount = reader.ReadInt32(); + for (var i = 0; i < locationsCount; i++) + { + var location = new Structs.Vector(reader); + var element = new StructProperty("location", i); + element.Data = location; + result.Elements.Add(element); + } + } + break; default: throw new NotImplementedException($"Parsing a set of {result.Type} is not yet supported."); } From a2d4250c8fc72eacfa3eb32924589aecd58e4a5f Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 16:45:55 +0100 Subject: [PATCH 05/11] add some metadata properties classes and reading of some trailing data in the SaveObjects. --- .../Metadata/ConveyorBeltLiftMetaData.cs | 22 +++ .../Metadata/ConveyorItemMetaData.cs | 22 +++ .../Metadata/SaveObjectMetaData.cs | 12 ++ .../SatisfactorySaveParser.csproj | 3 + SatisfactorySaveParser/SaveObject.cs | 156 +++++++++++++++++- 5 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 SatisfactorySaveParser/Metadata/ConveyorBeltLiftMetaData.cs create mode 100644 SatisfactorySaveParser/Metadata/ConveyorItemMetaData.cs create mode 100644 SatisfactorySaveParser/Metadata/SaveObjectMetaData.cs diff --git a/SatisfactorySaveParser/Metadata/ConveyorBeltLiftMetaData.cs b/SatisfactorySaveParser/Metadata/ConveyorBeltLiftMetaData.cs new file mode 100644 index 0000000..4981c01 --- /dev/null +++ b/SatisfactorySaveParser/Metadata/ConveyorBeltLiftMetaData.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace SatisfactorySaveParser.Metadata +{ + public class ConveyorBeltLiftMetaData : SaveObjectMetaData + { + public ConveyorItemMetaData[] Items; + + public override void ParseData(BinaryReader reader) + { + base.ParseData(reader); + + int numItems = reader.ReadInt32(); + Items = new ConveyorItemMetaData[numItems]; + for (int i = 0; i < numItems; i++) + { + Items[i] = new ConveyorItemMetaData(); + Items[i].ParseData(reader); + } + } + } +} \ No newline at end of file diff --git a/SatisfactorySaveParser/Metadata/ConveyorItemMetaData.cs b/SatisfactorySaveParser/Metadata/ConveyorItemMetaData.cs new file mode 100644 index 0000000..a3e1e82 --- /dev/null +++ b/SatisfactorySaveParser/Metadata/ConveyorItemMetaData.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace SatisfactorySaveParser.Metadata +{ + public class ConveyorItemMetaData : SaveObjectMetaData + { + + public string ItemPathName; + public float Position; + + public override void ParseData(BinaryReader reader) + { + base.ParseData(reader); + + reader.ReadInt32(); + ItemPathName = reader.ReadLengthPrefixedString(); + reader.ReadLengthPrefixedString(); + reader.ReadLengthPrefixedString(); + Position = reader.ReadSingle(); + } + } +} \ No newline at end of file diff --git a/SatisfactorySaveParser/Metadata/SaveObjectMetaData.cs b/SatisfactorySaveParser/Metadata/SaveObjectMetaData.cs new file mode 100644 index 0000000..9e109fd --- /dev/null +++ b/SatisfactorySaveParser/Metadata/SaveObjectMetaData.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace SatisfactorySaveParser.Metadata +{ + public class SaveObjectMetaData + { + public virtual void ParseData(BinaryReader reader) + { + + } + } +} \ No newline at end of file diff --git a/SatisfactorySaveParser/SatisfactorySaveParser.csproj b/SatisfactorySaveParser/SatisfactorySaveParser.csproj index 0462821..0e712f1 100644 --- a/SatisfactorySaveParser/SatisfactorySaveParser.csproj +++ b/SatisfactorySaveParser/SatisfactorySaveParser.csproj @@ -75,6 +75,9 @@ + + + diff --git a/SatisfactorySaveParser/SaveObject.cs b/SatisfactorySaveParser/SaveObject.cs index fa34de9..18f0d4c 100644 --- a/SatisfactorySaveParser/SaveObject.cs +++ b/SatisfactorySaveParser/SaveObject.cs @@ -1,5 +1,11 @@ -using NLog; +using System; +using System.Diagnostics; +using NLog; using System.IO; +using System.Linq; +using NLog.Fluent; +using SatisfactorySaveParser.Metadata; +using SatisfactorySaveParser.PropertyTypes.Structs; namespace SatisfactorySaveParser { @@ -31,11 +37,23 @@ public abstract class SaveObject /// public SerializedFields DataFields { get; set; } + /// + /// meta data from the trailing bytes + /// + public SaveObjectMetaData MetaData { get; set; } + + + /// + /// the level name this object belongs to (introduced in U6) + /// + public string LevelName { get; set; } + public SaveObject(string typePath, string rootObject, string instanceName) { TypePath = typePath; RootObject = rootObject; InstanceName = instanceName; + MetaData = new SaveObjectMetaData(); } protected SaveObject(BinaryReader reader) @@ -60,6 +78,142 @@ public virtual void SerializeData(BinaryWriter writer, int buildVersion) public virtual void ParseData(int length, BinaryReader reader, int buildVersion) { DataFields = SerializedFields.Parse(length, reader, buildVersion); + + // safely parse trailing data, there are some additional properties for some objects + if (DataFields.TrailingData != null && DataFields.TrailingData.Length > 0) + { + + var dataCopy = DataFields.TrailingData.ToArray(); + using (var trailingReader = new BinaryReader(new MemoryStream(dataCopy))) + { + ParseTrailingAdditionalData(trailingReader); + } + } + } + + private void ParseTrailingAdditionalData(BinaryReader reader) + { + switch (TypePath) + { + case "/Game/FactoryGame/Buildable/Factory/ConveyorBeltMk1/Build_ConveyorBeltMk1.Build_ConveyorBeltMk1_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorBeltMk2/Build_ConveyorBeltMk2.Build_ConveyorBeltMk2_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorBeltMk3/Build_ConveyorBeltMk3.Build_ConveyorBeltMk3_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorBeltMk4/Build_ConveyorBeltMk4.Build_ConveyorBeltMk4_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorBeltMk5/Build_ConveyorBeltMk5.Build_ConveyorBeltMk5_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorLiftMk1/Build_ConveyorLiftMk1.Build_ConveyorLiftMk1_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorLiftMk2/Build_ConveyorLiftMk2.Build_ConveyorLiftMk2_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorLiftMk3/Build_ConveyorLiftMk3.Build_ConveyorLiftMk3_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorLiftMk4/Build_ConveyorLiftMk4.Build_ConveyorLiftMk4_C": + case "/Game/FactoryGame/Buildable/Factory/ConveyorLiftMk5/Build_ConveyorLiftMk5.Build_ConveyorLiftMk5_C": + MetaData = new ConveyorBeltLiftMetaData(); + MetaData.ParseData(reader); + break; + case "/Game/FactoryGame/Buildable/Factory/DroneStation/BP_DroneTransport.BP_DroneTransport_C": + var droneStuff = reader.ReadInt32(); + break; + case "/Game/FactoryGame/-Shared/Blueprint/BP_GameMode.BP_GameMode_C": + var mode = reader.ReadInt32(); + break; + case "/Game/FactoryGame/-Shared/Blueprint/BP_GameState.BP_GameState_C": + // player state children + var numPlayerStates = reader.ReadInt32(); + for (int i = 0; i < numPlayerStates; i++) + { + var lvlname = reader.ReadLengthPrefixedString(); + var pthname = reader.ReadLengthPrefixedString(); + } + + break; + case "/Game/FactoryGame/Character/Player/BP_PlayerState.BP_PlayerState_C": + var playerType = reader.ReadByte(); + switch (playerType) + { + case 248: + var eos = reader.ReadLengthPrefixedString(); + var eosId = reader.ReadLengthPrefixedString(); + break; + case 25: + break; + case 8: + var platformId = reader.ReadLengthPrefixedString(); + break; + case 3: + break; + } + + break; + case "/Game/FactoryGame/-Shared/Blueprint/BP_CircuitSubsystem.BP_CircuitSubsystem_C": + var numCircuitSubsystems = reader.ReadInt32(); + for (int i = 0; i < numCircuitSubsystems; i++) + { + var circuitId = reader.ReadInt32(); + var lvlname = reader.ReadLengthPrefixedString(); + var pthname = reader.ReadLengthPrefixedString(); + } + + break; + case "/Game/FactoryGame/Buildable/Factory/PowerLine/Build_PowerLine.Build_PowerLine_C": + case "/Game/FactoryGame/Events/Christmas/Buildings/PowerLineLights/Build_XmassLightsLine.Build_XmassLightsLine_C": + + var lvlnameSource = reader.ReadLengthPrefixedString(); + var instanceNameSource = reader.ReadLengthPrefixedString(); + var lvlNameTarget = reader.ReadLengthPrefixedString(); + var instanceNameTarget = reader.ReadLengthPrefixedString(); + + // can have 6 floats + if (reader.BaseStream.Position < reader.BaseStream.Length) + { + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + } + + break; + case "/Game/FactoryGame/Buildable/Vehicle/Train/Locomotive/BP_Locomotive.BP_Locomotive_C": + case "/Game/FactoryGame/Buildable/Vehicle/Train/Wagon/BP_FreightWagon.BP_FreightWagon_C": + var padding = reader.ReadInt32(); + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var wagonLevelName = reader.ReadLengthPrefixedString(); + var wagonInstanceName = reader.ReadLengthPrefixedString(); + } + + break; + case "/Game/FactoryGame/Buildable/Vehicle/Tractor/BP_Tractor.BP_Tractor_C": + case "/Game/FactoryGame/Buildable/Vehicle/Truck/BP_Truck.BP_Truck_C": + case "/Game/FactoryGame/Buildable/Vehicle/Explorer/BP_Explorer.BP_Explorer_C": + case "/Game/FactoryGame/Buildable/Vehicle/Cyberwagon/Testa_BP_WB.Testa_BP_WB_C": + case "/Game/FactoryGame/Buildable/Vehicle/Golfcart/BP_Golfcart.BP_Golfcart_C": + case "/Game/FactoryGame/Buildable/Vehicle/Golfcart/BP_GolfcartGold.BP_GolfcartGold_C": + var hasAdditionalData = reader.ReadInt32(); + + // conditionally can have more + if (reader.BaseStream.Position < reader.BaseStream.Length) + { + var parentName = reader.ReadLengthPrefixedString(); + var position = reader.ReadVector3(); + var rotation = reader.ReadVector3(); + + // Not sure what it is. Always seems to be 29 Bytes. + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadByte(); + } + + break; + default: + // there are types that we did not cover yet. skip parsing until we update this behavior. + reader.BaseStream.Position = reader.BaseStream.Length; + break; + } } public override string ToString() From 579832ddb8f68f76b5de3f34fed06ad0e82b826e Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 16:47:21 +0100 Subject: [PATCH 06/11] add reading/saving of u6 + u7 saves. Reuse some logic of the approach so far, but u6 brings major changes to the save structure. --- SatisfactorySaveParser/SatisfactorySave.cs | 233 +++++++++++++++++++-- 1 file changed, 216 insertions(+), 17 deletions(-) diff --git a/SatisfactorySaveParser/SatisfactorySave.cs b/SatisfactorySaveParser/SatisfactorySave.cs index 6f062cf..0187102 100644 --- a/SatisfactorySaveParser/SatisfactorySave.cs +++ b/SatisfactorySaveParser/SatisfactorySave.cs @@ -6,8 +6,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Eventing; using System.IO; using System.Linq; +using SatisfactorySaveParser.Exceptions; namespace SatisfactorySaveParser { @@ -37,7 +39,13 @@ public class SatisfactorySave /// List of object references of all collected objects in the world (Nut/berry bushes, slugs, etc) /// public List CollectedObjects { get; set; } = new List(); + + /// + /// List of levels in the save + /// + public List Levels { get; set; } = new List(); + /// /// Open a savefile from disk /// @@ -60,7 +68,7 @@ public SatisfactorySave(string file) if (Header.SaveVersion < FSaveCustomVersion.SaveFileIsCompressed) { - LoadData(reader); + LoadDataU5AndBelow(reader); } else { @@ -104,14 +112,21 @@ public SatisfactorySave(string file) var dataLength = bufferReader.ReadInt32(); Trace.Assert(uncompressedSize == dataLength + 4); - LoadData(bufferReader); + if (Header.SaveVersion < FSaveCustomVersion.AddedSublevelStreaming) + { + LoadDataU5AndBelow(bufferReader); + } + else + { + LoadDataU6AndAbove(bufferReader); + } } } } } } - private void LoadData(BinaryReader reader) + private void LoadDataU5AndBelow(BinaryReader reader) { // Does not need to be a public property because it's equal to Entries.Count var totalSaveObjects = reader.ReadUInt32(); @@ -168,6 +183,103 @@ private void LoadData(BinaryReader reader) Trace.Assert(reader.BaseStream.Position == reader.BaseStream.Length); } + private void LoadDataU6AndAbove(BinaryReader reader) + { + var sublevelCount = reader.ReadInt32(); + for (int sublevelIndex = 0; + sublevelIndex <= sublevelCount; + sublevelIndex++) // sublevels and the "persistent level" at the end! + { + var levelName = (sublevelIndex == sublevelCount) + ? Header.MapName + : reader.ReadLengthPrefixedString(); // use MapName for the persistent level + SaveLevel level = new SaveLevel(levelName); + Levels.Add(level); + + var levelEntries = LoadLevelEntries(reader, levelName); + Entries.AddRange(levelEntries); + level.ContainedObjectInstances.AddRange(levelEntries.Select(entry => entry.InstanceName)); + + var levelCollectedObjects = LoadLevelCollectedObjects(reader); + CollectedObjects.AddRange(levelCollectedObjects); + level.ContainedCollectablesInstances.AddRange(levelCollectedObjects); + + log.Info($"Reading level [{sublevelIndex+1}/{Levels.Count}] with {Levels[sublevelIndex].ContainedObjectInstances.Count} objects and {Levels[sublevelIndex].ContainedCollectablesInstances.Count} collectables: {Levels[sublevelIndex].Name}."); + + ParseLevelEntries(levelEntries, reader); + LoadLevelCollectedObjects(reader); // skip second collectables + } + } + + + private List LoadLevelEntries(BinaryReader reader, string levelname) + { + reader.ReadInt32(); // skip "object header and collectables size" + var levelEntries = new List(); + var totalSaveObjects = reader.ReadInt32(); + + for (int i = 0; i < totalSaveObjects; i++) + { + var type = reader.ReadInt32(); + switch (type) + { + case SaveEntity.TypeID: + var entity = new SaveEntity(reader); + entity.LevelName = levelname; + levelEntries.Add(entity); + break; + case SaveComponent.TypeID: + var component = new SaveComponent(reader); + component.LevelName = levelname; + levelEntries.Add(component); + break; + default: + throw new InvalidOperationException($"Unexpected type {type}"); + } + } + + return levelEntries; + } + + private List LoadLevelCollectedObjects(BinaryReader reader) + { + var levelCollectedObjects = new List(); + var collectedObjectsCount = reader.ReadInt32(); + + for (int i = 0; i < collectedObjectsCount; i++) + { + var collected = new ObjectReference(reader); + levelCollectedObjects.Add(collected); + } + return levelCollectedObjects; + } + + private void ParseLevelEntries(List levelEntries, BinaryReader reader) + { + reader.ReadInt32(); // skip "objects binary size" + var objectCount = reader.ReadInt32(); + Trace.Assert(levelEntries.Count == objectCount); + + for (int i = 0; i < objectCount; i++) + { + var len = reader.ReadInt32(); + var before = reader.BaseStream.Position; + +#if DEBUG + //log.Trace($"Reading {len} bytes @ {before} for {levelEntries[i].TypePath}"); +#endif + + levelEntries[i].ParseData(len, reader, Header.BuildVersion); + var after = reader.BaseStream.Position; + + if (before + len != after) + { + throw new InvalidOperationException($"Expected {len} bytes read but got {after - before}"); + } + } + } + + public void Save() { Save(FileName); @@ -187,7 +299,7 @@ public void Save(string file) if (Header.SaveVersion < FSaveCustomVersion.SaveFileIsCompressed) { - SaveData(writer, Header.BuildVersion); + SaveDataU5AndBelow(writer, Header.BuildVersion); } else { @@ -196,7 +308,14 @@ public void Save(string file) { bufferWriter.Write(0); // Placeholder size - SaveData(bufferWriter, Header.BuildVersion); + if (Header.SaveVersion < FSaveCustomVersion.AddedSublevelStreaming) + { + SaveDataU5AndBelow(bufferWriter, Header.BuildVersion); + } + else + { + SaveDataU6AndAbove(bufferWriter, Header.BuildVersion); + } buffer.Position = 0; bufferWriter.Write((int)buffer.Length - 4); @@ -243,29 +362,116 @@ public void Save(string file) } } - private void SaveData(BinaryWriter writer, int buildVersion) + private void SaveDataU6AndAbove(BinaryWriter writer, int buildVersion) { - writer.Write(Entries.Count); + writer.Write(Levels.Count-1); + + for (int i = 0; i < Levels.Count; i++) + { + if (!Levels[i].Name.Equals(Header.MapName)) + { + writer.WriteLengthPrefixedString(Levels[i].Name); + } + + log.Info($"Writing level [{i+1}/{Levels.Count}] with {Levels[i].ContainedObjectInstances.Count} objects and {Levels[i].ContainedCollectablesInstances.Count} collectables: {Levels[i].Name}."); + + using (var buffer = new MemoryStream()) + using (var subLevelWriter = new BinaryWriter(buffer)) + { + + // write object headers (entities and components) + var levelObjects = Levels[i].ContainedObjectInstances.Select(instanceName => + Entries.First(entry => entry.InstanceName.Equals(instanceName))).ToList(); + var entities = levelObjects.Where(e => e is SaveEntity).Cast().ToArray(); + var components = levelObjects.Where(e => e is SaveComponent).Cast().ToArray(); + SaveObjectsHeaderList(subLevelWriter, buildVersion, entities, components); + + // write collected objects list + var levelCollectables = Levels[i].ContainedCollectablesInstances; + SaveCollectablesList(subLevelWriter, buildVersion, levelCollectables); + + var bufferedContent = buffer.ToArray(); + writer.Write(bufferedContent.Length); + writer.Write(bufferedContent); + } + + // write objects content + collectables. + using (var buffer = new MemoryStream()) + using (var subLevelWriter = new BinaryWriter(buffer)) + { + + // write object content (entities and components) + var levelObjects = Levels[i].ContainedObjectInstances.Select(instanceName => + Entries.First(entry => entry.InstanceName.Equals(instanceName))).ToList(); + var entities = levelObjects.Where(e => e is SaveEntity).Cast().ToArray(); + var components = levelObjects.Where(e => e is SaveComponent).Cast().ToArray(); + SaveObjectsContentList(subLevelWriter, buildVersion, entities, components); + + // write collected objects list + var levelCollectables = Levels[i].ContainedCollectablesInstances; + SaveCollectablesList(subLevelWriter, buildVersion, levelCollectables); + + var bufferedContent = buffer.ToArray(); + writer.Write(bufferedContent.Length); + writer.Write(bufferedContent); + } + } + } - var entities = Entries.Where(e => e is SaveEntity).ToArray(); + private void SaveDataU5AndBelow(BinaryWriter writer, int buildVersion) + { + SaveDataU5AndBelow(writer, buildVersion, Entries, CollectedObjects); + } + + private void SaveDataU5AndBelow(BinaryWriter writer, int buildVersion, List entries, List collectedObjects) + { + SaveObjectsList(writer, buildVersion, entries); + SaveCollectablesList(writer, buildVersion, collectedObjects); + } + + private void SaveCollectablesList(in BinaryWriter writer, in int buildVersion, in List collectedObjects) + { + writer.Write(collectedObjects.Count); + foreach (var collectedObject in collectedObjects) + { + writer.WriteLengthPrefixedString(collectedObject.LevelName); + writer.WriteLengthPrefixedString(collectedObject.PathName); + } + } + + private void SaveObjectsList(in BinaryWriter writer, in int buildVersion, in List objects) + { + var entities = objects.Where(e => e is SaveEntity).Cast().ToArray(); + var components = objects.Where(e => e is SaveComponent).Cast().ToArray(); + SaveObjectsHeaderList(writer, buildVersion, entities, components); + SaveObjectsContentList(writer, buildVersion, entities, components); + } + + private void SaveObjectsHeaderList(in BinaryWriter writer, in int buildVersion, in SaveEntity[] entities, in SaveComponent[] components) + { + writer.Write(entities.Length + components.Length); + for (var i = 0; i < entities.Length; i++) { writer.Write(SaveEntity.TypeID); entities[i].SerializeHeader(writer); } - var components = Entries.Where(e => e is SaveComponent).ToArray(); for (var i = 0; i < components.Length; i++) { writer.Write(SaveComponent.TypeID); components[i].SerializeHeader(writer); } - + } + + private void SaveObjectsContentList(in BinaryWriter writer, in int buildVersion, in SaveEntity[] entities, in SaveComponent[] components) + { writer.Write(entities.Length + components.Length); using (var ms = new MemoryStream()) using (var dataWriter = new BinaryWriter(ms)) { + for (var i = 0; i < entities.Length; i++) { entities[i].SerializeData(dataWriter, buildVersion); @@ -287,13 +493,6 @@ private void SaveData(BinaryWriter writer, int buildVersion) ms.SetLength(0); } } - - writer.Write(CollectedObjects.Count); - foreach (var collectedObject in CollectedObjects) - { - writer.WriteLengthPrefixedString(collectedObject.LevelName); - writer.WriteLengthPrefixedString(collectedObject.PathName); - } } } } From 22ca2808e2d81bf47b47e14ddccc3fff741abb4d Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 15 Jan 2023 17:00:29 +0100 Subject: [PATCH 07/11] add reading/writing of int64 arrays --- .../PropertyTypes/ArrayProperty.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/SatisfactorySaveParser/PropertyTypes/ArrayProperty.cs b/SatisfactorySaveParser/PropertyTypes/ArrayProperty.cs index 2de5b9f..9f364cc 100644 --- a/SatisfactorySaveParser/PropertyTypes/ArrayProperty.cs +++ b/SatisfactorySaveParser/PropertyTypes/ArrayProperty.cs @@ -85,6 +85,15 @@ public override void Serialize(BinaryWriter writer, int buildVersion, bool write } } break; + case Int64Property.TypeName: + { + msWriter.Write(Elements.Count); + foreach (var prop in Elements.Cast()) + { + msWriter.Write(prop.Value); + } + } + break; case ByteProperty.TypeName: { msWriter.Write(Elements.Count); @@ -196,6 +205,17 @@ public static ArrayProperty Parse(string propertyName, int index, BinaryReader r } } break; + + case Int64Property.TypeName: + { + var count = reader.ReadInt32(); + for (var i = 0; i < count; i++) + { + var value = reader.ReadInt64(); + result.Elements.Add(new Int64Property($"Element {i}") { Value = value }); + } + } + break; case ByteProperty.TypeName: { var count = reader.ReadInt32(); From 9535068861cce5b182be7e33c646c6131e2401fd Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 22 Jan 2023 17:33:26 +0100 Subject: [PATCH 08/11] bug fix for writing correct binary size and parsing trailing collectables --- SatisfactorySaveParser/SatisfactorySave.cs | 43 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/SatisfactorySaveParser/SatisfactorySave.cs b/SatisfactorySaveParser/SatisfactorySave.cs index 0187102..73a1bf1 100644 --- a/SatisfactorySaveParser/SatisfactorySave.cs +++ b/SatisfactorySaveParser/SatisfactorySave.cs @@ -44,6 +44,13 @@ public class SatisfactorySave /// List of levels in the save /// public List Levels { get; set; } = new List(); + + private byte[] TrailingBodyData { get; set; } + + /// + /// List of object references that do not belong to any particular level + /// + public List TrailingCollectedObjects { get; set; } = new List(); /// @@ -204,11 +211,19 @@ private void LoadDataU6AndAbove(BinaryReader reader) CollectedObjects.AddRange(levelCollectedObjects); level.ContainedCollectablesInstances.AddRange(levelCollectedObjects); - log.Info($"Reading level [{sublevelIndex+1}/{Levels.Count}] with {Levels[sublevelIndex].ContainedObjectInstances.Count} objects and {Levels[sublevelIndex].ContainedCollectablesInstances.Count} collectables: {Levels[sublevelIndex].Name}."); + log.Info($"Reading level [{sublevelIndex+1}/{sublevelCount+1}] with {Levels[sublevelIndex].ContainedObjectInstances.Count} objects and {Levels[sublevelIndex].ContainedCollectablesInstances.Count} collectables: {Levels[sublevelIndex].Name}."); ParseLevelEntries(levelEntries, reader); LoadLevelCollectedObjects(reader); // skip second collectables } + + // somewhere in the U6/U7 updates can be additional collected objects for a level that is not listed + var bytesLeft = reader.BaseStream.Length - reader.BaseStream.Position; + if (bytesLeft > 0) + { + log.Debug($"{bytesLeft} bytes are left at the end for some other collectables."); + TrailingCollectedObjects = LoadLevelCollectedObjects(reader); + } } @@ -237,7 +252,6 @@ private List LoadLevelEntries(BinaryReader reader, string levelname) throw new InvalidOperationException($"Unexpected type {type}"); } } - return levelEntries; } @@ -256,7 +270,8 @@ private List LoadLevelCollectedObjects(BinaryReader reader) private void ParseLevelEntries(List levelEntries, BinaryReader reader) { - reader.ReadInt32(); // skip "objects binary size" + var binarySize = reader.ReadInt32(); // skip "objects binary size" + long Before = reader.BaseStream.Position; var objectCount = reader.ReadInt32(); Trace.Assert(levelEntries.Count == objectCount); @@ -266,7 +281,10 @@ private void ParseLevelEntries(List levelEntries, BinaryReader reade var before = reader.BaseStream.Position; #if DEBUG - //log.Trace($"Reading {len} bytes @ {before} for {levelEntries[i].TypePath}"); + /*if (i % 10000 == 0) + { + log.Trace($"Having {Math.Round(((float)(i+1)/objectCount)*100, 2)}% Reading {len} bytes @ {before} for {levelEntries[i].TypePath}"); + }*/ #endif levelEntries[i].ParseData(len, reader, Header.BuildVersion); @@ -277,6 +295,9 @@ private void ParseLevelEntries(List levelEntries, BinaryReader reade throw new InvalidOperationException($"Expected {len} bytes read but got {after - before}"); } } + + long ReadBytesCount = reader.BaseStream.Position - Before; + Debug.Assert(ReadBytesCount == binarySize); } @@ -407,15 +428,23 @@ private void SaveDataU6AndAbove(BinaryWriter writer, int buildVersion) var components = levelObjects.Where(e => e is SaveComponent).Cast().ToArray(); SaveObjectsContentList(subLevelWriter, buildVersion, entities, components); + // the binary size this time however is only for object content. Without the collectables. + var bufferedContent = buffer.ToArray(); + writer.Write(bufferedContent.Length); + writer.Write(bufferedContent); + buffer.SetLength(0); + // write collected objects list var levelCollectables = Levels[i].ContainedCollectablesInstances; SaveCollectablesList(subLevelWriter, buildVersion, levelCollectables); - - var bufferedContent = buffer.ToArray(); - writer.Write(bufferedContent.Length); + + bufferedContent = buffer.ToArray(); writer.Write(bufferedContent); } } + + // save extra collectables + SaveCollectablesList(writer, Header.BuildVersion, TrailingCollectedObjects); } private void SaveDataU5AndBelow(BinaryWriter writer, int buildVersion) From eda7aa4eef685b56918e7f00f45b1ca36c5b2c54 Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 22 Jan 2023 19:44:49 +0100 Subject: [PATCH 09/11] use save level structure instead of global entry list to speed up editor. --- .../Cheats/RemoveSlugsCheat.cs | 18 +++-- .../Cheats/RestoreSlugsCheat.cs | 6 +- .../Model/SaveObjectModel.cs | 8 +++ SatisfactorySaveEditor/View/MainWindow.xaml | 54 ++++++++------- .../ViewModel/MainViewModel.cs | 31 +++++++-- SatisfactorySaveParser/SatisfactorySave.cs | 68 ++++++++----------- SatisfactorySaveParser/SaveLevel.cs | 4 +- 7 files changed, 112 insertions(+), 77 deletions(-) diff --git a/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs b/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs index 1e19e9a..bd0b362 100644 --- a/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs +++ b/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs @@ -23,14 +23,18 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) "/Game/FactoryGame/Resource/Environment/Crystal/BP_Crystal_mk3.BP_Crystal_mk3_C" }; - var slugs = saveGame.Entries.Where(x => slugTypes.Contains(x.TypePath)); - - saveGame.CollectedObjects.RemoveAll(x => x.PathName.Contains("PersistentLevel.BP_Crystal")); - saveGame.CollectedObjects.AddRange(slugs.Select(s => new ObjectReference + foreach (var level in saveGame.Levels) { - LevelName = s.RootObject, - PathName = s.InstanceName - })); + + var slugs = level.Entries.Where(x => slugTypes.Contains(x.TypePath)); + + level.CollectedObjects.RemoveAll(x => x.PathName.Contains("PersistentLevel.BP_Crystal")); + level.CollectedObjects.AddRange(slugs.Select(s => new ObjectReference + { + LevelName = s.RootObject, + PathName = s.InstanceName + })); + } return true; } diff --git a/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs b/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs index 9c880df..9119a3c 100644 --- a/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs +++ b/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs @@ -11,7 +11,11 @@ public class RestoreSlugsCheat : ICheat public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) { - saveGame.CollectedObjects.RemoveAll(x => x.PathName.Contains("PersistentLevel.BP_Crystal")); + foreach (var level in saveGame.Levels) + { + level.CollectedObjects.RemoveAll(x => x.PathName.Contains("PersistentLevel.BP_Crystal")); + } + return true; } } diff --git a/SatisfactorySaveEditor/Model/SaveObjectModel.cs b/SatisfactorySaveEditor/Model/SaveObjectModel.cs index d5652e7..84826e5 100644 --- a/SatisfactorySaveEditor/Model/SaveObjectModel.cs +++ b/SatisfactorySaveEditor/Model/SaveObjectModel.cs @@ -16,6 +16,7 @@ namespace SatisfactorySaveEditor.Model { public class SaveObjectModel : ViewModelBase { + private string levelName; private string title; private string rootObject; private string type; @@ -63,6 +64,12 @@ public List DescendantSelfViewModel } } + public string LevelName + { + get => levelName; + set { Set(() => LevelName, ref levelName, value); } + } + public string Title { get => title; @@ -98,6 +105,7 @@ public bool IsExpanded public SaveObjectModel(SaveObject model) { Model = model; + LevelName = model.LevelName; Title = model.InstanceName; RootObject = model.RootObject; Type = model.TypePath.Split('/').Last(); diff --git a/SatisfactorySaveEditor/View/MainWindow.xaml b/SatisfactorySaveEditor/View/MainWindow.xaml index c3f3240..12955ee 100644 --- a/SatisfactorySaveEditor/View/MainWindow.xaml +++ b/SatisfactorySaveEditor/View/MainWindow.xaml @@ -298,27 +298,30 @@ + - public FSaveHeader Header { get; private set; } - /// - /// Main content of the save game - /// - public List Entries { get; set; } = new List(); - - /// - /// List of object references of all collected objects in the world (Nut/berry bushes, slugs, etc) - /// - public List CollectedObjects { get; set; } = new List(); - /// /// List of levels in the save /// public List Levels { get; set; } = new List(); - - private byte[] TrailingBodyData { get; set; } - + /// /// List of object references that do not belong to any particular level /// @@ -66,7 +54,7 @@ public SatisfactorySave(string file) using (var stream = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var reader = new BinaryReader(stream)) { - if(stream.Length == 0) + if (stream.Length == 0) { throw new Exception("Save file is completely empty"); } @@ -110,7 +98,9 @@ public SatisfactorySave(string file) buffer.Position = 0; #if DEBUG - File.WriteAllBytes(Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".bin"), buffer.ToArray()); + File.WriteAllBytes( + Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".bin"), + buffer.ToArray()); #endif @@ -135,6 +125,10 @@ public SatisfactorySave(string file) private void LoadDataU5AndBelow(BinaryReader reader) { + // create a single level to contain the entries. To stay compatible with U6 structure of levels. + var defaultLevel = new SaveLevel(Header.MapName); + Levels.Add(defaultLevel); + // Does not need to be a public property because it's equal to Entries.Count var totalSaveObjects = reader.ReadUInt32(); log.Info($"Save contains {totalSaveObjects} object headers"); @@ -146,10 +140,10 @@ private void LoadDataU5AndBelow(BinaryReader reader) switch (type) { case SaveEntity.TypeID: - Entries.Add(new SaveEntity(reader)); + defaultLevel.Entries.Add(new SaveEntity(reader)); break; case SaveComponent.TypeID: - Entries.Add(new SaveComponent(reader)); + defaultLevel.Entries.Add(new SaveComponent(reader)); break; default: throw new InvalidOperationException($"Unexpected type {type}"); @@ -158,10 +152,10 @@ private void LoadDataU5AndBelow(BinaryReader reader) var totalSaveObjectData = reader.ReadInt32(); log.Info($"Save contains {totalSaveObjectData} object data"); - Trace.Assert(Entries.Count == totalSaveObjects); - Trace.Assert(Entries.Count == totalSaveObjectData); + Trace.Assert(defaultLevel.Entries.Count == totalSaveObjects); + Trace.Assert(defaultLevel.Entries.Count == totalSaveObjectData); - for (int i = 0; i < Entries.Count; i++) + for (int i = 0; i < defaultLevel.Entries.Count; i++) { var len = reader.ReadInt32(); var before = reader.BaseStream.Position; @@ -170,7 +164,7 @@ private void LoadDataU5AndBelow(BinaryReader reader) //log.Trace($"Reading {len} bytes @ {before} for {Entries[i].TypePath}"); #endif - Entries[i].ParseData(len, reader, Header.BuildVersion); + defaultLevel.Entries[i].ParseData(len, reader, Header.BuildVersion); var after = reader.BaseStream.Position; if (before + len != after) @@ -183,9 +177,10 @@ private void LoadDataU5AndBelow(BinaryReader reader) log.Info($"Save contains {collectedObjectsCount} collected objects"); for (int i = 0; i < collectedObjectsCount; i++) { - CollectedObjects.Add(new ObjectReference(reader)); + defaultLevel.CollectedObjects.Add(new ObjectReference(reader)); } + log.Debug($"Read {reader.BaseStream.Position} of total {reader.BaseStream.Length} bytes"); Trace.Assert(reader.BaseStream.Position == reader.BaseStream.Length); } @@ -204,24 +199,21 @@ private void LoadDataU6AndAbove(BinaryReader reader) Levels.Add(level); var levelEntries = LoadLevelEntries(reader, levelName); - Entries.AddRange(levelEntries); - level.ContainedObjectInstances.AddRange(levelEntries.Select(entry => entry.InstanceName)); + level.Entries.AddRange(levelEntries); var levelCollectedObjects = LoadLevelCollectedObjects(reader); - CollectedObjects.AddRange(levelCollectedObjects); - level.ContainedCollectablesInstances.AddRange(levelCollectedObjects); + level.CollectedObjects.AddRange(levelCollectedObjects); - log.Info($"Reading level [{sublevelIndex+1}/{sublevelCount+1}] with {Levels[sublevelIndex].ContainedObjectInstances.Count} objects and {Levels[sublevelIndex].ContainedCollectablesInstances.Count} collectables: {Levels[sublevelIndex].Name}."); + log.Info($"Reading level [{sublevelIndex+1}/{sublevelCount+1}] with {Levels[sublevelIndex].Entries.Count} objects and {Levels[sublevelIndex].CollectedObjects.Count} collectables: {Levels[sublevelIndex].Name}."); ParseLevelEntries(levelEntries, reader); LoadLevelCollectedObjects(reader); // skip second collectables } - // somewhere in the U6/U7 updates can be additional collected objects for a level that is not listed + // somewhere in the U6/U7 updates can be additional collected objects var bytesLeft = reader.BaseStream.Length - reader.BaseStream.Position; if (bytesLeft > 0) { - log.Debug($"{bytesLeft} bytes are left at the end for some other collectables."); TrailingCollectedObjects = LoadLevelCollectedObjects(reader); } } @@ -252,6 +244,7 @@ private List LoadLevelEntries(BinaryReader reader, string levelname) throw new InvalidOperationException($"Unexpected type {type}"); } } + return levelEntries; } @@ -270,10 +263,12 @@ private List LoadLevelCollectedObjects(BinaryReader reader) private void ParseLevelEntries(List levelEntries, BinaryReader reader) { + var binarySize = reader.ReadInt32(); // skip "objects binary size" long Before = reader.BaseStream.Position; var objectCount = reader.ReadInt32(); Trace.Assert(levelEntries.Count == objectCount); + for (int i = 0; i < objectCount; i++) { @@ -394,21 +389,20 @@ private void SaveDataU6AndAbove(BinaryWriter writer, int buildVersion) writer.WriteLengthPrefixedString(Levels[i].Name); } - log.Info($"Writing level [{i+1}/{Levels.Count}] with {Levels[i].ContainedObjectInstances.Count} objects and {Levels[i].ContainedCollectablesInstances.Count} collectables: {Levels[i].Name}."); + log.Info($"Writing level [{i+1}/{Levels.Count}] with {Levels[i].Entries.Count} objects and {Levels[i].CollectedObjects.Count} collectables: {Levels[i].Name}."); using (var buffer = new MemoryStream()) using (var subLevelWriter = new BinaryWriter(buffer)) { // write object headers (entities and components) - var levelObjects = Levels[i].ContainedObjectInstances.Select(instanceName => - Entries.First(entry => entry.InstanceName.Equals(instanceName))).ToList(); + var levelObjects = Levels[i].Entries; var entities = levelObjects.Where(e => e is SaveEntity).Cast().ToArray(); var components = levelObjects.Where(e => e is SaveComponent).Cast().ToArray(); SaveObjectsHeaderList(subLevelWriter, buildVersion, entities, components); // write collected objects list - var levelCollectables = Levels[i].ContainedCollectablesInstances; + var levelCollectables = Levels[i].CollectedObjects; SaveCollectablesList(subLevelWriter, buildVersion, levelCollectables); var bufferedContent = buffer.ToArray(); @@ -422,8 +416,7 @@ private void SaveDataU6AndAbove(BinaryWriter writer, int buildVersion) { // write object content (entities and components) - var levelObjects = Levels[i].ContainedObjectInstances.Select(instanceName => - Entries.First(entry => entry.InstanceName.Equals(instanceName))).ToList(); + var levelObjects = Levels[i].Entries; var entities = levelObjects.Where(e => e is SaveEntity).Cast().ToArray(); var components = levelObjects.Where(e => e is SaveComponent).Cast().ToArray(); SaveObjectsContentList(subLevelWriter, buildVersion, entities, components); @@ -435,7 +428,7 @@ private void SaveDataU6AndAbove(BinaryWriter writer, int buildVersion) buffer.SetLength(0); // write collected objects list - var levelCollectables = Levels[i].ContainedCollectablesInstances; + var levelCollectables = Levels[i].CollectedObjects; SaveCollectablesList(subLevelWriter, buildVersion, levelCollectables); bufferedContent = buffer.ToArray(); @@ -443,13 +436,12 @@ private void SaveDataU6AndAbove(BinaryWriter writer, int buildVersion) } } - // save extra collectables SaveCollectablesList(writer, Header.BuildVersion, TrailingCollectedObjects); } private void SaveDataU5AndBelow(BinaryWriter writer, int buildVersion) { - SaveDataU5AndBelow(writer, buildVersion, Entries, CollectedObjects); + SaveDataU5AndBelow(writer, buildVersion, Levels.SelectMany(level => level.Entries).ToList(), Levels.SelectMany(level => level.CollectedObjects).ToList()); } private void SaveDataU5AndBelow(BinaryWriter writer, int buildVersion, List entries, List collectedObjects) diff --git a/SatisfactorySaveParser/SaveLevel.cs b/SatisfactorySaveParser/SaveLevel.cs index a793062..571239b 100644 --- a/SatisfactorySaveParser/SaveLevel.cs +++ b/SatisfactorySaveParser/SaveLevel.cs @@ -15,13 +15,13 @@ public class SaveLevel /// /// a list of object instance names that belong in this level. /// - public List ContainedObjectInstances = new List(); + public List Entries = new List(); /// /// a list of references to collected objects, that belong in this level /// - public List ContainedCollectablesInstances = new List(); + public List CollectedObjects = new List(); public SaveLevel(string name) { From 6c551befa0774d9cc68b802cc1e2db7504432b08 Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 29 Jan 2023 15:32:59 +0100 Subject: [PATCH 10/11] fix some cheats by using the correct save level name. --- .../Cheats/CouponChangerCheat.cs | 32 ++++++++++----- .../Cheats/DeleteEnemiesCheat.cs | 39 ++++++++++--------- .../Cheats/EverythingBoxCheat.cs | 15 +++---- .../Cheats/KillPlayersCheat.cs | 4 +- .../Cheats/MassDismantleCheat.cs | 8 ++-- .../Cheats/RemoveSlugsCheat.cs | 29 ++++++++------ .../Cheats/RestoreSlugsCheat.cs | 3 +- .../Cheats/SpawnDoggoCheat.cs | 14 ++----- .../Cheats/UndoDeleteEnemiesCheat.cs | 7 ++-- SatisfactorySaveParser/SatisfactorySave.cs | 28 ++++++------- SatisfactorySaveParser/SaveComponent.cs | 4 +- SatisfactorySaveParser/SaveEntity.cs | 6 +-- SatisfactorySaveParser/SaveObject.cs | 6 ++- 13 files changed, 100 insertions(+), 95 deletions(-) diff --git a/SatisfactorySaveEditor/Cheats/CouponChangerCheat.cs b/SatisfactorySaveEditor/Cheats/CouponChangerCheat.cs index 703c86c..2579f33 100644 --- a/SatisfactorySaveEditor/Cheats/CouponChangerCheat.cs +++ b/SatisfactorySaveEditor/Cheats/CouponChangerCheat.cs @@ -17,12 +17,14 @@ public class CouponChangerCheat : ICheat private long pointsRequiredFromTicketCount(int tickets) { - //equation for ticket count from points is y={x>3:(ceil(x/3)^2)*1000, x<4:1000} where x is ticket count and y is points required. from here: https://satisfactory.gamepedia.com/AWESOME_Sink - //OLD ticket cost function for pre-0.3.3 AWESOME sink --TODO update to new ticket cost function once that is determined + // see ticket equations https://satisfactory.gamepedia.com/AWESOME_Sink + // ticket cost pre-0.3.3 was Pow(Ceiling(tickets / 3.0), 2) * 1000 + // ticket cost U7 is (Pow(Ceiling(tickets / 3.0)-1, 2) * 500) + 1000 + // TODO: keep the cost function up to date. if (tickets < 4) return 1000; else - return (long) (Pow(Ceiling(tickets / 3.0), 2) * 1000); + return (long) (Pow(Ceiling(tickets / 3.0)-1, 2) * 500) + 1000; } public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) @@ -35,7 +37,17 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) } var pointsTowardsCurrentTicket = sinkSubsystem.FindOrCreateField("mTotalResourceSinkPoints"); - var mCurrentPointLevel = sinkSubsystem.FindOrCreateField("mCurrentPointLevel"); + //var mCurrentPointLevel = sinkSubsystem.FindOrCreateField("mCurrentPointLevel"); + + // how many tickets are currently printable from sink. + var numResourceSinkCoupons = sinkSubsystem.FindOrCreateField("mNumResourceSinkCoupons"); + + // which ticket point level (modulo 3) the sink currently is (points, alien DNA). + var pointLevels = sinkSubsystem.FindOrCreateField("mCurrentPointLevels"); + + // the accumulated points the sink currently has (points, alien DNA). + var accumulatedPoints = sinkSubsystem.FindOrCreateField("mTotalPoints"); + var dialog = new StringPromptWindow { @@ -45,7 +57,8 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) cvm.WindowTitle = "Enter earned ticket count"; cvm.PromptMessage = "Tickets"; cvm.ValueChosen = "0"; - cvm.OldValueMessage = $"Sets the AWESOME Sink ticket prices as if you had earned N tickets.\nFor example, entering 0 sets the price for the next ticket back to 1,000\nCurrent tickets earned: {mCurrentPointLevel.Value}\nMore info on AWESOME Sink wiki page"; + //mCurrentPointLevel.Value + cvm.OldValueMessage = $"Sets the AWESOME Sink ticket prices as if you had earned N tickets.\nFor example, entering 0 sets the price for the next ticket back to 1,000\nCurrent tickets earned: {((IntPropertyViewModel)pointLevels.Elements[0]).Value}\nMore info on AWESOME Sink wiki page"; dialog.ShowDialog(); int requestedTicketCount = 0; @@ -65,14 +78,15 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) return false; } - mCurrentPointLevel.Value = requestedTicketCount; //"point level" is 0 if no tickets have been earned, 1 if one ticket has, etc. + //mCurrentPointLevel.Value = requestedTicketCount; //"point level" is 0 if no tickets have been earned, 1 if one ticket has, etc. + ((IntPropertyViewModel)pointLevels.Elements[0]).Value = requestedTicketCount; + pointsTowardsCurrentTicket.Value = 0; //reset progress towards the current ticket so the game GUI doesn't get confused - long calculatedPointsCount = pointsRequiredFromTicketCount(requestedTicketCount); + long calculatedPointsCountNextTicket = pointsRequiredFromTicketCount(requestedTicketCount+1); - MessageBox.Show($"Earned ticket count set to {requestedTicketCount}.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); - //MessageBox.Show($"Ticket count set to {requestedTicketCount}. The next ticket will take {calculatedPointsCount} points to earn.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + MessageBox.Show($"Earned ticket count set to {requestedTicketCount}. The next ticket will take {calculatedPointsCountNextTicket} points to earn.", "Success", MessageBoxButton.OK, MessageBoxImage.Information); return true; } catch (Exception) diff --git a/SatisfactorySaveEditor/Cheats/DeleteEnemiesCheat.cs b/SatisfactorySaveEditor/Cheats/DeleteEnemiesCheat.cs index 035ca89..7842df3 100644 --- a/SatisfactorySaveEditor/Cheats/DeleteEnemiesCheat.cs +++ b/SatisfactorySaveEditor/Cheats/DeleteEnemiesCheat.cs @@ -1,16 +1,14 @@ -using SatisfactorySaveEditor.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows; +using SatisfactorySaveEditor.Model; using SatisfactorySaveEditor.ViewModel.Property; using SatisfactorySaveEditor.ViewModel.Struct; using SatisfactorySaveParser; using SatisfactorySaveParser.PropertyTypes; using SatisfactorySaveParser.PropertyTypes.Structs; using SatisfactorySaveParser.Structures; -using System; -using System.Diagnostics; -using System.IO; -using System.Windows; -using Vector = SatisfactorySaveParser.PropertyTypes.Structs.Vector; -using Vector3 = SatisfactorySaveParser.Structures.Vector3; namespace SatisfactorySaveEditor.Cheats { @@ -91,7 +89,7 @@ public bool AddDoggo(SaveObjectModel rootItem, SatisfactorySave saveGame) } var player = (SaveEntityModel)hostPlayerModel.Items[0]; - SaveComponent healthComponent = new SaveComponent("/Script/FactoryGame.FGHealthComponent", "Persistent_Level", $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.HealthComponent") + SaveComponent healthComponent = new SaveComponent(saveGame.Header.MapName, "/Script/FactoryGame.FGHealthComponent", "Persistent_Level", $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.HealthComponent") { DataFields = new SerializedFields(), ParentEntityName = $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}" @@ -100,14 +98,14 @@ public bool AddDoggo(SaveObjectModel rootItem, SatisfactorySave saveGame) SaveComponent inventoryComponent; using (BinaryReader reader = new BinaryReader(new MemoryStream(bytes))) { - inventoryComponent = new SaveComponent("/Script/FactoryGame.FGInventoryComponent", "Persistent_Level", $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.mInventory") + inventoryComponent = new SaveComponent(saveGame.Header.MapName, "/Script/FactoryGame.FGInventoryComponent", "Persistent_Level", $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.mInventory") { DataFields = new SerializedFields() { new ArrayProperty("mInventoryStacks") { Type = StructProperty.TypeName, - Elements = new System.Collections.Generic.List() + Elements = new List() { SerializedProperty.Parse(reader, saveGame.Header.BuildVersion) } @@ -115,7 +113,7 @@ public bool AddDoggo(SaveObjectModel rootItem, SatisfactorySave saveGame) new ArrayProperty("mArbitrarySlotSizes") { Type = IntProperty.TypeName, - Elements = new System.Collections.Generic.List() + Elements = new List() { new IntProperty("Element") { Value = 0 } } @@ -123,7 +121,7 @@ public bool AddDoggo(SaveObjectModel rootItem, SatisfactorySave saveGame) new ArrayProperty("mAllowedItemDescriptors") { Type = ObjectProperty.TypeName, - Elements = new System.Collections.Generic.List() + Elements = new List() { new ObjectProperty("Element") { LevelName = "", PathName = "" } } @@ -132,7 +130,7 @@ public bool AddDoggo(SaveObjectModel rootItem, SatisfactorySave saveGame) ParentEntityName = $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}" }; } - SaveEntity doggo = new SaveEntity("/Game/FactoryGame/Character/Creature/Wildlife/SpaceRabbit/Char_SpaceRabbit.Char_SpaceRabbit_C", "Persistent_Level", $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}") + SaveEntity doggo = new SaveEntity(saveGame.Header.MapName, "/Game/FactoryGame/Character/Creature/Wildlife/SpaceRabbit/Char_SpaceRabbit.Char_SpaceRabbit_C", "Persistent_Level", $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}") { NeedTransform = true, Rotation = ((SaveEntity)player.Model).Rotation, @@ -147,10 +145,10 @@ public bool AddDoggo(SaveObjectModel rootItem, SatisfactorySave saveGame) ParentObjectName = "", ParentObjectRoot = "" }; - doggo.Components = new System.Collections.Generic.List() + doggo.Components = new List() { - new SatisfactorySaveParser.Structures.ObjectReference() {LevelName = "Persistent_Level", PathName = $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.mInventory"}, - new SatisfactorySaveParser.Structures.ObjectReference() {LevelName = "Persistent_Level", PathName = $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.HealthComponent"} + new ObjectReference() {LevelName = "Persistent_Level", PathName = $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.mInventory"}, + new ObjectReference() {LevelName = "Persistent_Level", PathName = $"Persistent_Level:PersistentLevel.Char_SpaceRabbit_C_{currentDoggoID}.HealthComponent"} }; byte[] emptyDynamicStructData = { 0x05, 0x00, 0x00, 0x00, 0x4e, 0x6f, 0x6e, 0x65 }; // Length prefixed "None" using (BinaryReader binaryReader = new BinaryReader(new MemoryStream(emptyDynamicStructData))) @@ -192,6 +190,7 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) return false; } + float offset = -50000; var hostPlayerModel = rootItem.FindChild("Char_Player.Char_Player_C", false); if (hostPlayerModel == null || hostPlayerModel.Items.Count < 1) @@ -211,11 +210,13 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) { foreach (StructPropertyViewModel elem in arrayProperty.Elements) { - ((Vector)((StructProperty)((DynamicStructDataViewModel)elem.StructData).Fields[0].Model).Data).Data.Z += offset; // Move the spawn point under the map + // Set WasKilled to true so they don't respawn after deleting them - ((BoolPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[2]).Value = true; + ((BoolPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[1]).Value = true; + // Set NumTimesKilled to at least 1 i guess. Better to increment. + ((IntPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[2]).Value += 1; // Set KilledOnDayNumber to a huge number (some far away animals respawn if the number is too small) - ((IntPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[3]).Value = (int)(Distance(playerPosition, ((Vector)((StructProperty)((DynamicStructDataViewModel)elem.StructData).Fields[0].Model).Data).Data) /10000); + ((IntPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[3]).Value = (int)(Distance(playerPosition, ((SaveEntityModel)animalSpawner).Position) /10000); } }); } diff --git a/SatisfactorySaveEditor/Cheats/EverythingBoxCheat.cs b/SatisfactorySaveEditor/Cheats/EverythingBoxCheat.cs index 4c1c6ce..a5ae044 100644 --- a/SatisfactorySaveEditor/Cheats/EverythingBoxCheat.cs +++ b/SatisfactorySaveEditor/Cheats/EverythingBoxCheat.cs @@ -1,16 +1,13 @@ -using SatisfactorySaveEditor.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows; +using SatisfactorySaveEditor.Model; using SatisfactorySaveEditor.View; using SatisfactorySaveEditor.ViewModel; -using SatisfactorySaveEditor.ViewModel.Property; using SatisfactorySaveParser; using SatisfactorySaveParser.Data; using SatisfactorySaveParser.PropertyTypes; -using SatisfactorySaveParser.PropertyTypes.Structs; -using SatisfactorySaveParser.Structures; -using System; -using System.Collections.Generic; -using System.IO; -using System.Windows; namespace SatisfactorySaveEditor.Cheats { @@ -101,7 +98,7 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) } //Use Mass Dismantle Cheat's crate creation function to package the items into a crate entity - MassDismantleCheat.CreateCrateEntityFromInventory(rootItem, inventory, saveGame.Header.BuildVersion); + MassDismantleCheat.CreateCrateEntityFromInventory(saveGame.Header.MapName, rootItem, inventory, saveGame.Header.BuildVersion); //MessageBox.Show("Player name " + playerEntityModel.Title); MessageBox.Show($"Crate created.\nNote that normally unstackable items will visually display as being stacked to 1. Use Ctrl+Click to transfer items out of the crate without deleting part of the stack.\n\nSkipped the following items marked as radioactive:\n\n{skipped}", $"Processed {resourceStrings.Count} resource paths"); diff --git a/SatisfactorySaveEditor/Cheats/KillPlayersCheat.cs b/SatisfactorySaveEditor/Cheats/KillPlayersCheat.cs index f8b2e4b..f69ec5d 100644 --- a/SatisfactorySaveEditor/Cheats/KillPlayersCheat.cs +++ b/SatisfactorySaveEditor/Cheats/KillPlayersCheat.cs @@ -37,13 +37,13 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) if (!InventoryEmpty(inventoryComponent)) { currentStorageID = GetNextStorageID(currentStorageID, rootItem); - SaveComponent newInventory = new SaveComponent(inventoryComponent.TypePath, inventoryComponent.RootObject, $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}.inventory") + SaveComponent newInventory = new SaveComponent(saveGame.Header.MapName, inventoryComponent.TypePath, inventoryComponent.RootObject, $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}.inventory") { ParentEntityName = $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}", DataFields = inventoryComponent.DataFields }; rootItem.FindChild("FactoryGame.FGInventoryComponent", false).Items.Add(new SaveComponentModel(newInventory)); - SaveEntity newSaveObject = new SaveEntity("/Game/FactoryGame/-Shared/Crate/BP_Crate.BP_Crate_C", "Persistent_Level", $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}") + SaveEntity newSaveObject = new SaveEntity(saveGame.Header.MapName, "/Game/FactoryGame/-Shared/Crate/BP_Crate.BP_Crate_C", "Persistent_Level", $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}") { NeedTransform = true, Rotation = ((SaveEntity)player.Model).Rotation, diff --git a/SatisfactorySaveEditor/Cheats/MassDismantleCheat.cs b/SatisfactorySaveEditor/Cheats/MassDismantleCheat.cs index f45867c..2f4fcda 100644 --- a/SatisfactorySaveEditor/Cheats/MassDismantleCheat.cs +++ b/SatisfactorySaveEditor/Cheats/MassDismantleCheat.cs @@ -217,16 +217,16 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) MessageBoxResult result = MessageBox.Show($"Dismantled {countFactory} factory buildings, {countBuilding} foundations and {countCrate} crates. Drop the items (including items in storages) in a single crate?", "Dismantled", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { - CreateCrateEntityFromInventory(rootItem, inventory, saveGame.Header.BuildVersion); + CreateCrateEntityFromInventory(saveGame.Header.MapName, rootItem, inventory, saveGame.Header.BuildVersion); } return true; } - public static SaveEntityModel CreateCrateEntityFromInventory(SaveObjectModel rootItem, ArrayProperty inventory, int buildVersion) + public static SaveEntityModel CreateCrateEntityFromInventory(string levelname, SaveObjectModel rootItem, ArrayProperty inventory, int buildVersion) { inventory = ArrangeInventory(inventory, buildVersion); int currentStorageID = GetNextStorageID(0, rootItem); - SaveComponent newInventory = new SaveComponent("/Script/FactoryGame.FGInventoryComponent", "Persistent_Level", $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}.inventory") + SaveComponent newInventory = new SaveComponent(levelname, "/Script/FactoryGame.FGInventoryComponent", "Persistent_Level", $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}.inventory") { ParentEntityName = $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}", DataFields = new SerializedFields() @@ -250,7 +250,7 @@ public static SaveEntityModel CreateCrateEntityFromInventory(SaveObjectModel roo }; rootItem.FindChild("FactoryGame.FGInventoryComponent", false).Items.Add(new SaveComponentModel(newInventory)); SaveEntity player = (SaveEntity)rootItem.FindChild("Char_Player.Char_Player_C", false).DescendantSelf[0]; - SaveEntity newSaveObject = new SaveEntity("/Game/FactoryGame/-Shared/Crate/BP_Crate.BP_Crate_C", "Persistent_Level", $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}") + SaveEntity newSaveObject = new SaveEntity(levelname, "/Game/FactoryGame/-Shared/Crate/BP_Crate.BP_Crate_C", "Persistent_Level", $"Persistent_Level:PersistentLevel.BP_Crate_C_{currentStorageID}") { NeedTransform = true, Rotation = player.Rotation, diff --git a/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs b/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs index bd0b362..68f6bd8 100644 --- a/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs +++ b/SatisfactorySaveEditor/Cheats/RemoveSlugsCheat.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - +using System.Linq; +using System.Windows; using SatisfactorySaveEditor.Model; - using SatisfactorySaveParser; using SatisfactorySaveParser.Structures; @@ -17,25 +12,35 @@ public class RemoveSlugsCheat : ICheat public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) { + MessageBox.Show("Can only remove already discovered slugs on the map. Continue?", "Remove slugs?", MessageBoxButton.YesNo); + var slugTypes = new[] { "/Game/FactoryGame/Resource/Environment/Crystal/BP_Crystal.BP_Crystal_C", "/Game/FactoryGame/Resource/Environment/Crystal/BP_Crystal_mk2.BP_Crystal_mk2_C", "/Game/FactoryGame/Resource/Environment/Crystal/BP_Crystal_mk3.BP_Crystal_mk3_C" }; + + var slugsMk1 = rootItem.FindChild("BP_Crystal.BP_Crystal_C", false); + var slugsMk2 = rootItem.FindChild("BP_Crystal_mk2.BP_Crystal_mk2_C", false); + var slugsMk3 = rootItem.FindChild("BP_Crystal_mk3.BP_Crystal_mk3_C", false); - foreach (var level in saveGame.Levels) + foreach (SaveLevel level in saveGame.Levels) { + var slugsForLevel = level.Entries.Where(x => slugTypes.Contains(x.TypePath)); + var readyToCollectSlugs = slugsForLevel.Where(slug => !slug.RootObject.Equals("Persistent_Exploration_2")); - var slugs = level.Entries.Where(x => slugTypes.Contains(x.TypePath)); - - level.CollectedObjects.RemoveAll(x => x.PathName.Contains("PersistentLevel.BP_Crystal")); - level.CollectedObjects.AddRange(slugs.Select(s => new ObjectReference + level.CollectedObjects.AddRange(readyToCollectSlugs.Select(s => new ObjectReference { LevelName = s.RootObject, PathName = s.InstanceName })); } + + slugsMk1.Items.Clear(); + slugsMk2.Items.Clear(); + slugsMk3.Items.Clear(); + MessageBox.Show("Removed discovered slugs on the map.", "Removed slugs.", MessageBoxButton.OK); return true; } } diff --git a/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs b/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs index 9119a3c..e34523d 100644 --- a/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs +++ b/SatisfactorySaveEditor/Cheats/RestoreSlugsCheat.cs @@ -1,6 +1,6 @@  +using System.Windows; using SatisfactorySaveEditor.Model; - using SatisfactorySaveParser; namespace SatisfactorySaveEditor.Cheats @@ -16,6 +16,7 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave saveGame) level.CollectedObjects.RemoveAll(x => x.PathName.Contains("PersistentLevel.BP_Crystal")); } + MessageBox.Show("Restored all slugs on the map.", "Restored Slugs.", MessageBoxButton.OK); return true; } } diff --git a/SatisfactorySaveEditor/Cheats/SpawnDoggoCheat.cs b/SatisfactorySaveEditor/Cheats/SpawnDoggoCheat.cs index ce5e446..a266dd6 100644 --- a/SatisfactorySaveEditor/Cheats/SpawnDoggoCheat.cs +++ b/SatisfactorySaveEditor/Cheats/SpawnDoggoCheat.cs @@ -1,17 +1,9 @@ -using SatisfactorySaveEditor.Model; +using System; +using System.Windows; +using SatisfactorySaveEditor.Model; using SatisfactorySaveEditor.View; using SatisfactorySaveEditor.ViewModel; using SatisfactorySaveParser; -using SatisfactorySaveParser.PropertyTypes; -using SatisfactorySaveParser.PropertyTypes.Structs; -using SatisfactorySaveParser.Structures; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; namespace SatisfactorySaveEditor.Cheats { diff --git a/SatisfactorySaveEditor/Cheats/UndoDeleteEnemiesCheat.cs b/SatisfactorySaveEditor/Cheats/UndoDeleteEnemiesCheat.cs index 5359c98..aac7f11 100644 --- a/SatisfactorySaveEditor/Cheats/UndoDeleteEnemiesCheat.cs +++ b/SatisfactorySaveEditor/Cheats/UndoDeleteEnemiesCheat.cs @@ -58,11 +58,10 @@ public bool Apply(SaveObjectModel rootItem, SatisfactorySave save) { foreach (StructPropertyViewModel elem in arrayProperty.Elements) { - if (probablyEdited) - ((Vector)((StructProperty)((DynamicStructDataViewModel)elem.StructData).Fields[0].Model).Data).Data.Z -= offset; // Move the spawn point under the map - // Set WasKilled to true so they don't respawn after deleting them - ((BoolPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[2]).Value = false; + ((BoolPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[1]).Value = false; + // Set NumTimesKilled to at least 1 i guess. Better to increment. + ((IntPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[2]).Value -= 1; // Set KilledOnDayNumber to a huge number (some far away animals respawn if the number is too small) ((IntPropertyViewModel)((DynamicStructDataViewModel)elem.StructData).Fields[3]).Value = (int)0; } diff --git a/SatisfactorySaveParser/SatisfactorySave.cs b/SatisfactorySaveParser/SatisfactorySave.cs index 5026b43..b49b212 100644 --- a/SatisfactorySaveParser/SatisfactorySave.cs +++ b/SatisfactorySaveParser/SatisfactorySave.cs @@ -1,15 +1,12 @@ -using NLog; -using SatisfactorySaveParser.Save; -using SatisfactorySaveParser.Structures; -using SatisfactorySaveParser.ZLib; - -using System; +using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.Eventing; using System.IO; using System.Linq; -using SatisfactorySaveParser.Exceptions; +using NLog; +using SatisfactorySaveParser.Save; +using SatisfactorySaveParser.Structures; +using SatisfactorySaveParser.ZLib; namespace SatisfactorySaveParser { @@ -140,10 +137,10 @@ private void LoadDataU5AndBelow(BinaryReader reader) switch (type) { case SaveEntity.TypeID: - defaultLevel.Entries.Add(new SaveEntity(reader)); + defaultLevel.Entries.Add(new SaveEntity(defaultLevel.Name, reader)); break; case SaveComponent.TypeID: - defaultLevel.Entries.Add(new SaveComponent(reader)); + defaultLevel.Entries.Add(new SaveComponent(defaultLevel.Name, reader)); break; default: throw new InvalidOperationException($"Unexpected type {type}"); @@ -221,7 +218,7 @@ private void LoadDataU6AndAbove(BinaryReader reader) private List LoadLevelEntries(BinaryReader reader, string levelname) { - reader.ReadInt32(); // skip "object header and collectables size" + var binarySize = reader.ReadInt32(); // skip "object header and collectables size" var levelEntries = new List(); var totalSaveObjects = reader.ReadInt32(); @@ -231,13 +228,11 @@ private List LoadLevelEntries(BinaryReader reader, string levelname) switch (type) { case SaveEntity.TypeID: - var entity = new SaveEntity(reader); - entity.LevelName = levelname; + var entity = new SaveEntity(levelname, reader); levelEntries.Add(entity); break; case SaveComponent.TypeID: - var component = new SaveComponent(reader); - component.LevelName = levelname; + var component = new SaveComponent(levelname, reader); levelEntries.Add(component); break; default: @@ -263,13 +258,12 @@ private List LoadLevelCollectedObjects(BinaryReader reader) private void ParseLevelEntries(List levelEntries, BinaryReader reader) { - + var binarySize = reader.ReadInt32(); // skip "objects binary size" long Before = reader.BaseStream.Position; var objectCount = reader.ReadInt32(); Trace.Assert(levelEntries.Count == objectCount); - for (int i = 0; i < objectCount; i++) { var len = reader.ReadInt32(); diff --git a/SatisfactorySaveParser/SaveComponent.cs b/SatisfactorySaveParser/SaveComponent.cs index df071b5..bae58ec 100644 --- a/SatisfactorySaveParser/SaveComponent.cs +++ b/SatisfactorySaveParser/SaveComponent.cs @@ -15,11 +15,11 @@ public class SaveComponent : SaveObject /// public string ParentEntityName { get; set; } - public SaveComponent(string typePath, string rootObject, string instanceName) : base(typePath, rootObject, instanceName) + public SaveComponent(string levelname, string typePath, string rootObject, string instanceName) : base(levelname, typePath, rootObject, instanceName) { } - public SaveComponent(BinaryReader reader) : base(reader) + public SaveComponent(string levelname, BinaryReader reader) : base(levelname, reader) { ParentEntityName = reader.ReadLengthPrefixedString(); } diff --git a/SatisfactorySaveParser/SaveEntity.cs b/SatisfactorySaveParser/SaveEntity.cs index 4682d5b..a328bd8 100644 --- a/SatisfactorySaveParser/SaveEntity.cs +++ b/SatisfactorySaveParser/SaveEntity.cs @@ -51,11 +51,11 @@ public class SaveEntity : SaveObject /// public List Components { get; set; } = new List(); - public SaveEntity(string typePath, string rootObject, string instanceName) : base(typePath, rootObject, instanceName) + public SaveEntity(string levelname, string typePath, string rootObject, string instanceName) : base(levelname, typePath, rootObject, instanceName) { } - - public SaveEntity(BinaryReader reader) : base(reader) + + public SaveEntity(string levelname, BinaryReader reader) : base(levelname, reader) { NeedTransform = reader.ReadInt32() == 1; Rotation = reader.ReadVector4(); diff --git a/SatisfactorySaveParser/SaveObject.cs b/SatisfactorySaveParser/SaveObject.cs index 18f0d4c..555899e 100644 --- a/SatisfactorySaveParser/SaveObject.cs +++ b/SatisfactorySaveParser/SaveObject.cs @@ -48,16 +48,18 @@ public abstract class SaveObject /// public string LevelName { get; set; } - public SaveObject(string typePath, string rootObject, string instanceName) + public SaveObject(string levelname, string typePath, string rootObject, string instanceName) { + LevelName = levelname; TypePath = typePath; RootObject = rootObject; InstanceName = instanceName; MetaData = new SaveObjectMetaData(); } - protected SaveObject(BinaryReader reader) + protected SaveObject(string levelname, BinaryReader reader) { + LevelName = levelname; TypePath = reader.ReadLengthPrefixedString(); RootObject = reader.ReadLengthPrefixedString(); InstanceName = reader.ReadLengthPrefixedString(); From 3282aea586a0092c04f0c7b0db5e4df559cbb1e6 Mon Sep 17 00:00:00 2001 From: "SCHECTER\\Freddi" Date: Sun, 29 Jan 2023 16:00:59 +0100 Subject: [PATCH 11/11] change assertion for binary size to log statement warning. --- SatisfactorySaveParser/SatisfactorySave.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SatisfactorySaveParser/SatisfactorySave.cs b/SatisfactorySaveParser/SatisfactorySave.cs index b49b212..a81738f 100644 --- a/SatisfactorySaveParser/SatisfactorySave.cs +++ b/SatisfactorySaveParser/SatisfactorySave.cs @@ -286,7 +286,10 @@ private void ParseLevelEntries(List levelEntries, BinaryReader reade } long ReadBytesCount = reader.BaseStream.Position - Before; - Debug.Assert(ReadBytesCount == binarySize); + if (ReadBytesCount != binarySize) + { + log.Warn("The indicated size does not match the real size! Save is probably corrupt. Attempting to load what's possible."); + } }