claude-code-arcane 2.2.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 (144) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +104 -55
  3. package/agents/career/career-strategist.md +88 -0
  4. package/agents/career/cv-reviewer.md +81 -0
  5. package/agents/career/hiring-manager.md +68 -0
  6. package/agents/career/mock-interviewer.md +76 -0
  7. package/agents/freelance/client-screener.md +84 -0
  8. package/agents/freelance/discovery-call.md +65 -0
  9. package/agents/freelance/pipeline-strategist.md +99 -0
  10. package/agents/freelance/proposal-reviewer.md +82 -0
  11. package/agents/game/ue-blueprint-specialist.md +125 -0
  12. package/agents/game/ue-gas-specialist.md +133 -0
  13. package/agents/game/ue-umg-specialist.md +131 -0
  14. package/dist/cli.js +57 -13
  15. package/docs/RELEASE-SETUP.md +1 -1
  16. package/docs/SKILLS-CATALOG.md +67 -4
  17. package/docs/USER-GUIDE.md +77 -72
  18. package/docs/agent-hierarchy.md +21 -1
  19. package/docs/presentations/build_arcane_deck.py +2 -2
  20. package/docs/presentations/build_arcane_deck_12.py +2 -2
  21. package/hooks/detect-unreal.sh +90 -0
  22. package/package.json +2 -2
  23. package/profiles/freelance.yaml +43 -0
  24. package/profiles/job-hunt.yaml +5 -2
  25. package/profiles/unreal-dev.yaml +97 -0
  26. package/rules/drafter-reviewer.md +16 -3
  27. package/rules/freelance-guardrails.md +53 -0
  28. package/rules/gamedev/unreal-code.md +125 -0
  29. package/skills/career-registry/SKILL.md +95 -0
  30. package/skills/career-registry/scripts/career_registry.py +488 -0
  31. package/skills/client-screen/SKILL.md +83 -0
  32. package/skills/client-screen/references/red-flags.md +95 -0
  33. package/skills/cover-letter/SKILL.md +4 -3
  34. package/skills/cv-tailor/SKILL.md +11 -2
  35. package/skills/freelance-deliver/SKILL.md +90 -0
  36. package/skills/freelance-hunt/SKILL.md +126 -0
  37. package/skills/freelance-hunt/references/templates/Connects.md +39 -0
  38. package/skills/freelance-hunt/references/templates/Contrato.md +60 -0
  39. package/skills/freelance-hunt/references/templates/Propuesta.md +66 -0
  40. package/skills/freelance-kickoff/SKILL.md +94 -0
  41. package/skills/freelance-pipeline/SKILL.md +109 -0
  42. package/skills/freelance-pipeline/scripts/import_history.py +425 -0
  43. package/skills/freelance-profile/SKILL.md +93 -0
  44. package/skills/freelance-proposal/SKILL.md +112 -0
  45. package/skills/freelance-scan/SKILL.md +196 -0
  46. package/skills/freelance-scan/references/dedup.md +35 -0
  47. package/skills/freelance-scan/references/platforms.md +167 -0
  48. package/skills/freelance-scan/scripts/freelance_search.py +1077 -0
  49. package/skills/install-mcp/SKILL.md +135 -46
  50. package/skills/install-mcp/references/unreal-operations.md +55 -0
  51. package/skills/interview-prep/SKILL.md +11 -3
  52. package/skills/job-aplicar/SKILL.md +5 -1
  53. package/skills/job-hunt/SKILL.md +17 -0
  54. package/skills/job-outcome/SKILL.md +15 -1
  55. package/skills/job-upskill/SKILL.md +10 -2
  56. package/skills/ue-actor-component-architecture/SKILL.md +509 -0
  57. package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
  58. package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
  59. package/skills/ue-ai-navigation/SKILL.md +526 -0
  60. package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
  61. package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
  62. package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
  63. package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
  64. package/skills/ue-animation-system/SKILL.md +521 -0
  65. package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
  66. package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
  67. package/skills/ue-async-threading/SKILL.md +433 -0
  68. package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
  69. package/skills/ue-async-threading/references/threading-patterns.md +345 -0
  70. package/skills/ue-audio-system/SKILL.md +527 -0
  71. package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
  72. package/skills/ue-blueprints/SKILL.md +134 -0
  73. package/skills/ue-blueprints/references/communication.md +82 -0
  74. package/skills/ue-character-movement/SKILL.md +475 -0
  75. package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
  76. package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
  77. package/skills/ue-cpp-foundations/SKILL.md +533 -0
  78. package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
  79. package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
  80. package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
  81. package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
  82. package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
  83. package/skills/ue-data-assets-tables/SKILL.md +527 -0
  84. package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
  85. package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
  86. package/skills/ue-editor-tools/SKILL.md +523 -0
  87. package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
  88. package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
  89. package/skills/ue-game-features/SKILL.md +477 -0
  90. package/skills/ue-game-features/references/experience-system.md +222 -0
  91. package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
  92. package/skills/ue-gameplay-abilities/SKILL.md +518 -0
  93. package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
  94. package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
  95. package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
  96. package/skills/ue-gameplay-framework/SKILL.md +466 -0
  97. package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
  98. package/skills/ue-input-system/SKILL.md +394 -0
  99. package/skills/ue-input-system/references/cpp-setup.md +131 -0
  100. package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
  101. package/skills/ue-input-system/references/input-action-reference.md +731 -0
  102. package/skills/ue-mass-entity/SKILL.md +476 -0
  103. package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
  104. package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
  105. package/skills/ue-materials-rendering/SKILL.md +526 -0
  106. package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
  107. package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
  108. package/skills/ue-module-build-system/SKILL.md +487 -0
  109. package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
  110. package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
  111. package/skills/ue-naming-conventions/SKILL.md +142 -0
  112. package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
  113. package/skills/ue-naming-conventions/references/content-structure.md +83 -0
  114. package/skills/ue-networking-replication/SKILL.md +452 -0
  115. package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
  116. package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
  117. package/skills/ue-niagara-effects/SKILL.md +531 -0
  118. package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
  119. package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
  120. package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
  121. package/skills/ue-packaging/SKILL.md +116 -0
  122. package/skills/ue-physics-collision/SKILL.md +423 -0
  123. package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
  124. package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
  125. package/skills/ue-procedural-generation/SKILL.md +505 -0
  126. package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
  127. package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
  128. package/skills/ue-project-context/SKILL.md +305 -0
  129. package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
  130. package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
  131. package/skills/ue-serialization-savegames/SKILL.md +524 -0
  132. package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
  133. package/skills/ue-state-trees/SKILL.md +489 -0
  134. package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
  135. package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
  136. package/skills/ue-testing-debugging/SKILL.md +520 -0
  137. package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
  138. package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
  139. package/skills/ue-ui-umg-slate/SKILL.md +431 -0
  140. package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
  141. package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
  142. package/skills/ue-world-level-streaming/SKILL.md +518 -0
  143. package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
  144. package/templates/gamedev/unreal-project-context.md +107 -0
@@ -0,0 +1,705 @@
1
+ # Save System Architecture
2
+
3
+ Complete patterns for production save systems: slot management, save metadata, async pipelines, multi-user handling, and corruption recovery.
4
+
5
+ ---
6
+
7
+ ## Architecture Overview
8
+
9
+ A production save system has three layers:
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────┐
13
+ │ Game Code (GameMode, PlayerController, Subsystems) │
14
+ │ -- calls SaveManager -- │
15
+ ├─────────────────────────────────────────────────────────┤
16
+ │ Save Manager (UGameInstanceSubsystem) │
17
+ │ -- slot routing, async coordination, migration -- │
18
+ ├─────────────────────────────────────────────────────────┤
19
+ │ UGameplayStatics / ISaveGameSystem │
20
+ │ -- platform-agnostic file I/O -- │
21
+ └─────────────────────────────────────────────────────────┘
22
+ ```
23
+
24
+ The save manager lives on the `UGameInstance` as a `UGameInstanceSubsystem` so it persists across level transitions and map loads.
25
+
26
+ ---
27
+
28
+ ## 1. Save Metadata (Slot List)
29
+
30
+ Store a lightweight metadata object in a fixed slot so the UI can display save slot info (timestamp, player name, level, screenshot thumbnail path) without loading the full save payload.
31
+
32
+ ```cpp
33
+ // SaveMetadata.h
34
+ #pragma once
35
+ #include "CoreMinimal.h"
36
+ #include "GameFramework/SaveGame.h"
37
+ #include "SaveMetadata.generated.h"
38
+
39
+ USTRUCT(BlueprintType)
40
+ struct FSaveSlotInfo
41
+ {
42
+ GENERATED_BODY()
43
+
44
+ UPROPERTY(SaveGame, BlueprintReadOnly)
45
+ int32 SlotIndex = -1;
46
+
47
+ UPROPERTY(SaveGame, BlueprintReadOnly)
48
+ FString PlayerDisplayName;
49
+
50
+ UPROPERTY(SaveGame, BlueprintReadOnly)
51
+ FString MapName;
52
+
53
+ UPROPERTY(SaveGame, BlueprintReadOnly)
54
+ int32 PlayerLevel = 0;
55
+
56
+ UPROPERTY(SaveGame, BlueprintReadOnly)
57
+ FDateTime LastSaveTime;
58
+
59
+ UPROPERTY(SaveGame, BlueprintReadOnly)
60
+ float TotalPlayTimeSeconds = 0.f;
61
+
62
+ UPROPERTY(SaveGame, BlueprintReadOnly)
63
+ bool bIsValid = false;
64
+
65
+ // Path to a thumbnail screenshot stored in Saved/Screenshots/
66
+ UPROPERTY(SaveGame, BlueprintReadOnly)
67
+ FString ThumbnailPath;
68
+ };
69
+
70
+ UCLASS()
71
+ class MYGAME_API USaveMetadataBank : public USaveGame
72
+ {
73
+ GENERATED_BODY()
74
+
75
+ public:
76
+ static constexpr int32 MaxSlots = 10;
77
+ static const FString SlotName; // "SaveMetadataBank"
78
+ static constexpr int32 UserIndex = 0;
79
+
80
+ UPROPERTY(SaveGame)
81
+ TArray<FSaveSlotInfo> Slots;
82
+
83
+ // Initialize with empty slots
84
+ void Init()
85
+ {
86
+ Slots.SetNum(MaxSlots);
87
+ for (int32 i = 0; i < MaxSlots; ++i)
88
+ {
89
+ Slots[i].SlotIndex = i;
90
+ Slots[i].bIsValid = false;
91
+ }
92
+ }
93
+
94
+ const FSaveSlotInfo* GetSlot(int32 Index) const
95
+ {
96
+ if (Slots.IsValidIndex(Index)) { return &Slots[Index]; }
97
+ return nullptr;
98
+ }
99
+
100
+ void UpdateSlot(int32 Index, const FSaveSlotInfo& Info)
101
+ {
102
+ if (Slots.IsValidIndex(Index))
103
+ {
104
+ Slots[Index] = Info;
105
+ Slots[Index].SlotIndex = Index;
106
+ Slots[Index].bIsValid = true;
107
+ }
108
+ }
109
+
110
+ void ClearSlot(int32 Index)
111
+ {
112
+ if (Slots.IsValidIndex(Index))
113
+ {
114
+ Slots[Index] = FSaveSlotInfo();
115
+ Slots[Index].SlotIndex = Index;
116
+ }
117
+ }
118
+ };
119
+
120
+ const FString USaveMetadataBank::SlotName = TEXT("SaveMetadataBank");
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 2. Save Manager Subsystem
126
+
127
+ ```cpp
128
+ // SaveManager.h
129
+ #pragma once
130
+ #include "CoreMinimal.h"
131
+ #include "Subsystems/GameInstanceSubsystem.h"
132
+ #include "SaveMetadata.h"
133
+ #include "MyGameSaveGame.h"
134
+ #include "SaveManager.generated.h"
135
+
136
+ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSaveComplete, int32, SlotIndex, bool, bSuccess);
137
+ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnLoadComplete, int32, SlotIndex, bool, bSuccess);
138
+
139
+ UCLASS()
140
+ class MYGAME_API USaveManager : public UGameInstanceSubsystem
141
+ {
142
+ GENERATED_BODY()
143
+
144
+ public:
145
+ // USubsystem interface
146
+ virtual void Initialize(FSubsystemCollectionBase& Collection) override;
147
+ virtual void Deinitialize() override;
148
+
149
+ // -- Public API --
150
+
151
+ /** Load the metadata bank (slot list). Call once at game startup. */
152
+ UFUNCTION(BlueprintCallable, Category = "Save")
153
+ void LoadMetadataBank();
154
+
155
+ /** Get info for all slots without loading save payloads. */
156
+ UFUNCTION(BlueprintCallable, Category = "Save")
157
+ TArray<FSaveSlotInfo> GetAllSlotInfo() const;
158
+
159
+ /** Async save to a slot. Fires OnSaveComplete when done. */
160
+ UFUNCTION(BlueprintCallable, Category = "Save")
161
+ void AsyncSaveToSlot(int32 SlotIndex);
162
+
163
+ /** Async load from a slot. Fires OnLoadComplete when done. */
164
+ UFUNCTION(BlueprintCallable, Category = "Save")
165
+ void AsyncLoadFromSlot(int32 SlotIndex);
166
+
167
+ /** Synchronous load — blocks game thread. Use only during initial boot. */
168
+ UFUNCTION(BlueprintCallable, Category = "Save")
169
+ bool SyncLoadFromSlot(int32 SlotIndex);
170
+
171
+ /** Delete a save slot and clear its metadata entry. */
172
+ UFUNCTION(BlueprintCallable, Category = "Save")
173
+ void DeleteSlot(int32 SlotIndex);
174
+
175
+ /** True if an async save is currently in flight. */
176
+ UFUNCTION(BlueprintCallable, Category = "Save")
177
+ bool IsSaveInProgress() const { return bSaveInProgress; }
178
+
179
+ /** The currently active save data (loaded slot). */
180
+ UFUNCTION(BlueprintCallable, Category = "Save")
181
+ UMyGameSaveGame* GetCurrentSave() const { return CurrentSave; }
182
+
183
+ // -- Events --
184
+ UPROPERTY(BlueprintAssignable, Category = "Save")
185
+ FOnSaveComplete OnSaveComplete;
186
+
187
+ UPROPERTY(BlueprintAssignable, Category = "Save")
188
+ FOnLoadComplete OnLoadComplete;
189
+
190
+ // -- Auto-Save --
191
+ UFUNCTION(BlueprintCallable, Category = "Save")
192
+ void StartAutoSave(int32 SlotIndex, float IntervalSeconds = 300.f);
193
+
194
+ UFUNCTION(BlueprintCallable, Category = "Save")
195
+ void StopAutoSave();
196
+
197
+ private:
198
+ static FString MakeSlotName(int32 SlotIndex);
199
+ void PopulateSaveData(UMyGameSaveGame* Save);
200
+ void ApplySaveData(UMyGameSaveGame* Save);
201
+ void SaveMetadataBank();
202
+ FSaveSlotInfo BuildSlotInfo(int32 SlotIndex, const UMyGameSaveGame* Save) const;
203
+
204
+ // Callbacks
205
+ UFUNCTION()
206
+ void OnAsyncSaveComplete(const FString& SlotName, int32 UserIndex, bool bSuccess);
207
+ UFUNCTION()
208
+ void OnAsyncLoadComplete(const FString& SlotName, int32 UserIndex, USaveGame* SaveGame);
209
+
210
+ UPROPERTY()
211
+ USaveMetadataBank* MetadataBank = nullptr;
212
+
213
+ UPROPERTY()
214
+ UMyGameSaveGame* CurrentSave = nullptr;
215
+
216
+ int32 ActiveSlotIndex = -1;
217
+ bool bSaveInProgress = false;
218
+
219
+ FTimerHandle AutoSaveTimerHandle;
220
+ int32 AutoSaveSlotIndex = 0;
221
+ };
222
+ ```
223
+
224
+ ```cpp
225
+ // SaveManager.cpp
226
+ #include "SaveManager.h"
227
+ #include "Kismet/GameplayStatics.h"
228
+ #include "Engine/World.h"
229
+ #include "TimerManager.h"
230
+
231
+ void USaveManager::Initialize(FSubsystemCollectionBase& Collection)
232
+ {
233
+ Super::Initialize(Collection);
234
+ LoadMetadataBank();
235
+ }
236
+
237
+ void USaveManager::Deinitialize()
238
+ {
239
+ StopAutoSave();
240
+ Super::Deinitialize();
241
+ }
242
+
243
+ FString USaveManager::MakeSlotName(int32 SlotIndex)
244
+ {
245
+ return FString::Printf(TEXT("GameSave_%02d"), SlotIndex);
246
+ }
247
+
248
+ void USaveManager::LoadMetadataBank()
249
+ {
250
+ if (UGameplayStatics::DoesSaveGameExist(USaveMetadataBank::SlotName,
251
+ USaveMetadataBank::UserIndex))
252
+ {
253
+ MetadataBank = Cast<USaveMetadataBank>(
254
+ UGameplayStatics::LoadGameFromSlot(
255
+ USaveMetadataBank::SlotName, USaveMetadataBank::UserIndex));
256
+ }
257
+
258
+ if (!MetadataBank)
259
+ {
260
+ MetadataBank = Cast<USaveMetadataBank>(
261
+ UGameplayStatics::CreateSaveGameObject(USaveMetadataBank::StaticClass()));
262
+ MetadataBank->Init();
263
+ }
264
+ }
265
+
266
+ TArray<FSaveSlotInfo> USaveManager::GetAllSlotInfo() const
267
+ {
268
+ if (MetadataBank)
269
+ {
270
+ return MetadataBank->Slots;
271
+ }
272
+ return {};
273
+ }
274
+
275
+ void USaveManager::AsyncSaveToSlot(int32 SlotIndex)
276
+ {
277
+ if (bSaveInProgress)
278
+ {
279
+ UE_LOG(LogTemp, Warning, TEXT("SaveManager: Save already in progress, ignoring request."));
280
+ return;
281
+ }
282
+
283
+ if (!CurrentSave)
284
+ {
285
+ CurrentSave = Cast<UMyGameSaveGame>(
286
+ UGameplayStatics::CreateSaveGameObject(UMyGameSaveGame::StaticClass()));
287
+ }
288
+
289
+ PopulateSaveData(CurrentSave);
290
+ ActiveSlotIndex = SlotIndex;
291
+ bSaveInProgress = true;
292
+
293
+ FAsyncSaveGameToSlotDelegate Delegate;
294
+ Delegate.BindUObject(this, &USaveManager::OnAsyncSaveComplete);
295
+ UGameplayStatics::AsyncSaveGameToSlot(
296
+ CurrentSave, MakeSlotName(SlotIndex), USaveMetadataBank::UserIndex, Delegate);
297
+ }
298
+
299
+ void USaveManager::OnAsyncSaveComplete(
300
+ const FString& SlotName, int32 UserIndex, bool bSuccess)
301
+ {
302
+ bSaveInProgress = false;
303
+
304
+ if (bSuccess && MetadataBank)
305
+ {
306
+ FSaveSlotInfo Info = BuildSlotInfo(ActiveSlotIndex, CurrentSave);
307
+ MetadataBank->UpdateSlot(ActiveSlotIndex, Info);
308
+ SaveMetadataBank();
309
+ }
310
+
311
+ OnSaveComplete.Broadcast(ActiveSlotIndex, bSuccess);
312
+
313
+ if (!bSuccess)
314
+ {
315
+ UE_LOG(LogTemp, Error, TEXT("SaveManager: Async save failed for slot %d"), ActiveSlotIndex);
316
+ }
317
+ }
318
+
319
+ void USaveManager::AsyncLoadFromSlot(int32 SlotIndex)
320
+ {
321
+ ActiveSlotIndex = SlotIndex;
322
+
323
+ FAsyncLoadGameFromSlotDelegate Delegate;
324
+ Delegate.BindUObject(this, &USaveManager::OnAsyncLoadComplete);
325
+ UGameplayStatics::AsyncLoadGameFromSlot(
326
+ MakeSlotName(SlotIndex), USaveMetadataBank::UserIndex, Delegate);
327
+ }
328
+
329
+ void USaveManager::OnAsyncLoadComplete(
330
+ const FString& SlotName, int32 UserIndex, USaveGame* SaveGame)
331
+ {
332
+ CurrentSave = Cast<UMyGameSaveGame>(SaveGame);
333
+
334
+ if (!CurrentSave)
335
+ {
336
+ UE_LOG(LogTemp, Warning,
337
+ TEXT("SaveManager: Load failed or no file for slot %d; using fresh save."),
338
+ ActiveSlotIndex);
339
+ CurrentSave = Cast<UMyGameSaveGame>(
340
+ UGameplayStatics::CreateSaveGameObject(UMyGameSaveGame::StaticClass()));
341
+ }
342
+
343
+ RunMigrations(CurrentSave);
344
+ ApplySaveData(CurrentSave);
345
+
346
+ OnLoadComplete.Broadcast(ActiveSlotIndex, SaveGame != nullptr);
347
+ }
348
+
349
+ bool USaveManager::SyncLoadFromSlot(int32 SlotIndex)
350
+ {
351
+ // Use only at startup before the first frame renders
352
+ USaveGame* Loaded = UGameplayStatics::LoadGameFromSlot(
353
+ MakeSlotName(SlotIndex), USaveMetadataBank::UserIndex);
354
+
355
+ CurrentSave = Cast<UMyGameSaveGame>(Loaded);
356
+ if (!CurrentSave)
357
+ {
358
+ CurrentSave = Cast<UMyGameSaveGame>(
359
+ UGameplayStatics::CreateSaveGameObject(UMyGameSaveGame::StaticClass()));
360
+ return false;
361
+ }
362
+
363
+ RunMigrations(CurrentSave);
364
+ ApplySaveData(CurrentSave);
365
+ ActiveSlotIndex = SlotIndex;
366
+ return true;
367
+ }
368
+
369
+ void USaveManager::DeleteSlot(int32 SlotIndex)
370
+ {
371
+ UGameplayStatics::DeleteGameInSlot(MakeSlotName(SlotIndex), USaveMetadataBank::UserIndex);
372
+
373
+ if (MetadataBank)
374
+ {
375
+ MetadataBank->ClearSlot(SlotIndex);
376
+ SaveMetadataBank();
377
+ }
378
+ }
379
+
380
+ void USaveManager::SaveMetadataBank()
381
+ {
382
+ UGameplayStatics::SaveGameToSlot(
383
+ MetadataBank, USaveMetadataBank::SlotName, USaveMetadataBank::UserIndex);
384
+ }
385
+
386
+ FSaveSlotInfo USaveManager::BuildSlotInfo(int32 SlotIndex, const UMyGameSaveGame* Save) const
387
+ {
388
+ FSaveSlotInfo Info;
389
+ Info.SlotIndex = SlotIndex;
390
+ Info.bIsValid = true;
391
+ Info.LastSaveTime = FDateTime::Now();
392
+
393
+ if (Save)
394
+ {
395
+ Info.PlayerDisplayName = Save->PlayerDisplayName;
396
+ Info.PlayerLevel = Save->PlayerLevel;
397
+ Info.TotalPlayTimeSeconds = Save->TotalPlayTimeSeconds;
398
+ // MapName from world:
399
+ if (UWorld* World = GetGameInstance() ? GetGameInstance()->GetWorld() : nullptr)
400
+ {
401
+ Info.MapName = World->GetMapName();
402
+ }
403
+ }
404
+
405
+ return Info;
406
+ }
407
+
408
+ void USaveManager::StartAutoSave(int32 SlotIndex, float IntervalSeconds)
409
+ {
410
+ AutoSaveSlotIndex = SlotIndex;
411
+ if (UWorld* World = GetGameInstance()->GetWorld())
412
+ {
413
+ World->GetTimerManager().SetTimer(
414
+ AutoSaveTimerHandle,
415
+ FTimerDelegate::CreateUObject(this, &USaveManager::AsyncSaveToSlot, SlotIndex),
416
+ IntervalSeconds,
417
+ /*bLoop=*/true);
418
+ }
419
+ }
420
+
421
+ void USaveManager::StopAutoSave()
422
+ {
423
+ if (UWorld* World = GetGameInstance() ? GetGameInstance()->GetWorld() : nullptr)
424
+ {
425
+ World->GetTimerManager().ClearTimer(AutoSaveTimerHandle);
426
+ }
427
+ }
428
+ ```
429
+
430
+ ---
431
+
432
+ ## 3. Migration Pipeline
433
+
434
+ Centralize all migration logic so it runs consistently on every load path (sync and async):
435
+
436
+ ```cpp
437
+ // Add to SaveManager or to UMyGameSaveGame
438
+
439
+ // Version constants
440
+ namespace ESaveVersion
441
+ {
442
+ enum Type : int32
443
+ {
444
+ Initial = 0,
445
+ AddedInventory = 1,
446
+ AddedAbilitySaveData = 2,
447
+ AddedPlayTime = 3,
448
+ SoftRefForWeapon = 4,
449
+
450
+ VersionPlusOne,
451
+ Latest = VersionPlusOne - 1
452
+ };
453
+ }
454
+
455
+ void USaveManager::RunMigrations(UMyGameSaveGame* Save)
456
+ {
457
+ if (!Save) { return; }
458
+
459
+ const int32 LoadedVersion = Save->SaveVersion;
460
+
461
+ if (LoadedVersion == ESaveVersion::Latest)
462
+ {
463
+ return; // Already up to date, fast path
464
+ }
465
+
466
+ UE_LOG(LogTemp, Log, TEXT("Migrating save from version %d to %d"),
467
+ LoadedVersion, (int32)ESaveVersion::Latest);
468
+
469
+ if (LoadedVersion < ESaveVersion::AddedInventory)
470
+ {
471
+ // No inventory existed; leave InventoryItems empty (already default)
472
+ }
473
+
474
+ if (LoadedVersion < ESaveVersion::AddedAbilitySaveData)
475
+ {
476
+ // Ability data didn't exist; initialize defaults
477
+ Save->SavedAbilities.Reset();
478
+ }
479
+
480
+ if (LoadedVersion < ESaveVersion::AddedPlayTime)
481
+ {
482
+ // Can't recover old play time; set to 0
483
+ Save->TotalPlayTimeSeconds = 0.f;
484
+ }
485
+
486
+ if (LoadedVersion < ESaveVersion::SoftRefForWeapon)
487
+ {
488
+ // Old save used a FName; new format uses FSoftObjectPath.
489
+ // If OldWeaponID_Deprecated was populated, convert it.
490
+ // Save->LastEquippedWeaponPath = ResolveWeaponIDToPath(Save->OldWeaponID_Deprecated);
491
+ }
492
+
493
+ // Stamp new version
494
+ Save->SaveVersion = ESaveVersion::Latest;
495
+ }
496
+ ```
497
+
498
+ ---
499
+
500
+ ## 4. Binary Blob Pattern (FArchive Inside USaveGame)
501
+
502
+ For complex subsystem data that warrants manual layout control, store it as `TArray<uint8>` inside the `USaveGame` and serialize/deserialize with `FMemoryWriter`/`FMemoryReader`:
503
+
504
+ ```cpp
505
+ // In UMyGameSaveGame:
506
+ UPROPERTY(SaveGame)
507
+ TArray<uint8> QuestSystemBlob; // Manually serialized binary payload
508
+
509
+ // In your quest subsystem:
510
+ bool UQuestSubsystem::SerializeToBlob(TArray<uint8>& OutBlob) const
511
+ {
512
+ FMemoryWriter Writer(OutBlob, /*bIsPersistent=*/true);
513
+
514
+ int32 BlobVersion = 2;
515
+ Writer << BlobVersion;
516
+
517
+ int32 NumActiveQuests = ActiveQuests.Num();
518
+ Writer << NumActiveQuests;
519
+
520
+ for (const FQuestState& Quest : ActiveQuests)
521
+ {
522
+ FName QID = Quest.QuestID;
523
+ int32 Stage = Quest.CurrentStage;
524
+ Writer << QID;
525
+ Writer << Stage;
526
+ }
527
+
528
+ return !Writer.IsError();
529
+ }
530
+
531
+ bool UQuestSubsystem::DeserializeFromBlob(const TArray<uint8>& Blob)
532
+ {
533
+ if (Blob.IsEmpty()) { return true; } // No quest data saved yet
534
+
535
+ FMemoryReader Reader(Blob, /*bIsPersistent=*/true);
536
+
537
+ int32 BlobVersion = 0;
538
+ Reader << BlobVersion;
539
+
540
+ if (BlobVersion < 1 || Reader.IsError())
541
+ {
542
+ UE_LOG(LogTemp, Error, TEXT("QuestSubsystem: Invalid blob version %d"), BlobVersion);
543
+ return false;
544
+ }
545
+
546
+ int32 NumQuests = 0;
547
+ Reader << NumQuests;
548
+
549
+ ActiveQuests.Empty(NumQuests);
550
+
551
+ for (int32 i = 0; i < NumQuests; ++i)
552
+ {
553
+ FName QID;
554
+ int32 Stage;
555
+ Reader << QID;
556
+ Reader << Stage;
557
+
558
+ if (Reader.IsError()) { return false; }
559
+
560
+ FQuestState& Q = ActiveQuests.AddDefaulted_GetRef();
561
+ Q.QuestID = QID;
562
+ Q.CurrentStage = Stage;
563
+ }
564
+
565
+ return !Reader.IsError();
566
+ }
567
+ ```
568
+
569
+ ---
570
+
571
+ ## 5. Multi-User (Local Multiplayer) Pattern
572
+
573
+ Each local player gets their own save slot. Use `ULocalPlayerSaveGame` and pass the `ULocalPlayer` or `APlayerController`.
574
+
575
+ ```cpp
576
+ // In your player controller or local player subsystem:
577
+ void AMyPlayerController::SavePlayerData()
578
+ {
579
+ if (!LocalPlayerSave)
580
+ {
581
+ UE_LOG(LogTemp, Warning, TEXT("No local player save — was it loaded?"));
582
+ return;
583
+ }
584
+
585
+ // Populate
586
+ LocalPlayerSave->AbilityLevels.Empty();
587
+ for (auto& [AbilityName, Level] : AbilityComponent->GetAllLevels())
588
+ {
589
+ LocalPlayerSave->AbilityLevels.Add(AbilityName, Level);
590
+ }
591
+
592
+ // Async save — hooks HandlePostSave for result
593
+ LocalPlayerSave->AsyncSaveGameToSlotForLocalPlayer();
594
+ }
595
+
596
+ void AMyPlayerController::LoadPlayerData()
597
+ {
598
+ // Async load or create a fresh save if none exists
599
+ bool bScheduled = ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
600
+ UMyLocalPlayerSave::StaticClass(),
601
+ this, // APlayerController* LocalPlayerController
602
+ TEXT("PlayerData"),
603
+ FOnLocalPlayerSaveGameLoadedNative::CreateUObject(
604
+ this, &AMyPlayerController::OnPlayerDataLoaded));
605
+
606
+ if (!bScheduled)
607
+ {
608
+ UE_LOG(LogTemp, Error, TEXT("Failed to schedule async load for local player save."));
609
+ }
610
+ }
611
+
612
+ void AMyPlayerController::OnPlayerDataLoaded(ULocalPlayerSaveGame* SaveGame)
613
+ {
614
+ LocalPlayerSave = Cast<UMyLocalPlayerSave>(SaveGame);
615
+ if (!LocalPlayerSave)
616
+ {
617
+ UE_LOG(LogTemp, Error, TEXT("LocalPlayerSave cast failed after load."));
618
+ return;
619
+ }
620
+
621
+ // Apply
622
+ for (auto& [AbilityName, Level] : LocalPlayerSave->AbilityLevels)
623
+ {
624
+ AbilityComponent->SetAbilityLevel(AbilityName, Level);
625
+ }
626
+ }
627
+ ```
628
+
629
+ For the save slot name per player, derive it from the player index or platform user ID so each user has isolated data:
630
+
631
+ ```cpp
632
+ FString GetPlayerSlotName(const APlayerController* PC)
633
+ {
634
+ if (!PC) { return TEXT("PlayerData_0"); }
635
+ if (const ULocalPlayer* LP = PC->GetLocalPlayer())
636
+ {
637
+ return FString::Printf(TEXT("PlayerData_%d"), LP->GetControllerId());
638
+ }
639
+ return TEXT("PlayerData_0");
640
+ }
641
+ ```
642
+
643
+ ---
644
+
645
+ ## 6. Platform Considerations
646
+
647
+ | Platform | Key Difference |
648
+ |---|---|
649
+ | PC / Mac | `UGameplayStatics` writes to `Saved/SaveGames/`. No user index required in most cases. |
650
+ | Console (PS5, Xbox) | User index maps to a controller/account. Always use `GetPlatformUserIndex()` from `ULocalPlayerSaveGame`. |
651
+ | Mobile (iOS, Android) | Save path is app-specific sandbox; `UGameplayStatics` handles it. Watch for size limits. |
652
+ | PIE | Saves go to the project's `Saved/SaveGames/`. Clean between test runs with `DeleteGameInSlot`. |
653
+
654
+ To get the platform user index correctly:
655
+
656
+ ```cpp
657
+ int32 GetUserIndex(const APlayerController* PC)
658
+ {
659
+ if (const ULocalPlayer* LP = PC ? PC->GetLocalPlayer() : nullptr)
660
+ {
661
+ return LP->GetPlatformUserIndex(); // returns int32 directly
662
+ }
663
+ return 0;
664
+ }
665
+ ```
666
+
667
+ ---
668
+
669
+ ## 7. Save File Path Reference (Debug Only)
670
+
671
+ Never rely on these paths in shipping code. They are useful for wiping saves in automation tests or editor utilities:
672
+
673
+ ```cpp
674
+ FString GetSaveGamePath(const FString& SlotName, int32 UserIndex)
675
+ {
676
+ // Only valid on PC / development. Do not use in shipping builds.
677
+ ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem();
678
+ // ISaveGameSystem does not expose paths directly; use FPaths for debug only:
679
+ return FPaths::ProjectSavedDir()
680
+ / TEXT("SaveGames")
681
+ / FString::Printf(TEXT("%s.sav"), *SlotName);
682
+ }
683
+ ```
684
+
685
+ ---
686
+
687
+ ## 8. Save System Registration in GameInstance
688
+
689
+ Register the save manager and trigger initial metadata load in `UGameInstance::Init()`:
690
+
691
+ ```cpp
692
+ // MyGameInstance.cpp
693
+ void UMyGameInstance::Init()
694
+ {
695
+ Super::Init();
696
+ // Subsystem is created automatically; access it via GetSubsystem<>
697
+ // No manual construction needed.
698
+ }
699
+
700
+ // In any game code:
701
+ USaveManager* SM = GetGameInstance()->GetSubsystem<USaveManager>();
702
+ SM->AsyncSaveToSlot(0);
703
+ ```
704
+
705
+ Because `USaveManager` is a `UGameInstanceSubsystem`, UE creates it automatically when the `GameInstance` initializes. `Initialize()` on the subsystem fires before the first level loads, making it the correct place to load the metadata bank.