Skip to content

Commit e0beb15

Browse files
cryptobenchclaude
andcommitted
Add multi-threaded architecture documentation to CLAUDE.md
Document Hytale's multi-threaded server model including: - Core architecture (HytaleServer, Universe, World threading) - Thread-bound rule for EntityStore/ECS operations - world.execute() bridge pattern for cross-thread operations - Thread-safe types for shared plugin state (AtomicInteger, ConcurrentHashMap) - Common mistakes: executor trap, blocking, race conditions - Technical specs: 30 TPS, 33ms tick budget - Performance best practices and debugging tips Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 440938d commit e0beb15

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,160 @@ getTaskRegistry() // For scheduled tasks
158158
getDataDirectory() // Plugin data folder: mods/Group_PluginName/
159159
```
160160

161+
## CRITICAL: Multi-Threaded Architecture & Thread Safety
162+
163+
**Hytale uses a multi-threaded server model. Understanding this is MANDATORY before writing any plugin code.**
164+
165+
### Core Architecture
166+
167+
| Component | Description |
168+
|-----------|-------------|
169+
| **HytaleServer** | Singleton root; owns `SCHEDULED_EXECUTOR` for background tasks |
170+
| **Universe** | Singleton container for all worlds; thread-safe player lookups via `ConcurrentHashMap` |
171+
| **World** | Each world runs on its **own dedicated thread** |
172+
173+
**Key Benefit:** Lag in "World A" does NOT cause lag in "World B" - worlds run in parallel.
174+
175+
### The Thread-Bound Rule (CRITICAL)
176+
177+
**The `EntityStore` and ALL ECS operations (`getComponent`, `addComponent`, `removeComponent`) are THREAD-BOUND.**
178+
179+
They can ONLY be accessed from their specific world's thread. Hytale uses `assertThread()` internally - accessing from the wrong thread throws `IllegalStateException` immediately to prevent silent data corruption.
180+
181+
```java
182+
// WRONG - will crash if called from wrong thread
183+
store.getComponent(playerRef, Player.getComponentType());
184+
185+
// CORRECT - ensures execution on world thread
186+
world.execute(() -> {
187+
store.getComponent(playerRef, Player.getComponentType());
188+
});
189+
```
190+
191+
### The Bridge: `world.execute()`
192+
193+
To run code on a specific world's thread from an external thread (background task, different world, etc.), use `world.execute()`:
194+
195+
```java
196+
// From a background task or different thread
197+
world.execute(() -> {
198+
// This code runs safely on the world's thread
199+
Store<EntityStore> store = world.getEntityStore().getStore();
200+
// Now safe to access ECS components
201+
});
202+
```
203+
204+
### Thread-Safe vs Thread-Bound Operations
205+
206+
| Always Safe (Any Thread) | Unsafe (Requires `world.execute()`) |
207+
|-------------------------|-------------------------------------|
208+
| `Universe.get().getPlayer(uuid)` | `store.getComponent(ref, type)` |
209+
| `playerRef.sendMessage(message)` | `store.addComponent(...)` |
210+
| `HytaleServer.SCHEDULED_EXECUTOR.schedule(...)` | `store.removeComponent(...)` |
211+
| `world.execute(runnable)` | Modifying entity position/health/inventory |
212+
213+
### Managing Shared Plugin State
214+
215+
When sharing data across multiple worlds (global state), use Java's thread-safe types:
216+
217+
```java
218+
// Counters - use AtomicInteger
219+
private final AtomicInteger globalKills = new AtomicInteger(0);
220+
globalKills.incrementAndGet();
221+
222+
// Collections/Maps - use ConcurrentHashMap
223+
private final ConcurrentHashMap<UUID, Integer> playerKills = new ConcurrentHashMap<>();
224+
playerKills.merge(playerId, 1, Integer::sum);
225+
226+
// One-time initialization - use AtomicBoolean
227+
private final AtomicBoolean initialized = new AtomicBoolean(false);
228+
if (initialized.compareAndSet(false, true)) {
229+
// Initialize only once
230+
}
231+
232+
// Simple flags - use volatile
233+
private volatile boolean enabled = true;
234+
```
235+
236+
### Common Mistakes & Patterns
237+
238+
#### The Executor Trap
239+
`SCHEDULED_EXECUTOR` runs on its own background thread, NOT a world thread:
240+
```java
241+
// WRONG - crashes when touching entity
242+
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> {
243+
store.getComponent(ref, type); // IllegalStateException!
244+
}, 1, TimeUnit.SECONDS);
245+
246+
// CORRECT - bridge back to world thread
247+
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> {
248+
world.execute(() -> {
249+
store.getComponent(ref, type); // Safe!
250+
});
251+
}, 1, TimeUnit.SECONDS);
252+
```
253+
254+
#### Avoid Blocking World Threads
255+
Never call `.join()` or `.get()` on a `CompletableFuture` inside a world thread - it blocks the entire world tick:
256+
```java
257+
// WRONG - blocks world tick
258+
CompletableFuture<Data> future = fetchDataAsync();
259+
Data data = future.get(); // DON'T DO THIS
260+
261+
// CORRECT - use callbacks
262+
fetchDataAsync().thenAccept(data -> {
263+
world.execute(() -> {
264+
// Process data on world thread
265+
});
266+
});
267+
```
268+
269+
#### Race Conditions
270+
Remember that `counter++` is secretly three operations (read, increment, write):
271+
```java
272+
// WRONG - race condition
273+
private int counter = 0;
274+
counter++; // Lost updates!
275+
276+
// CORRECT - atomic operation
277+
private final AtomicInteger counter = new AtomicInteger(0);
278+
counter.incrementAndGet();
279+
```
280+
281+
### Technical Specifications
282+
283+
| Spec | Value | Notes |
284+
|------|-------|-------|
285+
| **Tick Rate** | 30 TPS | 33.3ms per tick (vs Minecraft's 20 TPS) |
286+
| **Tick Budget** | 33ms | Heavy logic (>33ms) lags the entire world |
287+
| **Scaling** | Per-core | More CPU cores = more parallel worlds |
288+
289+
### Performance Best Practices
290+
291+
1. **Offload Heavy Work:** Move expensive operations (pathfinding, database I/O, HTTP requests) to `SCHEDULED_EXECUTOR` or `CompletableFuture.runAsync()`
292+
2. **Avoid Object Creation in Ticks:** Reuse objects where possible to reduce GC pressure
293+
3. **Use `world.execute()` Sparingly:** Queue minimal work back to world threads
294+
295+
### Local vs Global Events
296+
297+
| Event Type | Thread Context | Example |
298+
|------------|---------------|---------|
299+
| **Local Events** | Fires on the World Thread | `PlayerInteractEvent`, `BreakBlockEvent` - safe to touch ECS directly |
300+
| **Global Events** | May fire on different thread | Server-wide events - must use `world.execute()` before touching entities |
301+
302+
### The Golden Rule
303+
304+
> **"Always assume you are on the wrong thread unless you are inside a standard World System or event handler. If you touch `store`, verify you are thread-bound or wrapped in `world.execute()`."**
305+
306+
### Debugging Thread Issues
307+
308+
If you see:
309+
- `IllegalStateException: Assert not in thread!` → You're accessing ECS from wrong thread
310+
- `IllegalStateException: Store is currently processing!` → You're modifying during iteration
311+
- Random crashes or data corruption → Race condition, use atomic types
312+
313+
**First debug step:** "Is this code touching a Store/Component while running on an Executor thread?"
314+
161315
## Two Event Systems in Hytale
162316

163317
### 1. Standard Events (EventRegistry)

0 commit comments

Comments
 (0)