A .NET CLI for extracting and replacing Xbox PC saves, based on XGP-save-extractor. It supports legacy WGS containers, file-oriented PGS saves, and game-specific conversion logic.
- Generic WGS extraction and direct-entry replacement
- Consistent PGS snapshot backup and file extraction
- Game-specific export/import operations
- Automatic backups and rollback for multi-file replacements
- .NET 6 runtime (recent releases are self-contained)
- Windows 10/11
- Select a game or enter a custom
wgsdirectory. - Select the user container.
- Choose Extract Files.
- Confirm the plan to create a ZIP archive.
- Choose Replace Entry.
- Select the WGS entry and replacement file.
- Review and confirm the plan.
- The tool creates a complete backup before writing.
Caution: Not every entry is a save slot. Keep the generated backup until the game has loaded successfully.
Newer games can store their saves under %SystemDrive%\XboxGames\GameSave\pgs instead of a package's SystemAppData\wgs directory. The tool discovers these saves by PGS game ID and resolves the active current snapshot.
PGS games provide two operations:
- Export Complete PGS Backup includes the active snapshot, PGS metadata, and a SHA-256 integrity manifest.
- Extract Game Files exports only the untouched
ContainersRootfile tree.
Download the latest release from GitHub Releases, or publish it locally:
dotnet publish Xgpst-ConsoleApp/Xgpst-ConsoleApp.csproj \
-c Release \
-r win-x64 \
--self-contained false \
/p:PublishSingleFile=true \
--output bin/Release/net6.0/publish/win-x64Most WGS games need no custom code. Register the package in games.json and use a built-in mapping:
| Handler | Behavior |
|---|---|
generic |
Export every WGS entry using its stored filename |
1c1f |
Export the first file from each container |
1cnf |
Export every file from the first container |
1cnf-folder |
Export containers as folders |
{
"name": "Example Game",
"package": "Publisher.ExampleGame_abc123",
"handler": "1c1f",
"handler_args": { "suffix": ".sav" }
}source defaults to wgs. For a file-oriented PGS game, register its PGS game ID separately from handler arguments:
{
"name": "Example PGS Game",
"package": "Publisher.ExampleGame_abc123",
"source": "pgs",
"source_args": { "game_id": "ABC123" },
"handler": "pgs-files"
}pgs-files provides complete backup and raw ContainersRoot extraction without custom code. Add a game-specific handler only when those files need filtering or transformation. Storage discovery belongs in a save source; game-format logic belongs in a handler.
Custom handlers follow one simple flow:
flowchart LR
A["games.json"] --> B{"Save source"}
B -->|WGS| C["WGS containers"]
B -->|PGS| D["PGS snapshot"]
C --> E["IGameSaveHandler"]
D --> E
E --> F["Operation"]
F --> G{"OperationPlan"}
G -->|ExportPlan| H["ZIP archive"]
G -->|ImportPlan| I["Backup and atomic WGS update"]
Implement IGameSaveHandler, expose the operations your game supports, and prepare a plan:
public sealed class ExampleHandler : IGameSaveHandler
{
public const string HandlerId = "example";
public string Id => HandlerId;
public IReadOnlyList<IGameSaveOperation> GetOperations(GameSaveContext context) =>
new IGameSaveOperation[] { new ExportOperation() };
private sealed class ExportOperation : IGameSaveOperation
{
public OperationDefinition Definition { get; } = new(
"export", "Export", "Convert this save.", OperationKind.Export);
public IReadOnlyList<OperationParameter> GetParameters(GameSaveContext context) =>
new OperationParameter[]
{
new TextParameter("account-id", "Target account ID")
};
public Task<OperationPlan> PrepareAsync(
GameSaveContext context,
OperationArguments arguments,
ITempWorkspace workspace,
CancellationToken cancellationToken)
{
arguments.Validate(GetParameters(context));
var accountId = arguments.GetRequiredString("account-id");
var source = context.Containers.First().Files.First();
var output = workspace.GetPath("save.dat");
File.Copy(source.Path, output); // Apply the game-specific conversion here.
return Task.FromResult<OperationPlan>(new ExportPlan(
new[] { new ExportArtifact("save.dat", output) },
"example-save.zip"));
}
}
}Prompts are declared with TextParameter, FileParameter, DirectoryParameter, ChoiceParameter, or BooleanParameter. The console collects them automatically—handlers should not call Console.
For imports, prepare every replacement in ITempWorkspace and return a single atomic ImportPlan:
return new ImportPlan(
new PlannedWgsMutation[]
{
new PlannedReplacement(
new WgsEntryKey("TARGET-SLOT", "save.dat"),
preparedFile)
},
new[] { "Close the game before continuing." });Finally:
- Add
"handler": "example"to the game ingames.json. - Add
[ExampleHandler.HandlerId] = new ExampleHandler()toGameSaveHandlerRegistry.cs. - Add plan tests for the output files and WGS targets.
For export-only transformations without prompts, inherit ExportOnlyGameSaveHandler. For filename changes without transformations, add a MappedSaveEntry function to StandardGameMappings.cs.
Keep these rules in mind:
- Never modify WGS in
PrepareAsync; only return a plan. - Write transformed files inside
ITempWorkspace. - Put related replacements in one
ImportPlanso backup and rollback cover everything. - For PGS handlers, read
context.PgsSnapshot.Files; do not parse or rewrite Gaming Services metadata in the handler. - PGS imports are intentionally unavailable until the source can perform a safe cloud-aware transaction.
See DoomDarkAgesHandler.cs for a complete bidirectional example.
- Port inspired by Z1ni’s XGP-save-extractor.
- @snoozbuster for reverse engineering the container format.
- @mi5hmash for documenting the idTech Steam save encryption scheme.
- id Software's DOOM 3 BFG source for the
MD5_BlockChecksumreference. - Contributions are welcome. Include the game package name and representative save samples with new handler requests.

