[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6FES1SNZ0EKDR53MREST7X2":3,"skill-same:01M6FES1SNZ0EKDR53MREST7X2":47},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":20,"linkOnly":21,"markdownContent":22,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":29,"ownerHtmlUrl":30,"ownerBlogUrl":6,"ownerDescription":31,"updatedAt":32,"install":33,"resources":46},"01M6FES1SNZ0EKDR53MREST7X2","ue-serialization-savegames",null,"ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fquodsoler\u002Funreal-engine-skills\u002Fue-serialization-savegames\u002FSKILL.md",[],[18,19],"serialization","ue serialization savegames","2026-11-10 10:12:30",false,"# UE Serialization & Save Games\n\nYou are an expert in Unreal Engine's serialization and save game systems. You implement save\u002Fload pipelines using `USaveGame`, `FArchive`, config files, and versioning so player progress persists correctly across sessions and game updates.\n\n---\n\n## Step 1: Read Project Context\n\nRead `.agents\u002Fue-project-context.md` before giving any recommendations. You need:\n- Engine version (UE 5.0+ has `ULocalPlayerSaveGame`; earlier versions differ)\n- Module names (the save system lives in a specific module)\n- Target platforms (console vs. PC save paths and user indices differ)\n- Whether multiplayer is in scope (server-authoritative vs. client-local saves)\n\nIf the file does not exist, ask the user to run `\u002Fue-project-context` first.\n\n---\n\n## Step 2: Gather Requirements\n\nAsk before writing code:\n1. **Save complexity**: Simple key\u002Fvalue data, or complex world state with hundreds of objects?\n2. **Data types**: Primitives, nested structs, asset references (soft vs. hard)?\n3. **Versioning needs**: Live game with future patches? Old saves must keep working?\n4. **Multiple save slots**: How many? Does each player\u002Fuser get their own?\n5. **Async requirement**: Can save\u002Fload stall the game thread, or must it be background?\n\n---\n\n## Step 3: USaveGame Subclass\n\n`USaveGame` is an abstract `UObject` from `GameFramework\u002FSaveGame.h`. Subclass it and mark fields with `UPROPERTY(SaveGame)` for automatic tagged serialization by `UGameplayStatics`.\n\n```cpp\n\u002F\u002F MyGameSaveGame.h\n#pragma once\n#include \"CoreMinimal.h\"\n#include \"GameFramework\u002FSaveGame.h\"\n#include \"MyGameSaveGame.generated.h\"\n\nUSTRUCT(BlueprintType)\nstruct FInventoryItemData\n{\n    GENERATED_BODY() \u002F\u002F Required — missing GENERATED_BODY() breaks struct serialization silently\n\n    UPROPERTY(SaveGame) FName  ItemID;\n    UPROPERTY(SaveGame) int32  Quantity = 0;\n    UPROPERTY(SaveGame) bool   bIsEquipped = false;\n};\n\nUCLASS(BlueprintType)\nclass MYGAME_API UMyGameSaveGame : public USaveGame\n{\n    GENERATED_BODY()\npublic:\n    UPROPERTY(SaveGame) int32   SaveVersion = 0;      \u002F\u002F Always include a version field\n    UPROPERTY(SaveGame) float   PlayerHealth = 100.f;\n    UPROPERTY(SaveGame) int32   PlayerLevel = 1;\n    UPROPERTY(SaveGame) FVector LastCheckpointLocation = FVector::ZeroVector;\n    UPROPERTY(SaveGame) FString PlayerDisplayName;\n    UPROPERTY(SaveGame) float   TotalPlayTimeSeconds = 0.f;\n    UPROPERTY(SaveGame) TArray\u003CFInventoryItemData>   InventoryItems;\n    UPROPERTY(SaveGame) TMap\u003CFName, int32>            AbilityLevels;\n    \u002F\u002F TSet\u003CFName> is also supported in UPROPERTY(SaveGame) fields and serializes\u002Fdeserializes automatically.\n\n    \u002F\u002F Asset references: FSoftObjectPath stores a string path — safe across saves\n    \u002F\u002F Never use raw UObject* or hard TObjectPtr\u003C> to content assets in save data\n    UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeaponPath;\n};\n```\n\n### Saving and Loading\n\n```cpp\n#include \"Kismet\u002FGameplayStatics.h\"\n\nstatic const FString SlotName  = TEXT(\"MainSave\");\nstatic constexpr int32 UserIdx = 0; \u002F\u002F Always 0 on PC; use GetPlatformUserIndex() on console\n\n\u002F\u002F Create the object first, populate its fields, then save\nUMySaveGame* SaveGame = Cast\u003CUMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));\nSaveGame->PlayerHealth = 75.f;\n\u002F\u002F Then pass SaveGame to SaveGameToSlot \u002F AsyncSaveGameToSlot below\n\n\u002F\u002F Sync save (blocks game thread — avoid in gameplay)\nbool bSaved = UGameplayStatics::SaveGameToSlot(SaveData, SlotName, UserIdx);\n\n\u002F\u002F Async save (preferred — does not block)\nFAsyncSaveGameToSlotDelegate OnSaved;\nOnSaved.BindUObject(this, &USaveManager::OnAsyncSaveComplete);\nUGameplayStatics::AsyncSaveGameToSlot(SaveData, SlotName, UserIdx, OnSaved);\n\n\u002F\u002F Load\nif (UGameplayStatics::DoesSaveGameExist(SlotName, UserIdx))\n{\n    UMyGameSaveGame* Save = Cast\u003CUMyGameSaveGame>(\n        UGameplayStatics::LoadGameFromSlot(SlotName, UserIdx));\n}\n\n\u002F\u002F Async load\nFAsyncLoadGameFromSlotDelegate OnLoaded;\nOnLoaded.BindUObject(this, &USaveManager::OnAsyncLoadComplete);\nUGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIdx, OnLoaded);\n\n\u002F\u002F Delete\nUGameplayStatics::DeleteGameInSlot(SlotName, UserIdx);\n```\n\n---\n\n## Step 4: ULocalPlayerSaveGame (UE 5.0+)\n\n`ULocalPlayerSaveGame` ties a save to a specific local player, tracks versioning via `GetLatestDataVersion()`, and provides `HandlePostLoad()` for migrations.\n\n```cpp\nUCLASS()\nclass MYGAME_API UMyLocalPlayerSave : public ULocalPlayerSaveGame\n{\n    GENERATED_BODY()\npublic:\n    virtual int32 GetLatestDataVersion() const override { return 3; }\n    virtual void  HandlePostLoad() override;\n\n    UPROPERTY(SaveGame) TMap\u003CFName, int32> UnlockedAbilities;\n};\n\nvoid UMyLocalPlayerSave::HandlePostLoad()\n{\n    Super::HandlePostLoad();\n    const int32 Ver = GetSavedDataVersion(); \u002F\u002F version when last saved\n\n    if (Ver \u003C 2) { UnlockedAbilities.Add(TEXT(\"Dash\"), 1); }\n    \u002F\u002F Ver \u003C 3 migrations go here\n}\n```\n\n```cpp\n\u002F\u002F Load or create (sync)\nUMyLocalPlayerSave* Save = ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(\n    UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT(\"PlayerSlot0\"));\n\n\u002F\u002F Load or create (async)\nULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(\n    UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT(\"PlayerSlot0\"),\n    FOnLocalPlayerSaveGameLoadedNative::CreateUObject(this, &AMyPC::OnSaveLoaded));\n\n\u002F\u002F Save back\nSave->AsyncSaveGameToSlotForLocalPlayer(); \u002F\u002F async (preferred)\nSave->SaveGameToSlotForLocalPlayer();      \u002F\u002F sync\n```\n\n---\n\n## Step 5: FArchive and Custom Serialization\n\n`FArchive` (from `Serialization\u002FArchive.h`) is the base for all UE serialization. Key API:\n\n```cpp\nAr.IsLoading()    \u002F\u002F true when deserializing — same operator\u003C\u003C handles both directions\nAr.IsSaving()     \u002F\u002F true when serializing to output\nAr.IsError()      \u002F\u002F true after any read\u002Fwrite failure — always check before continuing\nAr.Tell()         \u002F\u002F current position (int64); -1 if not seekable\nAr.CustomVer(Key) \u002F\u002F returns the registered version number for a FGuid key\n```\n\n### FMemoryWriter and FMemoryReader\n\n`FMemoryWriter`\u002F`FMemoryReader` (from `Serialization\u002FMemoryWriter.h` \u002F `MemoryReader.h`) serialize to\u002Ffrom `TArray\u003Cuint8>`:\n\n```cpp\n\u002F\u002F Serialize to bytes\nTArray\u003Cuint8> OutBytes;\nFMemoryWriter Writer(OutBytes, \u002F*bIsPersistent=*\u002Ftrue);\nint32 Version = 2;\nWriter \u003C\u003C Version;          \u002F\u002F Serialize version header first — always\nWriter \u003C\u003C SomeData;\ncheckf(!Writer.IsError(), TEXT(\"Serialization failed\"));\n\n\u002F\u002F Deserialize from bytes\nFMemoryReader Reader(OutBytes, \u002F*bIsPersistent=*\u002Ftrue);\nint32 LoadedVersion = 0;\nReader \u003C\u003C LoadedVersion;\nif (LoadedVersion \u003C 1 || Reader.IsError()) { \u002F* corrupt data *\u002F return; }\nReader \u003C\u003C SomeData;\n```\n\n### FBufferArchive\n\n`FBufferArchive` (from `Serialization\u002FBufferArchive.h`) combines `FMemoryWriter` + `TArray\u003Cuint8>` — the object *is* the output buffer:\n\n```cpp\nFBufferArchive Buffer(\u002F*bIsPersistent=*\u002Ftrue);\nint32 Magic = 0x53415645; \u002F\u002F 'SAVE'\nBuffer \u003C\u003C Magic;\nBuffer \u003C\u003C MyStruct;        \u002F\u002F requires operator\u003C\u003C overload\nTArray\u003Cuint8> Bytes = MoveTemp(Buffer); \u002F\u002F FBufferArchive IS a TArray\u003Cuint8>\n```\n\n### Custom operator\u003C\u003C for Structs\n\nDefine `operator\u003C\u003C` to make a struct serializable via any `FArchive` (required when passing it to `FBufferArchive`, `FMemoryWriter`, etc.):\n\n```cpp\nFArchive& operator\u003C\u003C(FArchive& Ar, FMyCustomData& Data)\n{\n    Ar \u003C\u003C Data.Name \u003C\u003C Data.Value \u003C\u003C Data.Timestamp;\n    return Ar;\n}\n```\n\n### Compressed Archives\n\nFor large saves, use `FArchiveSaveCompressedProxy` \u002F `FArchiveLoadCompressedProxy` (from `Serialization\u002FArchiveSaveCompressedProxy.h`):\n\n```cpp\n\u002F\u002F Compress\nTArray\u003Cuint8> Compressed;\nFArchiveSaveCompressedProxy Comp(Compressed, NAME_Zlib);\nComp.Serialize(RawData.GetData(), RawData.Num());\nComp.Flush();\n\n\u002F\u002F Decompress\nFArchiveLoadCompressedProxy Decomp(Compressed, NAME_Zlib);\nTArray\u003Cuint8> Raw;\nRaw.SetNum(KnownUncompressedSize);\nDecomp.Serialize(Raw.GetData(), Raw.Num());\n```\n\n### Custom Serialize() on UObject\n\nOverride `Serialize(FArchive& Ar)` for precise binary layout control:\n\n```cpp\nvoid UMyObject::Serialize(FArchive& Ar)\n{\n    Super::Serialize(Ar); \u002F\u002F always call Super first\n    Ar \u003C\u003C BinaryField;\n    Ar \u003C\u003C UniqueRunID;\n    if (Ar.IsLoading() && Ar.IsError()) { \u002F* handle corruption *\u002F }\n}\n```\n\n---\n\n## Step 6: Versioning\n\n### Integer Versioning in USaveGame\n\n```cpp\nnamespace ESaveVersion\n{\n    enum Type : int32\n    {\n        Initial          = 0,\n        AddedInventory   = 1,\n        SoftRefForWeapon = 2,\n        VersionPlusOne,\n        Latest = VersionPlusOne - 1\n    };\n}\n\nvoid USaveManager::RunMigrations(UMyGameSaveGame* Save)\n{\n    if (Save->SaveVersion == ESaveVersion::Latest) { return; }\n\n    if (Save->SaveVersion \u003C ESaveVersion::AddedInventory)\n        Save->InventoryItems.Reset();\n\n    if (Save->SaveVersion \u003C ESaveVersion::SoftRefForWeapon)\n    { \u002F* convert old FName field to FSoftObjectPath *\u002F }\n\n    Save->SaveVersion = ESaveVersion::Latest; \u002F\u002F stamp after migration\n}\n```\n\n### FCustomVersionRegistration (FArchive-based saves)\n\n```cpp\n\u002F\u002F Declare version enum + GUID (generate once with FGuid::NewGuid(), then hardcode)\nstruct FMySaveVersion\n{\n    enum Type { Initial = 0, AddedQuestData = 1, VersionPlusOne, Latest = VersionPlusOne - 1 };\n    static const FGuid GUID;\n};\nconst FGuid FMySaveVersion::GUID(0xA1B2C3D4, 0xE5F60718, 0x293A4B5C, 0x6D7E8F90);\n\n\u002F\u002F Register globally (module startup or static):\nFCustomVersionRegistration GReg(FMySaveVersion::GUID, FMySaveVersion::Latest, TEXT(\"MySave\"));\n\n\u002F\u002F In Serialize():\nAr.UsingCustomVersion(FMySaveVersion::GUID);\nconst int32 Ver = Ar.CustomVer(FMySaveVersion::GUID);\nAr \u003C\u003C CoreData;\nif (Ver >= FMySaveVersion::AddedQuestData)\n    Ar \u003C\u003C QuestData;\nelse if (Ar.IsLoading())\n    QuestData.Reset(); \u002F\u002F Initialize missing data on old saves\n```\n\n### Struct Field Migration\n\nWhen a struct field is renamed or its type changes, override `Serialize()` on the struct to migrate old data:\n\n```cpp\nvoid FMyStruct::Serialize(FArchive& Ar)\n{\n    Ar.UsingCustomVersion(FMySaveVersion::GUID);\n    if (Ar.CustomVer(FMySaveVersion::GUID) \u003C FMySaveVersion::RenamedHealthToHP)\n    {\n        float OldHealth;\n        Ar \u003C\u003C OldHealth;\n        HP = OldHealth; \u002F\u002F Migrate old field name to new\n    }\n    else\n    {\n        Ar \u003C\u003C HP;\n    }\n}\n```\n\n---\n\n## Step 7: Config Files\n\n### UGameUserSettings (user preferences)\n\n```cpp\nUCLASS()\nclass MYGAME_API UMyGameUserSettings : public UGameUserSettings\n{\n    GENERATED_BODY()\npublic:\n    UPROPERTY(Config, BlueprintReadWrite, Category=\"Game\")\n    float MasterVolume = 1.0f;\n\n    UPROPERTY(Config, BlueprintReadWrite, Category=\"Game\")\n    bool bSubtitlesEnabled = true;\n\n    void ApplyAndSave() { ApplySettings(false); SaveSettings(); }\n};\n\u002F\u002F Register in DefaultEngine.ini:\n\u002F\u002F [\u002FScript\u002FEngine.Engine]\n\u002F\u002F GameUserSettingsClassName=\u002FScript\u002FMyGame.MyGameUserSettings\n```\n\n### UDeveloperSettings (project settings)\n\n```cpp\nUCLASS(Config=Game, DefaultConfig, meta=(DisplayName=\"My Game Settings\"))\nclass MYGAME_API UMyProjectSettings : public UDeveloperSettings\n{\n    GENERATED_BODY()\npublic:\n    UPROPERTY(Config, EditAnywhere, Category=\"Save\") int32 MaxSaveSlots = 5;\n    UPROPERTY(Config, EditAnywhere, Category=\"Save\") bool  bEnableAutoSave = true;\n    UPROPERTY(Config, EditAnywhere, Category=\"Save\") float AutoSaveIntervalSeconds = 300.f;\n    static const UMyProjectSettings* Get() { return GetDefault\u003CUMyProjectSettings>(); }\n};\n```\n\n### GConfig Direct Access\n\n```cpp\n#include \"Misc\u002FConfigCacheIni.h\"\n\nFString Value;\nGConfig->GetString(TEXT(\"\u002FScript\u002FMyGame.MyConfig\"), TEXT(\"Key\"), Value, GGameIni);\nGConfig->SetString(TEXT(\"\u002FScript\u002FMyGame.MyConfig\"), TEXT(\"Key\"), TEXT(\"Val\"), GGameIni);\nGConfig->Flush(\u002F*bRemoveFromCache=*\u002Ffalse, GGameIni);\n\nMyObject->SaveConfig();  \u002F\u002F writes UPROPERTY(Config) fields to .ini\nMyObject->LoadConfig();  \u002F\u002F reloads from .ini\n```\n\n**INI section naming**: Section `[\u002FScript\u002FModuleName.ClassName]` maps to the CDO. `SaveConfig()` writes from the object to INI; `LoadConfig()` reads INI into the object and is called automatically for the CDO at startup. Custom section names require overriding `OverrideConfigSection(FString& SectionName)`.\n\n---\n\n## Cloud Save Integration\n\n```cpp\n\u002F\u002F Platform save systems (Steam, EOS, console) provide ISaveGameSystem\n\u002F\u002F Access via IPlatformFeaturesModule:\nISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem();\nif (SaveSystem && SaveSystem->DoesSaveSystemSupportMultipleUsers())\n{\n    \u002F\u002F Platform handles cloud sync — use UGameplayStatics normally\n    \u002F\u002F Steam: auto-syncs Saved\u002FSaveGames\u002F via Steam Cloud if configured in Steamworks\n    \u002F\u002F EOS: use IOnlineSubsystem → IOnlineTitleFileInterface for explicit cloud read\u002Fwrite\n}\n\n\u002F\u002F Cross-platform pattern: serialize to TArray\u003Cuint8>, then write via platform API\nTArray\u003Cuint8> SaveData;\nFMemoryWriter Ar(SaveData);\nSaveObject->Serialize(Ar);\n\u002F\u002F Upload SaveData via platform SDK\n\n\u002F\u002F Steam Cloud — write save slot directly via Steamworks API\nISteamRemoteStorage* SteamStorage = SteamRemoteStorage();\nif (SteamStorage && SteamStorage->IsCloudEnabledForApp())\n{\n    SteamStorage->FileWrite(\"SaveSlot1.sav\", SaveData.GetData(), SaveData.Num());\n}\n\u002F\u002F Read back: SteamStorage->FileRead(\"SaveSlot1.sav\", Buffer, Size)\n```\n\n## Save Data Encryption\n\n```cpp\n\u002F\u002F Use FAES for symmetric encryption of save data\n#include \"Misc\u002FAES.h\"\n\u002F\u002F Build a zero-padded 32-byte FAESKey from a string.\n\u002F\u002F Do NOT use Key.Left(32): if the string is shorter than 32 chars it silently\n\u002F\u002F produces a truncated key, corrupting every encrypt\u002Fdecrypt call.\nstatic FAESKey MakeAESKey(const FString& KeyString)\n{\n    FAESKey AESKey;\n    FMemory::Memzero(AESKey.Key, FAESKey::KeySize);\n    const FTCHARToUTF8 Utf8(*KeyString);\n    FMemory::Memcpy(AESKey.Key, Utf8.Get(), FMath::Min(Utf8.Length(), FAESKey::KeySize));\n    return AESKey;\n}\n\nvoid EncryptSaveData(TArray\u003Cuint8>& Data, const FString& KeyString)\n{\n    int32 PaddedSize = Align(Data.Num(), FAES::AESBlockSize);\n    Data.SetNumZeroed(PaddedSize);\n    FAES::EncryptData(Data.GetData(), PaddedSize, MakeAESKey(KeyString));\n}\n\nvoid DecryptSaveData(TArray\u003Cuint8>& Data, const FString& KeyString)\n{\n    FAES::DecryptData(Data.GetData(), Data.Num(), MakeAESKey(KeyString));\n}\n```\n\n**Why encrypt**: Prevents casual save editing for competitive\u002Feconomy-sensitive games. Not foolproof — determined players can still extract keys from the binary. Combine with server-side validation for authoritative saves.\n\n---\n\n## Step 8: Common Mistakes\n\n| Anti-Pattern | Problem | Fix |\n|---|---|---|\n| Saving raw `UObject*` or `AActor*` | Pointers invalid between sessions | Save `FSoftObjectPath` or a stable unique ID |\n| No version field | Adding\u002Fremoving fields corrupts old saves silently | Always include `int32 SaveVersion`; run migrations on load |\n| `SaveGameToSlot` on game thread per frame | Blocks rendering, causes hitches | Use `AsyncSaveGameToSlot` |\n| `USTRUCT` without `GENERATED_BODY()` in a saved field | Silent serialization failure | Add `GENERATED_BODY()` to all saved structs |\n| Ignoring `Ar.IsError()` | Reads past corrupted data, applies garbage | Check after every block; abort immediately if set |\n| Overlapping async saves | Second save starts before first completes | Guard with `bSaveInProgress` flag or `IsSaveInProgress()` |\n| Hardcoded save file paths | Breaks on consoles and different platforms | Use `UGameplayStatics` APIs; `FPaths::ProjectSavedDir()` only for debug |\n\n**PIE vs. Packaged \u002F platform paths**: In PIE, saves go to `\u003CProject>\u002FSaved\u002FSaveGames\u002F`. Packaged Windows builds write to `%LocalAppData%\u002F\u003CProjectName>\u002FSaved\u002FSaveGames\u002F`. Console platforms use title storage APIs. `UGameplayStatics::SaveGameToSlot` abstracts all of this through the platform's `ISaveGameSystem` — never hardcode OS paths; use `FPaths::ProjectSavedDir()` only for debug logging.\n\n---\n\n## Advanced Edge Cases\n\n**Corruption recovery**: When `Ar.IsError()` returns true mid-read or magic\u002Fversion checks fail, discard the corrupt data and fall back to a fresh save. Optionally maintain a backup slot (write to `Slot_Backup` before overwriting `Slot_Primary`) so players never lose all progress:\n\n```cpp\nUSaveGame* LoadedSave = UGameplayStatics::LoadGameFromSlot(PrimarySlot, 0);\nif (!LoadedSave)\n    LoadedSave = UGameplayStatics::LoadGameFromSlot(BackupSlot, 0);\nif (!LoadedSave)\n    LoadedSave = UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass());\n```\n\n**Large saves — chunked approach**: Split world state across multiple slots by subsystem (e.g., `Save_World_00`, `Save_Inventory`, `Save_Quests`). Load each with `AsyncLoadGameFromSlot` in parallel. This prevents single-file bottlenecks and lets you load only what's needed for the current level.\n\n**Multiplayer save ownership**: Shared world state (quests, economy, enemy state) belongs to server-authoritative saves — the server's `AGameMode` writes these; clients send state changes via RPCs, never write shared saves directly. Per-player preferences (keybinds, UI layout) remain client-local via `ULocalPlayerSaveGame`. This split prevents desync and cheating.\n\n---\n\n## Module Dependencies (Build.cs)\n\n```csharp\nPublicDependencyModuleNames.AddRange(new string[] { \"Core\", \"CoreUObject\", \"Engine\" });\n\u002F\u002F For UDeveloperSettings:\nPublicDependencyModuleNames.Add(\"DeveloperSettings\");\n```\n\n---\n\n## Related Skills\n\n- `ue-cpp-foundations` — UPROPERTY, USTRUCT, UObject lifetime\n- `ue-data-assets-tables` — FSoftObjectPath patterns for asset references in saves\n- `ue-gameplay-framework` — GameInstance as save manager host; GameMode auto-save integration\n\n## Reference Files\n\n- `references\u002Fsave-system-architecture.md` — Full slot manager subsystem, metadata bank, multi-user patterns, and migration pipeline\n",{},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Fquodsoler\u002Funreal-engine-skills\u002Fue-serialization-savegames",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":7,"skillName":5,"commands":34,"prompt":44,"zipAvailable":45},[35,38,41],{"tool":36,"command":37},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill ue-serialization-savegames",{"tool":39,"command":40},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill ue-serialization-savegames",{"tool":42,"command":43},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill ue-serialization-savegames","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fquodsoler\u002Funreal-engine-skills\u002Fue-serialization-savegames\u002FSKILL.md",true,[],[48,62,72,81,87,94],{"skillNo":49,"name":50,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":51,"categories":52,"keywords":58,"publishedAt":20},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[53],{"categoryNo":54,"categoryName":55,"taxonomyType":56,"taxonomyTypeDesc":57},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[59,60,61],"model","model hierarchy","model hierarchy skill",{"skillNo":63,"name":64,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":65,"categories":66,"keywords":68,"publishedAt":20},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[67],{"categoryNo":54,"categoryName":55,"taxonomyType":56,"taxonomyTypeDesc":57},[69,70,71],"MCP","zoom","zoom mcp",{"skillNo":73,"name":74,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":20},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[78,79,80,70],"sdk","zoom apps sdk","apps",{"skillNo":82,"name":83,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":20},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[83],{"skillNo":88,"name":89,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":93,"publishedAt":20},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[92],{"categoryNo":54,"categoryName":55,"taxonomyType":56,"taxonomyTypeDesc":57},[89],{"skillNo":95,"name":96,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":99,"publishedAt":20},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[96]]