-
Notifications
You must be signed in to change notification settings - Fork 0
Transaction and Recovery
UEBPCracker wraps all Blueprint mutations in Unreal's transaction system and provides durable recovery for interrupted operations.
Every write operation opens an FScopedTransaction:
FScopedTransaction Transaction(
TEXT("UEBPCracker"), // context (FString, not FName)
LOCTEXT("OpName", "..."), // display text
Blueprint, // primary UObject
/*bShouldActuallyTransact=*/true
);The transaction captures all subsequent Modify() calls on Unreal objects. On success, the transaction is committed to the undo buffer. On failure, rollback is performed.
-
FScopedTransaction::Cancel()does NOT roll back — it only drops the undo record. Actual rollback requires callingGEditor->UndoTransaction()explicitly. - The 2-arg
FScopedTransaction(FText, bool)ctor uses an EMPTY context — use the 4-arg ctor withGEditor->BeginTransaction. -
FTransactionContext::ContextisFString, notFName(don't call.ToString()). -
GEditor->CanTransact()requires!UE::GetIsEditorLoadingPackage() && !IsRoutingPostLoad. - UE 5.8 compile/reinstancing creates a transaction with no recorded changes →
UTransBuffer::End()pops it from the undo buffer. Strict top-context checks will fail after compile; use a bounded undo loop instead.
When a postcondition assertion fails after mutation, UEBPCracker performs a bounded undo loop:
1. Record BeforeFullFingerprint (before mutation started)
2. Mutation + postcondition check
3. [Postcondition fails]
4. Loop: GEditor->UndoTransaction() → up to 16 times
5. After each undo: check if current fingerprint == BeforeFullFingerprint
6. If match: rollback verified ✅
7. If 16 undos exhausted without match: flag bRecoveryRequired=true
Why ≤16 undos? The compile/reinstancing transaction can silently appear between the mutation and our transaction on the undo stack, so simple "pop top" isn't reliable.
Inner services (Variable, Component, Graph) must not open their own FScopedTransaction when called from within a patch/spec transaction.
FUEBPTransactionGuard enforces this:
// Opens a transaction ONLY if no transaction is currently active
if (GEditor && GEditor->Trans && !GEditor->Trans->IsActive()) {
Transaction = MakeUnique<FScopedTransaction>(...);
}This prevents nested transactions from breaking the "patch txn is the top context" invariant.
Before any patch or spec operation that could fail mid-way, UEBPCracker creates a durable backup:
1. DuplicateAsset(Blueprint, BackupRoot, BackupName)
2. Record backup path in durable journal (on disk)
3. Proceed with operation
4. [On success]: mark journal entry "committed"
5. [On failure]: journal entry remains "open" → GetRecoveryStatus shows it
UEditorAssetSubsystem::DuplicateAsset regenerates all GUIDs (node/graph/component/variable) and drops package metadata tags (ownership manifest lost). Consequences:
- Exact full-fingerprint verification is impossible — use content fingerprint (GUID-normalized) instead
-
Node array order is unstable — sort nodes by
(identity_kind, identity_name, x, y)before ordinal assignment -
Ownership manifest is stale after restore — must call
RecommitOwnership(see Patch and Idempotency)
GetRecoveryStatus
→ lists pending recovery entries with operation IDs
RestoreOperationBackup (operation_id, expected_content_fingerprint)
→ loads backup asset from BackupRoot
→ verifies content fingerprint (GUID-normalized comparison)
→ replaces current Blueprint with backup content
→ calls RecommitOwnership to fix stale manifest GUIDs
→ marks journal entry "restored"
→ returns new fingerprint
[After successful restore:]
DiscardOperationBackup (operation_id)
→ deletes backup asset from BackupRoot
→ marks journal entry "discarded"
RestoreOperationBackup on a discarded entry returns:
{ "success": false, "errorCode": "UEBP.BACKUP_INVALID", "message": "Asset not found in the Asset Registry" }This is irreversible — once discarded, there is no further restore option from the plugin's side.
FUEBPFailpoints provides test-only injection points that are production no-ops:
| Failpoint | Fires after... |
|---|---|
AfterVariableMutation |
Variable written to Blueprint |
AfterNodeCreation |
K2 node created in graph |
BeforeCompile |
Before engine compile call |
AfterSave |
After successful save |
AfterBackup |
After DuplicateAsset |
BeforeManifestCommit |
Before writing ownership manifest |
AfterManifestCommit |
After writing ownership manifest |
Usage in test scripts:
# Arm failpoint before apply
call_tool SetFailpoint AfterVariableMutation enabled=true
# Apply spec (will fault mid-way)
call_tool ApplyBlueprintSpec file=...
# Verify recovery state
call_tool GetRecoveryStatus
# → should show pending restore entry
# Restore
call_tool RestoreOperationBackup operation_id=...| Code | Meaning |
|---|---|
UEBP.BACKUP_INVALID |
Backup asset not found (discarded or never created) |
UEBP.RECOVERY_BLOCKS_UPGRADE |
Pending recovery prevents version upgrade |
UEBP.STATE_INTEGRITY_FAILED |
Journal/state file checksum mismatch |
If there is a pending recovery operation (journal entry not closed), FUEBPVersionInfo::CheckUpgradeSafety returns a warning. The upgrade gate (check_completion criterion t07) will not pass until all recovery entries are resolved.
- Patch and Idempotency — when backups are created, RecommitOwnership
- Snapshot and Fingerprint — content fingerprint for restore verification
-
Tool Reference —
GetRecoveryStatus,RestoreOperationBackup,DiscardOperationBackup - UE 5.8 Gotchas — DuplicateAsset GUID regeneration, FScopedTransaction cancel behavior