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,433 @@
1
+ ---
2
+ name: ue-async-threading
3
+ description: "Async operations, threading, parallel execution, tasks, FRunnable, AsyncTask"
4
+ category: "gamedev"
5
+ argument-hint: "[operation o task]"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Bash, Write, Edit
8
+ ---
9
+
10
+ # UE Async and Threading
11
+
12
+ You are an expert in Unreal Engine's threading model, async task systems, and concurrent programming patterns.
13
+
14
+ Ask before writing: this skill edits async and threaded code paths. Propose the change and get approval before using Write or Edit.
15
+
16
+ ## Context Check
17
+
18
+ Read `docs/unreal/project-context.md` before proceeding. Engine version matters: `UE::Tasks::Launch` is the modern preferred API (UE 5.0+), while `FAsyncTask` and TaskGraph remain fully supported. Determine: What work needs to be offloaded? Is UObject access required? What latency/throughput tradeoff is acceptable?
19
+
20
+ ## Information Gathering
21
+
22
+ Ask the user if unclear:
23
+ - **Offload type** — CPU-bound computation, I/O wait, or periodic background work?
24
+ - **UObject interaction** — Does the background work need to read/write UObject state?
25
+ - **Lifetime** — One-shot task, recurring work, or long-lived thread?
26
+ - **Result delivery** — Fire-and-forget, or does the game thread need results back?
27
+
28
+ ---
29
+
30
+ ## UE Threading Model
31
+
32
+ UE runs several named threads plus a scalable worker pool. Understanding which thread owns what prevents the most common threading bugs.
33
+
34
+ **Named threads:**
35
+ - **Game Thread** — All UObject access, Blueprint execution, gameplay logic. Check with `IsInGameThread()`.
36
+ - **Render Thread** — Render commands, scene proxy updates. `IsInRenderingThread()`.
37
+ - **RHI Thread** — GPU command submission (platform-dependent).
38
+ - **Worker Threads** — Unnamed pool threads for task dispatch. Count scales with CPU cores.
39
+
40
+ **The golden rule:** UObjects are game-thread-only. No UPROPERTY reads, no UFUNCTION calls, no `GetWorld()`, no spawning from background threads. Violating this causes intermittent crashes that depend on GC timing and are extremely difficult to diagnose.
41
+
42
+ ---
43
+
44
+ ## Pattern Selection Guide
45
+
46
+ Choose the simplest API that fits your needs.
47
+
48
+ | Pattern | Best For | Lifetime | Result? |
49
+ |---------|----------|----------|---------|
50
+ | `AsyncTask(GameThread, Lambda)` | Dispatch to game thread from background | One-shot | No |
51
+ | `UE::Tasks::Launch` | General async work (preferred, UE5+) | One-shot | `TTask<T>` |
52
+ | `Async(EAsyncExecution, Lambda)` | Flexible dispatch with `TFuture` | One-shot | `TFuture<T>` |
53
+ | `FAsyncTask<T>` | Reusable pooled work units | Reusable | Via `GetTask()` |
54
+ | `FAutoDeleteAsyncTask<T>` | Fire-and-forget pooled work | One-shot | No |
55
+ | `TGraphTask<T>` | Complex dependency graphs | One-shot | `FGraphEvent` |
56
+ | `ParallelFor` | Data-parallel loops | Blocking | No |
57
+ | `FRunnable` + `FRunnableThread` | Long-lived dedicated threads | Persistent | Manual |
58
+
59
+ ---
60
+
61
+ ## FRunnable and FRunnableThread
62
+
63
+ Use `FRunnable` only when you need a **dedicated, long-lived thread** -- a socket listener, a file watcher, or a continuous processing loop. For one-shot work, prefer `UE::Tasks::Launch` or `FAsyncTask`.
64
+
65
+ **Lifecycle:** `Init()` (new thread) -> `Run()` (new thread) -> `Exit()` (new thread, after Run returns). `Stop()` is called externally to request shutdown.
66
+
67
+ **FRunnableThread::Create** signature: `static FRunnableThread* Create(FRunnable*, const TCHAR* ThreadName, uint32 StackSize = 0, EThreadPriority = TPri_Normal, uint64 AffinityMask, EThreadCreateFlags)`.
68
+
69
+ **Key points:** `Stop()` signals the thread -- it does not block. `Kill(true)` calls `Stop()` then waits for completion. Always `delete` the `FRunnableThread*` after `Kill`. Use `std::atomic<bool> bShouldStop` in `Run()` loop, set it in `Stop()`.
70
+
71
+ See `references/threading-patterns.md` for a complete `FRunnable` subclass template with proper shutdown.
72
+
73
+ ---
74
+
75
+ ## FAsyncTask and FAutoDeleteAsyncTask
76
+
77
+ For **reusable work units** on the engine thread pool (`GThreadPool`). Subclass `FNonAbandonableTask` and implement `DoWork()` + `GetStatId()`.
78
+
79
+ ```cpp
80
+ class FMyComputeTask : public FNonAbandonableTask
81
+ {
82
+ friend class FAsyncTask<FMyComputeTask>;
83
+ int32 Result = 0;
84
+ TArray<int32> InputData;
85
+
86
+ FMyComputeTask(TArray<int32> InData) : InputData(MoveTemp(InData)) {}
87
+
88
+ void DoWork()
89
+ {
90
+ for (int32 Val : InputData) { Result += Val; }
91
+ }
92
+
93
+ FORCEINLINE TStatId GetStatId() const
94
+ {
95
+ RETURN_QUICK_DECLARE_CYCLE_STAT(FMyComputeTask, STATGROUP_ThreadPoolAsyncTasks);
96
+ }
97
+ };
98
+ ```
99
+
100
+ **Usage:**
101
+
102
+ ```cpp
103
+ // Reusable — you manage lifetime
104
+ auto* Task = new FAsyncTask<FMyComputeTask>(MoveTemp(Data));
105
+ Task->StartBackgroundTask(); // dispatches to GThreadPool
106
+ Task->EnsureCompletion(); // blocks or runs inline if not started
107
+ int32 R = Task->GetTask().Result;
108
+ delete Task;
109
+
110
+ // Fire-and-forget — auto-deletes on completion
111
+ (new FAutoDeleteAsyncTask<FMyComputeTask>(MoveTemp(Data)))->StartBackgroundTask();
112
+ ```
113
+
114
+ `IsWorkDone()` is the non-blocking completion check. `Cancel()` prevents execution if not yet started. `StartSynchronousTask()` runs inline on the calling thread.
115
+
116
+ ---
117
+
118
+ ## TaskGraph
119
+
120
+ For work with **complex dependency chains**. Each task declares prerequisites; the scheduler handles ordering.
121
+
122
+ ```cpp
123
+ class FMyGraphTask
124
+ {
125
+ public:
126
+ FMyGraphTask(int32 InValue) : Value(InValue) {}
127
+
128
+ static ESubsequentsMode::Type GetSubsequentsMode()
129
+ { return ESubsequentsMode::TrackSubsequents; }
130
+
131
+ ENamedThreads::Type GetDesiredThread()
132
+ { return ENamedThreads::AnyThread; }
133
+
134
+ TStatId GetStatId() const
135
+ { RETURN_QUICK_DECLARE_CYCLE_STAT(FMyGraphTask, STATGROUP_TaskGraphTasks); }
136
+
137
+ void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
138
+ { /* work here */ }
139
+
140
+ private:
141
+ int32 Value;
142
+ };
143
+ ```
144
+
145
+ **Dispatching with prerequisites:**
146
+
147
+ ```cpp
148
+ FGraphEventArray Prerequisites; // TArray<FGraphEventRef, TInlineAllocator<4>>
149
+ Prerequisites.Add(SomePriorEvent);
150
+
151
+ FGraphEventRef TaskEvent = TGraphTask<FMyGraphTask>::CreateTask(&Prerequisites)
152
+ .ConstructAndDispatchWhenReady(42); // args forwarded to constructor
153
+
154
+ FTaskGraphInterface::Get().WaitUntilTaskCompletes(TaskEvent, ENamedThreads::GameThread);
155
+ ```
156
+
157
+ **Quick dispatch** (no custom class needed):
158
+
159
+ ```cpp
160
+ AsyncTask(ENamedThreads::GameThread, [this]()
161
+ {
162
+ MyActor->UpdateHealth(NewValue); // safe — runs on game thread
163
+ });
164
+ ```
165
+
166
+ ---
167
+
168
+ ## UE::Tasks::Launch (Modern Preferred API)
169
+
170
+ Recommended for new code (UE 5.0+). Simpler syntax than TaskGraph, automatic thread pool dispatch, built-in chaining.
171
+
172
+ ```cpp
173
+ #include "Tasks/Task.h"
174
+
175
+ UE::Tasks::TTask<int32> Task = UE::Tasks::Launch(
176
+ UE_SOURCE_LOCATION,
177
+ []() { return ExpensiveComputation(); }
178
+ );
179
+ int32 Result = Task.GetResult(); // blocks until complete
180
+
181
+ // With prerequisites
182
+ UE::Tasks::TTask<FVector> TaskA = UE::Tasks::Launch(UE_SOURCE_LOCATION,
183
+ []() { return ComputePosition(); });
184
+
185
+ UE::Tasks::TTask<void> TaskB = UE::Tasks::Launch(UE_SOURCE_LOCATION,
186
+ [&TaskA]() { ProcessPosition(TaskA.GetResult()); },
187
+ UE::Tasks::Prerequisites(TaskA)
188
+ );
189
+ ```
190
+
191
+ **TTask<T> API:** `GetResult()` blocks and returns result. `IsCompleted()` non-blocking. `Wait()` / `Wait(FTimespan)` for timed blocking. `TryRetractAndExecute()` runs inline if not yet started (work stealing).
192
+
193
+ **FTaskEvent** for manual synchronization -- call `Trigger()` to unblock dependent tasks.
194
+
195
+ ---
196
+
197
+ ## Async, TFuture, and TPromise
198
+
199
+ `Async()` is the most flexible one-shot dispatch. Returns `TFuture<T>` with execution context control.
200
+
201
+ ```cpp
202
+ TFuture<FMyResult> Future = Async(EAsyncExecution::ThreadPool,
203
+ []() -> FMyResult { return ComputeResult(); },
204
+ []() { /* completion callback — runs on unspecified thread */ }
205
+ );
206
+ FMyResult R = Future.Get(); // blocks, does NOT invalidate (unlike std::future)
207
+ ```
208
+
209
+ **EAsyncExecution modes:**
210
+
211
+ | Mode | Thread |
212
+ |------|--------|
213
+ | `TaskGraph` | Worker via TaskGraph |
214
+ | `TaskGraphMainThread` | Game thread via TaskGraph |
215
+ | `Thread` | New dedicated thread |
216
+ | `ThreadPool` | `GThreadPool` worker |
217
+ | `LargeThreadPool` | `GLargeThreadPool` (WITH_EDITOR only) |
218
+
219
+ **Convenience:** `AsyncPool(GThreadPool, Lambda)`, `AsyncThread(Lambda, StackSize, Priority)`.
220
+
221
+ ### TFuture<T> API
222
+
223
+ Key difference from `std::future`: `Get()` does **not** invalidate. Call it multiple times safely. `Consume()` invalidates like `std::future::get()`.
224
+
225
+ - `IsReady()` -- non-blocking check
226
+ - `Wait()` / `WaitFor(FTimespan)` -- block without consuming
227
+ - `Then(Continuation)` / `Next(Continuation)` -- chaining, continuation runs on any thread
228
+ - `Share()` -- convert to shared future
229
+
230
+ ### TPromise<T>
231
+
232
+ For producer-consumer patterns where producing and consuming sides are decoupled.
233
+
234
+ ```cpp
235
+ TPromise<FMyData> Promise;
236
+ TFuture<FMyData> Future = Promise.GetFuture(); // call once
237
+
238
+ Async(EAsyncExecution::ThreadPool, [P = MoveTemp(Promise)]() mutable
239
+ {
240
+ P.SetValue(GenerateData()); // or EmplaceValue()
241
+ });
242
+
243
+ FMyData Result = Future.Get(); // blocks on game thread
244
+ ```
245
+
246
+ ---
247
+
248
+ ## ParallelFor
249
+
250
+ For **data-parallel loops** where each iteration is independent. The calling thread participates -- `ParallelFor` blocks until all iterations complete.
251
+
252
+ ```cpp
253
+ ParallelFor(Meshes.Num(), [&Meshes](int32 Index)
254
+ {
255
+ ProcessMesh(Meshes[Index]);
256
+ });
257
+
258
+ // With MinBatchSize — prevents overhead for small workloads
259
+ ParallelFor(TEXT("ProcessMeshes"), Meshes.Num(), 64,
260
+ [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); }
261
+ );
262
+ ```
263
+
264
+ **EParallelForFlags:**
265
+
266
+ | Flag | Value | Effect |
267
+ |------|-------|--------|
268
+ | `None` | 0 | Default behavior |
269
+ | `ForceSingleThread` | 1 | Debug: run sequentially |
270
+ | `Unbalanced` | 2 | Iterations have variable cost |
271
+ | `PumpRenderingThread` | 4 | Pump render commands while waiting |
272
+ | `BackgroundPriority` | 8 | Lower priority for workers |
273
+
274
+ `ParallelForWithTaskContext` provides a per-worker context object -- use when workers need scratch memory to avoid per-iteration allocation.
275
+
276
+ ---
277
+
278
+ ## Game Thread Safety
279
+
280
+ Threading bugs in UE are **silent** -- they corrupt state, cause GC races, and produce bugs that only reproduce under load. See `references/thread-safety-guide.md` for complete patterns.
281
+
282
+ ### UObject Access Rules
283
+
284
+ 1. **All UObject access must happen on the game thread.** No reads, writes, or function calls from background threads.
285
+ 2. **GC can destroy UObjects between ticks.** A raw `UObject*` captured in a lambda may be dangling by execution time.
286
+ 3. **Spawning, destroying, modifying components** -- game thread only.
287
+
288
+ ### Safe Dispatch Pattern
289
+
290
+ ```cpp
291
+ AsyncTask(ENamedThreads::GameThread, [WeakActor = TWeakObjectPtr<AActor>(MyActor)]()
292
+ {
293
+ if (AActor* Actor = WeakActor.Get()) // nullptr if GC'd
294
+ {
295
+ Actor->UpdateFromBackgroundWork(NewData);
296
+ }
297
+ });
298
+ ```
299
+
300
+ **Always capture `TWeakObjectPtr`**, never raw `UObject*`. For non-UObject shared data, use `TSharedPtr<T, ESPMode::ThreadSafe>` -- the default `ESPMode::NotThreadSafe` has non-atomic refcounting.
301
+
302
+ ---
303
+
304
+ ## Synchronization Primitives
305
+
306
+ ### FCriticalSection (Recursive Mutex)
307
+
308
+ `FCriticalSection` is `UE::FPlatformRecursiveMutex`. Same thread can lock multiple times without deadlocking.
309
+
310
+ ```cpp
311
+ FCriticalSection DataLock;
312
+ void AddPosition(const FVector& Pos)
313
+ {
314
+ FScopeLock Lock(&DataLock); // RAII — unlocks on scope exit
315
+ SharedPositions.Add(Pos);
316
+ }
317
+ ```
318
+
319
+ ### FRWLock (Read-Write Lock)
320
+
321
+ Multiple readers OR one exclusive writer. `FRWLock` is **not** recursive -- do not nest.
322
+
323
+ ```cpp
324
+ FRWLock CacheLock;
325
+ FVector Read(FName Key) { FReadScopeLock RL(CacheLock); return Cache.FindRef(Key); }
326
+ void Write(FName K, FVector V) { FWriteScopeLock WL(CacheLock); Cache.Add(K, V); }
327
+ ```
328
+
329
+ ### FEventRef and Atomics
330
+
331
+ `FEventRef` is the RAII wrapper for thread signaling. Prefer over raw `FEvent*`.
332
+
333
+ ```cpp
334
+ FEventRef WorkReady(EEventMode::AutoReset);
335
+ WorkReady->Trigger(); // producer signals
336
+ WorkReady->Wait(); // consumer blocks
337
+ ```
338
+
339
+ `FThreadSafeCounter` and `FThreadSafeBool` are deprecated -- use `std::atomic<int32>` and `std::atomic<bool>` directly.
340
+
341
+ ---
342
+
343
+ ## TQueue (Lock-Free Queue)
344
+
345
+ Thread-safe queue for producer-consumer without locks.
346
+
347
+ ```cpp
348
+ TQueue<FMyMessage, EQueueMode::Mpsc> MessageQueue;
349
+
350
+ // Producer (any thread)
351
+ MessageQueue.Enqueue(FMyMessage{...});
352
+
353
+ // Consumer (game thread tick)
354
+ FMyMessage Msg;
355
+ while (MessageQueue.Dequeue(Msg)) { ProcessMessage(Msg); }
356
+ ```
357
+
358
+ `Spsc` -- single-producer, single-consumer (slightly faster). `Mpsc` -- multiple-producer, single-consumer (most common). `Peek()` reads without dequeuing.
359
+
360
+ ---
361
+
362
+ ## Common Mistakes
363
+
364
+ **Accessing UObject from background thread -- dispatch results back:**
365
+ ```cpp
366
+ // WRONG — UObject access off game thread
367
+ Async(EAsyncExecution::ThreadPool, [this]()
368
+ { MyActor->Health = ComputeNewHealth(); });
369
+
370
+ // RIGHT — compute off-thread, apply on game thread via weak pointer
371
+ Async(EAsyncExecution::ThreadPool, [WeakActor = TWeakObjectPtr<AActor>(MyActor)]()
372
+ {
373
+ float NewHealth = ComputeNewHealth();
374
+ AsyncTask(ENamedThreads::GameThread, [WeakActor, NewHealth]()
375
+ { if (AActor* A = WeakActor.Get()) { A->SetHealth(NewHealth); } });
376
+ });
377
+ ```
378
+
379
+ **TSharedPtr with default ESPMode across threads:**
380
+ ```cpp
381
+ // WRONG — non-atomic refcount
382
+ auto Data = MakeShared<FMyData>();
383
+ // RIGHT
384
+ auto Data = MakeShared<FMyData, ESPMode::ThreadSafe>();
385
+ ```
386
+
387
+ **FRWLock nested acquisition -- deadlock:**
388
+ ```cpp
389
+ // WRONG — FRWLock is NOT recursive
390
+ FReadScopeLock Outer(Lock);
391
+ FReadScopeLock Inner(Lock); // DEADLOCK on some platforms
392
+ // RIGHT — acquire once, do all reads, release
393
+ ```
394
+
395
+ **ParallelFor with shared mutable state:**
396
+ ```cpp
397
+ // WRONG — concurrent writes
398
+ int32 Total = 0;
399
+ ParallelFor(Data.Num(), [&](int32 i) { Total += Data[i]; });
400
+ // RIGHT — atomic accumulation
401
+ std::atomic<int32> Total{0};
402
+ ParallelFor(Data.Num(), [&](int32 i)
403
+ { Total.fetch_add(Data[i], std::memory_order_relaxed); });
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Related Skills
409
+
410
+ - `/ue-cpp-foundations` -- TSharedPtr, TWeakObjectPtr, GC lifetime, smart pointer rules
411
+ - `/ue-gameplay-framework` -- game thread tick flow, actor lifecycle ordering
412
+ - `/ue-networking-replication` -- RPC dispatch threads, replication callbacks
413
+ - `/ue-data-assets-tables` -- FStreamableManager async loading, soft references
414
+
415
+ ---
416
+
417
+ ## Checklist
418
+
419
+ Validate before considering the work done:
420
+
421
+ - [ ] UObject access happens on the game thread only
422
+ - [ ] Every async completion path handles the owner being destroyed mid-flight
423
+ - [ ] Shared state is protected, or ownership is transferred instead of shared
424
+ - [ ] Conventions in `docs/unreal/project-context.md` were read and followed
425
+ - [ ] The Common Mistakes section of this skill was reviewed against the change
426
+
427
+ **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.
428
+
429
+ ## Next Steps
430
+
431
+ - `/perf-profile` — confirm the work actually moved off the critical path
432
+ - `/code-review` — architectural and quality review of the change
433
+ - `/commit` — conventional commit once the change is verified
@@ -0,0 +1,213 @@
1
+ # Thread Safety Guide
2
+
3
+ Patterns and rules for safe cross-thread data access in Unreal Engine.
4
+
5
+ ---
6
+
7
+ ## UObject Access Rules
8
+
9
+ UObjects are managed by the garbage collector, which runs on the game thread. Any access from another thread risks reading a dangling pointer or corrupting GC bookkeeping.
10
+
11
+ **What counts as "access":** Reading any UPROPERTY, calling any UFUNCTION, calling `GetWorld()`, `GetOwner()`, `GetComponentByClass()`, iterating component arrays, spawning or destroying actors, modifying transform -- all of these are game-thread-only operations.
12
+
13
+ **Why it seems to work sometimes:** GC only runs at specific points in the frame. A background thread that finishes quickly may never collide with GC. But under load, longer GC pauses, or different hardware timing, the race condition triggers -- producing crashes that are nearly impossible to reproduce in development.
14
+
15
+ ---
16
+
17
+ ## TWeakObjectPtr for Cross-Thread UObject References
18
+
19
+ Never capture raw `UObject*` in lambdas dispatched to other threads or deferred to future game-thread ticks. The object may be garbage collected before execution.
20
+
21
+ ```cpp
22
+ // Capture weak pointer BEFORE dispatching
23
+ TWeakObjectPtr<AMyActor> WeakActor(MyActor);
24
+
25
+ UE::Tasks::Launch(UE_SOURCE_LOCATION, [WeakActor]()
26
+ {
27
+ float Result = ExpensiveCompute();
28
+
29
+ // Dispatch result back to game thread with safety check
30
+ AsyncTask(ENamedThreads::GameThread, [WeakActor, Result]()
31
+ {
32
+ if (AMyActor* Actor = WeakActor.Get())
33
+ {
34
+ Actor->ApplyComputedResult(Result);
35
+ }
36
+ // If Get() returns nullptr, actor was GC'd — silently skip
37
+ });
38
+ });
39
+ ```
40
+
41
+ **Rule:** Create the `TWeakObjectPtr` on the game thread, then copy it into lambdas. `TWeakObjectPtr::Get()` is safe to call from the game thread to check validity.
42
+
43
+ ---
44
+
45
+ ## TSharedPtr Thread Safety Modes
46
+
47
+ `TSharedPtr<T>` defaults to `ESPMode::NotThreadSafe` -- the reference count uses non-atomic operations. Sharing across threads causes data races on the refcount itself.
48
+
49
+ ```cpp
50
+ // WRONG — default ESPMode is not thread-safe
51
+ TSharedPtr<FComputeBuffer> Buffer = MakeShared<FComputeBuffer>();
52
+
53
+ // RIGHT — atomic refcount for cross-thread sharing
54
+ TSharedPtr<FComputeBuffer, ESPMode::ThreadSafe> Buffer =
55
+ MakeShared<FComputeBuffer, ESPMode::ThreadSafe>();
56
+
57
+ // The data INSIDE the buffer still needs its own synchronization
58
+ // ESPMode::ThreadSafe only protects the pointer/refcount operations
59
+ ```
60
+
61
+ **Key distinction:** `ESPMode::ThreadSafe` makes the *pointer operations* (copy, destroy, reset) thread-safe. It does NOT protect the pointed-to data. You still need locks or atomics for the actual data if mutated concurrently.
62
+
63
+ ---
64
+
65
+ ## FCriticalSection + FScopeLock Pattern
66
+
67
+ Use `FCriticalSection` (recursive mutex) when multiple threads read and write shared non-UObject data.
68
+
69
+ ```cpp
70
+ class FThreadSafeAccumulator
71
+ {
72
+ public:
73
+ void AddSample(float Value)
74
+ {
75
+ FScopeLock Lock(&CriticalSection); // acquires on construction
76
+ Samples.Add(Value);
77
+ Total += Value;
78
+ } // releases when Lock goes out of scope
79
+
80
+ float GetAverage() const
81
+ {
82
+ FScopeLock Lock(&CriticalSection);
83
+ return Samples.Num() > 0 ? Total / Samples.Num() : 0.0f;
84
+ }
85
+
86
+ private:
87
+ mutable FCriticalSection CriticalSection;
88
+ TArray<float> Samples;
89
+ float Total = 0.0f;
90
+ };
91
+ ```
92
+
93
+ **Always use `FScopeLock`** -- never call `Lock()` / `Unlock()` manually. Manual lock/unlock risks missing the unlock path on early returns or exceptions.
94
+
95
+ ---
96
+
97
+ ## FRWLock for Read-Heavy Scenarios
98
+
99
+ When reads vastly outnumber writes, `FRWLock` allows concurrent readers while writers get exclusive access. Significant performance win over `FCriticalSection` in read-heavy workloads.
100
+
101
+ ```cpp
102
+ class FThreadSafeCache
103
+ {
104
+ public:
105
+ FVector LookupPosition(FName Key) const
106
+ {
107
+ FReadScopeLock ReadLock(RWLock); // multiple readers allowed
108
+ const FVector* Found = PositionCache.Find(Key);
109
+ return Found ? *Found : FVector::ZeroVector;
110
+ }
111
+
112
+ void UpdatePosition(FName Key, FVector NewPos)
113
+ {
114
+ FWriteScopeLock WriteLock(RWLock); // exclusive access
115
+ PositionCache.Add(Key, NewPos);
116
+ }
117
+
118
+ private:
119
+ mutable FRWLock RWLock;
120
+ TMap<FName, FVector> PositionCache;
121
+ };
122
+ ```
123
+
124
+ **Critical:** `FRWLock` is NOT recursive. Do not acquire a read lock while holding a read lock, and do not acquire a write lock while holding a read lock. Both cause deadlocks on some platforms.
125
+
126
+ ---
127
+
128
+ ## std::atomic Patterns
129
+
130
+ `FThreadSafeCounter` and `FThreadSafeBool` are deprecated in engine source comments. Use `std::atomic` directly.
131
+
132
+ ```cpp
133
+ class FBackgroundProcessor
134
+ {
135
+ public:
136
+ void RequestStop() { bShouldStop.store(true, std::memory_order_relaxed); }
137
+ bool IsStopped() const { return bShouldStop.load(std::memory_order_relaxed); }
138
+
139
+ int32 GetProcessedCount() const { return ProcessedCount.load(std::memory_order_relaxed); }
140
+ void IncrementProcessed() { ProcessedCount.fetch_add(1, std::memory_order_relaxed); }
141
+
142
+ private:
143
+ std::atomic<bool> bShouldStop{false};
144
+ std::atomic<int32> ProcessedCount{0};
145
+ };
146
+ ```
147
+
148
+ **Memory ordering:** `std::memory_order_relaxed` is sufficient for simple flags and counters where you do not need happens-before guarantees with other data. Use `std::memory_order_acquire` / `std::memory_order_release` when the atomic guards visibility of other non-atomic writes.
149
+
150
+ ---
151
+
152
+ ## Double-Buffering Pattern
153
+
154
+ For transferring bulk data from a background thread to the game thread without locks during the hot path. The background thread writes to one buffer while the game thread reads from the other.
155
+
156
+ ```cpp
157
+ class FDoubleBufferedData
158
+ {
159
+ public:
160
+ // Background thread writes to back buffer
161
+ void WriteBackBuffer(TArray<FVector>&& NewData)
162
+ {
163
+ FScopeLock Lock(&SwapLock);
164
+ BackBuffer = MoveTemp(NewData);
165
+ bSwapPending.store(true, std::memory_order_release);
166
+ }
167
+
168
+ // Game thread swaps and reads — call once per tick
169
+ const TArray<FVector>& SwapAndRead()
170
+ {
171
+ if (bSwapPending.load(std::memory_order_acquire))
172
+ {
173
+ FScopeLock Lock(&SwapLock);
174
+ Swap(FrontBuffer, BackBuffer);
175
+ bSwapPending.store(false, std::memory_order_relaxed);
176
+ }
177
+ return FrontBuffer; // lock-free read on game thread
178
+ }
179
+
180
+ private:
181
+ TArray<FVector> FrontBuffer; // game thread reads
182
+ TArray<FVector> BackBuffer; // background thread writes
183
+ FCriticalSection SwapLock; // only held during swap
184
+ std::atomic<bool> bSwapPending{false};
185
+ };
186
+ ```
187
+
188
+ The lock is only contended during the brief swap operation. The game thread reads the front buffer without any synchronization for the rest of the frame.
189
+
190
+ ---
191
+
192
+ ## Lock Ordering Rules
193
+
194
+ When multiple locks must be held simultaneously, always acquire them in a consistent global order. Inconsistent ordering between threads causes deadlocks.
195
+
196
+ **Rules:**
197
+ 1. **Define a total order** for all locks in your system (e.g., by subsystem, then by allocation order).
198
+ 2. **Always acquire in ascending order.** If thread A holds Lock1 and needs Lock2, Lock2 must have a higher order than Lock1.
199
+ 3. **Never hold a lock while calling unknown code** (callbacks, delegates, virtual functions). The callee may try to acquire a lock with a lower order.
200
+ 4. **Prefer lock-free patterns** (`TQueue`, `std::atomic`, double-buffering) when they fit. No locks means no deadlocks.
201
+ 5. **Keep critical sections short.** Do expensive computation outside the lock, then lock briefly to commit results.
202
+
203
+ ```cpp
204
+ // WRONG — inconsistent order causes deadlock
205
+ // Thread 1: Lock(A) -> Lock(B)
206
+ // Thread 2: Lock(B) -> Lock(A)
207
+
208
+ // RIGHT — consistent order
209
+ // Thread 1: Lock(A) -> Lock(B)
210
+ // Thread 2: Lock(A) -> Lock(B)
211
+ ```
212
+
213
+ When in doubt, restructure to use a single lock or a lock-free queue. Two locks that must be held simultaneously usually indicate a design that can be simplified.