From 081bc07d81855da3fbbed9ba757b211e0793944b Mon Sep 17 00:00:00 2001 From: Moritz Heinemann Date: Mon, 30 Sep 2024 23:17:18 +0200 Subject: [PATCH] add FFGDynamicStruct, update FInventoryItem, add object save version to archive --- docs/SATISFACTORY_SAVE.md | 37 +++++++++-- .../UE/Satisfactory/FGDynamicStruct.h | 62 +++++++++++++++++++ .../GameTypes/UE/Satisfactory/InventoryItem.h | 11 +++- .../SatisfactorySave/IO/Archive/Archive.h | 15 +++++ libsave/src/GameTypes/Save/SaveGame.cpp | 3 + .../src/GameTypes/Save/SerializationUtils.h | 9 +++ libsavepy/GameTypes/UE/Satisfactory.cpp | 8 ++- map/src/MapWindow/UI/PropertyTableEditor.cpp | 20 ++++-- .../MapWindow/UI/PropertyTableGuiRenderer.cpp | 20 ++++-- 9 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/FGDynamicStruct.h diff --git a/docs/SATISFACTORY_SAVE.md b/docs/SATISFACTORY_SAVE.md index 8caeda54..8912ca6a 100644 --- a/docs/SATISFACTORY_SAVE.md +++ b/docs/SATISFACTORY_SAVE.md @@ -693,7 +693,7 @@ The following structs are binary structs with the type described in the table: | `Guid` | `FGuid` | [FGuid](#fguid) | | | `IntPoint` | `FIntPoint` | `int32 X`
`int32 Y` | | | `IntVector` | `FIntVector` | `int32 X`
`int32 Y`
`int32 Z` | | -| `InventoryItem` | `FInventoryItem` | `FObjectReferenceDisc ItemClass`
`FObjectReferenceDisc ItemState` | | +| `InventoryItem` | `FInventoryItem` | [FInventoryItem](#finventoryitem) | | | `LBBalancerIndexing` | `FLBBalancerIndexing` | `int32 mNormalIndex`
`int32 mOverflowIndex`
`int32 mFilterIndex` | (Mod LoadBalancers) | | `LinearColor` | `FLinearColor` | `float R`
`float G`
`float B`
`float A` | | | `Quat` | `FQuat` | `double X`
`double Y`
`double Z`
`double W` | | @@ -703,11 +703,6 @@ The following structs are binary structs with the type described in the table: | `Vector2D` | `FVector2D` | `double X`
`double Y` | | | `Vector` | `FVector` | `double X`
`double Y`
`double Z` | | -> Notes on `FInventoryItem`: -> The internally used types are `TSubclassOf ItemClass` and `FSharedInventoryStatePtr ItemState`. -> The struct can be found in `FGInventoryComponent.h`. -> Both types are serialized as `FObjectReferenceDisc`. - ## Class-Specific Binary Data TODO @@ -842,6 +837,22 @@ Another common type used within the save data is an `FObjectReferenceDisc`, defi - Satisfactory internal struct, header file: `FGObjectReference.h` +#### FFGDynamicStruct + +``` ++--------------------------+------------------+ +| bool | bHasValidStruct | +| if bHasValidStruct: | | +| FObjectReferenceDisc | ScriptStruct | +| int32 | savedPayloadSize | (size of StructInstance) +| List of Properties | StructInstance | ++--------------------------+------------------+ +``` + +> Unreal archives internally can select binary, native or tagged property serialization for the content of StructInstance, see +> https://github.com/EpicGames/UnrealEngine/blob/5.3.2-release/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L2761. +> Here is assumed that the game will always use tagged properties. + #### FWorldPartitionValidationData ``` @@ -932,3 +943,17 @@ Defined in `FGActorSaveHeaderTypes.h`. | TArray | DestroyedActors | +------------------------------+-----------------+ ``` + +#### FInventoryItem + +``` ++--------------------------+----------------------+ +| FObjectReferenceDisc | ItemClass | +| if SaveVersion >= 43: | | (see SaveVersion from parent object) +| FFGDynamicStruct | ItemState | +| else: | | +| FObjectReferenceDisc | LegacyItemStateActor | ++--------------------------+----------------------+ +``` + +> The internal type for ItemClass is `TSubclassOf` which is serialized as `FObjectReferenceDisc`. diff --git a/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/FGDynamicStruct.h b/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/FGDynamicStruct.h new file mode 100644 index 00000000..68d6ef39 --- /dev/null +++ b/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/FGDynamicStruct.h @@ -0,0 +1,62 @@ +#pragma once + +#include "../../../IO/Archive/Archive.h" +#include "../../Properties/Base/PropertyList.h" +#include "ObjectReference.h" +#include "satisfactorysave_export.h" + +namespace SatisfactorySave { + + struct SATISFACTORYSAVE_API FFGDynamicStruct { + public: + FObjectReferenceDisc ScriptStruct; + PropertyList StructInstance; + + void serialize(Archive& ar) { + if (ar.isIArchive()) { + bool bHasValidStruct = false; + ar << bHasValidStruct; + + if (bHasValidStruct) { + ar << ScriptStruct; + + int32_t savedPayloadSize = -1; + ar << savedPayloadSize; + + const auto pos_before = ar.tell(); + + // Archive has flags to switch between binary, native and tagged property serialization, see + // https://github.com/EpicGames/UnrealEngine/blob/5.3.2-release/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L2761 + // We assume tagged properties here. + ar << StructInstance; + + if (ar.tell() - pos_before != savedPayloadSize) { + throw std::runtime_error("Invalid FFGDynamicStruct!"); + } + } + } else { + bool bHasValidStruct = !ScriptStruct.PathName.empty(); + ar << bHasValidStruct; + + if (bHasValidStruct) { + ar << ScriptStruct; + + const auto offsetBeforeSavedPayloadSize = ar.tell(); + + int32_t savedPayloadSize = -1; + ar << savedPayloadSize; + + const auto offsetBeforeStructPayload = ar.tell(); + + ar << StructInstance; + + savedPayloadSize = static_cast(ar.tell() - offsetBeforeStructPayload); + const auto currentArchiveOffset = ar.tell(); + ar.seek(offsetBeforeSavedPayloadSize); + ar << savedPayloadSize; + ar.seek(currentArchiveOffset); + } + } + } + }; +} // namespace SatisfactorySave diff --git a/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/InventoryItem.h b/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/InventoryItem.h index 2d780f74..31c0e92e 100644 --- a/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/InventoryItem.h +++ b/libsave/include/SatisfactorySave/GameTypes/UE/Satisfactory/InventoryItem.h @@ -1,20 +1,25 @@ #pragma once #include "../../../IO/Archive/Archive.h" +#include "FGDynamicStruct.h" #include "ObjectReference.h" #include "satisfactorysave_export.h" namespace SatisfactorySave { - // FInventoryItem struct SATISFACTORYSAVE_API FInventoryItem { public: FObjectReferenceDisc ItemClass; - FObjectReferenceDisc ItemState; + FFGDynamicStruct ItemState; + FObjectReferenceDisc LegacyItemStateActor; void serialize(Archive& ar) { ar << ItemClass; - ar << ItemState; + if (ar.getSaveVersion() >= 43) { + ar << ItemState; + } else { + ar << LegacyItemStateActor; + } } }; } // namespace SatisfactorySave diff --git a/libsave/include/SatisfactorySave/IO/Archive/Archive.h b/libsave/include/SatisfactorySave/IO/Archive/Archive.h index 8ee73d5f..ac4c81e7 100644 --- a/libsave/include/SatisfactorySave/IO/Archive/Archive.h +++ b/libsave/include/SatisfactorySave/IO/Archive/Archive.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -116,6 +117,18 @@ namespace SatisfactorySave { virtual std::size_t tell() = 0; virtual void seek(std::size_t pos) = 0; + inline void pushSaveVersion(auto saveVersion) { + save_version_stack_.push(saveVersion); + } + + inline void popSaveVersion() { + save_version_stack_.pop(); + } + + inline int32_t getSaveVersion() { + return !save_version_stack_.empty() ? save_version_stack_.top() : 0; + } + protected: Archive() = default; ~Archive() = default; @@ -125,5 +138,7 @@ namespace SatisfactorySave { virtual void serializeName(FName& n); virtual void serializeObjectReference(FObjectReferenceDisc& ref); virtual void validateReadLimit(std::size_t) {}; + + std::stack save_version_stack_; }; } // namespace SatisfactorySave diff --git a/libsave/src/GameTypes/Save/SaveGame.cpp b/libsave/src/GameTypes/Save/SaveGame.cpp index 6c439b3d..dd369f81 100644 --- a/libsave/src/GameTypes/Save/SaveGame.cpp +++ b/libsave/src/GameTypes/Save/SaveGame.cpp @@ -73,6 +73,9 @@ SatisfactorySave::SaveGame::SaveGame(const std::filesystem::path& filepath) { const auto file_data_blob_size = file_data_blob->size(); IStreamArchive ar(std::make_unique(std::move(file_data_blob))); + // Store header SaveVersion as first stack entry. + ar.pushSaveVersion(header_.SaveVersion); + // Validate blob size if (file_data_blob_size - sizeof(int64_t) != ar.read()) { throw std::runtime_error("Bad blob size!"); diff --git a/libsave/src/GameTypes/Save/SerializationUtils.h b/libsave/src/GameTypes/Save/SerializationUtils.h index d99f8847..f3d22b08 100644 --- a/libsave/src/GameTypes/Save/SerializationUtils.h +++ b/libsave/src/GameTypes/Save/SerializationUtils.h @@ -54,6 +54,7 @@ namespace SatisfactorySave { if constexpr (object_headers) { ar << saveObjects[i]->SaveVersion; ar << saveObjects[i]->ShouldMigrateObjectRefsToPersistent; + ar.pushSaveVersion(saveObjects[i]->SaveVersion); } // Check stream pos to validate parser. @@ -64,6 +65,9 @@ namespace SatisfactorySave { if (pos_after - pos_before != length) { throw std::runtime_error("Error parsing object data!"); } + if constexpr (object_headers) { + ar.popSaveVersion(); + } } TIME_MEASURE_END("ObjProp"); @@ -111,6 +115,7 @@ namespace SatisfactorySave { if constexpr (object_headers) { ar << obj->SaveVersion; ar << obj->ShouldMigrateObjectRefsToPersistent; + ar.pushSaveVersion(obj->SaveVersion); } auto pos_size = ar.tell(); @@ -123,6 +128,10 @@ namespace SatisfactorySave { ar.seek(pos_size); ar.write(static_cast(pos_after - pos_before)); ar.seek(pos_after); + + if constexpr (object_headers) { + ar.popSaveVersion(); + } } auto blob_pos_after = ar.tell(); diff --git a/libsavepy/GameTypes/UE/Satisfactory.cpp b/libsavepy/GameTypes/UE/Satisfactory.cpp index 78c44110..27e0d1dc 100644 --- a/libsavepy/GameTypes/UE/Satisfactory.cpp +++ b/libsavepy/GameTypes/UE/Satisfactory.cpp @@ -35,6 +35,11 @@ void init_GameTypes_UE_Satisfactory(py::module_& m) { .def_readwrite("Cost", &s::FBlueprintHeader::Cost) .def_readwrite("RecipeRefs", &s::FBlueprintHeader::RecipeRefs); + py::class_(m, "FFGDynamicStruct") + .def(py::init<>()) + .def_readwrite("ScriptStruct", &s::FFGDynamicStruct::ScriptStruct) + .def_readwrite("StructInstance", &s::FFGDynamicStruct::StructInstance); + py::class_(m, "FFluidBox") .def(py::init<>()) .def_readwrite("Value", &s::FFluidBox::Value); @@ -42,7 +47,8 @@ void init_GameTypes_UE_Satisfactory(py::module_& m) { py::class_(m, "FInventoryItem") .def(py::init<>()) .def_readwrite("ItemClass", &s::FInventoryItem::ItemClass) - .def_readwrite("ItemState", &s::FInventoryItem::ItemState); + .def_readwrite("ItemState", &s::FInventoryItem::ItemState) + .def_readwrite("LegacyItemStateActor", &s::FInventoryItem::LegacyItemStateActor); py::class_(m, "FObjectReferenceDisc") .def(py::init<>()) diff --git a/map/src/MapWindow/UI/PropertyTableEditor.cpp b/map/src/MapWindow/UI/PropertyTableEditor.cpp index 77dff9db..24d09672 100644 --- a/map/src/MapWindow/UI/PropertyTableEditor.cpp +++ b/map/src/MapWindow/UI/PropertyTableEditor.cpp @@ -61,14 +61,24 @@ namespace { } void visit(SatisfactorySave::InventoryItemStruct& s) override { - ImGui::Text("ItemClass Lvl: %s", s.Data.ItemClass.LevelName.c_str()); - ImGui::Text("ItemClass Path:"); + ImGui::Text("ItemClass:"); + ImGui::Text("Lvl: %s", s.Data.ItemClass.LevelName.c_str()); + ImGui::Text("Path:"); ImGui::SameLine(); Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.ItemClass.PathName, callback_); - ImGui::Text("ItemState Lvl: %s", s.Data.ItemState.LevelName.c_str()); - ImGui::Text("ItemState Path:"); + ImGui::Text("ItemState.ScriptStruct:"); + ImGui::Text("Lvl: %s", s.Data.ItemState.ScriptStruct.LevelName.c_str()); + ImGui::Text("Path:"); + ImGui::SameLine(); + Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.ItemState.ScriptStruct.PathName, callback_); + ImGui::Text("ItemState.StructInstance:"); + Satisfactory3DMap::PropertyTableEditor r; + r.renderGui(s.Data.ItemState.StructInstance, callback_); + ImGui::Text("LegacyItemStateActor:"); + ImGui::Text("Lvl: %s", s.Data.LegacyItemStateActor.LevelName.c_str()); + ImGui::Text("Path:"); ImGui::SameLine(); - Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.ItemState.PathName, callback_); + Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.LegacyItemStateActor.PathName, callback_); } void visit(SatisfactorySave::LBBalancerIndexingStruct& s) override { diff --git a/map/src/MapWindow/UI/PropertyTableGuiRenderer.cpp b/map/src/MapWindow/UI/PropertyTableGuiRenderer.cpp index 6f146eec..5d2357bf 100644 --- a/map/src/MapWindow/UI/PropertyTableGuiRenderer.cpp +++ b/map/src/MapWindow/UI/PropertyTableGuiRenderer.cpp @@ -56,14 +56,24 @@ namespace { } void visit(SatisfactorySave::InventoryItemStruct& s) override { - ImGui::Text("ItemClass Lvl: %s", s.Data.ItemClass.LevelName.c_str()); - ImGui::Text("ItemClass Path:"); + ImGui::Text("ItemClass:"); + ImGui::Text("Lvl: %s", s.Data.ItemClass.LevelName.c_str()); + ImGui::Text("Path:"); ImGui::SameLine(); Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.ItemClass.PathName, callback_); - ImGui::Text("ItemState Lvl: %s", s.Data.ItemState.LevelName.c_str()); - ImGui::Text("ItemState Path:"); + ImGui::Text("ItemState.ScriptStruct:"); + ImGui::Text("Lvl: %s", s.Data.ItemState.ScriptStruct.LevelName.c_str()); + ImGui::Text("Path:"); + ImGui::SameLine(); + Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.ItemState.ScriptStruct.PathName, callback_); + ImGui::Text("ItemState.StructInstance:"); + Satisfactory3DMap::PropertyTableGuiRenderer r; + r.renderGui(s.Data.ItemState.StructInstance, callback_); + ImGui::Text("LegacyItemStateActor:"); + ImGui::Text("Lvl: %s", s.Data.LegacyItemStateActor.LevelName.c_str()); + ImGui::Text("Path:"); ImGui::SameLine(); - Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.ItemState.PathName, callback_); + Satisfactory3DMap::ImGuiUtil::PathLink(s.Data.LegacyItemStateActor.PathName, callback_); } void visit(SatisfactorySave::LBBalancerIndexingStruct& s) override {