claude-code-arcane 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +54 -6
  3. package/agents/game/ue-blueprint-specialist.md +125 -0
  4. package/agents/game/ue-gas-specialist.md +133 -0
  5. package/agents/game/ue-umg-specialist.md +131 -0
  6. package/dist/cli.js +5 -0
  7. package/docs/SKILLS-CATALOG.md +42 -3
  8. package/docs/USER-GUIDE.md +2 -2
  9. package/hooks/detect-unreal.sh +90 -0
  10. package/package.json +1 -1
  11. package/profiles/unreal-dev.yaml +97 -0
  12. package/rules/gamedev/unreal-code.md +125 -0
  13. package/skills/install-mcp/SKILL.md +135 -46
  14. package/skills/install-mcp/references/unreal-operations.md +55 -0
  15. package/skills/ue-actor-component-architecture/SKILL.md +509 -0
  16. package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
  17. package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
  18. package/skills/ue-ai-navigation/SKILL.md +526 -0
  19. package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
  20. package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
  21. package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
  22. package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
  23. package/skills/ue-animation-system/SKILL.md +521 -0
  24. package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
  25. package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
  26. package/skills/ue-async-threading/SKILL.md +433 -0
  27. package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
  28. package/skills/ue-async-threading/references/threading-patterns.md +345 -0
  29. package/skills/ue-audio-system/SKILL.md +527 -0
  30. package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
  31. package/skills/ue-blueprints/SKILL.md +134 -0
  32. package/skills/ue-blueprints/references/communication.md +82 -0
  33. package/skills/ue-character-movement/SKILL.md +475 -0
  34. package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
  35. package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
  36. package/skills/ue-cpp-foundations/SKILL.md +533 -0
  37. package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
  38. package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
  39. package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
  40. package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
  41. package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
  42. package/skills/ue-data-assets-tables/SKILL.md +527 -0
  43. package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
  44. package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
  45. package/skills/ue-editor-tools/SKILL.md +523 -0
  46. package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
  47. package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
  48. package/skills/ue-game-features/SKILL.md +477 -0
  49. package/skills/ue-game-features/references/experience-system.md +222 -0
  50. package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
  51. package/skills/ue-gameplay-abilities/SKILL.md +518 -0
  52. package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
  53. package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
  54. package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
  55. package/skills/ue-gameplay-framework/SKILL.md +466 -0
  56. package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
  57. package/skills/ue-input-system/SKILL.md +394 -0
  58. package/skills/ue-input-system/references/cpp-setup.md +131 -0
  59. package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
  60. package/skills/ue-input-system/references/input-action-reference.md +731 -0
  61. package/skills/ue-mass-entity/SKILL.md +476 -0
  62. package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
  63. package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
  64. package/skills/ue-materials-rendering/SKILL.md +526 -0
  65. package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
  66. package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
  67. package/skills/ue-module-build-system/SKILL.md +487 -0
  68. package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
  69. package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
  70. package/skills/ue-naming-conventions/SKILL.md +142 -0
  71. package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
  72. package/skills/ue-naming-conventions/references/content-structure.md +83 -0
  73. package/skills/ue-networking-replication/SKILL.md +452 -0
  74. package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
  75. package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
  76. package/skills/ue-niagara-effects/SKILL.md +531 -0
  77. package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
  78. package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
  79. package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
  80. package/skills/ue-packaging/SKILL.md +116 -0
  81. package/skills/ue-physics-collision/SKILL.md +423 -0
  82. package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
  83. package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
  84. package/skills/ue-procedural-generation/SKILL.md +505 -0
  85. package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
  86. package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
  87. package/skills/ue-project-context/SKILL.md +305 -0
  88. package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
  89. package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
  90. package/skills/ue-serialization-savegames/SKILL.md +524 -0
  91. package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
  92. package/skills/ue-state-trees/SKILL.md +489 -0
  93. package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
  94. package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
  95. package/skills/ue-testing-debugging/SKILL.md +520 -0
  96. package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
  97. package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
  98. package/skills/ue-ui-umg-slate/SKILL.md +431 -0
  99. package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
  100. package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
  101. package/skills/ue-world-level-streaming/SKILL.md +518 -0
  102. package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
  103. package/templates/gamedev/unreal-project-context.md +107 -0
@@ -0,0 +1,524 @@
1
+ ---
2
+ name: ue-serialization-savegames
3
+ description: "Save/load systems, player progress persistence, data serialization, FArchive"
4
+ category: "gamedev"
5
+ argument-hint: "[save system o data]"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Bash, Write, Edit
8
+ ---
9
+
10
+ # UE Serialization & Save Games
11
+
12
+ You are an expert in Unreal Engine's serialization and save game systems. You implement save/load pipelines using `USaveGame`, `FArchive`, config files, and versioning so player progress persists correctly across sessions and game updates.
13
+
14
+ Ask before writing: this skill edits SaveGame classes and serialization code. Propose the change and get approval before using Write or Edit.
15
+
16
+ ---
17
+
18
+ ## Step 1: Read Project Context
19
+
20
+ Read `docs/unreal/project-context.md` before giving any recommendations. You need:
21
+ - Engine version (UE 5.0+ has `ULocalPlayerSaveGame`; earlier versions differ)
22
+ - Module names (the save system lives in a specific module)
23
+ - Target platforms (console vs. PC save paths and user indices differ)
24
+ - Whether multiplayer is in scope (server-authoritative vs. client-local saves)
25
+
26
+ If the file does not exist, ask the user to run `/ue-project-context` first.
27
+
28
+ ---
29
+
30
+ ## Step 2: Gather Requirements
31
+
32
+ Ask before writing code:
33
+ 1. **Save complexity**: Simple key/value data, or complex world state with hundreds of objects?
34
+ 2. **Data types**: Primitives, nested structs, asset references (soft vs. hard)?
35
+ 3. **Versioning needs**: Live game with future patches? Old saves must keep working?
36
+ 4. **Multiple save slots**: How many? Does each player/user get their own?
37
+ 5. **Async requirement**: Can save/load stall the game thread, or must it be background?
38
+
39
+ ---
40
+
41
+ ## Step 3: USaveGame Subclass
42
+
43
+ `USaveGame` is an abstract `UObject` from `GameFramework/SaveGame.h`. Subclass it and mark fields with `UPROPERTY(SaveGame)` for automatic tagged serialization by `UGameplayStatics`.
44
+
45
+ ```cpp
46
+ // MyGameSaveGame.h
47
+ #pragma once
48
+ #include "CoreMinimal.h"
49
+ #include "GameFramework/SaveGame.h"
50
+ #include "MyGameSaveGame.generated.h"
51
+
52
+ USTRUCT(BlueprintType)
53
+ struct FInventoryItemData
54
+ {
55
+ GENERATED_BODY() // Required — missing GENERATED_BODY() breaks struct serialization silently
56
+
57
+ UPROPERTY(SaveGame) FName ItemID;
58
+ UPROPERTY(SaveGame) int32 Quantity = 0;
59
+ UPROPERTY(SaveGame) bool bIsEquipped = false;
60
+ };
61
+
62
+ UCLASS(BlueprintType)
63
+ class MYGAME_API UMyGameSaveGame : public USaveGame
64
+ {
65
+ GENERATED_BODY()
66
+ public:
67
+ UPROPERTY(SaveGame) int32 SaveVersion = 0; // Always include a version field
68
+ UPROPERTY(SaveGame) float PlayerHealth = 100.f;
69
+ UPROPERTY(SaveGame) int32 PlayerLevel = 1;
70
+ UPROPERTY(SaveGame) FVector LastCheckpointLocation = FVector::ZeroVector;
71
+ UPROPERTY(SaveGame) FString PlayerDisplayName;
72
+ UPROPERTY(SaveGame) float TotalPlayTimeSeconds = 0.f;
73
+ UPROPERTY(SaveGame) TArray<FInventoryItemData> InventoryItems;
74
+ UPROPERTY(SaveGame) TMap<FName, int32> AbilityLevels;
75
+ // TSet<FName> is also supported in UPROPERTY(SaveGame) fields and serializes/deserializes automatically.
76
+
77
+ // Asset references: FSoftObjectPath stores a string path — safe across saves
78
+ // Never use raw UObject* or hard TObjectPtr<> to content assets in save data
79
+ UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeaponPath;
80
+ };
81
+ ```
82
+
83
+ ### Saving and Loading
84
+
85
+ ```cpp
86
+ #include "Kismet/GameplayStatics.h"
87
+
88
+ static const FString SlotName = TEXT("MainSave");
89
+ static constexpr int32 UserIdx = 0; // Always 0 on PC; use GetPlatformUserIndex() on console
90
+
91
+ // Create the object first, populate its fields, then save
92
+ UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
93
+ SaveGame->PlayerHealth = 75.f;
94
+ // Then pass SaveGame to SaveGameToSlot / AsyncSaveGameToSlot below
95
+
96
+ // Sync save (blocks game thread — avoid in gameplay)
97
+ bool bSaved = UGameplayStatics::SaveGameToSlot(SaveData, SlotName, UserIdx);
98
+
99
+ // Async save (preferred — does not block)
100
+ FAsyncSaveGameToSlotDelegate OnSaved;
101
+ OnSaved.BindUObject(this, &USaveManager::OnAsyncSaveComplete);
102
+ UGameplayStatics::AsyncSaveGameToSlot(SaveData, SlotName, UserIdx, OnSaved);
103
+
104
+ // Load
105
+ if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIdx))
106
+ {
107
+ UMyGameSaveGame* Save = Cast<UMyGameSaveGame>(
108
+ UGameplayStatics::LoadGameFromSlot(SlotName, UserIdx));
109
+ }
110
+
111
+ // Async load
112
+ FAsyncLoadGameFromSlotDelegate OnLoaded;
113
+ OnLoaded.BindUObject(this, &USaveManager::OnAsyncLoadComplete);
114
+ UGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIdx, OnLoaded);
115
+
116
+ // Delete
117
+ UGameplayStatics::DeleteGameInSlot(SlotName, UserIdx);
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Step 4: ULocalPlayerSaveGame (UE 5.0+)
123
+
124
+ `ULocalPlayerSaveGame` ties a save to a specific local player, tracks versioning via `GetLatestDataVersion()`, and provides `HandlePostLoad()` for migrations.
125
+
126
+ ```cpp
127
+ UCLASS()
128
+ class MYGAME_API UMyLocalPlayerSave : public ULocalPlayerSaveGame
129
+ {
130
+ GENERATED_BODY()
131
+ public:
132
+ virtual int32 GetLatestDataVersion() const override { return 3; }
133
+ virtual void HandlePostLoad() override;
134
+
135
+ UPROPERTY(SaveGame) TMap<FName, int32> UnlockedAbilities;
136
+ };
137
+
138
+ void UMyLocalPlayerSave::HandlePostLoad()
139
+ {
140
+ Super::HandlePostLoad();
141
+ const int32 Ver = GetSavedDataVersion(); // version when last saved
142
+
143
+ if (Ver < 2) { UnlockedAbilities.Add(TEXT("Dash"), 1); }
144
+ // Ver < 3 migrations go here
145
+ }
146
+ ```
147
+
148
+ ```cpp
149
+ // Load or create (sync)
150
+ UMyLocalPlayerSave* Save = ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
151
+ UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"));
152
+
153
+ // Load or create (async)
154
+ ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
155
+ UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"),
156
+ FOnLocalPlayerSaveGameLoadedNative::CreateUObject(this, &AMyPC::OnSaveLoaded));
157
+
158
+ // Save back
159
+ Save->AsyncSaveGameToSlotForLocalPlayer(); // async (preferred)
160
+ Save->SaveGameToSlotForLocalPlayer(); // sync
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Step 5: FArchive and Custom Serialization
166
+
167
+ `FArchive` (from `Serialization/Archive.h`) is the base for all UE serialization. Key API:
168
+
169
+ ```cpp
170
+ Ar.IsLoading() // true when deserializing — same operator<< handles both directions
171
+ Ar.IsSaving() // true when serializing to output
172
+ Ar.IsError() // true after any read/write failure — always check before continuing
173
+ Ar.Tell() // current position (int64); -1 if not seekable
174
+ Ar.CustomVer(Key) // returns the registered version number for a FGuid key
175
+ ```
176
+
177
+ ### FMemoryWriter and FMemoryReader
178
+
179
+ `FMemoryWriter`/`FMemoryReader` (from `Serialization/MemoryWriter.h` / `MemoryReader.h`) serialize to/from `TArray<uint8>`:
180
+
181
+ ```cpp
182
+ // Serialize to bytes
183
+ TArray<uint8> OutBytes;
184
+ FMemoryWriter Writer(OutBytes, /*bIsPersistent=*/true);
185
+ int32 Version = 2;
186
+ Writer << Version; // Serialize version header first — always
187
+ Writer << SomeData;
188
+ checkf(!Writer.IsError(), TEXT("Serialization failed"));
189
+
190
+ // Deserialize from bytes
191
+ FMemoryReader Reader(OutBytes, /*bIsPersistent=*/true);
192
+ int32 LoadedVersion = 0;
193
+ Reader << LoadedVersion;
194
+ if (LoadedVersion < 1 || Reader.IsError()) { /* corrupt data */ return; }
195
+ Reader << SomeData;
196
+ ```
197
+
198
+ ### FBufferArchive
199
+
200
+ `FBufferArchive` (from `Serialization/BufferArchive.h`) combines `FMemoryWriter` + `TArray<uint8>` — the object *is* the output buffer:
201
+
202
+ ```cpp
203
+ FBufferArchive Buffer(/*bIsPersistent=*/true);
204
+ int32 Magic = 0x53415645; // 'SAVE'
205
+ Buffer << Magic;
206
+ Buffer << MyStruct; // requires operator<< overload
207
+ TArray<uint8> Bytes = MoveTemp(Buffer); // FBufferArchive IS a TArray<uint8>
208
+ ```
209
+
210
+ ### Custom operator<< for Structs
211
+
212
+ Define `operator<<` to make a struct serializable via any `FArchive` (required when passing it to `FBufferArchive`, `FMemoryWriter`, etc.):
213
+
214
+ ```cpp
215
+ FArchive& operator<<(FArchive& Ar, FMyCustomData& Data)
216
+ {
217
+ Ar << Data.Name << Data.Value << Data.Timestamp;
218
+ return Ar;
219
+ }
220
+ ```
221
+
222
+ ### Compressed Archives
223
+
224
+ For large saves, use `FArchiveSaveCompressedProxy` / `FArchiveLoadCompressedProxy` (from `Serialization/ArchiveSaveCompressedProxy.h`):
225
+
226
+ ```cpp
227
+ // Compress
228
+ TArray<uint8> Compressed;
229
+ FArchiveSaveCompressedProxy Comp(Compressed, NAME_Zlib);
230
+ Comp.Serialize(RawData.GetData(), RawData.Num());
231
+ Comp.Flush();
232
+
233
+ // Decompress
234
+ FArchiveLoadCompressedProxy Decomp(Compressed, NAME_Zlib);
235
+ TArray<uint8> Raw;
236
+ Raw.SetNum(KnownUncompressedSize);
237
+ Decomp.Serialize(Raw.GetData(), Raw.Num());
238
+ ```
239
+
240
+ ### Custom Serialize() on UObject
241
+
242
+ Override `Serialize(FArchive& Ar)` for precise binary layout control:
243
+
244
+ ```cpp
245
+ void UMyObject::Serialize(FArchive& Ar)
246
+ {
247
+ Super::Serialize(Ar); // always call Super first
248
+ Ar << BinaryField;
249
+ Ar << UniqueRunID;
250
+ if (Ar.IsLoading() && Ar.IsError()) { /* handle corruption */ }
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Step 6: Versioning
257
+
258
+ ### Integer Versioning in USaveGame
259
+
260
+ ```cpp
261
+ namespace ESaveVersion
262
+ {
263
+ enum Type : int32
264
+ {
265
+ Initial = 0,
266
+ AddedInventory = 1,
267
+ SoftRefForWeapon = 2,
268
+ VersionPlusOne,
269
+ Latest = VersionPlusOne - 1
270
+ };
271
+ }
272
+
273
+ void USaveManager::RunMigrations(UMyGameSaveGame* Save)
274
+ {
275
+ if (Save->SaveVersion == ESaveVersion::Latest) { return; }
276
+
277
+ if (Save->SaveVersion < ESaveVersion::AddedInventory)
278
+ Save->InventoryItems.Reset();
279
+
280
+ if (Save->SaveVersion < ESaveVersion::SoftRefForWeapon)
281
+ { /* convert old FName field to FSoftObjectPath */ }
282
+
283
+ Save->SaveVersion = ESaveVersion::Latest; // stamp after migration
284
+ }
285
+ ```
286
+
287
+ ### FCustomVersionRegistration (FArchive-based saves)
288
+
289
+ ```cpp
290
+ // Declare version enum + GUID (generate once with FGuid::NewGuid(), then hardcode)
291
+ struct FMySaveVersion
292
+ {
293
+ enum Type { Initial = 0, AddedQuestData = 1, VersionPlusOne, Latest = VersionPlusOne - 1 };
294
+ static const FGuid GUID;
295
+ };
296
+ const FGuid FMySaveVersion::GUID(0xA1B2C3D4, 0xE5F60718, 0x293A4B5C, 0x6D7E8F90);
297
+
298
+ // Register globally (module startup or static):
299
+ FCustomVersionRegistration GReg(FMySaveVersion::GUID, FMySaveVersion::Latest, TEXT("MySave"));
300
+
301
+ // In Serialize():
302
+ Ar.UsingCustomVersion(FMySaveVersion::GUID);
303
+ const int32 Ver = Ar.CustomVer(FMySaveVersion::GUID);
304
+ Ar << CoreData;
305
+ if (Ver >= FMySaveVersion::AddedQuestData)
306
+ Ar << QuestData;
307
+ else if (Ar.IsLoading())
308
+ QuestData.Reset(); // Initialize missing data on old saves
309
+ ```
310
+
311
+ ### Struct Field Migration
312
+
313
+ When a struct field is renamed or its type changes, override `Serialize()` on the struct to migrate old data:
314
+
315
+ ```cpp
316
+ void FMyStruct::Serialize(FArchive& Ar)
317
+ {
318
+ Ar.UsingCustomVersion(FMySaveVersion::GUID);
319
+ if (Ar.CustomVer(FMySaveVersion::GUID) < FMySaveVersion::RenamedHealthToHP)
320
+ {
321
+ float OldHealth;
322
+ Ar << OldHealth;
323
+ HP = OldHealth; // Migrate old field name to new
324
+ }
325
+ else
326
+ {
327
+ Ar << HP;
328
+ }
329
+ }
330
+ ```
331
+
332
+ ---
333
+
334
+ ## Step 7: Config Files
335
+
336
+ ### UGameUserSettings (user preferences)
337
+
338
+ ```cpp
339
+ UCLASS()
340
+ class MYGAME_API UMyGameUserSettings : public UGameUserSettings
341
+ {
342
+ GENERATED_BODY()
343
+ public:
344
+ UPROPERTY(Config, BlueprintReadWrite, Category="Game")
345
+ float MasterVolume = 1.0f;
346
+
347
+ UPROPERTY(Config, BlueprintReadWrite, Category="Game")
348
+ bool bSubtitlesEnabled = true;
349
+
350
+ void ApplyAndSave() { ApplySettings(false); SaveSettings(); }
351
+ };
352
+ // Register in DefaultEngine.ini:
353
+ // [/Script/Engine.Engine]
354
+ // GameUserSettingsClassName=/Script/MyGame.MyGameUserSettings
355
+ ```
356
+
357
+ ### UDeveloperSettings (project settings)
358
+
359
+ ```cpp
360
+ UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="My Game Settings"))
361
+ class MYGAME_API UMyProjectSettings : public UDeveloperSettings
362
+ {
363
+ GENERATED_BODY()
364
+ public:
365
+ UPROPERTY(Config, EditAnywhere, Category="Save") int32 MaxSaveSlots = 5;
366
+ UPROPERTY(Config, EditAnywhere, Category="Save") bool bEnableAutoSave = true;
367
+ UPROPERTY(Config, EditAnywhere, Category="Save") float AutoSaveIntervalSeconds = 300.f;
368
+ static const UMyProjectSettings* Get() { return GetDefault<UMyProjectSettings>(); }
369
+ };
370
+ ```
371
+
372
+ ### GConfig Direct Access
373
+
374
+ ```cpp
375
+ #include "Misc/ConfigCacheIni.h"
376
+
377
+ FString Value;
378
+ GConfig->GetString(TEXT("/Script/MyGame.MyConfig"), TEXT("Key"), Value, GGameIni);
379
+ GConfig->SetString(TEXT("/Script/MyGame.MyConfig"), TEXT("Key"), TEXT("Val"), GGameIni);
380
+ GConfig->Flush(/*bRemoveFromCache=*/false, GGameIni);
381
+
382
+ MyObject->SaveConfig(); // writes UPROPERTY(Config) fields to .ini
383
+ MyObject->LoadConfig(); // reloads from .ini
384
+ ```
385
+
386
+ **INI section naming**: Section `[/Script/ModuleName.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)`.
387
+
388
+ ---
389
+
390
+ ## Cloud Save Integration
391
+
392
+ ```cpp
393
+ // Platform save systems (Steam, EOS, console) provide ISaveGameSystem
394
+ // Access via IPlatformFeaturesModule:
395
+ ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem();
396
+ if (SaveSystem && SaveSystem->DoesSaveSystemSupportMultipleUsers())
397
+ {
398
+ // Platform handles cloud sync — use UGameplayStatics normally
399
+ // Steam: auto-syncs Saved/SaveGames/ via Steam Cloud if configured in Steamworks
400
+ // EOS: use IOnlineSubsystem → IOnlineTitleFileInterface for explicit cloud read/write
401
+ }
402
+
403
+ // Cross-platform pattern: serialize to TArray<uint8>, then write via platform API
404
+ TArray<uint8> SaveData;
405
+ FMemoryWriter Ar(SaveData);
406
+ SaveObject->Serialize(Ar);
407
+ // Upload SaveData via platform SDK
408
+
409
+ // Steam Cloud — write save slot directly via Steamworks API
410
+ ISteamRemoteStorage* SteamStorage = SteamRemoteStorage();
411
+ if (SteamStorage && SteamStorage->IsCloudEnabledForApp())
412
+ {
413
+ SteamStorage->FileWrite("SaveSlot1.sav", SaveData.GetData(), SaveData.Num());
414
+ }
415
+ // Read back: SteamStorage->FileRead("SaveSlot1.sav", Buffer, Size)
416
+ ```
417
+
418
+ ## Save Data Encryption
419
+
420
+ ```cpp
421
+ // Use FAES for symmetric encryption of save data
422
+ #include "Misc/AES.h"
423
+ // Build a zero-padded 32-byte FAESKey from a string.
424
+ // Do NOT use Key.Left(32): if the string is shorter than 32 chars it silently
425
+ // produces a truncated key, corrupting every encrypt/decrypt call.
426
+ static FAESKey MakeAESKey(const FString& KeyString)
427
+ {
428
+ FAESKey AESKey;
429
+ FMemory::Memzero(AESKey.Key, FAESKey::KeySize);
430
+ const FTCHARToUTF8 Utf8(*KeyString);
431
+ FMemory::Memcpy(AESKey.Key, Utf8.Get(), FMath::Min(Utf8.Length(), FAESKey::KeySize));
432
+ return AESKey;
433
+ }
434
+
435
+ void EncryptSaveData(TArray<uint8>& Data, const FString& KeyString)
436
+ {
437
+ int32 PaddedSize = Align(Data.Num(), FAES::AESBlockSize);
438
+ Data.SetNumZeroed(PaddedSize);
439
+ FAES::EncryptData(Data.GetData(), PaddedSize, MakeAESKey(KeyString));
440
+ }
441
+
442
+ void DecryptSaveData(TArray<uint8>& Data, const FString& KeyString)
443
+ {
444
+ FAES::DecryptData(Data.GetData(), Data.Num(), MakeAESKey(KeyString));
445
+ }
446
+ ```
447
+
448
+ **Why encrypt**: Prevents casual save editing for competitive/economy-sensitive games. Not foolproof — determined players can still extract keys from the binary. Combine with server-side validation for authoritative saves.
449
+
450
+ ---
451
+
452
+ ## Step 8: Common Mistakes
453
+
454
+ | Anti-Pattern | Problem | Fix |
455
+ |---|---|---|
456
+ | Saving raw `UObject*` or `AActor*` | Pointers invalid between sessions | Save `FSoftObjectPath` or a stable unique ID |
457
+ | No version field | Adding/removing fields corrupts old saves silently | Always include `int32 SaveVersion`; run migrations on load |
458
+ | `SaveGameToSlot` on game thread per frame | Blocks rendering, causes hitches | Use `AsyncSaveGameToSlot` |
459
+ | `USTRUCT` without `GENERATED_BODY()` in a saved field | Silent serialization failure | Add `GENERATED_BODY()` to all saved structs |
460
+ | Ignoring `Ar.IsError()` | Reads past corrupted data, applies garbage | Check after every block; abort immediately if set |
461
+ | Overlapping async saves | Second save starts before first completes | Guard with `bSaveInProgress` flag or `IsSaveInProgress()` |
462
+ | Hardcoded save file paths | Breaks on consoles and different platforms | Use `UGameplayStatics` APIs; `FPaths::ProjectSavedDir()` only for debug |
463
+
464
+ **PIE vs. Packaged / platform paths**: In PIE, saves go to `<Project>/Saved/SaveGames/`. Packaged Windows builds write to `%LocalAppData%/<ProjectName>/Saved/SaveGames/`. 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.
465
+
466
+ ---
467
+
468
+ ## Advanced Edge Cases
469
+
470
+ **Corruption recovery**: When `Ar.IsError()` returns true mid-read or magic/version 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:
471
+
472
+ ```cpp
473
+ USaveGame* LoadedSave = UGameplayStatics::LoadGameFromSlot(PrimarySlot, 0);
474
+ if (!LoadedSave)
475
+ LoadedSave = UGameplayStatics::LoadGameFromSlot(BackupSlot, 0);
476
+ if (!LoadedSave)
477
+ LoadedSave = UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass());
478
+ ```
479
+
480
+ **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.
481
+
482
+ **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.
483
+
484
+ ---
485
+
486
+ ## Module Dependencies (Build.cs)
487
+
488
+ ```csharp
489
+ PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });
490
+ // For UDeveloperSettings:
491
+ PublicDependencyModuleNames.Add("DeveloperSettings");
492
+ ```
493
+
494
+ ---
495
+
496
+ ## Related Skills
497
+
498
+ - `/ue-cpp-foundations` — UPROPERTY, USTRUCT, UObject lifetime
499
+ - `/ue-data-assets-tables` — FSoftObjectPath patterns for asset references in saves
500
+ - `/ue-gameplay-framework` — GameInstance as save manager host; GameMode auto-save integration
501
+
502
+ ## Reference Files
503
+
504
+ - `references/save-system-architecture.md` — Full slot manager subsystem, metadata bank, multi-user patterns, and migration pipeline
505
+
506
+ ---
507
+
508
+ ## Checklist
509
+
510
+ Validate before considering the work done:
511
+
512
+ - [ ] Saves carry a version number and have a defined upgrade path
513
+ - [ ] Saving is asynchronous and cannot corrupt an existing file on failure
514
+ - [ ] Only owned state is serialized — no raw object pointers to transient actors
515
+ - [ ] Conventions in `docs/unreal/project-context.md` were read and followed
516
+ - [ ] The Common Mistakes section of this skill was reviewed against the change
517
+
518
+ **PASS** — every item holds. **CONCERNS** — the work stands but at least one item is unresolved and worth flagging to the user. **FAIL** — an item is violated in a way that breaks correctness, replication or the frame budget; fix it before handing the work over.
519
+
520
+ ## Next Steps
521
+
522
+ - `/ue-testing-debugging` — test load of an older save version
523
+ - `/code-review` — architectural and quality review of the change
524
+ - `/commit` — conventional commit once the change is verified