Skip to content

Commit 093734c

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 54e96bb commit 093734c

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
@@ -109,6 +109,160 @@ getTaskRegistry() // For scheduled tasks
109109
getDataDirectory() // Plugin data folder: mods/Group_PluginName/
110110
```
111111

112+
## CRITICAL: Multi-Threaded Architecture & Thread Safety
113+
114+
**Hytale uses a multi-threaded server model. Understanding this is MANDATORY before writing any plugin code.**
115+
116+
### Core Architecture
117+
118+
| Component | Description |
119+
|-----------|-------------|
120+
| **HytaleServer** | Singleton root; owns `SCHEDULED_EXECUTOR` for background tasks |
121+
| **Universe** | Singleton container for all worlds; thread-safe player lookups via `ConcurrentHashMap` |
122+
| **World** | Each world runs on its **own dedicated thread** |
123+
124+
**Key Benefit:** Lag in "World A" does NOT cause lag in "World B" - worlds run in parallel.
125+
126+
### The Thread-Bound Rule (CRITICAL)
127+
128+
**The `EntityStore` and ALL ECS operations (`getComponent`, `addComponent`, `removeComponent`) are THREAD-BOUND.**
129+
130+
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.
131+
132+
```java
133+
// WRONG - will crash if called from wrong thread
134+
store.getComponent(playerRef, Player.getComponentType());
135+
136+
// CORRECT - ensures execution on world thread
137+
world.execute(() -> {
138+
store.getComponent(playerRef, Player.getComponentType());
139+
});
140+
```
141+
142+
### The Bridge: `world.execute()`
143+
144+
To run code on a specific world's thread from an external thread (background task, different world, etc.), use `world.execute()`:
145+
146+
```java
147+
// From a background task or different thread
148+
world.execute(() -> {
149+
// This code runs safely on the world's thread
150+
Store<EntityStore> store = world.getEntityStore().getStore();
151+
// Now safe to access ECS components
152+
});
153+
```
154+
155+
### Thread-Safe vs Thread-Bound Operations
156+
157+
| Always Safe (Any Thread) | Unsafe (Requires `world.execute()`) |
158+
|-------------------------|-------------------------------------|
159+
| `Universe.get().getPlayer(uuid)` | `store.getComponent(ref, type)` |
160+
| `playerRef.sendMessage(message)` | `store.addComponent(...)` |
161+
| `HytaleServer.SCHEDULED_EXECUTOR.schedule(...)` | `store.removeComponent(...)` |
162+
| `world.execute(runnable)` | Modifying entity position/health/inventory |
163+
164+
### Managing Shared Plugin State
165+
166+
When sharing data across multiple worlds (global state), use Java's thread-safe types:
167+
168+
```java
169+
// Counters - use AtomicInteger
170+
private final AtomicInteger globalKills = new AtomicInteger(0);
171+
globalKills.incrementAndGet();
172+
173+
// Collections/Maps - use ConcurrentHashMap
174+
private final ConcurrentHashMap<UUID, Integer> playerKills = new ConcurrentHashMap<>();
175+
playerKills.merge(playerId, 1, Integer::sum);
176+
177+
// One-time initialization - use AtomicBoolean
178+
private final AtomicBoolean initialized = new AtomicBoolean(false);
179+
if (initialized.compareAndSet(false, true)) {
180+
// Initialize only once
181+
}
182+
183+
// Simple flags - use volatile
184+
private volatile boolean enabled = true;
185+
```
186+
187+
### Common Mistakes & Patterns
188+
189+
#### The Executor Trap
190+
`SCHEDULED_EXECUTOR` runs on its own background thread, NOT a world thread:
191+
```java
192+
// WRONG - crashes when touching entity
193+
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> {
194+
store.getComponent(ref, type); // IllegalStateException!
195+
}, 1, TimeUnit.SECONDS);
196+
197+
// CORRECT - bridge back to world thread
198+
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> {
199+
world.execute(() -> {
200+
store.getComponent(ref, type); // Safe!
201+
});
202+
}, 1, TimeUnit.SECONDS);
203+
```
204+
205+
#### Avoid Blocking World Threads
206+
Never call `.join()` or `.get()` on a `CompletableFuture` inside a world thread - it blocks the entire world tick:
207+
```java
208+
// WRONG - blocks world tick
209+
CompletableFuture<Data> future = fetchDataAsync();
210+
Data data = future.get(); // DON'T DO THIS
211+
212+
// CORRECT - use callbacks
213+
fetchDataAsync().thenAccept(data -> {
214+
world.execute(() -> {
215+
// Process data on world thread
216+
});
217+
});
218+
```
219+
220+
#### Race Conditions
221+
Remember that `counter++` is secretly three operations (read, increment, write):
222+
```java
223+
// WRONG - race condition
224+
private int counter = 0;
225+
counter++; // Lost updates!
226+
227+
// CORRECT - atomic operation
228+
private final AtomicInteger counter = new AtomicInteger(0);
229+
counter.incrementAndGet();
230+
```
231+
232+
### Technical Specifications
233+
234+
| Spec | Value | Notes |
235+
|------|-------|-------|
236+
| **Tick Rate** | 30 TPS | 33.3ms per tick (vs Minecraft's 20 TPS) |
237+
| **Tick Budget** | 33ms | Heavy logic (>33ms) lags the entire world |
238+
| **Scaling** | Per-core | More CPU cores = more parallel worlds |
239+
240+
### Performance Best Practices
241+
242+
1. **Offload Heavy Work:** Move expensive operations (pathfinding, database I/O, HTTP requests) to `SCHEDULED_EXECUTOR` or `CompletableFuture.runAsync()`
243+
2. **Avoid Object Creation in Ticks:** Reuse objects where possible to reduce GC pressure
244+
3. **Use `world.execute()` Sparingly:** Queue minimal work back to world threads
245+
246+
### Local vs Global Events
247+
248+
| Event Type | Thread Context | Example |
249+
|------------|---------------|---------|
250+
| **Local Events** | Fires on the World Thread | `PlayerInteractEvent`, `BreakBlockEvent` - safe to touch ECS directly |
251+
| **Global Events** | May fire on different thread | Server-wide events - must use `world.execute()` before touching entities |
252+
253+
### The Golden Rule
254+
255+
> **"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()`."**
256+
257+
### Debugging Thread Issues
258+
259+
If you see:
260+
- `IllegalStateException: Assert not in thread!` → You're accessing ECS from wrong thread
261+
- `IllegalStateException: Store is currently processing!` → You're modifying during iteration
262+
- Random crashes or data corruption → Race condition, use atomic types
263+
264+
**First debug step:** "Is this code touching a Store/Component while running on an Executor thread?"
265+
112266
## Two Event Systems in Hytale
113267

114268
### 1. Standard Events (EventRegistry)

0 commit comments

Comments
 (0)