Skip to content

Commit 9cb0d9d

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 dea747a commit 9cb0d9d

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
@@ -133,6 +133,160 @@ getTaskRegistry() // For scheduled tasks
133133
getDataDirectory() // Plugin data folder: mods/Group_PluginName/
134134
```
135135

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

138292
### Tile Generation Flow

0 commit comments

Comments
 (0)