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.
- package/CHANGELOG.md +37 -0
- package/README.md +104 -55
- package/agents/career/career-strategist.md +88 -0
- package/agents/career/cv-reviewer.md +81 -0
- package/agents/career/hiring-manager.md +68 -0
- package/agents/career/mock-interviewer.md +76 -0
- package/agents/freelance/client-screener.md +84 -0
- package/agents/freelance/discovery-call.md +65 -0
- package/agents/freelance/pipeline-strategist.md +99 -0
- package/agents/freelance/proposal-reviewer.md +82 -0
- package/agents/game/ue-blueprint-specialist.md +125 -0
- package/agents/game/ue-gas-specialist.md +133 -0
- package/agents/game/ue-umg-specialist.md +131 -0
- package/dist/cli.js +57 -13
- package/docs/RELEASE-SETUP.md +1 -1
- package/docs/SKILLS-CATALOG.md +67 -4
- package/docs/USER-GUIDE.md +77 -72
- package/docs/agent-hierarchy.md +21 -1
- package/docs/presentations/build_arcane_deck.py +2 -2
- package/docs/presentations/build_arcane_deck_12.py +2 -2
- package/hooks/detect-unreal.sh +90 -0
- package/package.json +2 -2
- package/profiles/freelance.yaml +43 -0
- package/profiles/job-hunt.yaml +5 -2
- package/profiles/unreal-dev.yaml +97 -0
- package/rules/drafter-reviewer.md +16 -3
- package/rules/freelance-guardrails.md +53 -0
- package/rules/gamedev/unreal-code.md +125 -0
- package/skills/career-registry/SKILL.md +95 -0
- package/skills/career-registry/scripts/career_registry.py +488 -0
- package/skills/client-screen/SKILL.md +83 -0
- package/skills/client-screen/references/red-flags.md +95 -0
- package/skills/cover-letter/SKILL.md +4 -3
- package/skills/cv-tailor/SKILL.md +11 -2
- package/skills/freelance-deliver/SKILL.md +90 -0
- package/skills/freelance-hunt/SKILL.md +126 -0
- package/skills/freelance-hunt/references/templates/Connects.md +39 -0
- package/skills/freelance-hunt/references/templates/Contrato.md +60 -0
- package/skills/freelance-hunt/references/templates/Propuesta.md +66 -0
- package/skills/freelance-kickoff/SKILL.md +94 -0
- package/skills/freelance-pipeline/SKILL.md +109 -0
- package/skills/freelance-pipeline/scripts/import_history.py +425 -0
- package/skills/freelance-profile/SKILL.md +93 -0
- package/skills/freelance-proposal/SKILL.md +112 -0
- package/skills/freelance-scan/SKILL.md +196 -0
- package/skills/freelance-scan/references/dedup.md +35 -0
- package/skills/freelance-scan/references/platforms.md +167 -0
- package/skills/freelance-scan/scripts/freelance_search.py +1077 -0
- package/skills/install-mcp/SKILL.md +135 -46
- package/skills/install-mcp/references/unreal-operations.md +55 -0
- package/skills/interview-prep/SKILL.md +11 -3
- package/skills/job-aplicar/SKILL.md +5 -1
- package/skills/job-hunt/SKILL.md +17 -0
- package/skills/job-outcome/SKILL.md +15 -1
- package/skills/job-upskill/SKILL.md +10 -2
- package/skills/ue-actor-component-architecture/SKILL.md +509 -0
- package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
- package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
- package/skills/ue-ai-navigation/SKILL.md +526 -0
- package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
- package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
- package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
- package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
- package/skills/ue-animation-system/SKILL.md +521 -0
- package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
- package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
- package/skills/ue-async-threading/SKILL.md +433 -0
- package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
- package/skills/ue-async-threading/references/threading-patterns.md +345 -0
- package/skills/ue-audio-system/SKILL.md +527 -0
- package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
- package/skills/ue-blueprints/SKILL.md +134 -0
- package/skills/ue-blueprints/references/communication.md +82 -0
- package/skills/ue-character-movement/SKILL.md +475 -0
- package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
- package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
- package/skills/ue-cpp-foundations/SKILL.md +533 -0
- package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
- package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
- package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
- package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
- package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
- package/skills/ue-data-assets-tables/SKILL.md +527 -0
- package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
- package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
- package/skills/ue-editor-tools/SKILL.md +523 -0
- package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
- package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
- package/skills/ue-game-features/SKILL.md +477 -0
- package/skills/ue-game-features/references/experience-system.md +222 -0
- package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
- package/skills/ue-gameplay-abilities/SKILL.md +518 -0
- package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
- package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
- package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
- package/skills/ue-gameplay-framework/SKILL.md +466 -0
- package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
- package/skills/ue-input-system/SKILL.md +394 -0
- package/skills/ue-input-system/references/cpp-setup.md +131 -0
- package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
- package/skills/ue-input-system/references/input-action-reference.md +731 -0
- package/skills/ue-mass-entity/SKILL.md +476 -0
- package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
- package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
- package/skills/ue-materials-rendering/SKILL.md +526 -0
- package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
- package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
- package/skills/ue-module-build-system/SKILL.md +487 -0
- package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
- package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
- package/skills/ue-naming-conventions/SKILL.md +142 -0
- package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
- package/skills/ue-naming-conventions/references/content-structure.md +83 -0
- package/skills/ue-networking-replication/SKILL.md +452 -0
- package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
- package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
- package/skills/ue-niagara-effects/SKILL.md +531 -0
- package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
- package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
- package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
- package/skills/ue-packaging/SKILL.md +116 -0
- package/skills/ue-physics-collision/SKILL.md +423 -0
- package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
- package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
- package/skills/ue-procedural-generation/SKILL.md +505 -0
- package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
- package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
- package/skills/ue-project-context/SKILL.md +305 -0
- package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
- package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
- package/skills/ue-serialization-savegames/SKILL.md +524 -0
- package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
- package/skills/ue-state-trees/SKILL.md +489 -0
- package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
- package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
- package/skills/ue-testing-debugging/SKILL.md +520 -0
- package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
- package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
- package/skills/ue-ui-umg-slate/SKILL.md +431 -0
- package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
- package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
- package/skills/ue-world-level-streaming/SKILL.md +518 -0
- package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
- 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
|