Update script to write to vendor/hytale-server

This commit is contained in:
luk
2026-01-25 21:13:30 +00:00
parent 0ad4b55303
commit 3bdfaf38b4
17 changed files with 6057 additions and 1 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ repo/
patcher
Assets
.hytale-downloader-credentials.json
Assets.zip

777
docs/00-llm-reference.md Normal file
View File

@@ -0,0 +1,777 @@
# Hytale Server API Reference (LLM-Optimized)
This document is a comprehensive API reference for the Hytale Server modding system, optimized for LLM consumption. All class names, method signatures, and JSON structures are validated against the actual codebase.
---
## PLUGIN SYSTEM
### Core Classes
| Class | Package |
|-------|---------|
| `JavaPlugin` | `com.hypixel.hytale.server.core.plugin.JavaPlugin` |
| `PluginBase` | `com.hypixel.hytale.server.core.plugin.PluginBase` |
| `PluginManifest` | `com.hypixel.hytale.common.plugin.PluginManifest` |
| `JavaPluginInit` | `com.hypixel.hytale.server.core.plugin.JavaPluginInit` |
| `PluginState` | `com.hypixel.hytale.server.core.plugin.PluginState` |
### Plugin Lifecycle
```
PluginState: NONE -> SETUP -> START -> ENABLED -> SHUTDOWN -> DISABLED
```
**Lifecycle Methods (exact names):**
- `setup()` - Called during initialization, register components here
- `start()` - Called after setup, plugin becomes active
- `shutdown()` - Called during server stop/plugin disable
### Plugin Manifest JSON (exact field names, case-sensitive)
```json
{
"Group": "com.example",
"Name": "MyPlugin",
"Version": "1.0.0",
"Description": "Plugin description",
"Authors": [{"Name": "Dev", "Email": "dev@example.com", "Url": "https://example.com"}],
"Website": "https://example.com",
"Main": "com.example.MyPlugin",
"ServerVersion": ">=1.0.0",
"Dependencies": {"OtherGroup/OtherPlugin": ">=1.0.0"},
"OptionalDependencies": {},
"LoadBefore": {},
"DisabledByDefault": false,
"IncludesAssetPack": false,
"SubPlugins": []
}
```
### Registry Methods on PluginBase
| Method | Return Type |
|--------|-------------|
| `getClientFeatureRegistry()` | `ClientFeatureRegistry` |
| `getCommandRegistry()` | `CommandRegistry` |
| `getEventRegistry()` | `EventRegistry` |
| `getBlockStateRegistry()` | `BlockStateRegistry` |
| `getEntityRegistry()` | `EntityRegistry` |
| `getTaskRegistry()` | `TaskRegistry` |
| `getEntityStoreRegistry()` | `ComponentRegistryProxy<EntityStore>` |
| `getChunkStoreRegistry()` | `ComponentRegistryProxy<ChunkStore>` |
| `getAssetRegistry()` | `AssetRegistry` |
### Configuration Pattern
```java
// In constructor (BEFORE setup):
Config<MyConfig> config = withConfig(MyConfig.CODEC);
Config<MyConfig> config = withConfig("filename", MyConfig.CODEC);
// Access in setup/start:
MyConfig cfg = config.get();
```
---
## EVENT SYSTEM
### Core Interfaces (Package: `com.hypixel.hytale.event`)
| Interface | Description |
|-----------|-------------|
| `IBaseEvent<KeyType>` | Base event interface |
| `IEvent<KeyType>` | Synchronous event, extends IBaseEvent |
| `IAsyncEvent<KeyType>` | Async event, extends IBaseEvent |
| `ICancellable` | Mixin: `isCancelled()`, `setCancelled(boolean)` |
| `IProcessedEvent` | Mixin: `processEvent(String)` |
### EventPriority (exact values)
```java
FIRST = (short)-21844 // Runs first
EARLY = (short)-10922
NORMAL = (short)0 // Default
LATE = (short)10922
LAST = (short)21844 // Runs last
```
### EventRegistry Methods (IEventRegistry interface)
**Sync Registration:**
```java
// Without key (Void key)
EventRegistration register(Class<? super EventType> eventClass, Consumer<EventType> consumer)
EventRegistration register(EventPriority priority, Class<? super EventType> eventClass, Consumer<EventType> consumer)
// With key
EventRegistration register(Class<? super EventType> eventClass, KeyType key, Consumer<EventType> consumer)
EventRegistration register(EventPriority priority, Class<? super EventType> eventClass, KeyType key, Consumer<EventType> consumer)
```
**Global Registration (receives all keys):**
```java
EventRegistration registerGlobal(Class<? super EventType> eventClass, Consumer<EventType> consumer)
EventRegistration registerGlobal(EventPriority priority, Class<? super EventType> eventClass, Consumer<EventType> consumer)
```
**Unhandled Registration (when no other handler processed):**
```java
EventRegistration registerUnhandled(Class<? super EventType> eventClass, Consumer<EventType> consumer)
```
**Async Registration:**
```java
EventRegistration registerAsync(Class<? super EventType> eventClass, Function<CompletableFuture<EventType>, CompletableFuture<EventType>> function)
EventRegistration registerAsyncGlobal(...)
EventRegistration registerAsyncUnhandled(...)
```
### Key Event Classes
**Server Events (`com.hypixel.hytale.server.core.event.events`):**
- `BootEvent` - IEvent<Void>
- `ShutdownEvent` - IEvent<Void>
- `PrepareUniverseEvent` - IEvent<Void>
**Player Events (`...event.events.player`):**
- `PlayerConnectEvent` - IEvent<Void>
- `PlayerSetupConnectEvent` - IEvent<Void>, ICancellable
- `PlayerDisconnectEvent` - PlayerRefEvent<Void>
- `PlayerChatEvent` - IAsyncEvent<String>, ICancellable
- `PlayerInteractEvent` - PlayerEvent<String>, ICancellable
- `PlayerMouseButtonEvent` - PlayerEvent<Void>, ICancellable
- `AddPlayerToWorldEvent` - IEvent<String>
- `DrainPlayerFromWorldEvent` - IEvent<String>
**World Events (`...universe.world.events`):**
- `AddWorldEvent` - WorldEvent, ICancellable
- `RemoveWorldEvent` - WorldEvent, ICancellable
- `StartWorldEvent` - WorldEvent
- `AllWorldsLoadedEvent` - IEvent<Void>
**ECS Events (`...event.events.ecs`):**
- `BreakBlockEvent` - CancellableEcsEvent
- `PlaceBlockEvent` - CancellableEcsEvent
- `UseBlockEvent` - EcsEvent (with nested `Pre` implementing ICancellableEcsEvent)
- `DamageBlockEvent` - CancellableEcsEvent
- `DropItemEvent` - CancellableEcsEvent
---
## COMMAND SYSTEM
### Core Classes (Package: `com.hypixel.hytale.server.core.command.system`)
| Class | Description |
|-------|-------------|
| `AbstractCommand` | Base command class |
| `CommandRegistry` | Plugin command registration |
| `CommandContext` | Execution context |
| `CommandBase` | Sync command base (override `executeSync`) |
| `AbstractAsyncCommand` | Async command base |
| `AbstractPlayerCommand` | Player-required command |
| `AbstractWorldCommand` | World context command |
| `AbstractCommandCollection` | Parent with subcommands only |
### AbstractCommand Key Methods
```java
// Constructor
protected AbstractCommand(String name, String description)
protected AbstractCommand(String name, String description, boolean requiresConfirmation)
protected AbstractCommand(String description) // For variants (no name)
// Abstract - must implement
protected abstract CompletableFuture<Void> execute(CommandContext context)
// Configuration
void requirePermission(String permission)
void addAliases(String... aliases)
void addSubCommand(AbstractCommand command)
void addUsageVariant(AbstractCommand command)
// Arguments
RequiredArg<D> withRequiredArg(String name, String description, ArgumentType<D> argType)
OptionalArg<D> withOptionalArg(String name, String description, ArgumentType<D> argType)
DefaultArg<D> withDefaultArg(String name, String description, ArgumentType<D> argType, D defaultValue, String defaultValueDescription)
FlagArg withFlagArg(String name, String description)
RequiredArg<List<D>> withListRequiredArg(String name, String description, ArgumentType<D> argType)
```
### CommandContext Key Methods
```java
<DataType> DataType get(Argument<?, DataType> argument) // Get argument value
boolean provided(Argument<?, ?> argument) // Check if provided
void sendMessage(Message message) // Send to sender
boolean isPlayer() // Is sender a player
CommandSender sender() // Get sender
<T extends CommandSender> T senderAs(Class<T> type) // Cast sender
Ref<EntityStore> senderAsPlayerRef() // Get as player ref
```
### Built-in ArgTypes (`com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes`)
```java
ArgTypes.BOOLEAN, ArgTypes.INTEGER, ArgTypes.FLOAT, ArgTypes.DOUBLE, ArgTypes.STRING
ArgTypes.UUID, ArgTypes.PLAYER_UUID, ArgTypes.PLAYER_REF, ArgTypes.WORLD
ArgTypes.RELATIVE_DOUBLE_COORD, ArgTypes.RELATIVE_INT_COORD
ArgTypes.RELATIVE_BLOCK_POSITION, ArgTypes.RELATIVE_POSITION
ArgTypes.VECTOR2I, ArgTypes.VECTOR3I, ArgTypes.ROTATION, ArgTypes.COLOR
ArgTypes.GAME_MODE, ArgTypes.ITEM_ASSET, ArgTypes.BLOCK_TYPE_ASSET
ArgTypes.WEATHER_ASSET, ArgTypes.SOUND_EVENT_ASSET, ArgTypes.PARTICLE_SYSTEM
ArgTypes.forEnum(String name, Class<E> enumClass) // Create enum type
```
---
## ENTITY COMPONENT SYSTEM (ECS)
### Core Classes (Package: `com.hypixel.hytale.component`)
| Class | Description |
|-------|-------------|
| `Component<ECS_TYPE>` | Base component interface |
| `ComponentRegistry` | Component type registration |
| `ComponentType<ECS_TYPE, T>` | Registered component type |
| `Store` | ECS data storage |
| `Ref` | Entity reference (ID) |
| `Holder` | Component holder for entity construction |
| `Query` | Entity filtering |
### Entity Hierarchy
```
Entity (com.hypixel.hytale.server.core.entity.Entity)
└── LivingEntity (com.hypixel.hytale.server.core.entity.LivingEntity)
├── Player (com.hypixel.hytale.server.core.entity.entities.Player)
└── NPCEntity (com.hypixel.hytale.server.npc.entities.NPCEntity)
```
### EntityStore (Package: `com.hypixel.hytale.server.core.universe.world.storage`)
```java
public static final ComponentRegistry<EntityStore> REGISTRY = new ComponentRegistry<>();
// Registration
ComponentType<EntityStore, T> registerComponent(Class<? super T> tClass, Supplier<T> supplier)
ComponentType<EntityStore, T> registerComponent(Class<? super T> tClass, String id, BuilderCodec<T> codec)
void registerSystem(ISystem<EntityStore> system)
```
### Key Built-in Components
**Transform/Position:**
- `TransformComponent` - Position (Vector3d) and rotation (Vector3f)
- `HeadRotation` - Head rotation angles
- `EntityScaleComponent` - Scale modifier
**Physics:**
- `Velocity` - Velocity vector
- `BoundingBox` - Collision box
- `CollisionResultComponent` - Collision results
- `MovementStatesComponent` - Movement flags (onGround, swimming, etc.)
**Identity:**
- `UUIDComponent` - Unique identifier
- `NetworkId` - Network sync ID
- `DisplayNameComponent` - Display name
**Visual:**
- `ModelComponent` - 3D model reference
- `ActiveAnimationComponent` - Current animations
- `DynamicLight` - Dynamic lighting
**State Flags:**
- `Invulnerable` - Immune to damage
- `Intangible` - Non-collidable
- `Interactable` - Can be interacted with
- `Frozen` - Frozen state
**Player-specific:**
- `Player` - Core player component
- `PlayerRef` - Network connection reference
- `ChunkTracker` - Loaded chunks tracking
- `PlayerInput` - Input state
### Working with Components
```java
Store<EntityStore> store = world.getEntityStore().getStore();
// Get component
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
// Add/Put component
store.addComponent(entityRef, ComponentType, component);
store.putComponent(entityRef, ComponentType, component); // Add or replace
// Remove component
store.removeComponent(entityRef, ComponentType);
// Create entity
Holder<EntityStore> holder = EntityStore.REGISTRY.newHolder();
holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(pos, rot));
Ref<EntityStore> ref = store.addEntity(holder, AddReason.SPAWN);
```
---
## UI SYSTEM
### Page System Classes
| Class | Package |
|-------|---------|
| `CustomUIPage` | `com.hypixel.hytale.server.core.entity.entities.player.pages.CustomUIPage` |
| `BasicCustomUIPage` | Same package - no event data parsing |
| `InteractiveCustomUIPage<T>` | Same package - typed event handling |
| `PageManager` | Same package - page lifecycle |
| `UICommandBuilder` | `com.hypixel.hytale.server.core.ui.builder.UICommandBuilder` |
| `UIEventBuilder` | `com.hypixel.hytale.server.core.ui.builder.UIEventBuilder` |
### CustomPageLifetime
```java
CantClose(0) // Cannot be closed
CanDismiss(1) // ESC to close
CanDismissOrCloseThroughInteraction(2) // ESC or interaction
```
### UICommandBuilder Methods
```java
// Load documents
void append(String documentPath)
void append(String selector, String documentPath)
void appendInline(String selector, String xmlContent)
void insertBefore(String selector, String documentPath)
// Manipulate DOM
void clear(String selector)
void remove(String selector)
void set(String selector, String value)
void set(String selector, int value)
void set(String selector, float value)
void set(String selector, boolean value)
void set(String selector, Message value)
void set(String selector, Value<T> ref)
void setObject(String selector, Object data)
void set(String selector, T[] data)
void set(String selector, List<T> data)
```
### CustomUIEventBindingType
```java
Activating(0), RightClicking(1), DoubleClicking(2)
MouseEntered(3), MouseExited(4), ValueChanged(5)
ElementReordered(6), Validating(7), Dismissing(8)
FocusGained(9), FocusLost(10), KeyDown(11)
MouseButtonReleased(12), SlotClicking(13), SlotDoubleClicking(14)
SlotMouseEntered(15), SlotMouseExited(16), DragCancelled(17)
Dropped(18), SlotMouseDragCompleted(19), SlotMouseDragExited(20)
SlotClickReleaseWhileDragging(21), SlotClickPressWhileDragging(22)
SelectedTabChanged(23)
```
### UIEventBuilder Methods
```java
void addEventBinding(CustomUIEventBindingType type, String selector)
void addEventBinding(CustomUIEventBindingType type, String selector, boolean locksInterface)
void addEventBinding(CustomUIEventBindingType type, String selector, EventData data)
void addEventBinding(CustomUIEventBindingType type, String selector, EventData data, boolean locksInterface)
```
### EventData
```java
EventData.of("Key", "Value")
new EventData().append("Key", "Value").append("@ElementValue", "#Element.Property")
// @ prefix = extract value from element
```
### Value<T> References
```java
Value.of(directValue) // Direct value
Value.ref("Document.ui", "ValueName") // Reference from UI document
```
### Page Example
```java
public class MyPage extends InteractiveCustomUIPage<MyPage.Data> {
public MyPage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss, Data.CODEC);
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder cmd,
UIEventBuilder events, Store<EntityStore> store) {
cmd.append("Pages/MyPage.ui");
cmd.set("#Title.Text", "Hello");
events.addEventBinding(CustomUIEventBindingType.Activating, "#Button",
EventData.of("Action", "Click"));
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, Data data) {
if ("Click".equals(data.action)) { close(); }
}
public static class Data {
public static final BuilderCodec<Data> CODEC = BuilderCodec.builder(Data.class, Data::new)
.addField(new KeyedCodec<>("Action", Codec.STRING), (d,v) -> d.action = v, d -> d.action)
.build();
String action;
}
}
// Open page:
player.getPageManager().openCustomPage(ref, store, new MyPage(playerRef));
```
### Window System
| Class | Package |
|-------|---------|
| `Window` | `com.hypixel.hytale.server.core.entity.entities.player.windows.Window` |
| `WindowManager` | Same package |
| `ContainerWindow`, `CraftingWindow`, etc. | Same package |
### WindowType
```java
Container(0), PocketCrafting(1), BasicCrafting(2)
DiagramCrafting(3), StructuralCrafting(4), Processing(5), Memories(6)
```
---
## CODEC SYSTEM
### Core Classes (Package: `com.hypixel.hytale.codec`)
| Class | Description |
|-------|-------------|
| `Codec<T>` | Base codec interface |
| `BuilderCodec<T>` | Builder-based codec |
| `KeyedCodec<T>` | Key-value codec |
### Primitive Codecs
```java
Codec.STRING, Codec.BOOLEAN, Codec.INTEGER, Codec.LONG
Codec.FLOAT, Codec.DOUBLE, Codec.BYTE_ARRAY
```
### Collection Codecs
```java
Codec.STRING.listOf() // List<String>
Codec.STRING.setOf() // Set<String>
Codec.mapOf(Codec.STRING, Codec.INTEGER) // Map<String, Integer>
Codec.arrayOf(Codec.STRING, String[]::new) // String[]
```
### BuilderCodec Pattern
```java
public static final BuilderCodec<MyClass> CODEC = BuilderCodec.of(MyClass::new)
.with("fieldName", Codec.STRING, c -> c.field) // Required
.with("fieldName", Codec.STRING, c -> c.field, "default") // With default
.withOptional("fieldName", Codec.STRING, c -> c.field) // Optional
.build();
```
### Codec Transformations
```java
Codec<UUID> UUID_CODEC = Codec.STRING.xmap(UUID::fromString, UUID::toString);
Codec<Integer> PORT = Codec.INTEGER.validate(p -> p > 0 && p < 65536, "Invalid port");
Codec<Float> VOLUME = Codec.FLOAT.clamp(0.0f, 1.0f);
Codec<MyEnum> ENUM = Codec.enumCodec(MyEnum.class);
```
---
## ASSET SYSTEM
### Core Classes
| Class | Package |
|-------|---------|
| `JsonAsset<K>` | `com.hypixel.hytale.assetstore.JsonAsset` |
| `AssetStore<K,T,M>` | `com.hypixel.hytale.assetstore.AssetStore` |
| `AssetRegistry` | `com.hypixel.hytale.server.core.plugin.AssetRegistry` |
### Key Asset Types (Package: `com.hypixel.hytale.server.core.asset.type`)
**Blocks:**
- `BlockType` - blocktype.config.BlockType
- `BlockSet` - blockset.config.BlockSet
- `BlockSoundSet` - blocksound.config.BlockSoundSet
**Items:**
- `Item` - item.config.Item
- `ItemCategory` - item.config.ItemCategory
- `CraftingRecipe` - item.config.CraftingRecipe
**Visual:**
- `ModelAsset` - model.config.ModelAsset
- `ParticleSystem` - particle.config.ParticleSystem
- `EntityEffect` - entityeffect.config.EntityEffect
**Audio:**
- `SoundEvent` - soundevent.config.SoundEvent
- `SoundSet` - soundset.config.SoundSet
**Environment:**
- `Environment` - environment.config.Environment
- `Weather` - weather.config.Weather
- `Fluid` - fluid.Fluid
**Gameplay:**
- `Projectile` - projectile.config.Projectile
- `GameplayConfig` - gameplay.GameplayConfig
### Asset JSON Structure Pattern
```json
{
"Id": "namespace:asset_id",
"Parent": "namespace:parent_id",
"Type": "ConcreteType",
...fields
}
```
---
## WORLD MANAGEMENT
### Core Classes
| Class | Package |
|-------|---------|
| `Universe` | `com.hypixel.hytale.server.core.universe.Universe` |
| `World` | `com.hypixel.hytale.server.core.universe.world.World` |
| `EntityStore` | `com.hypixel.hytale.server.core.universe.world.storage.EntityStore` |
| `ChunkStore` | `com.hypixel.hytale.server.core.universe.world.storage.ChunkStore` |
### Universe Access
```java
Universe universe = Universe.get();
Collection<World> worlds = universe.getWorlds();
World world = universe.getWorld("worldName");
Collection<Player> players = universe.getPlayers();
Player player = universe.getPlayer("name");
Player player = universe.getPlayer(uuid);
```
### World Access
```java
String name = world.getName();
EntityStore entityStore = world.getEntityStore();
Store<EntityStore> store = entityStore.getStore();
long tick = world.getTick();
boolean paused = world.isPaused();
```
---
## NPC/AI SYSTEM
### Core Classes (Package: `com.hypixel.hytale.server.npc`)
| Class | Description |
|-------|-------------|
| `NPCEntity` | entities.NPCEntity - NPC entity class |
| `Role` | role.Role - Behavior definition |
| `Instruction` | instructions.Instruction - Behavior action |
| `PathManager` | navigation.PathManager - Pathfinding |
### NPCEntity Key Methods
```java
Role getRole()
void setRole(Role role)
String getRoleName()
void setRoleName(String name)
PathManager getPathManager()
void playAnimation(...)
```
### Flock System (Package: `com.hypixel.hytale.server.flock`)
| Component | Description |
|-----------|-------------|
| `Flock` | Flock leader/group |
| `FlockMembership` | Entity's flock membership |
---
## NETWORKING
### Packet System (Package: `com.hypixel.hytale.protocol`)
| Class | Description |
|-------|-------------|
| `Packet` | Base packet interface |
| `PacketRegistry` | Packet type registration |
### Key Packet Categories (`protocol.packets.*`)
- `connection/` - Connect, disconnect, ping
- `auth/` - Authentication
- `player/` - Player state
- `entities/` - Entity sync
- `world/` - Chunk/block data
- `inventory/` - Inventory ops
- `interface_/` - UI packets
- `window/` - Window packets
### UI Packets
| Packet | ID | Direction |
|--------|-----|-----------|
| `SetPage` | 216 | S->C |
| `CustomHud` | 217 | S->C |
| `CustomPage` | 218 | S->C |
| `CustomPageEvent` | 219 | C->S |
### Window Packets
| Packet | ID | Direction |
|--------|-----|-----------|
| `OpenWindow` | 200 | S->C |
| `UpdateWindow` | 201 | S->C |
| `CloseWindow` | 202 | S->C |
| `ClientOpenWindow` | 203 | C->S |
| `SendWindowAction` | 204 | C->S |
---
## MATH UTILITIES (Package: `com.hypixel.hytale.math`)
### Vector Types
```java
Vector3d // 3D double precision
Vector3f // 3D float precision
Vector3i // 3D integer
Vector2d // 2D double
Vector2i // 2D integer
```
### Vector Operations
```java
Vector3d a = new Vector3d(x, y, z);
a.add(b), a.subtract(b), a.multiply(scalar)
a.dot(b), a.cross(b), a.length(), a.normalize()
a.distance(b)
```
### Shapes
```java
Box, Ellipsoid, Cylinder
Transform, Location
```
---
## COMMON UTILITIES (Package: `com.hypixel.hytale.common`)
### Utility Classes
```java
StringUtil, ArrayUtil, ListUtil, MapUtil
TimeUtil, FormatUtil, MathUtil, TrigMathUtil
Semver, SemverRange
WeightedMap<T> // Weighted random selection
```
---
## EARLY PLUGIN SYSTEM
### ClassTransformer Interface (Package: `com.hypixel.hytale.plugin.early`)
```java
public interface ClassTransformer {
int priority(); // Higher = loaded first
byte[] transform(String className, String transformedName, byte[] classBytes);
}
```
### Service Registration
Create `META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer` containing transformer class name.
Place JAR in `earlyplugins/` directory.
---
## COMPLETE COMPONENT LIST
### EntityStore Components (130+)
**Transform:** TransformComponent, HeadRotation, PositionDataComponent, EntityScaleComponent, RotateObjectComponent, SnapshotBuffer
**Physics:** Velocity, PhysicsValues, BoundingBox, CollisionResultComponent, KnockbackComponent, MovementStatesComponent, HitboxCollision, Repulsion
**Player:** Player, MovementManager, CameraManager, ChunkTracker, PlayerInput, PlayerSettings, PlayerSkinComponent, PlayerRef
**NPC:** NPCEntity, ValueStore, StateEvaluator, StepComponent, Timers, FailedSpawnComponent
**Combat:** DamageDataComponent, DeathComponent, DeferredCorpseRemoval, CombatActionEvaluator, TargetMemory, DamageMemory
**Visual:** ModelComponent, PersistentModel, PropComponent, DisplayNameComponent, ActiveAnimationComponent, DynamicLight, Nameplate
**Audio:** AudioComponent, MovementAudioComponent
**Identity:** UUIDComponent, NetworkId, EntityViewer, Visible, PersistentRefCount
**State Flags:** Frozen, Intangible, Invulnerable, Interactable, RespondToHit, HiddenFromAdventurePlayers, NewSpawnComponent, FromPrefab, FromWorldGen, DespawnComponent
**Teleport:** Teleport, PendingTeleport, TeleportHistory, WarpComponent
**Projectile:** ProjectileComponent, Projectile, PredictedProjectile
**Item:** ItemComponent, ItemPhysicsComponent, PickupItemComponent, PreventPickup, PreventItemMerging
**Mount:** MountedComponent, MountedByComponent, NPCMountComponent, MinecartComponent
**Deployable:** DeployableComponent, DeployableOwnerComponent, DeployableProjectileShooterComponent
**Spawning:** SpawnMarkerEntity, LocalSpawnController, LocalSpawnBeacon, SpawnSuppressionComponent
**Flock:** Flock, FlockMembership, PersistentFlockData
**Effects:** EffectControllerComponent
**Stats:** EntityStatMap
### ChunkStore Components (25+)
**Structure:** BlockChunk, BlockComponentChunk, EntityChunk, ChunkColumn, ChunkSection, BlockSection, FluidSection, EnvironmentChunk
**Block State:** BlockState, RespawnBlock, LaunchPad, BlockMapMarker
**Physics:** BlockPhysics, BlockHealthChunk
**Farming:** FarmingBlock, FarmingBlockState, TilledSoilBlock, CoopBlock
**Instance:** InstanceBlock, ConfigurableInstanceBlock
**Portal:** PortalDevice
**Spawning:** BlockSpawner, ChunkSuppressionEntry, ChunkSpawnedNPCData, ChunkSpawnData

142
docs/01-getting-started.md Normal file
View File

@@ -0,0 +1,142 @@
# Getting Started with Hytale Server Modding
## Overview
The Hytale Server provides a comprehensive modding API that allows developers to extend and customize the game. This documentation covers the essential systems and APIs available for mod development.
## Architecture Overview
The server is built on several core systems:
| System | Purpose |
|--------|---------|
| Plugin System | Mod loading, lifecycle management, and dependency resolution |
| Entity Component System (ECS) | High-performance entity management with components and systems |
| Event System | Prioritized event dispatching with sync/async support |
| Command System | Player and console command handling |
| Asset System | Game asset loading and registration |
| Codec System | Data serialization/deserialization framework |
| Protocol Layer | Network communication and packet handling |
## Package Structure
The codebase is organized under `com.hypixel.hytale`:
```
com.hypixel.hytale/
├── server/core/ # Core server functionality
│ ├── plugin/ # Plugin management
│ ├── command/ # Command system
│ ├── entity/ # Entity management
│ ├── universe/ # World/Universe management
│ ├── registry/ # Server registries
│ └── ...
├── event/ # Event bus and handlers
├── component/ # ECS framework
├── codec/ # Serialization framework
├── protocol/ # Network protocol
├── common/ # Shared utilities
├── math/ # Math utilities and vectors
└── builtin/ # Built-in game modules
```
## Creating Your First Mod
### 1. Project Setup
Create a new Java project with the Hytale Server API as a dependency.
### 2. Create the Plugin Manifest
Create a `manifest.json` file in your JAR's root:
```json
{
"Group": "com.example",
"Name": "MyFirstMod",
"Version": "1.0.0",
"Description": "My first Hytale mod",
"Authors": ["YourName"],
"Main": "com.example.MyFirstMod",
"ServerVersion": ">=1.0.0",
"Dependencies": {},
"IncludesAssetPack": false
}
```
### 3. Create the Main Plugin Class
```java
package com.example;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.java.JavaPluginInit;
public class MyFirstMod extends JavaPlugin {
public MyFirstMod(JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
// Called during plugin setup phase
// Register commands, events, components here
getLogger().info("MyFirstMod is setting up!");
}
@Override
protected void start() {
// Called when the plugin starts
getLogger().info("MyFirstMod has started!");
}
@Override
protected void shutdown() {
// Called when the plugin is shutting down
// Clean up resources here
getLogger().info("MyFirstMod is shutting down!");
}
}
```
### 4. Build and Deploy
1. Build your project as a JAR file
2. Place the JAR in the server's `mods/` directory
3. Start the server
## Plugin Lifecycle
Plugins go through the following states:
1. **NONE** - Initial state before loading
2. **SETUP** - `setup()` is called; register components, commands, events
3. **START** - `start()` is called; plugin becomes active
4. **ENABLED** - Plugin is fully operational
5. **SHUTDOWN** - `shutdown()` is called during server stop
6. **DISABLED** - Plugin is disabled and unloaded
## Available Registries
Your plugin has access to several registries for registration:
| Registry | Access Method | Purpose |
|----------|---------------|---------|
| CommandRegistry | `getCommandRegistry()` | Register custom commands |
| EventRegistry | `getEventRegistry()` | Register event listeners |
| EntityRegistry | `getEntityRegistry()` | Register custom entity types |
| BlockStateRegistry | `getBlockStateRegistry()` | Register block states |
| TaskRegistry | `getTaskRegistry()` | Schedule recurring tasks |
| AssetRegistry | `getAssetRegistry()` | Register custom asset types |
| ClientFeatureRegistry | `getClientFeatureRegistry()` | Register client-side features |
| EntityStoreRegistry | `getEntityStoreRegistry()` | Register ECS entity stores |
| ChunkStoreRegistry | `getChunkStoreRegistry()` | Register chunk data stores |
## Next Steps
- [Plugin Development](02-plugin-development.md) - In-depth plugin creation guide
- [Event System](03-event-system.md) - Handling game events
- [Command System](04-command-system.md) - Creating custom commands
- [Entity System](05-entity-system.md) - Working with entities and ECS
- [World Management](06-world-management.md) - Managing worlds and chunks

View File

@@ -0,0 +1,351 @@
# Plugin Development
## Plugin Structure
A Hytale plugin consists of:
1. A main class extending `JavaPlugin`
2. A `manifest.json` file with plugin metadata
3. Optional asset packs for client-side resources
## The Plugin Manifest
The `manifest.json` file must be placed at the root of your JAR:
```json
{
"Group": "com.example",
"Name": "MyPlugin",
"Version": "1.0.0",
"Description": "A description of what this plugin does",
"Authors": ["Author1", "Author2"],
"Website": "https://example.com",
"Main": "com.example.MyPlugin",
"ServerVersion": ">=1.0.0",
"Dependencies": {
"com.other:OtherPlugin": ">=2.0.0"
},
"OptionalDependencies": {
"com.soft:SoftDep": ">=1.0.0"
},
"LoadBefore": ["com.load:BeforeThis"],
"DisabledByDefault": false,
"IncludesAssetPack": true
}
```
### Manifest Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `Group` | String | Yes | Maven-style group ID (e.g., `com.example`) |
| `Name` | String | Yes | Plugin name (unique identifier within group) |
| `Version` | String | Yes | Semantic version (e.g., `1.0.0`) |
| `Description` | String | No | Brief description of the plugin |
| `Authors` | String[] | No | List of author names |
| `Website` | String | No | Plugin website or repository URL |
| `Main` | String | Yes | Fully qualified main class name |
| `ServerVersion` | String | No | Required server version range |
| `Dependencies` | Object | No | Required plugin dependencies with version ranges |
| `OptionalDependencies` | Object | No | Optional plugin dependencies |
| `LoadBefore` | String[] | No | Plugins that should load after this one |
| `DisabledByDefault` | Boolean | No | If true, plugin must be explicitly enabled |
| `IncludesAssetPack` | Boolean | No | If true, plugin includes client assets |
### Plugin Identifier
Plugins are identified by `Group:Name` format (e.g., `com.example:MyPlugin`). This identifier is used for:
- Dependency resolution
- Permission namespacing (`com.example.myplugin.*`)
- Configuration keys
## The JavaPlugin Class
```java
package com.example;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.java.JavaPluginInit;
public class MyPlugin extends JavaPlugin {
private Config<MyConfig> config;
public MyPlugin(JavaPluginInit init) {
super(init);
// Initialize config BEFORE setup() is called
this.config = withConfig(MyConfig.CODEC);
}
@Override
protected void setup() {
// Register all components during setup
registerCommands();
registerEvents();
registerEntities();
}
@Override
protected void start() {
// Plugin is now active
// Access other plugins, start services, etc.
}
@Override
protected void shutdown() {
// Clean up resources
// Save data, close connections, etc.
}
private void registerCommands() {
getCommandRegistry().registerCommand(new MyCommand());
}
private void registerEvents() {
getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect);
}
private void registerEntities() {
getEntityRegistry().register("customEntity", CustomEntity.class, CustomEntity::new);
}
private void onPlayerConnect(PlayerConnectEvent event) {
getLogger().info("Player connected: " + event.getPlayer().getName());
}
}
```
## Plugin Lifecycle States
```
NONE -> SETUP -> START -> ENABLED -> SHUTDOWN -> DISABLED
```
| State | Description |
|-------|-------------|
| `NONE` | Initial state before any lifecycle methods |
| `SETUP` | `setup()` is executing; register components here |
| `START` | `start()` is executing; plugin becoming active |
| `ENABLED` | Plugin is fully operational and handling events |
| `SHUTDOWN` | `shutdown()` is executing; cleanup in progress |
| `DISABLED` | Plugin is fully disabled and unloaded |
## Configuration
### Defining a Configuration Class
```java
public class MyConfig {
public static final Codec<MyConfig> CODEC = BuilderCodec.of(MyConfig::new)
.with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
.with("maxPlayers", Codec.INTEGER, c -> c.maxPlayers, 100)
.with("welcomeMessage", Codec.STRING, c -> c.welcomeMessage, "Welcome!")
.build();
public final boolean enabled;
public final int maxPlayers;
public final String welcomeMessage;
private MyConfig(boolean enabled, int maxPlayers, String welcomeMessage) {
this.enabled = enabled;
this.maxPlayers = maxPlayers;
this.welcomeMessage = welcomeMessage;
}
}
```
### Using Configuration
```java
public class MyPlugin extends JavaPlugin {
private Config<MyConfig> config;
public MyPlugin(JavaPluginInit init) {
super(init);
this.config = withConfig(MyConfig.CODEC);
}
@Override
protected void setup() {
MyConfig cfg = config.get();
if (cfg.enabled) {
getLogger().info("Max players: " + cfg.maxPlayers);
}
}
}
```
Configuration is stored in the server's config under your plugin identifier.
## Available Registries
### CommandRegistry
Register custom commands:
```java
getCommandRegistry().registerCommand(new MyCommand());
```
See [Command System](04-command-system.md) for details.
### EventRegistry
Register event listeners:
```java
// Simple registration
getEventRegistry().register(PlayerConnectEvent.class, this::onConnect);
// With priority
getEventRegistry().register(EventPriority.EARLY, PlayerConnectEvent.class, this::onConnect);
// Global listener (all events of type)
getEventRegistry().registerGlobal(EntityEvent.class, this::onAnyEntity);
// Async event
getEventRegistry().registerAsync(AsyncEvent.class, this::onAsync);
```
See [Event System](03-event-system.md) for details.
### EntityRegistry
Register custom entity types:
```java
getEntityRegistry().register("myEntity", MyEntity.class, MyEntity::new);
// With serialization codec
getEntityRegistry().register("myEntity", MyEntity.class, MyEntity::new, MyEntity.CODEC);
```
### BlockStateRegistry
Register custom block states:
```java
getBlockStateRegistry().register("myBlock", MyBlockState.class);
```
### TaskRegistry
Schedule recurring tasks:
```java
getTaskRegistry().register(new MyTask());
```
### AssetRegistry
Register custom asset types:
```java
getAssetRegistry().register(MyAsset.class, MyAsset.CODEC);
```
### ClientFeatureRegistry
Register client-side features:
```java
getClientFeatureRegistry().register("myFeature", MyFeature.class);
```
## Permissions
Plugins automatically receive a base permission derived from their identifier:
```
{group}.{name} -> com.example.myplugin
```
Commands registered by your plugin will have permissions under:
```
{basePermission}.command.{commandName}
```
For example, if your plugin is `com.example:MyPlugin` and you register a command `spawn`:
- Base permission: `com.example.myplugin`
- Command permission: `com.example.myplugin.command.spawn`
## Logging
Use the built-in logger:
```java
getLogger().info("Information message");
getLogger().warn("Warning message");
getLogger().error("Error message");
getLogger().debug("Debug message");
```
## Dependencies
### Hard Dependencies
Hard dependencies must be present for your plugin to load:
```json
{
"Dependencies": {
"com.example:RequiredPlugin": ">=1.0.0"
}
}
```
### Optional Dependencies
Optional dependencies are loaded if present but not required:
```json
{
"OptionalDependencies": {
"com.example:OptionalPlugin": ">=1.0.0"
}
}
```
Check if optional dependency is loaded:
```java
@Override
protected void setup() {
if (getPluginManager().isPluginLoaded("com.example:OptionalPlugin")) {
// Optional plugin is available
}
}
```
### Load Order
Use `LoadBefore` to ensure specific plugins load after yours:
```json
{
"LoadBefore": ["com.example:LoadAfterMe"]
}
```
## Asset Packs
If your plugin includes client-side assets, set `IncludesAssetPack` to `true`:
```json
{
"IncludesAssetPack": true
}
```
Place assets in your JAR under the `assets/` directory following Hytale's asset structure.
## Best Practices
1. **Register everything in `setup()`** - Commands, events, entities should all be registered during setup
2. **Use `start()` for initialization logic** - Access other plugins, connect to databases, etc.
3. **Clean up in `shutdown()`** - Close connections, save data, cancel tasks
4. **Use configuration** - Make your plugin configurable instead of hardcoding values
5. **Handle errors gracefully** - Log errors but don't crash the server
6. **Respect the lifecycle** - Don't access other plugins during setup, wait for start()
7. **Use meaningful permissions** - Follow the automatic permission naming convention

335
docs/03-event-system.md Normal file
View File

@@ -0,0 +1,335 @@
# Event System
The Hytale event system provides a powerful mechanism for plugins to react to game events. It supports both synchronous and asynchronous events with priority-based dispatching.
## Event Architecture
### Event Interface Hierarchy
```
IBaseEvent<KeyType>
├── IEvent<KeyType> // Synchronous events
└── IAsyncEvent<KeyType> // Asynchronous events (CompletableFuture)
ICancellable // Mixin interface for cancellable events
```
### Key Classes
| Class | Description |
|-------|-------------|
| `IEvent<K>` | Synchronous event interface |
| `IAsyncEvent<K>` | Asynchronous event interface |
| `ICancellable` | Interface for events that can be cancelled |
| `EventRegistry` | Plugin-specific event registration |
| `SyncEventBusRegistry` | Global synchronous event bus |
| `EventPriority` | Event listener priority levels |
## Event Priorities
Events are dispatched to listeners in priority order:
| Priority | Value | Description |
|----------|-------|-------------|
| `FIRST` | -21844 | Runs first, before all others |
| `EARLY` | -10922 | Runs early, after FIRST |
| `NORMAL` | 0 | Default priority |
| `LATE` | 10922 | Runs late, after NORMAL |
| `LAST` | 21844 | Runs last, after all others |
Lower values run first. Use `FIRST` sparingly - typically for monitoring/logging. Use `LAST` for final processing after other plugins have had a chance to modify the event.
## Registering Event Listeners
### Basic Registration
```java
@Override
protected void setup() {
// Register with default NORMAL priority
getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect);
}
private void onPlayerConnect(PlayerConnectEvent event) {
getLogger().info("Player connected: " + event.getPlayer().getName());
}
```
### With Priority
```java
// Run before other listeners
getEventRegistry().register(EventPriority.FIRST, PlayerConnectEvent.class, event -> {
getLogger().debug("Logging player connect (first)");
});
// Run after other listeners
getEventRegistry().register(EventPriority.LAST, PlayerConnectEvent.class, event -> {
getLogger().debug("Final processing (last)");
});
```
### Global Listeners
Global listeners receive all events of a type, regardless of the event's key:
```java
// Receive all entity events
getEventRegistry().registerGlobal(EntityEvent.class, event -> {
// Handle any entity event
});
```
### Keyed Events
Some events are keyed (e.g., by world name). You can listen for specific keys:
```java
// Listen for events in a specific world
getEventRegistry().register(AddPlayerToWorldEvent.class, "worldName", event -> {
// Only triggered for "worldName"
});
```
### Unhandled Events
Register for events that no other listener handled:
```java
getEventRegistry().registerUnhandled(CustomEvent.class, event -> {
// Only called if no other listener handled this event
});
```
## Asynchronous Events
Async events return `CompletableFuture` and are handled on separate threads:
```java
// Register async event listener
getEventRegistry().registerAsync(AsyncDataLoadEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Perform async operation
return loadData(event.getDataId());
});
});
// Global async listener
getEventRegistry().registerAsyncGlobal(AsyncEvent.class, this::handleAsync);
// Unhandled async listener
getEventRegistry().registerAsyncUnhandled(AsyncEvent.class, this::handleUnhandledAsync);
```
## Cancellable Events
Events implementing `ICancellable` can be cancelled by listeners:
```java
getEventRegistry().register(PlayerInteractEvent.class, event -> {
if (shouldBlock(event)) {
event.setCancelled(true);
}
});
```
When checking if an event was cancelled:
```java
getEventRegistry().register(EventPriority.LAST, PlayerInteractEvent.class, event -> {
if (!event.isCancelled()) {
// Event was not cancelled by earlier listeners
processInteraction(event);
}
});
```
## Common Event Types
### Server Lifecycle Events
| Event | Description |
|-------|-------------|
| `BootEvent` | Server boot complete |
| `ShutdownEvent` | Server shutting down |
| `PrepareUniverseEvent` | Universe initialization |
### Player Events
| Event | Description |
|-------|-------------|
| `PlayerConnectEvent` | Player connected to server |
| `PlayerDisconnectEvent` | Player disconnected |
| `AddPlayerToWorldEvent` | Player added to a world |
| `DrainPlayerFromWorldEvent` | Player removed from a world |
| `PlayerInteractEvent` | Player interaction (cancellable) |
| `PlayerMouseButtonEvent` | Mouse button input (cancellable) |
### Entity Events
| Event | Description |
|-------|-------------|
| `EntityRemoveEvent` | Entity removed from world |
| `LivingEntityUseBlockEvent` | Living entity using a block |
### World Events
| Event | Description |
|-------|-------------|
| `AddWorldEvent` | World added to universe |
| `RemoveWorldEvent` | World removed |
| `StartWorldEvent` | World started |
| `AllWorldsLoadedEvent` | All worlds finished loading |
### ECS Events
| Event | Description |
|-------|-------------|
| `UseBlockEvent` | Block usage event |
| `DiscoverZoneEvent` | Zone discovery |
## Creating Custom Events
### Synchronous Event
```java
public class MyCustomEvent implements IEvent<String> {
private final String key;
private final Player player;
private final String data;
public MyCustomEvent(String key, Player player, String data) {
this.key = key;
this.player = player;
this.data = data;
}
@Override
public String getKey() {
return key;
}
public Player getPlayer() {
return player;
}
public String getData() {
return data;
}
}
```
### Cancellable Event
```java
public class MyCancellableEvent implements IEvent<String>, ICancellable {
private final String key;
private boolean cancelled = false;
public MyCancellableEvent(String key) {
this.key = key;
}
@Override
public String getKey() {
return key;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
```
### Async Event
```java
public class MyAsyncEvent implements IAsyncEvent<String> {
private final String key;
public MyAsyncEvent(String key) {
this.key = key;
}
@Override
public String getKey() {
return key;
}
}
```
## Dispatching Events
To dispatch custom events:
```java
// Get the event bus
SyncEventBusRegistry<String, MyCustomEvent> eventBus = getEventBus(MyCustomEvent.class);
// Dispatch event
MyCustomEvent event = new MyCustomEvent("key", player, "data");
eventBus.dispatch(event);
// For cancellable events, check result
MyCancellableEvent cancellable = new MyCancellableEvent("key");
eventBus.dispatch(cancellable);
if (!cancellable.isCancelled()) {
// Proceed with action
}
```
## Event Registration Management
### Unregistering Listeners
Event registration returns an `EventRegistration` that can be used to unregister:
```java
EventRegistration registration = getEventRegistry().register(
PlayerConnectEvent.class,
this::onConnect
);
// Later, to unregister:
registration.unregister();
```
### Combining Registrations
Multiple registrations can be combined for bulk management:
```java
EventRegistration reg1 = getEventRegistry().register(Event1.class, this::handle1);
EventRegistration reg2 = getEventRegistry().register(Event2.class, this::handle2);
EventRegistration combined = reg1.combine(reg2);
// Unregister both at once
combined.unregister();
```
### Conditional Listeners
Registrations can be conditionally enabled:
```java
// Listener only active while condition is true
Supplier<Boolean> isEnabled = () -> config.get().featureEnabled;
// The registration checks isEnabled before calling the listener
```
## Best Practices
1. **Use appropriate priorities** - Don't use `FIRST` unless you need to monitor/log before modifications
2. **Check cancellation state** - If using `LATE` or `LAST`, check if event was cancelled
3. **Don't block in sync handlers** - Use async events for long-running operations
4. **Clean up registrations** - Unregister listeners in `shutdown()` if needed
5. **Use keyed events** - When possible, listen for specific keys to reduce unnecessary handler calls
6. **Handle exceptions** - Wrap handler code in try-catch to prevent crashing other listeners
7. **Document custom events** - When creating custom events, document their key type and when they're dispatched

413
docs/04-command-system.md Normal file
View File

@@ -0,0 +1,413 @@
# Command System
The Hytale command system allows plugins to register custom commands that can be executed by players and the console.
## Overview
Commands are defined by extending `AbstractCommand` and registered through the plugin's `CommandRegistry`. The system supports:
- Required and optional arguments
- Subcommands
- Command variants (alternative syntax)
- Automatic permission generation
- Tab completion
- Flag arguments
## Creating a Basic Command
```java
public class HelloCommand extends AbstractCommand {
public HelloCommand() {
super("hello", "Says hello to a player");
// Add command aliases
addAliases("hi", "greet");
// Require permission (auto-generated if not specified)
requirePermission("myplugin.command.hello");
// Add a required argument
withRequiredArg("player", "The player to greet");
}
@Override
public void execute(CommandContext context, Arguments args) {
String playerName = args.getString("player");
context.sendMessage("Hello, " + playerName + "!");
}
}
```
## Registering Commands
Register commands in your plugin's `setup()` method:
```java
@Override
protected void setup() {
getCommandRegistry().registerCommand(new HelloCommand());
getCommandRegistry().registerCommand(new TeleportCommand());
}
```
## Command Arguments
### Required Arguments
Required arguments must be provided by the user:
```java
public MyCommand() {
super("mycommand", "Description");
// Single required argument
withRequiredArg("target", "The target player");
// Multiple required arguments
withRequiredArg("x", "X coordinate");
withRequiredArg("y", "Y coordinate");
withRequiredArg("z", "Z coordinate");
}
@Override
public void execute(CommandContext context, Arguments args) {
String target = args.getString("target");
int x = args.getInt("x");
int y = args.getInt("y");
int z = args.getInt("z");
}
```
### Optional Arguments
Optional arguments have default values:
```java
public MyCommand() {
super("mycommand", "Description");
withRequiredArg("player", "Target player");
withOptionalArg("message", "Optional message");
}
@Override
public void execute(CommandContext context, Arguments args) {
String player = args.getString("player");
String message = args.getStringOrDefault("message", "Default message");
}
```
### Default Arguments
Arguments with default values that are filled in if not provided:
```java
public MyCommand() {
super("mycommand", "Description");
withDefaultArg("count", "Number of items", "1");
}
```
### Flag Arguments
Boolean flags that can be toggled:
```java
public MyCommand() {
super("mycommand", "Description");
withFlagArg("silent", "Execute silently");
withFlagArg("force", "Force execution");
}
@Override
public void execute(CommandContext context, Arguments args) {
boolean silent = args.hasFlag("silent");
boolean force = args.hasFlag("force");
}
```
Usage: `/mycommand --silent --force`
### List Arguments
Arguments that accept multiple values:
```java
public MyCommand() {
super("mycommand", "Description");
withListRequiredArg("players", "List of players");
}
@Override
public void execute(CommandContext context, Arguments args) {
List<String> players = args.getStringList("players");
}
```
Usage: `/mycommand player1 player2 player3`
## Subcommands
Create hierarchical command structures:
```java
public class AdminCommand extends AbstractCommand {
public AdminCommand() {
super("admin", "Administration commands");
// Add subcommands
addSubCommand(new AdminKickCommand());
addSubCommand(new AdminBanCommand());
addSubCommand(new AdminMuteCommand());
}
@Override
public void execute(CommandContext context, Arguments args) {
// Called when no subcommand is specified
context.sendMessage("Usage: /admin <kick|ban|mute>");
}
}
public class AdminKickCommand extends AbstractCommand {
public AdminKickCommand() {
super("kick", "Kick a player");
withRequiredArg("player", "Player to kick");
withOptionalArg("reason", "Kick reason");
}
@Override
public void execute(CommandContext context, Arguments args) {
String player = args.getString("player");
String reason = args.getStringOrDefault("reason", "No reason specified");
// Kick logic
}
}
```
Usage: `/admin kick PlayerName "Breaking rules"`
## Command Variants
Define alternative syntax for the same command:
```java
public class TeleportCommand extends AbstractCommand {
public TeleportCommand() {
super("teleport", "Teleport to a location");
addAliases("tp");
// Variant 1: teleport to player
withRequiredArg("target", "Target player");
// Variant 2: teleport to coordinates
addUsageVariant(new TeleportCoordsVariant());
}
@Override
public void execute(CommandContext context, Arguments args) {
String target = args.getString("target");
// Teleport to player
}
}
public class TeleportCoordsVariant extends AbstractCommand {
public TeleportCoordsVariant() {
super("teleport", "Teleport to coordinates");
withRequiredArg("x", "X coordinate");
withRequiredArg("y", "Y coordinate");
withRequiredArg("z", "Z coordinate");
}
@Override
public void execute(CommandContext context, Arguments args) {
double x = args.getDouble("x");
double y = args.getDouble("y");
double z = args.getDouble("z");
// Teleport to coordinates
}
}
```
## Command Context
The `CommandContext` provides information about the command execution:
```java
@Override
public void execute(CommandContext context, Arguments args) {
// Get the command sender
CommandSender sender = context.getSender();
// Check if sender is a player
if (sender instanceof Player player) {
// Player-specific logic
World world = player.getWorld();
}
// Send messages
context.sendMessage("Success!");
context.sendError("Something went wrong!");
// Check permissions
if (sender.hasPermission("myplugin.admin")) {
// Admin logic
}
}
```
## Permissions
### Automatic Permission Generation
Commands automatically receive permissions based on the plugin's base permission:
```
{plugin.basePermission}.command.{commandName}
```
For example, if your plugin is `com.example:MyPlugin` and command is `spawn`:
- Permission: `com.example.myplugin.command.spawn`
### Custom Permissions
Override the automatic permission:
```java
public MyCommand() {
super("mycommand", "Description");
requirePermission("custom.permission.name");
}
```
### Permission Checks in Execution
```java
@Override
public void execute(CommandContext context, Arguments args) {
CommandSender sender = context.getSender();
if (!sender.hasPermission("myplugin.admin")) {
context.sendError("You don't have permission!");
return;
}
// Continue execution
}
```
## Command Sender Types
Commands can be executed by different sender types:
| Sender Type | Description |
|-------------|-------------|
| `Player` | In-game player |
| `Console` | Server console |
| `CommandBlock` | Command block (if applicable) |
```java
@Override
public void execute(CommandContext context, Arguments args) {
CommandSender sender = context.getSender();
if (sender instanceof Player player) {
// Player-specific logic
} else if (sender instanceof Console) {
// Console-specific logic
} else {
context.sendError("This command can only be run by players!");
}
}
```
## Complete Example
```java
public class GameModeCommand extends AbstractCommand {
public GameModeCommand() {
super("gamemode", "Change your game mode");
addAliases("gm");
// Required: game mode
withRequiredArg("mode", "Game mode (survival, creative, adventure, spectator)");
// Optional: target player (admin only)
withOptionalArg("player", "Target player (requires admin permission)");
requirePermission("myplugin.command.gamemode");
}
@Override
public void execute(CommandContext context, Arguments args) {
CommandSender sender = context.getSender();
String modeStr = args.getString("mode");
// Parse game mode
GameMode mode = parseGameMode(modeStr);
if (mode == null) {
context.sendError("Invalid game mode: " + modeStr);
return;
}
// Determine target player
Player target;
if (args.has("player")) {
// Check admin permission for targeting others
if (!sender.hasPermission("myplugin.command.gamemode.others")) {
context.sendError("You don't have permission to change others' game mode!");
return;
}
String playerName = args.getString("player");
target = findPlayer(playerName);
if (target == null) {
context.sendError("Player not found: " + playerName);
return;
}
} else {
// Self - must be a player
if (!(sender instanceof Player)) {
context.sendError("Console must specify a target player!");
return;
}
target = (Player) sender;
}
// Apply game mode
target.setGameMode(mode);
context.sendMessage("Set " + target.getName() + "'s game mode to " + mode.name());
}
private GameMode parseGameMode(String str) {
return switch (str.toLowerCase()) {
case "survival", "s", "0" -> GameMode.SURVIVAL;
case "creative", "c", "1" -> GameMode.CREATIVE;
case "adventure", "a", "2" -> GameMode.ADVENTURE;
case "spectator", "sp", "3" -> GameMode.SPECTATOR;
default -> null;
};
}
private Player findPlayer(String name) {
return Universe.get().getPlayer(name);
}
}
```
## Best Practices
1. **Provide clear descriptions** - Help users understand what the command does
2. **Use meaningful aliases** - Common abbreviations improve usability
3. **Validate input** - Check argument values before using them
4. **Handle errors gracefully** - Provide helpful error messages
5. **Check permissions appropriately** - Use automatic permissions or explicit checks
6. **Support tab completion** - Implement tab completion for better UX
7. **Document usage** - Include usage examples in the description
8. **Use subcommands for complex functionality** - Organize related commands together
9. **Consider console execution** - Check if the command can run from console

414
docs/05-entity-system.md Normal file
View File

@@ -0,0 +1,414 @@
# Entity Component System (ECS)
The Hytale server uses an Entity Component System architecture for managing game entities. This provides high performance and flexibility for handling large numbers of entities with varied behaviors.
## ECS Concepts
### Overview
The ECS pattern separates data (Components) from behavior (Systems):
- **Entities**: Unique identifiers (IDs) that represent game objects
- **Components**: Pure data containers attached to entities
- **Systems**: Logic that operates on entities with specific component combinations
- **Stores**: Data storage containers that hold components
- **Archetypes**: Predefined combinations of components
### Key Classes
| Class | Description |
|-------|-------------|
| `Component<S>` | Base component interface |
| `ComponentRegistry` | Central registry for component types |
| `Store` | ECS data storage |
| `Ref` | Entity reference (ID wrapper) |
| `Holder<C>` | Component holder/accessor |
| `Archetype` | Entity archetype definition |
| `SystemType` | System type definition |
| `Query` | ECS query for finding entities |
## Components
### Defining a Component
Components are pure data classes implementing `Component<Store>`:
```java
public class HealthComponent implements Component<EntityStore> {
public static final ComponentRegistry.Entry<HealthComponent> ENTRY =
EntityStore.REGISTRY.register("health", HealthComponent.class, HealthComponent::new);
private float health;
private float maxHealth;
public HealthComponent() {
this.health = 20.0f;
this.maxHealth = 20.0f;
}
public float getHealth() {
return health;
}
public void setHealth(float health) {
this.health = Math.max(0, Math.min(health, maxHealth));
}
public float getMaxHealth() {
return maxHealth;
}
public void setMaxHealth(float maxHealth) {
this.maxHealth = maxHealth;
}
public boolean isDead() {
return health <= 0;
}
public void damage(float amount) {
setHealth(health - amount);
}
public void heal(float amount) {
setHealth(health + amount);
}
}
```
### Built-in Components
The server provides several built-in components in `EntityStore.REGISTRY`:
| Component | Description |
|-----------|-------------|
| `TransformComponent` | Position, rotation, and scale |
| `UUIDComponent` | Unique entity identifier |
| `ModelComponent` | Visual model reference |
| `MovementAudioComponent` | Movement sound effects |
| `PositionDataComponent` | Position tracking data |
| `HeadRotation` | Head rotation for living entities |
### Registering Components
Register custom components in your plugin's `setup()`:
```java
@Override
protected void setup() {
// Components are registered through the EntityStoreRegistry
getEntityStoreRegistry().registerComponent(MyComponent.ENTRY);
}
```
## Entities
### Entity Hierarchy
```
Component<EntityStore>
└── Entity // Base entity
└── LivingEntity // Has health and can take damage
├── Player // Player entity
└── NPCEntity // NPC entity
```
### Creating a Custom Entity
```java
public class CustomEntity extends Entity {
public static final Codec<CustomEntity> CODEC = BuilderCodec.of(CustomEntity::new)
// Add codec fields
.build();
private int customData;
public CustomEntity(World world) {
super(world);
this.customData = 0;
}
public int getCustomData() {
return customData;
}
public void setCustomData(int data) {
this.customData = data;
}
@Override
public void tick() {
super.tick();
// Custom tick logic
}
}
```
### Registering Entities
```java
@Override
protected void setup() {
getEntityRegistry().register(
"customEntity",
CustomEntity.class,
CustomEntity::new
);
// With serialization codec
getEntityRegistry().register(
"customEntity",
CustomEntity.class,
CustomEntity::new,
CustomEntity.CODEC
);
}
```
### Accessing Entity Components
```java
Entity entity = world.getEntity(entityId);
// Get a component
TransformComponent transform = entity.get(TransformComponent.ENTRY);
if (transform != null) {
Vector3d position = transform.getPosition();
}
// Check if entity has component
if (entity.has(HealthComponent.ENTRY)) {
// Entity has health
}
// Add a component
entity.add(MyComponent.ENTRY, new MyComponent());
// Remove a component
entity.remove(MyComponent.ENTRY);
```
## Entity Store
The `EntityStore` manages entity data within a world:
```java
World world = Universe.get().getWorld("default");
EntityStore store = world.getEntityStore();
// Create entity
Ref entityRef = store.create();
// Add components to entity
store.add(entityRef, TransformComponent.ENTRY, new TransformComponent(position));
store.add(entityRef, HealthComponent.ENTRY, new HealthComponent());
// Get component from entity
HealthComponent health = store.get(entityRef, HealthComponent.ENTRY);
// Remove entity
store.remove(entityRef);
```
## Queries
Queries find entities with specific component combinations:
```java
// Query for entities with both Transform and Health components
Query query = Query.builder()
.with(TransformComponent.ENTRY)
.with(HealthComponent.ENTRY)
.build();
// Execute query
EntityStore store = world.getEntityStore();
store.query(query, (ref, transform, health) -> {
// Process each matching entity
if (health.isDead()) {
// Handle dead entity
}
});
```
### Query Builder
```java
Query query = Query.builder()
.with(ComponentA.ENTRY) // Must have ComponentA
.with(ComponentB.ENTRY) // Must have ComponentB
.without(ComponentC.ENTRY) // Must NOT have ComponentC
.build();
```
## Archetypes
Archetypes define common component combinations for entity types:
```java
// Define an archetype for enemies
Archetype enemyArchetype = Archetype.builder()
.add(TransformComponent.ENTRY)
.add(HealthComponent.ENTRY)
.add(AIComponent.ENTRY)
.add(CombatComponent.ENTRY)
.build();
// Create entity with archetype
Ref enemy = store.create(enemyArchetype);
```
## Systems
Systems contain the logic that operates on entities:
```java
public class HealthRegenSystem implements System {
private static final Query QUERY = Query.builder()
.with(HealthComponent.ENTRY)
.build();
@Override
public void tick(World world) {
EntityStore store = world.getEntityStore();
store.query(QUERY, (ref, health) -> {
if (!health.isDead() && health.getHealth() < health.getMaxHealth()) {
// Regenerate 0.1 health per tick
health.heal(0.1f);
}
});
}
}
```
### Registering Systems
```java
@Override
protected void setup() {
// Register system with the world module
getEntityStoreRegistry().registerSystem(new HealthRegenSystem());
}
```
## Working with Players
### Getting Players
```java
// Get all players
Collection<Player> players = Universe.get().getPlayers();
// Get player by name
Player player = Universe.get().getPlayer("PlayerName");
// Get player by UUID
Player player = Universe.get().getPlayer(uuid);
// Get players in world
World world = Universe.get().getWorld("default");
Collection<Player> worldPlayers = world.getPlayers();
```
### Player Properties
```java
Player player = getPlayer();
// Position
Vector3d position = player.getPosition();
player.setPosition(new Vector3d(x, y, z));
// Rotation
float yaw = player.getYaw();
float pitch = player.getPitch();
// Health (if LivingEntity)
player.setHealth(20.0f);
float health = player.getHealth();
// Game mode
GameMode mode = player.getGameMode();
player.setGameMode(GameMode.CREATIVE);
// Inventory
Inventory inventory = player.getInventory();
```
## Living Entities
`LivingEntity` extends `Entity` with health and damage capabilities:
```java
public class CustomMob extends LivingEntity {
public CustomMob(World world) {
super(world);
setMaxHealth(50.0f);
setHealth(50.0f);
}
@Override
public void onDamage(DamageSource source, float amount) {
super.onDamage(source, amount);
// Custom damage handling
}
@Override
public void onDeath() {
super.onDeath();
// Drop loot, play effects, etc.
}
}
```
## Entity Events
Subscribe to entity-related events:
```java
// Entity removed from world
getEventRegistry().register(EntityRemoveEvent.class, event -> {
Entity entity = event.getEntity();
// Handle entity removal
});
// Living entity uses block
getEventRegistry().register(LivingEntityUseBlockEvent.class, event -> {
LivingEntity entity = event.getEntity();
// Handle block usage
});
```
## Spawning Entities
```java
// Get the world
World world = Universe.get().getWorld("default");
// Spawn entity at position
Vector3d position = new Vector3d(100, 64, 100);
CustomEntity entity = new CustomEntity(world);
entity.setPosition(position);
world.addEntity(entity);
// Spawn with specific properties
CustomEntity entity = new CustomEntity(world);
entity.setPosition(position);
entity.setCustomData(42);
world.addEntity(entity);
```
## Best Practices
1. **Keep components data-only** - Components should not contain logic, only data
2. **Use systems for behavior** - Put game logic in systems, not components
3. **Prefer composition** - Use multiple small components instead of large monolithic ones
4. **Query efficiently** - Cache queries and reuse them
5. **Use archetypes** - Define archetypes for common entity types
6. **Handle null components** - Always check if a component exists before using it
7. **Clean up entities** - Remove entities when they're no longer needed
8. **Use appropriate entity types** - Extend `LivingEntity` for damageable entities

396
docs/06-world-management.md Normal file
View File

@@ -0,0 +1,396 @@
# World Management
The Hytale server supports multiple worlds within a single universe. This guide covers world management, chunk handling, and related systems.
## Universe
The `Universe` is the top-level container for all worlds and global game state.
### Accessing the Universe
```java
Universe universe = Universe.get();
```
### Universe Properties
```java
// Get all loaded worlds
Collection<World> worlds = universe.getWorlds();
// Get a specific world
World world = universe.getWorld("worldName");
// Get player count
int playerCount = universe.getPlayerCount();
// Get all players
Collection<Player> players = universe.getPlayers();
// Get player by name
Player player = universe.getPlayer("PlayerName");
// Get player by UUID
Player player = universe.getPlayer(uuid);
```
## Worlds
Each `World` represents a separate game environment with its own terrain, entities, and rules.
### World Properties
```java
World world = universe.getWorld("default");
// Basic properties
String name = world.getName();
WorldConfig config = world.getConfig();
// State
boolean isPaused = world.isPaused();
long tick = world.getTick();
// Stores
EntityStore entityStore = world.getEntityStore();
ChunkStore chunkStore = world.getChunkStore();
```
### World Configuration
```java
WorldConfig config = WorldConfig.builder()
.generator(myWorldGenProvider) // World generation
.storage(myChunkStorageProvider) // Chunk persistence
.resourceStorage(myResourceProvider) // Resource storage
.worldMap(myWorldMapProvider) // World map provider
.spawn(mySpawnProvider) // Spawn point provider
.gameplay(gameplayConfig) // Gameplay settings
.build();
```
## Chunks
Worlds are divided into chunks for efficient loading and rendering.
### Chunk Access
```java
World world = universe.getWorld("default");
// Get chunk at world coordinates
Chunk chunk = world.getChunk(x, y, z);
// Get chunk column (all chunks at x,z)
ChunkColumn column = world.getChunkColumn(x, z);
// Check if chunk is loaded
boolean loaded = world.isChunkLoaded(x, y, z);
```
### Chunk Store
```java
ChunkStore chunkStore = world.getChunkStore();
// Get chunk data
ChunkData data = chunkStore.getChunk(chunkX, chunkY, chunkZ);
// Iterate loaded chunks
chunkStore.forEachLoaded((chunkPos, chunk) -> {
// Process chunk
});
```
### Block Access
```java
World world = universe.getWorld("default");
// Get block at position
BlockState block = world.getBlock(x, y, z);
// Set block at position
world.setBlock(x, y, z, newBlockState);
// Get block type
BlockType type = block.getType();
```
## World Events
### Listening for World Events
```java
// World added
getEventRegistry().register(AddWorldEvent.class, event -> {
World world = event.getWorld();
getLogger().info("World added: " + world.getName());
});
// World removed
getEventRegistry().register(RemoveWorldEvent.class, event -> {
World world = event.getWorld();
getLogger().info("World removed: " + world.getName());
});
// World started
getEventRegistry().register(StartWorldEvent.class, event -> {
World world = event.getWorld();
getLogger().info("World started: " + world.getName());
});
// All worlds loaded
getEventRegistry().register(AllWorldsLoadedEvent.class, event -> {
getLogger().info("All worlds have been loaded!");
});
// Player added to world
getEventRegistry().register(AddPlayerToWorldEvent.class, event -> {
Player player = event.getPlayer();
World world = event.getWorld();
getLogger().info(player.getName() + " entered " + world.getName());
});
// Player removed from world
getEventRegistry().register(DrainPlayerFromWorldEvent.class, event -> {
Player player = event.getPlayer();
World world = event.getWorld();
getLogger().info(player.getName() + " left " + world.getName());
});
```
### Keyed World Events
Some events are keyed by world name:
```java
// Listen for events in a specific world
getEventRegistry().register(AddPlayerToWorldEvent.class, "spawn_world", event -> {
// Only triggered for "spawn_world"
event.getPlayer().sendMessage("Welcome to spawn!");
});
```
## World Generation
### World Generation Provider
Implement `IWorldGenProvider` for custom world generation:
```java
public class MyWorldGenProvider implements IWorldGenProvider {
@Override
public void generate(ChunkData chunk, int chunkX, int chunkY, int chunkZ) {
// Generate terrain for this chunk
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int height = calculateHeight(chunkX * 16 + x, chunkZ * 16 + z);
for (int y = 0; y < 16; y++) {
int worldY = chunkY * 16 + y;
BlockState block = getBlockForPosition(worldY, height);
chunk.setBlock(x, y, z, block);
}
}
}
}
private int calculateHeight(int worldX, int worldZ) {
// Use noise functions for terrain generation
return 64 + (int)(Math.sin(worldX * 0.1) * 10);
}
private BlockState getBlockForPosition(int y, int surfaceHeight) {
if (y > surfaceHeight) {
return BlockStates.AIR;
} else if (y == surfaceHeight) {
return BlockStates.GRASS;
} else if (y > surfaceHeight - 3) {
return BlockStates.DIRT;
} else {
return BlockStates.STONE;
}
}
}
```
### Procedural Generation Utilities
The `procedurallib` package provides utilities for procedural generation:
```java
// Noise generation
PerlinNoise noise = new PerlinNoise(seed);
double value = noise.getValue(x, y, z);
// Multi-octave noise
FractalNoise fractal = FractalNoise.builder()
.octaves(4)
.persistence(0.5)
.lacunarity(2.0)
.build();
double terrain = fractal.getValue(x, z);
```
## Spawn System
### Spawn Provider
Implement `ISpawnProvider` for custom spawn logic:
```java
public class MySpawnProvider implements ISpawnProvider {
@Override
public Vector3d getSpawnLocation(Player player, World world) {
// Calculate spawn location for player
return new Vector3d(0, 64, 0);
}
@Override
public float getSpawnYaw(Player player, World world) {
return 0.0f;
}
}
```
### Built-in Spawning
The `spawning` module provides entity spawning capabilities:
```java
// Spawn entities based on spawn rules
SpawnManager spawner = world.getSpawnManager();
// Trigger spawn checks in area
spawner.checkSpawns(position, radius);
```
## Chunk Storage
### Storage Provider
Implement `IChunkStorageProvider` for custom chunk persistence:
```java
public class MyChunkStorageProvider implements IChunkStorageProvider {
@Override
public CompletableFuture<ChunkData> load(int x, int y, int z) {
// Load chunk from storage
return CompletableFuture.supplyAsync(() -> {
return loadFromDatabase(x, y, z);
});
}
@Override
public CompletableFuture<Void> save(int x, int y, int z, ChunkData data) {
// Save chunk to storage
return CompletableFuture.runAsync(() -> {
saveToDatabase(x, y, z, data);
});
}
}
```
## Teleportation
### Teleporting Players
```java
Player player = getPlayer();
// Teleport to position in same world
Vector3d destination = new Vector3d(100, 64, 200);
player.teleport(destination);
// Teleport with rotation
player.teleport(destination, yaw, pitch);
// Teleport to another world
World targetWorld = universe.getWorld("nether");
player.teleport(targetWorld, destination);
```
### Teleportation Events
The `teleport` built-in module handles teleportation:
```java
// Custom teleportation with effects
TeleportManager.teleport(player, destination, new TeleportOptions()
.withEffect(TeleportEffect.PARTICLES)
.withSound(true));
```
## Portals
The `portals` built-in module provides portal functionality:
```java
// Create a portal
Portal portal = Portal.builder()
.position(sourcePosition)
.destination(targetWorld, destPosition)
.size(2, 3) // width, height
.build();
// Register portal with world
world.addPortal(portal);
```
## Multi-World Management
### Creating Multiple Worlds
```java
@Override
protected void start() {
Universe universe = Universe.get();
// Create custom world configurations
WorldConfig spawnConfig = WorldConfig.builder()
.generator(new FlatWorldGenerator())
.gameplay(GameplayConfig.CREATIVE)
.build();
WorldConfig survivalConfig = WorldConfig.builder()
.generator(new HytaleWorldGenerator())
.gameplay(GameplayConfig.SURVIVAL)
.build();
// Register worlds (if supported)
// universe.createWorld("spawn", spawnConfig);
// universe.createWorld("survival", survivalConfig);
}
```
### World Transitions
```java
public void sendToWorld(Player player, String worldName) {
World targetWorld = Universe.get().getWorld(worldName);
if (targetWorld == null) {
player.sendMessage("World not found: " + worldName);
return;
}
// Get spawn location for target world
Vector3d spawn = targetWorld.getSpawnLocation();
// Teleport player
player.teleport(targetWorld, spawn);
}
```
## Best Practices
1. **Cache world references** - Don't repeatedly call `Universe.get().getWorld()` in hot paths
2. **Handle world events** - Listen for world lifecycle events for initialization/cleanup
3. **Use async chunk operations** - Chunk loading/saving should be asynchronous
4. **Respect chunk boundaries** - Be aware of chunk boundaries when modifying blocks
5. **Clean up on world removal** - Handle `RemoveWorldEvent` to clean up resources
6. **Use keyed events** - Listen for world-specific events when possible
7. **Consider view distance** - Be mindful of server view distance settings
8. **Test multi-world scenarios** - Ensure your plugin works across multiple worlds

348
docs/07-networking.md Normal file
View File

@@ -0,0 +1,348 @@
# Networking and Protocol
The Hytale server uses a packet-based networking system for client-server communication. This guide covers the protocol system, packet types, and how to work with networking in your mods.
## Overview
The networking layer is built on QUIC protocol and handles:
- Client connections and authentication
- Game state synchronization
- Player input handling
- Entity updates
- World data streaming
- UI communication
## Key Components
| Component | Description |
|-----------|-------------|
| `Packet` | Base interface for all packets |
| `PacketRegistry` | Central packet type registration |
| `PacketHandler` | Manages client connections and packet processing |
| `ServerManager` | Server network manager |
## Packet Categories
Packets are organized into categories in `com.hypixel.hytale.protocol.packets`:
| Category | Description |
|----------|-------------|
| `connection/` | Connection lifecycle (connect, disconnect, ping) |
| `auth/` | Authentication and session management |
| `player/` | Player-specific packets (movement, actions) |
| `entities/` | Entity creation, updates, removal |
| `world/` | World/chunk data streaming |
| `inventory/` | Inventory operations |
| `interaction/` | Player interactions |
| `interface_/` | UI/interface packets |
| `camera/` | Camera control |
| `setup/` | Initial setup and configuration |
| `window/` | Window/container management |
| `assets/` | Asset loading and management |
| `asseteditor/` | Asset editor tools (dev) |
| `buildertools/` | Building tools |
| `machinima/` | Machinima/cinematic tools |
| `serveraccess/` | Server access control |
| `worldmap/` | World map data |
## Packet Structure
### Packet Interface
All packets implement the `Packet` interface:
```java
public interface Packet {
// Packets are typically data classes
// with fields for the packet data
}
```
### Example Packet
```java
public class PlayerPositionPacket implements Packet {
private final UUID playerId;
private final Vector3d position;
private final float yaw;
private final float pitch;
public PlayerPositionPacket(UUID playerId, Vector3d position, float yaw, float pitch) {
this.playerId = playerId;
this.position = position;
this.yaw = yaw;
this.pitch = pitch;
}
public UUID getPlayerId() { return playerId; }
public Vector3d getPosition() { return position; }
public float getYaw() { return yaw; }
public float getPitch() { return pitch; }
}
```
## Client Sessions
### Managing Clients
```java
// Get the server manager
ServerManager serverManager = ServerManager.get();
// Get handler for a client
PacketHandler handler = serverManager.getHandler(channel);
// Send packet to client
handler.sendPacket(packet);
// Send multiple packets
handler.sendPackets(Arrays.asList(packet1, packet2, packet3));
```
### Client Events
```java
// Player connected
getEventRegistry().register(PlayerConnectEvent.class, event -> {
Player player = event.getPlayer();
// Handle connection
});
// Player disconnected
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
Player player = event.getPlayer();
// Handle disconnection
});
```
## Sending Packets to Players
### Single Player
```java
Player player = getPlayer();
// Send a packet
player.sendPacket(new MyPacket(data));
```
### Multiple Players
```java
// Send to all players
for (Player player : Universe.get().getPlayers()) {
player.sendPacket(packet);
}
// Send to players in a world
World world = Universe.get().getWorld("default");
for (Player player : world.getPlayers()) {
player.sendPacket(packet);
}
// Broadcast to all
Universe.get().broadcast(packet);
```
### Area-Based Broadcasting
```java
// Send to players near a position
Vector3d center = new Vector3d(100, 64, 100);
double radius = 50.0;
for (Player player : Universe.get().getPlayers()) {
if (player.getPosition().distance(center) <= radius) {
player.sendPacket(packet);
}
}
```
## Common Packet Types
### Connection Packets
| Packet | Direction | Description |
|--------|-----------|-------------|
| `ConnectionRequestPacket` | C->S | Client requests connection |
| `ConnectionResponsePacket` | S->C | Server accepts/rejects connection |
| `DisconnectPacket` | Both | Connection termination |
| `PingPacket` | Both | Latency measurement |
### Player Packets
| Packet | Direction | Description |
|--------|-----------|-------------|
| `PlayerPositionPacket` | Both | Position update |
| `PlayerInputPacket` | C->S | Player input (movement, actions) |
| `PlayerActionPacket` | C->S | Player actions |
### Entity Packets
| Packet | Direction | Description |
|--------|-----------|-------------|
| `EntitySpawnPacket` | S->C | Entity creation |
| `EntityUpdatePacket` | S->C | Entity state update |
| `EntityRemovePacket` | S->C | Entity removal |
| `EntityMovePacket` | S->C | Entity movement |
### World Packets
| Packet | Direction | Description |
|--------|-----------|-------------|
| `ChunkDataPacket` | S->C | Chunk data transfer |
| `BlockChangePacket` | S->C | Block state change |
| `WorldTimePacket` | S->C | World time sync |
### Inventory Packets
| Packet | Direction | Description |
|--------|-----------|-------------|
| `InventoryUpdatePacket` | S->C | Inventory contents |
| `SlotChangePacket` | Both | Single slot update |
| `ItemPickupPacket` | S->C | Item pickup notification |
## Rate Limiting
The server includes built-in rate limiting for packet handling:
```java
// Configuration in HytaleServerConfig
RateLimitConfig {
enabled: boolean // Enable rate limiting
packetsPerSecond: int // Max packets per second
burstCapacity: int // Burst allowance
}
```
Rate limiting protects against packet spam and potential exploits.
## Connection Timeouts
```java
// Configuration in HytaleServerConfig
ConnectionTimeouts {
initialTimeout: Duration // Initial connection timeout
authTimeout: Duration // Authentication timeout
playTimeout: Duration // Gameplay timeout
joinTimeouts: Map<String, Duration> // Per-world join timeouts
}
```
## Custom Packet Handling
### Receiving Packets (Server-Side)
To handle incoming packets, use the receiver system:
```java
// Register a packet receiver
getPacketReceiverRegistry().register(MyCustomPacket.class, (player, packet) -> {
// Handle the packet
String data = packet.getData();
processData(player, data);
});
```
### Custom Packet Registration
For advanced mods that need custom packet types:
```java
// Define a custom packet with codec
public class MyCustomPacket implements Packet {
public static final Codec<MyCustomPacket> CODEC = BuilderCodec.of(MyCustomPacket::new)
.with("data", Codec.STRING, p -> p.data)
.build();
private final String data;
public MyCustomPacket(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
```
## UI Communication
### Server UI Pages
The server can send UI pages to clients:
```java
// Send a UI page to player
player.showUI(new MyUIPage(data));
// Close UI
player.closeUI();
```
### UI Packets
| Packet | Direction | Description |
|--------|-----------|-------------|
| `UIOpenPacket` | S->C | Open a UI screen |
| `UIClosePacket` | S->C | Close UI |
| `UIUpdatePacket` | S->C | Update UI data |
| `UIInteractionPacket` | C->S | UI button/element interaction |
## Network Compression
The server supports packet compression for bandwidth optimization:
```java
// In server config
localCompressionEnabled: boolean
```
When enabled, large packets are compressed before transmission.
## Authentication
### Auth Flow
1. Client sends `ConnectionRequestPacket`
2. Server validates and sends auth challenge
3. Client responds with credentials
4. Server verifies and sends `ConnectionResponsePacket`
5. If successful, player enters play state
### Auth Events
```java
// Handle authentication
getEventRegistry().register(PlayerAuthEvent.class, event -> {
Player player = event.getPlayer();
// Custom auth logic
if (!isAuthorized(player)) {
event.setCancelled(true);
event.setKickMessage("Not authorized!");
}
});
```
## Best Practices
1. **Minimize packet size** - Only send necessary data
2. **Batch updates** - Combine multiple small updates into single packets when possible
3. **Use appropriate packet types** - Don't repurpose packets for unintended uses
4. **Handle disconnections gracefully** - Clean up resources when players disconnect
5. **Respect rate limits** - Don't spam packets to clients
6. **Validate incoming data** - Never trust client data without validation
7. **Use async for heavy operations** - Don't block the network thread
8. **Consider bandwidth** - Players may have limited bandwidth
9. **Test latency scenarios** - Test with simulated network delays
10. **Secure sensitive operations** - Validate permissions server-side
## Security Considerations
- **Never trust client data** - Always validate on the server
- **Use server authority** - Server is the source of truth for game state
- **Validate permissions** - Check permissions before processing sensitive packets
- **Rate limit custom packets** - Prevent abuse of custom packet handlers
- **Sanitize inputs** - Prevent injection attacks in text data

434
docs/08-asset-system.md Normal file
View File

@@ -0,0 +1,434 @@
# Asset System
The Hytale asset system manages game assets including blocks, items, models, sounds, particles, and more. This guide covers how to work with assets and create custom content.
## Overview
Assets are defined in JSON files and loaded by the `AssetStore`. The server provides a type-safe asset system with:
- JSON-based asset definitions
- Codec-based serialization
- Asset type registration
- Asset pack support for mods
## Asset Store
### Accessing Assets
```java
// Get the asset store
AssetStore store = HytaleAssetStore.get();
// Get a specific asset by ID
BlockType block = store.get(BlockType.class, "stone");
ItemType item = store.get(ItemType.class, "sword");
// Get all assets of a type
Collection<BlockType> allBlocks = store.getAll(BlockType.class);
```
### Asset Loading
Assets are loaded from:
1. Core game assets
2. Plugin asset packs (when `IncludesAssetPack: true` in manifest)
## Asset Types
The server supports many built-in asset types located in `server/core/asset/type/`:
| Asset Type | Description |
|------------|-------------|
| `BlockType` | Block definitions (stone, dirt, etc.) |
| `ItemType` | Item definitions |
| `ModelAsset` | 3D model definitions |
| `SoundEvent` | Sound effect definitions |
| `ParticleAsset` | Particle system definitions |
| `WeatherAsset` | Weather type definitions |
| `EnvironmentAsset` | Environment settings |
| `FluidAsset` | Fluid definitions |
| `EntityEffect` | Entity effect definitions |
| `BiomeAsset` | Biome definitions |
| `StructureAsset` | Structure/prefab definitions |
## Blocks
### Block Type Definition
Block types are defined in JSON:
```json
{
"id": "mymod:custom_block",
"name": "Custom Block",
"model": "mymod:models/custom_block",
"hardness": 2.0,
"resistance": 6.0,
"drops": ["mymod:custom_block"],
"sounds": {
"break": "block.stone.break",
"place": "block.stone.place",
"step": "block.stone.step"
},
"properties": {
"transparent": false,
"solid": true,
"flammable": false
}
}
```
### Block States
Blocks can have multiple states (orientations, variations):
```java
// Get block state
BlockState state = BlockStates.get("stone");
// Create state with properties
BlockState orientedBlock = BlockStates.get("log", Map.of(
"axis", "y"
));
// Check block properties
boolean isSolid = state.isSolid();
boolean isTransparent = state.isTransparent();
```
### Registering Block States
```java
@Override
protected void setup() {
getBlockStateRegistry().register("custom_block", CustomBlockState.class);
}
```
## Items
### Item Type Definition
```json
{
"id": "mymod:custom_sword",
"name": "Custom Sword",
"model": "mymod:models/custom_sword",
"maxStackSize": 1,
"durability": 500,
"itemType": "weapon",
"damage": 7.0,
"attackSpeed": 1.6,
"enchantable": true
}
```
### Working with Items
```java
// Get item type
ItemType swordType = store.get(ItemType.class, "mymod:custom_sword");
// Create item stack
ItemStack stack = new ItemStack(swordType, 1);
// Set item data
stack.setDurability(400);
stack.setCustomName("Legendary Sword");
// Add to inventory
player.getInventory().addItem(stack);
```
## Models
### Model Asset Definition
```json
{
"id": "mymod:models/custom_block",
"type": "block",
"textures": {
"all": "mymod:textures/custom_block"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": {"texture": "#all"},
"south": {"texture": "#all"},
"east": {"texture": "#all"},
"west": {"texture": "#all"},
"up": {"texture": "#all"},
"down": {"texture": "#all"}
}
}
]
}
```
## Sounds
### Sound Event Definition
```json
{
"id": "mymod:custom_sound",
"sounds": [
{
"name": "mymod:sounds/custom1",
"volume": 1.0,
"pitch": 1.0,
"weight": 1
},
{
"name": "mymod:sounds/custom2",
"volume": 0.8,
"pitch": 1.2,
"weight": 1
}
],
"category": "block"
}
```
### Playing Sounds
```java
// Get sound event
SoundEvent sound = store.get(SoundEvent.class, "mymod:custom_sound");
// Play at position
world.playSound(sound, position, volume, pitch);
// Play to specific player
player.playSound(sound, volume, pitch);
// Play to all nearby players
world.playSoundNearby(sound, position, radius, volume, pitch);
```
## Particles
### Particle Asset Definition
```json
{
"id": "mymod:custom_particle",
"texture": "mymod:textures/particles/custom",
"lifetime": 20,
"scale": 1.0,
"gravity": 0.04,
"color": [1.0, 1.0, 1.0, 1.0],
"fadeOut": true
}
```
### Spawning Particles
```java
// Get particle asset
ParticleAsset particle = store.get(ParticleAsset.class, "mymod:custom_particle");
// Spawn particles
world.spawnParticle(particle, position, count, spreadX, spreadY, spreadZ, speed);
// Spawn for specific player
player.spawnParticle(particle, position, count);
```
## Custom Asset Types
### Defining a Custom Asset Type
```java
public class MyCustomAsset {
public static final Codec<MyCustomAsset> CODEC = BuilderCodec.of(MyCustomAsset::new)
.with("id", Codec.STRING, a -> a.id)
.with("value", Codec.INTEGER, a -> a.value, 0)
.with("enabled", Codec.BOOLEAN, a -> a.enabled, true)
.build();
private final String id;
private final int value;
private final boolean enabled;
public MyCustomAsset(String id, int value, boolean enabled) {
this.id = id;
this.value = value;
this.enabled = enabled;
}
public String getId() { return id; }
public int getValue() { return value; }
public boolean isEnabled() { return enabled; }
}
```
### Registering Custom Asset Types
```java
@Override
protected void setup() {
getAssetRegistry().register(MyCustomAsset.class, MyCustomAsset.CODEC);
}
```
### Using Custom Assets
```java
@Override
protected void start() {
AssetStore store = HytaleAssetStore.get();
// Get all custom assets
Collection<MyCustomAsset> assets = store.getAll(MyCustomAsset.class);
for (MyCustomAsset asset : assets) {
if (asset.isEnabled()) {
processAsset(asset);
}
}
}
```
## Asset Packs
### Creating an Asset Pack
1. Set `IncludesAssetPack: true` in your `manifest.json`
2. Create an `assets/` directory in your JAR
3. Organize assets following Hytale's structure:
```
assets/
├── mymod/
│ ├── blocks/
│ │ └── custom_block.json
│ ├── items/
│ │ └── custom_item.json
│ ├── models/
│ │ ├── block/
│ │ │ └── custom_block.json
│ │ └── item/
│ │ └── custom_item.json
│ ├── textures/
│ │ ├── blocks/
│ │ │ └── custom_block.png
│ │ └── items/
│ │ └── custom_item.png
│ └── sounds/
│ └── custom_sound.ogg
```
### Asset Namespacing
Assets are namespaced by plugin ID:
```
{group}:{name} -> mymod:custom_block
```
Reference assets using their full namespaced ID to avoid conflicts.
## Entity Assets
### Entity Type Definition
Entities can have associated assets:
```json
{
"id": "mymod:custom_mob",
"name": "Custom Mob",
"model": "mymod:models/entity/custom_mob",
"textures": {
"default": "mymod:textures/entity/custom_mob"
},
"animations": {
"idle": "mymod:animations/custom_mob_idle",
"walk": "mymod:animations/custom_mob_walk",
"attack": "mymod:animations/custom_mob_attack"
},
"sounds": {
"hurt": "mymod:entity.custom_mob.hurt",
"death": "mymod:entity.custom_mob.death",
"ambient": "mymod:entity.custom_mob.ambient"
},
"attributes": {
"maxHealth": 20.0,
"movementSpeed": 0.3,
"attackDamage": 4.0
}
}
```
## Weather Assets
### Weather Definition
```json
{
"id": "mymod:custom_weather",
"name": "Custom Weather",
"particles": "mymod:weather_particle",
"skyColor": [0.5, 0.5, 0.7],
"fogColor": [0.6, 0.6, 0.8],
"lightLevel": 0.7,
"sounds": {
"ambient": "mymod:weather.custom.ambient"
}
}
```
## Environment Assets
### Environment Definition
```json
{
"id": "mymod:custom_environment",
"skybox": "mymod:textures/skybox/custom",
"ambientLight": 0.4,
"sunLight": 1.0,
"fogDensity": 0.02,
"music": "mymod:music/custom_ambient"
}
```
## Best Practices
1. **Use namespaces** - Always prefix asset IDs with your mod namespace
2. **Follow conventions** - Use lowercase, underscores for asset IDs
3. **Optimize textures** - Use appropriate resolutions and compression
4. **Provide fallbacks** - Handle missing assets gracefully
5. **Document assets** - Comment complex asset definitions
6. **Test loading** - Verify all assets load correctly
7. **Version assets** - Track asset changes with mod versions
8. **Reuse when possible** - Reference existing assets instead of duplicating
## Troubleshooting
### Asset Not Found
```java
// Check if asset exists
if (store.has(MyAsset.class, "mymod:asset_id")) {
MyAsset asset = store.get(MyAsset.class, "mymod:asset_id");
}
```
### Asset Loading Errors
- Check JSON syntax
- Verify codec definitions match JSON structure
- Ensure all referenced assets (textures, models) exist
- Check namespace spelling
### Asset Pack Not Loading
- Verify `IncludesAssetPack: true` in manifest
- Check asset directory structure
- Ensure JAR file includes assets

457
docs/10-configuration.md Normal file
View File

@@ -0,0 +1,457 @@
# Configuration System
The Hytale server provides a robust configuration system for both server settings and plugin configurations. This guide covers how to use and extend the configuration system.
## Server Configuration
### HytaleServerConfig
The main server configuration is stored in `HytaleServerConfig`:
```java
HytaleServerConfig config = HytaleServer.get().getConfig();
// Access configuration values
String serverName = config.getServerName();
String motd = config.getMotd();
int maxPlayers = config.getMaxPlayers();
int maxViewRadius = config.getMaxViewRadius();
```
### Server Configuration Structure
```java
HytaleServerConfig {
// Basic settings
serverName: String // Server display name
motd: String // Message of the day
password: String // Server password (optional)
maxPlayers: int // Maximum player count
maxViewRadius: int // Maximum view distance
localCompressionEnabled: boolean // Enable packet compression
// Nested configurations
Defaults {
world: String // Default world name
gameMode: GameMode // Default game mode
}
ConnectionTimeouts {
initialTimeout: Duration
authTimeout: Duration
playTimeout: Duration
joinTimeouts: Map<String, Duration>
}
RateLimitConfig {
enabled: boolean
packetsPerSecond: int
burstCapacity: int
}
// Dynamic configurations
modules: Map<String, Module>
logLevels: Map<String, Level>
modConfig: Map<PluginIdentifier, ModConfig>
// Storage providers
playerStorageProvider: PlayerStorageProvider
authCredentialStoreConfig: BsonDocument
}
```
## Plugin Configuration
### Defining Plugin Config
Create a configuration class with a Codec:
```java
public class MyPluginConfig {
// Define the codec for serialization
public static final Codec<MyPluginConfig> CODEC = BuilderCodec.of(MyPluginConfig::new)
.with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
.with("maxItems", Codec.INTEGER, c -> c.maxItems, 100)
.with("welcomeMessage", Codec.STRING, c -> c.welcomeMessage, "Welcome!")
.with("debugMode", Codec.BOOLEAN, c -> c.debugMode, false)
.with("spawnLocation", Vector3d.CODEC, c -> c.spawnLocation, new Vector3d(0, 64, 0))
.build();
// Configuration fields (final for immutability)
public final boolean enabled;
public final int maxItems;
public final String welcomeMessage;
public final boolean debugMode;
public final Vector3d spawnLocation;
// Private constructor used by codec
private MyPluginConfig(boolean enabled, int maxItems, String welcomeMessage,
boolean debugMode, Vector3d spawnLocation) {
this.enabled = enabled;
this.maxItems = maxItems;
this.welcomeMessage = welcomeMessage;
this.debugMode = debugMode;
this.spawnLocation = spawnLocation;
}
}
```
### Using Plugin Config
```java
public class MyPlugin extends JavaPlugin {
private Config<MyPluginConfig> config;
public MyPlugin(JavaPluginInit init) {
super(init);
// Initialize config BEFORE setup() is called
this.config = withConfig(MyPluginConfig.CODEC);
}
@Override
protected void setup() {
MyPluginConfig cfg = config.get();
if (!cfg.enabled) {
getLogger().info("Plugin is disabled in config");
return;
}
getLogger().info("Max items: " + cfg.maxItems);
getLogger().info("Debug mode: " + cfg.debugMode);
}
@Override
protected void start() {
MyPluginConfig cfg = config.get();
// Use config values
}
// Provide access to config for other classes
public MyPluginConfig getPluginConfig() {
return config.get();
}
}
```
### Configuration Storage
Plugin configurations are stored in the server's main config under the plugin identifier:
```json
{
"modConfig": {
"com.example:MyPlugin": {
"enabled": true,
"maxItems": 100,
"welcomeMessage": "Welcome!",
"debugMode": false,
"spawnLocation": {
"x": 0.0,
"y": 64.0,
"z": 0.0
}
}
}
}
```
## Nested Configurations
### Defining Nested Config
```java
public class AdvancedConfig {
public static final Codec<AdvancedConfig> CODEC = BuilderCodec.of(AdvancedConfig::new)
.with("general", GeneralConfig.CODEC, c -> c.general, new GeneralConfig())
.with("features", FeaturesConfig.CODEC, c -> c.features, new FeaturesConfig())
.with("limits", LimitsConfig.CODEC, c -> c.limits, new LimitsConfig())
.build();
public final GeneralConfig general;
public final FeaturesConfig features;
public final LimitsConfig limits;
private AdvancedConfig(GeneralConfig general, FeaturesConfig features, LimitsConfig limits) {
this.general = general;
this.features = features;
this.limits = limits;
}
}
public class GeneralConfig {
public static final Codec<GeneralConfig> CODEC = BuilderCodec.of(GeneralConfig::new)
.with("name", Codec.STRING, c -> c.name, "Default")
.with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
.build();
public final String name;
public final boolean enabled;
public GeneralConfig() {
this("Default", true);
}
private GeneralConfig(String name, boolean enabled) {
this.name = name;
this.enabled = enabled;
}
}
```
## Lists and Maps in Config
### List Configuration
```java
public class ListConfig {
public static final Codec<ListConfig> CODEC = BuilderCodec.of(ListConfig::new)
.with("allowedPlayers", Codec.STRING.listOf(), c -> c.allowedPlayers, List.of())
.with("spawnPoints", Vector3d.CODEC.listOf(), c -> c.spawnPoints, List.of())
.build();
public final List<String> allowedPlayers;
public final List<Vector3d> spawnPoints;
private ListConfig(List<String> allowedPlayers, List<Vector3d> spawnPoints) {
this.allowedPlayers = List.copyOf(allowedPlayers);
this.spawnPoints = List.copyOf(spawnPoints);
}
}
```
### Map Configuration
```java
public class MapConfig {
public static final Codec<MapConfig> CODEC = BuilderCodec.of(MapConfig::new)
.with("playerHomes", Codec.mapOf(Codec.STRING, Vector3d.CODEC), c -> c.playerHomes, Map.of())
.with("warpPoints", Codec.mapOf(Codec.STRING, WarpConfig.CODEC), c -> c.warpPoints, Map.of())
.build();
public final Map<String, Vector3d> playerHomes;
public final Map<String, WarpConfig> warpPoints;
private MapConfig(Map<String, Vector3d> playerHomes, Map<String, WarpConfig> warpPoints) {
this.playerHomes = Map.copyOf(playerHomes);
this.warpPoints = Map.copyOf(warpPoints);
}
}
```
## Optional Configuration Values
### Using Optional
```java
public class OptionalConfig {
public static final Codec<OptionalConfig> CODEC = BuilderCodec.of(OptionalConfig::new)
.with("requiredValue", Codec.STRING, c -> c.requiredValue)
.withOptional("optionalValue", Codec.STRING, c -> c.optionalValue)
.build();
public final String requiredValue;
public final Optional<String> optionalValue;
private OptionalConfig(String requiredValue, Optional<String> optionalValue) {
this.requiredValue = requiredValue;
this.optionalValue = optionalValue;
}
}
```
### Nullable Values with Defaults
```java
.with("value", Codec.STRING, c -> c.value, "default") // Default if not present
.withOptional("value", Codec.STRING, c -> c.value) // Optional, may be absent
```
## Configuration Validation
### Custom Validation
```java
public class ValidatedConfig {
public static final Codec<ValidatedConfig> CODEC = BuilderCodec.of(ValidatedConfig::new)
.with("port", Codec.INTEGER.validate(p -> p > 0 && p < 65536, "Port must be 1-65535"),
c -> c.port, 25565)
.with("name", Codec.STRING.validate(s -> !s.isBlank(), "Name cannot be blank"),
c -> c.name, "Server")
.build();
public final int port;
public final String name;
private ValidatedConfig(int port, String name) {
this.port = port;
this.name = name;
}
}
```
### Range Validation
```java
.with("level", Codec.INTEGER.clamp(1, 100), c -> c.level, 1)
```
## Enum Configuration
```java
public enum Difficulty {
EASY, NORMAL, HARD, HARDCORE
}
public class GameConfig {
public static final Codec<GameConfig> CODEC = BuilderCodec.of(GameConfig::new)
.with("difficulty", Codec.enumCodec(Difficulty.class), c -> c.difficulty, Difficulty.NORMAL)
.build();
public final Difficulty difficulty;
private GameConfig(Difficulty difficulty) {
this.difficulty = difficulty;
}
}
```
## Runtime Configuration Access
### Reading Config Values
```java
// In your plugin
MyPluginConfig cfg = config.get();
// Use values
if (cfg.enabled) {
doSomething(cfg.maxItems);
}
```
### Checking Config Changes
Configuration is typically loaded at startup. For dynamic updates:
```java
// Store reference to check later
private MyPluginConfig lastConfig;
@Override
protected void start() {
lastConfig = config.get();
}
// Check for changes (if config reload is supported)
public void checkConfigUpdate() {
MyPluginConfig current = config.get();
if (!current.equals(lastConfig)) {
onConfigChanged(lastConfig, current);
lastConfig = current;
}
}
```
## Per-Module Configuration
### Module Config Pattern
```java
// Server modules can have their own config sections
Map<String, Module> modules = serverConfig.getModules();
// Get specific module config
Module myModule = modules.get("myModule");
if (myModule != null && myModule.isEnabled()) {
// Module-specific logic
}
```
## Best Practices
1. **Use immutable configs** - Make config fields `final` and use `List.copyOf()`, `Map.copyOf()`
2. **Provide sensible defaults** - Every config field should have a reasonable default
3. **Validate early** - Use codec validation to catch invalid values at load time
4. **Document config options** - Create documentation for all config options
5. **Use nested configs** - Organize related settings into nested config classes
6. **Keep configs simple** - Don't over-complicate with too many options
7. **Type safety** - Use enums for fixed choices, proper types for values
8. **Test default configs** - Ensure your plugin works with all-default configuration
9. **Handle missing configs** - Gracefully handle when config file doesn't exist
10. **Version your configs** - Consider config versioning for migrations
## Configuration Example
Complete example of a well-structured plugin configuration:
```java
public class ShopPluginConfig {
public static final Codec<ShopPluginConfig> CODEC = BuilderCodec.of(ShopPluginConfig::new)
.with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
.with("currency", CurrencyConfig.CODEC, c -> c.currency, new CurrencyConfig())
.with("shops", ShopsConfig.CODEC, c -> c.shops, new ShopsConfig())
.with("debug", Codec.BOOLEAN, c -> c.debug, false)
.build();
public final boolean enabled;
public final CurrencyConfig currency;
public final ShopsConfig shops;
public final boolean debug;
private ShopPluginConfig(boolean enabled, CurrencyConfig currency,
ShopsConfig shops, boolean debug) {
this.enabled = enabled;
this.currency = currency;
this.shops = shops;
this.debug = debug;
}
}
public class CurrencyConfig {
public static final Codec<CurrencyConfig> CODEC = BuilderCodec.of(CurrencyConfig::new)
.with("name", Codec.STRING, c -> c.name, "Gold")
.with("symbol", Codec.STRING, c -> c.symbol, "G")
.with("startingAmount", Codec.INTEGER.clamp(0, 1000000), c -> c.startingAmount, 100)
.build();
public final String name;
public final String symbol;
public final int startingAmount;
public CurrencyConfig() { this("Gold", "G", 100); }
private CurrencyConfig(String name, String symbol, int startingAmount) {
this.name = name;
this.symbol = symbol;
this.startingAmount = startingAmount;
}
}
```
JSON representation:
```json
{
"enabled": true,
"currency": {
"name": "Gold",
"symbol": "G",
"startingAmount": 100
},
"shops": {
"maxItems": 54,
"allowPlayerShops": true
},
"debug": false
}
```

View File

@@ -0,0 +1,402 @@
# Codec and Serialization System
The Hytale server uses a powerful codec system for data serialization and deserialization. This system is used throughout the codebase for configuration, network packets, asset definitions, and data persistence.
## Overview
The codec system provides:
- Type-safe serialization/deserialization
- Support for JSON and BSON formats
- Validation and schema generation
- Composable codecs for complex types
- Builder pattern for object construction
## Core Concepts
### Codec Interface
```java
public interface Codec<T> {
T decode(DataInput input);
void encode(T value, DataOutput output);
}
```
### DataInput/DataOutput
Codecs work with abstract `DataInput` and `DataOutput` interfaces that can represent different formats (JSON, BSON, etc.).
## Primitive Codecs
The `Codec` class provides built-in codecs for primitive types:
```java
// String
Codec.STRING // "hello"
// Numbers
Codec.INTEGER // 42
Codec.LONG // 123456789L
Codec.FLOAT // 3.14f
Codec.DOUBLE // 3.14159
// Boolean
Codec.BOOLEAN // true/false
// Byte arrays
Codec.BYTE_ARRAY // [1, 2, 3]
```
## Collection Codecs
### Lists
```java
// List of strings
Codec<List<String>> stringList = Codec.STRING.listOf();
// List of integers
Codec<List<Integer>> intList = Codec.INTEGER.listOf();
// List of custom objects
Codec<List<MyObject>> objectList = MyObject.CODEC.listOf();
```
### Sets
```java
// Set of strings
Codec<Set<String>> stringSet = Codec.STRING.setOf();
```
### Maps
```java
// Map with string keys
Codec<Map<String, Integer>> stringToInt = Codec.mapOf(Codec.STRING, Codec.INTEGER);
// Map with custom key type
Codec<Map<UUID, PlayerData>> playerMap = Codec.mapOf(UUID_CODEC, PlayerData.CODEC);
```
### Arrays
```java
// Array codec
Codec<String[]> stringArray = Codec.arrayOf(Codec.STRING, String[]::new);
```
## BuilderCodec
`BuilderCodec` is the primary way to create codecs for complex objects:
### Basic Usage
```java
public class Person {
public static final Codec<Person> CODEC = BuilderCodec.of(Person::new)
.with("name", Codec.STRING, p -> p.name)
.with("age", Codec.INTEGER, p -> p.age)
.with("email", Codec.STRING, p -> p.email)
.build();
private final String name;
private final int age;
private final String email;
private Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
```
### With Default Values
```java
public static final Codec<Settings> CODEC = BuilderCodec.of(Settings::new)
.with("volume", Codec.FLOAT, s -> s.volume, 1.0f) // Default: 1.0
.with("muted", Codec.BOOLEAN, s -> s.muted, false) // Default: false
.with("language", Codec.STRING, s -> s.language, "en") // Default: "en"
.build();
```
### Optional Fields
```java
public static final Codec<User> CODEC = BuilderCodec.of(User::new)
.with("username", Codec.STRING, u -> u.username)
.withOptional("nickname", Codec.STRING, u -> u.nickname) // May be absent
.build();
```
### Nested Objects
```java
public class Address {
public static final Codec<Address> CODEC = BuilderCodec.of(Address::new)
.with("street", Codec.STRING, a -> a.street)
.with("city", Codec.STRING, a -> a.city)
.with("zip", Codec.STRING, a -> a.zip)
.build();
// ...
}
public class Customer {
public static final Codec<Customer> CODEC = BuilderCodec.of(Customer::new)
.with("name", Codec.STRING, c -> c.name)
.with("address", Address.CODEC, c -> c.address)
.build();
// ...
}
```
## KeyedCodec
For objects that have a key/identifier:
```java
public class ItemType {
public static final KeyedCodec<String, ItemType> KEYED_CODEC = KeyedCodec.of(
"id",
Codec.STRING,
ItemType::getId,
ItemType.CODEC
);
// ...
}
```
## Codec Transformations
### Mapping Values
```java
// Transform between types
Codec<UUID> UUID_CODEC = Codec.STRING.xmap(
UUID::fromString, // decode: String -> UUID
UUID::toString // encode: UUID -> String
);
// Transform integers to enum
Codec<MyEnum> ENUM_CODEC = Codec.INTEGER.xmap(
i -> MyEnum.values()[i],
MyEnum::ordinal
);
```
### Validation
```java
// Validate during decode
Codec<Integer> PORT_CODEC = Codec.INTEGER.validate(
port -> port > 0 && port < 65536,
"Port must be between 1 and 65535"
);
// Clamp values
Codec<Float> VOLUME_CODEC = Codec.FLOAT.clamp(0.0f, 1.0f);
```
### Enum Codecs
```java
// Automatic enum codec
Codec<GameMode> GAME_MODE_CODEC = Codec.enumCodec(GameMode.class);
// Custom enum serialization
Codec<Direction> DIRECTION_CODEC = Codec.STRING.xmap(
Direction::valueOf,
Direction::name
);
```
## Complex Examples
### Polymorphic Types
```java
// For types with multiple implementations
public interface Shape {
Codec<Shape> CODEC = Codec.dispatch(
"type",
Codec.STRING,
shape -> shape.getType(),
type -> switch (type) {
case "circle" -> Circle.CODEC;
case "rectangle" -> Rectangle.CODEC;
default -> throw new IllegalArgumentException("Unknown shape: " + type);
}
);
String getType();
}
public class Circle implements Shape {
public static final Codec<Circle> CODEC = BuilderCodec.of(Circle::new)
.with("radius", Codec.DOUBLE, c -> c.radius)
.build();
@Override
public String getType() { return "circle"; }
// ...
}
```
### Recursive Types
```java
public class TreeNode {
public static final Codec<TreeNode> CODEC = BuilderCodec.of(TreeNode::new)
.with("value", Codec.STRING, n -> n.value)
.with("children", Codec.lazy(() -> TreeNode.CODEC.listOf()), n -> n.children, List.of())
.build();
private final String value;
private final List<TreeNode> children;
// ...
}
```
### Record Types (Java 16+)
```java
public record Point(double x, double y, double z) {
public static final Codec<Point> CODEC = BuilderCodec.of(Point::new)
.with("x", Codec.DOUBLE, Point::x)
.with("y", Codec.DOUBLE, Point::y)
.with("z", Codec.DOUBLE, Point::z)
.build();
}
```
## Vector and Math Codecs
The math library provides codecs for common types:
```java
// 3D Vector (double)
Vector3d.CODEC // {"x": 1.0, "y": 2.0, "z": 3.0}
// 3D Vector (int)
Vector3i.CODEC // {"x": 1, "y": 2, "z": 3}
// 2D Vector
Vector2d.CODEC // {"x": 1.0, "y": 2.0}
```
## Working with JSON
### Encoding to JSON
```java
// Create JSON output
JsonDataOutput output = new JsonDataOutput();
MyObject.CODEC.encode(myObject, output);
String json = output.toJsonString();
```
### Decoding from JSON
```java
// Parse JSON input
JsonDataInput input = JsonDataInput.fromString(jsonString);
MyObject obj = MyObject.CODEC.decode(input);
```
## Working with BSON
### BSON Encoding/Decoding
```java
// BSON output
BsonDataOutput output = new BsonDataOutput();
MyObject.CODEC.encode(myObject, output);
BsonDocument bson = output.toBsonDocument();
// BSON input
BsonDataInput input = new BsonDataInput(bsonDocument);
MyObject obj = MyObject.CODEC.decode(input);
```
## Schema Generation
Codecs can generate JSON schemas for documentation:
```java
// Generate schema
JsonSchema schema = MyObject.CODEC.generateSchema();
String schemaJson = schema.toJson();
```
## Best Practices
1. **Make codecs static final** - Codecs are immutable and should be reused
2. **Use BuilderCodec for objects** - It's the most flexible approach
3. **Provide defaults** - Use sensible defaults for optional fields
4. **Validate input** - Use validation to catch errors early
5. **Keep codecs near their classes** - Define codec as a static field in the class
6. **Test serialization roundtrips** - Ensure encode/decode produces identical objects
7. **Use meaningful field names** - JSON keys should be clear and consistent
8. **Handle null carefully** - Use Optional or defaults for nullable fields
9. **Consider versioning** - Plan for schema evolution
10. **Document complex codecs** - Add comments for non-obvious serialization
## Error Handling
### Decode Errors
```java
try {
MyObject obj = MyObject.CODEC.decode(input);
} catch (CodecException e) {
// Handle missing fields, wrong types, validation failures
logger.error("Failed to decode: " + e.getMessage());
}
```
### Validation Errors
```java
Codec<Integer> validated = Codec.INTEGER.validate(
i -> i > 0,
"Value must be positive"
);
// Throws CodecException with message "Value must be positive"
validated.decode(input); // if input is <= 0
```
## Integration with Assets
Asset types use codecs for JSON definitions:
```java
public class BlockType {
public static final Codec<BlockType> CODEC = BuilderCodec.of(BlockType::new)
.with("id", Codec.STRING, b -> b.id)
.with("name", Codec.STRING, b -> b.name)
.with("hardness", Codec.FLOAT, b -> b.hardness, 1.0f)
.with("drops", Codec.STRING.listOf(), b -> b.drops, List.of())
.build();
// ...
}
```
## Integration with Packets
Network packets use codecs for serialization:
```java
public class PlayerPositionPacket implements Packet {
public static final Codec<PlayerPositionPacket> CODEC = BuilderCodec.of(PlayerPositionPacket::new)
.with("playerId", UUID_CODEC, p -> p.playerId)
.with("position", Vector3d.CODEC, p -> p.position)
.with("yaw", Codec.FLOAT, p -> p.yaw)
.with("pitch", Codec.FLOAT, p -> p.pitch)
.build();
// ...
}
```

416
docs/13-utilities.md Normal file
View File

@@ -0,0 +1,416 @@
# Utilities and Common APIs
The Hytale server provides a comprehensive set of utility classes and common APIs. This guide covers the most useful utilities for mod development.
## Math Utilities
### Vectors
The math package provides vector classes for 2D and 3D operations:
```java
// 3D Vectors
Vector3d posD = new Vector3d(1.5, 2.5, 3.5); // double precision
Vector3f posF = new Vector3f(1.5f, 2.5f, 3.5f); // float precision
Vector3i posI = new Vector3i(1, 2, 3); // integer
// 2D Vectors
Vector2d pos2D = new Vector2d(1.5, 2.5);
Vector2i pos2I = new Vector2i(1, 2);
// Vector operations
Vector3d a = new Vector3d(1, 2, 3);
Vector3d b = new Vector3d(4, 5, 6);
Vector3d sum = a.add(b); // (5, 7, 9)
Vector3d diff = a.subtract(b); // (-3, -3, -3)
Vector3d scaled = a.multiply(2); // (2, 4, 6)
double dot = a.dot(b); // 32
Vector3d cross = a.cross(b); // (-3, 6, -3)
double length = a.length(); // ~3.74
Vector3d normalized = a.normalize(); // unit vector
double distance = a.distance(b); // distance between points
```
### Vector Codecs
```java
// Serialization
Vector3d.CODEC // {"x": 1.0, "y": 2.0, "z": 3.0}
Vector3i.CODEC // {"x": 1, "y": 2, "z": 3}
Vector2d.CODEC // {"x": 1.0, "y": 2.0}
```
### Shapes
```java
// Bounding box
Box box = new Box(minPoint, maxPoint);
boolean contains = box.contains(point);
boolean intersects = box.intersects(otherBox);
Vector3d center = box.getCenter();
Vector3d size = box.getSize();
// Sphere/Ellipsoid
Ellipsoid sphere = new Ellipsoid(center, radius, radius, radius);
boolean inSphere = sphere.contains(point);
// Cylinder
Cylinder cyl = new Cylinder(base, height, radius);
```
### Transform
```java
// Transform with position and rotation
Transform transform = new Transform(position, rotation);
Vector3d worldPos = transform.toWorld(localPos);
Vector3d localPos = transform.toLocal(worldPos);
```
### Location
```java
// Location combines world, position, and rotation
Location loc = new Location(world, position, yaw, pitch);
World world = loc.getWorld();
Vector3d pos = loc.getPosition();
```
### Math Utilities
```java
// MathUtil
double clamped = MathUtil.clamp(value, min, max);
double lerp = MathUtil.lerp(start, end, t);
int floor = MathUtil.floor(3.7); // 3
int ceil = MathUtil.ceil(3.1); // 4
double wrap = MathUtil.wrap(angle, 0, 360);
// TrigMathUtil
double sin = TrigMathUtil.sin(angle);
double cos = TrigMathUtil.cos(angle);
double atan2 = TrigMathUtil.atan2(y, x);
```
## Collection Utilities
### ArrayUtil
```java
// Array operations
String[] combined = ArrayUtil.combine(array1, array2);
int index = ArrayUtil.indexOf(array, element);
boolean contains = ArrayUtil.contains(array, element);
String[] filtered = ArrayUtil.filter(array, predicate);
```
### ListUtil
```java
// List operations
List<String> shuffled = ListUtil.shuffle(list);
List<String> filtered = ListUtil.filter(list, predicate);
Optional<String> random = ListUtil.random(list);
List<List<String>> partitioned = ListUtil.partition(list, 10);
```
### MapUtil
```java
// Map operations
Map<K, V> filtered = MapUtil.filter(map, predicate);
Map<K, V> merged = MapUtil.merge(map1, map2);
<K, V> V getOrCreate(Map<K, V> map, K key, Supplier<V> creator);
```
### WeightedMap
For weighted random selection:
```java
WeightedMap<String> lootTable = new WeightedMap<>();
lootTable.add("common_item", 70);
lootTable.add("rare_item", 25);
lootTable.add("epic_item", 5);
// Random selection based on weights
String selected = lootTable.random(); // 70% common, 25% rare, 5% epic
```
## String Utilities
### StringUtil
```java
// String operations
boolean isEmpty = StringUtil.isEmpty(str);
boolean isBlank = StringUtil.isBlank(str);
String trimmed = StringUtil.trim(str);
String capitalized = StringUtil.capitalize("hello"); // "Hello"
String joined = StringUtil.join(list, ", ");
List<String> split = StringUtil.split(str, ",");
// Formatting
String formatted = StringUtil.format("{0} has {1} items", player, count);
```
### FormatUtil
```java
// Number formatting
String formatted = FormatUtil.formatNumber(1234567); // "1,234,567"
String decimal = FormatUtil.formatDecimal(3.14159, 2); // "3.14"
String percent = FormatUtil.formatPercent(0.75); // "75%"
// Time formatting
String duration = FormatUtil.formatDuration(3661000); // "1h 1m 1s"
String time = FormatUtil.formatTime(timestamp);
```
## Time Utilities
### TimeUtil
```java
// Time conversions
long ticks = TimeUtil.secondsToTicks(5); // 100 ticks
long seconds = TimeUtil.ticksToSeconds(100); // 5 seconds
long millis = TimeUtil.ticksToMillis(20); // 1000ms
// Current time
long now = TimeUtil.now();
long serverTick = TimeUtil.currentTick();
// Duration parsing
Duration duration = TimeUtil.parseDuration("1h30m");
```
### Tickable Interface
```java
public interface Tickable {
void tick();
}
// Implement for objects that update each tick
public class MyTickable implements Tickable {
@Override
public void tick() {
// Update logic
}
}
```
## Version Management
### Semver
```java
// Semantic versioning
Semver version = Semver.parse("1.2.3");
int major = version.getMajor(); // 1
int minor = version.getMinor(); // 2
int patch = version.getPatch(); // 3
// Comparison
boolean isNewer = version.isNewerThan(Semver.parse("1.2.0"));
boolean isCompatible = version.isCompatibleWith(Semver.parse("1.0.0"));
```
### SemverRange
```java
// Version ranges
SemverRange range = SemverRange.parse(">=1.0.0 <2.0.0");
boolean matches = range.matches(Semver.parse("1.5.0")); // true
boolean matches2 = range.matches(Semver.parse("2.0.0")); // false
// Common patterns
SemverRange.parse(">=1.0.0"); // 1.0.0 and above
SemverRange.parse("~1.2.3"); // >=1.2.3 <1.3.0
SemverRange.parse("^1.2.3"); // >=1.2.3 <2.0.0
```
## Random Utilities
### Random Number Generation
```java
// Thread-safe random
Random random = RandomUtil.getRandom();
int randInt = random.nextInt(100);
double randDouble = random.nextDouble();
boolean randBool = random.nextBoolean();
// Range-based random
int inRange = RandomUtil.nextInt(10, 20); // 10-19
double inRangeD = RandomUtil.nextDouble(1.0, 5.0);
// Random selection
String selected = RandomUtil.select(list);
String[] selectedMultiple = RandomUtil.select(list, 3);
```
## Logging
### Logger Access
```java
// In your plugin
Logger logger = getLogger();
logger.info("Information message");
logger.warn("Warning message");
logger.error("Error message");
logger.debug("Debug message");
// With formatting
logger.info("Player {} joined from {}", playerName, ip);
// With exception
try {
// ...
} catch (Exception e) {
logger.error("Operation failed", e);
}
```
### Log Levels
Log levels can be configured per-package in server config:
```json
{
"logLevels": {
"com.example.myplugin": "DEBUG",
"com.hypixel.hytale.server": "INFO"
}
}
```
## Functional Interfaces
### Common Functional Interfaces
```java
// Consumers
Consumer<Player> playerHandler = player -> { /* handle */ };
BiConsumer<Player, String> messageHandler = (player, msg) -> { /* handle */ };
// Suppliers
Supplier<World> worldSupplier = () -> Universe.get().getWorld("default");
// Predicates
Predicate<Entity> isPlayer = entity -> entity instanceof Player;
BiPredicate<Player, String> hasPermission = Player::hasPermission;
// Functions
Function<String, Player> findPlayer = Universe.get()::getPlayer;
```
## Task Scheduling
### Task Registry
```java
// Register a recurring task
getTaskRegistry().register(new MyTask());
public class MyTask implements Task {
@Override
public void tick() {
// Called every server tick
}
@Override
public int getInterval() {
return 20; // Run every 20 ticks (1 second)
}
}
```
### Delayed Execution
```java
// Schedule for later
getTaskRegistry().runLater(() -> {
// Execute after delay
}, 100); // 100 ticks = 5 seconds
// Schedule repeating
getTaskRegistry().runRepeating(() -> {
// Execute repeatedly
}, 0, 20); // Start immediately, repeat every 20 ticks
```
## Data Persistence
### IndexedStorageFile
For storing data in indexed files:
```java
IndexedStorageFile<MyData> storage = new IndexedStorageFile<>(
path,
MyData.CODEC
);
// Write data
storage.write("key1", data1);
storage.write("key2", data2);
// Read data
MyData data = storage.read("key1");
// Check existence
boolean exists = storage.exists("key1");
// Delete
storage.delete("key1");
```
## UUID Utilities
```java
// UUID operations
UUID uuid = UUID.randomUUID();
String uuidString = uuid.toString();
UUID parsed = UUID.fromString(uuidString);
// Offline UUID (name-based)
UUID offlineUUID = UUIDUtil.getOfflineUUID("PlayerName");
```
## Reflection Utilities (Unsafe)
The `unsafe` package provides low-level utilities:
```java
// Use with caution - for advanced use cases only
UnsafeUtil.allocateInstance(MyClass.class);
```
## Exception Handling
### SneakyThrow
For throwing checked exceptions without declaring them:
```java
// Throw checked exception without declaring
SneakyThrow.sneakyThrow(new IOException("Error"));
```
## Best Practices
1. **Use provided utilities** - Don't reinvent the wheel
2. **Prefer immutable types** - Use Vector3d over mutable alternatives
3. **Use codecs** - Serialize with built-in codecs when possible
4. **Handle nulls** - Check for null returns from utility methods
5. **Log appropriately** - Use correct log levels
6. **Cache computations** - Don't recalculate expensive operations
7. **Use thread-safe utilities** - RandomUtil is thread-safe
8. **Validate input** - Use MathUtil.clamp for ranges
9. **Format consistently** - Use FormatUtil for user-facing strings
10. **Test thoroughly** - Utility edge cases can cause subtle bugs

View File

@@ -0,0 +1,429 @@
# Early Plugin System (Class Transformation)
The Early Plugin System allows advanced mods to transform Java bytecode before classes are loaded. This is a powerful feature for core modifications that cannot be achieved through the standard plugin API.
## Overview
Early plugins are loaded before the main server initialization and can:
- Transform class bytecode
- Modify method implementations
- Add fields or methods to existing classes
- Inject custom behavior into core systems
**Warning**: This is an advanced feature. Incorrect use can break the server or cause incompatibilities with other mods.
## When to Use Early Plugins
Early plugins are appropriate when:
- You need to modify core server behavior not exposed through APIs
- You need to patch bugs or security issues
- You're implementing advanced hooks not available through events
- Standard plugin APIs don't provide sufficient access
**Prefer standard plugins** when possible. Early plugins:
- Are harder to maintain across server updates
- May conflict with other early plugins
- Can introduce hard-to-debug issues
## Creating an Early Plugin
### Project Structure
```
my-early-plugin/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/
│ │ └── MyTransformer.java
│ └── resources/
│ └── META-INF/
│ └── services/
│ └── com.hypixel.hytale.plugin.early.ClassTransformer
└── build.gradle
```
### Implementing ClassTransformer
```java
package com.example;
import com.hypixel.hytale.plugin.early.ClassTransformer;
public class MyTransformer implements ClassTransformer {
@Override
public int priority() {
// Higher priority = loaded first
// Use 0 for normal priority
return 0;
}
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// Return null to skip transformation
// Return modified bytes to transform
// Only transform specific classes
if (!className.equals("com/hypixel/hytale/server/core/SomeClass")) {
return null;
}
// Use ASM or similar library to transform bytecode
return transformClass(classBytes);
}
private byte[] transformClass(byte[] classBytes) {
// Bytecode transformation logic
// Use ASM, Javassist, etc.
return classBytes;
}
}
```
### Service Registration
Create `META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer`:
```
com.example.MyTransformer
```
### Deployment
Place the compiled JAR in the server's `earlyplugins/` directory.
## Bytecode Transformation with ASM
### Basic ASM Example
```java
import org.objectweb.asm.*;
public class MyTransformer implements ClassTransformer {
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
if (!className.equals("com/hypixel/hytale/target/TargetClass")) {
return null;
}
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new MyClassVisitor(writer);
reader.accept(visitor, 0);
return writer.toByteArray();
}
}
class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("targetMethod")) {
return new MyMethodVisitor(mv);
}
return mv;
}
}
class MyMethodVisitor extends MethodVisitor {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
super.visitCode();
// Inject code at method start
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("Method called!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
}
}
```
### Adding Method Hooks
```java
class HookMethodVisitor extends MethodVisitor {
private final String hookClass;
private final String hookMethod;
public HookMethodVisitor(MethodVisitor mv, String hookClass, String hookMethod) {
super(Opcodes.ASM9, mv);
this.hookClass = hookClass;
this.hookMethod = hookMethod;
}
@Override
public void visitCode() {
super.visitCode();
// Call hook at method start
mv.visitMethodInsn(Opcodes.INVOKESTATIC, hookClass, hookMethod, "()V", false);
}
@Override
public void visitInsn(int opcode) {
// Inject before return
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, hookClass, hookMethod + "End", "()V", false);
}
super.visitInsn(opcode);
}
}
```
### Modifying Method Parameters
```java
class ParameterModifierVisitor extends MethodVisitor {
public ParameterModifierVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
super.visitCode();
// Modify first parameter (index 1 for instance methods, 0 for static)
// Load parameter, modify, store back
mv.visitVarInsn(Opcodes.ALOAD, 1); // Load first object parameter
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/Hooks", "modifyParam",
"(Ljava/lang/Object;)Ljava/lang/Object;", false);
mv.visitVarInsn(Opcodes.ASTORE, 1); // Store modified value
}
}
```
## Priority System
Transformer priority determines load order:
```java
@Override
public int priority() {
return 100; // Higher = loaded first
}
```
| Priority | Use Case |
|----------|----------|
| 1000+ | Critical patches (security fixes) |
| 100-999 | Core modifications |
| 0 | Standard transformations |
| -100 to -1 | Post-processing |
## Compatibility Considerations
### Class Name Handling
```java
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// className: Internal name (com/example/MyClass)
// transformedName: May differ if class was renamed
// Always use transformedName for matching
if (!transformedName.equals("com.hypixel.hytale.target.TargetClass")) {
return null;
}
return transformBytes(classBytes);
}
```
### Version Checking
```java
public class MyTransformer implements ClassTransformer {
private static final String TARGET_VERSION = "1.0.0";
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// Check server version compatibility
if (!isCompatibleVersion()) {
System.err.println("MyTransformer incompatible with this server version");
return null;
}
return doTransform(classBytes);
}
private boolean isCompatibleVersion() {
// Check against known compatible versions
return true;
}
}
```
### Chaining with Other Transformers
```java
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// Be careful not to break other transformers
// Avoid removing methods/fields other transformers may depend on
// Add, don't remove when possible
return safeTransform(classBytes);
}
```
## Debugging Early Plugins
### Logging
```java
public class MyTransformer implements ClassTransformer {
private static final boolean DEBUG = Boolean.getBoolean("mymod.debug");
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
if (DEBUG) {
System.out.println("[MyTransformer] Processing: " + transformedName);
}
byte[] result = doTransform(classBytes);
if (DEBUG && result != null) {
System.out.println("[MyTransformer] Transformed: " + transformedName);
}
return result;
}
}
```
### Bytecode Verification
```java
private byte[] transformWithVerification(byte[] classBytes) {
try {
byte[] transformed = doTransform(classBytes);
// Verify bytecode is valid
ClassReader verifyReader = new ClassReader(transformed);
ClassWriter verifyWriter = new ClassWriter(0);
verifyReader.accept(new CheckClassAdapter(verifyWriter), 0);
return transformed;
} catch (Exception e) {
System.err.println("Transformation produced invalid bytecode: " + e.getMessage());
return null; // Return null to skip transformation
}
}
```
### Dumping Transformed Classes
```java
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
byte[] result = doTransform(classBytes);
if (result != null && Boolean.getBoolean("mymod.dumpClasses")) {
try {
Path dumpPath = Path.of("transformed", transformedName.replace('.', '/') + ".class");
Files.createDirectories(dumpPath.getParent());
Files.write(dumpPath, result);
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
```
## Common Transformation Patterns
### Adding a Field
```java
class AddFieldVisitor extends ClassVisitor {
public AddFieldVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public void visitEnd() {
// Add field before class end
FieldVisitor fv = cv.visitField(
Opcodes.ACC_PUBLIC,
"myCustomField",
"Ljava/lang/Object;",
null,
null
);
if (fv != null) {
fv.visitEnd();
}
super.visitEnd();
}
}
```
### Redirecting Method Calls
```java
class RedirectMethodVisitor extends MethodVisitor {
public RedirectMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
// Redirect specific method call
if (owner.equals("com/hypixel/hytale/Original") && name.equals("oldMethod")) {
super.visitMethodInsn(opcode, "com/example/Replacement", "newMethod",
descriptor, isInterface);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
```
## Best Practices
1. **Minimize transformations** - Only transform what's absolutely necessary
2. **Use priorities wisely** - Don't use high priority without good reason
3. **Handle errors gracefully** - Return null on failure, don't crash
4. **Log transformations** - Provide debug logging for troubleshooting
5. **Document changes** - Clearly document what your transformer modifies
6. **Test thoroughly** - Test with different server versions
7. **Check compatibility** - Verify compatibility with other known early plugins
8. **Version your transformer** - Track which server versions are supported
9. **Provide fallbacks** - If transformation fails, the mod should degrade gracefully
10. **Keep it simple** - Complex transformations are hard to maintain
## Security Considerations
- Early plugins have full access to server internals
- Malicious early plugins could compromise server security
- Only use early plugins from trusted sources
- Review transformer code before deployment
- Monitor for unexpected behavior after installing early plugins

551
docs/15-ui-system.md Normal file
View File

@@ -0,0 +1,551 @@
# UI System
The Hytale server implements a server-authoritative UI system with two main subsystems: Pages (full-screen UI) and Windows (inventory-style containers).
## Architecture Overview
```
UI System
├── Pages System - Full-screen UI (dialogs, menus, custom UIs)
│ ├── CustomUIPage - Base page class
│ ├── UICommandBuilder - DOM manipulation
│ └── UIEventBuilder - Event bindings
└── Windows System - Inventory-style containers
├── Window - Base window class
└── WindowManager - Window lifecycle
```
## Page System
### Core Classes
| Class | Package | Description |
|-------|---------|-------------|
| `CustomUIPage` | `com.hypixel.hytale.server.core.entity.entities.player.pages` | Abstract base for all custom pages |
| `BasicCustomUIPage` | Same | Simple pages without event data parsing |
| `InteractiveCustomUIPage<T>` | Same | Pages with typed event handling |
| `PageManager` | Same | Manages page state for a player |
| `UICommandBuilder` | `com.hypixel.hytale.server.core.ui.builder` | Builds UI DOM commands |
| `UIEventBuilder` | Same | Builds event bindings |
### Page Lifetime
```java
public enum CustomPageLifetime {
CantClose(0), // User cannot close
CanDismiss(1), // ESC key to dismiss
CanDismissOrCloseThroughInteraction(2) // Dismiss or click to close
}
```
### CustomUIPage Base Class
```java
public abstract class CustomUIPage {
protected final PlayerRef playerRef;
protected CustomPageLifetime lifetime;
// Build the UI - called to construct the page
public abstract void build(Ref<EntityStore> ref, UICommandBuilder cmd,
UIEventBuilder events, Store<EntityStore> store);
// Handle event data from client
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, String rawData);
// Rebuild entire page
protected void rebuild();
// Send partial update
protected void sendUpdate(UICommandBuilder commandBuilder, boolean clear);
// Close the page
protected void close();
// Called when page is dismissed by user
public void onDismiss(Ref<EntityStore> ref, Store<EntityStore> store);
}
```
### InteractiveCustomUIPage<T>
For pages with typed event handling:
```java
public abstract class InteractiveCustomUIPage<T> extends CustomUIPage {
protected final BuilderCodec<T> eventDataCodec;
public InteractiveCustomUIPage(PlayerRef playerRef, CustomPageLifetime lifetime,
BuilderCodec<T> eventDataCodec) {
super(playerRef, lifetime);
this.eventDataCodec = eventDataCodec;
}
// Override this for type-safe event handling
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, T data);
// Send update with new event bindings
protected void sendUpdate(UICommandBuilder cmd, UIEventBuilder events, boolean clear);
}
```
## UICommandBuilder
Builds commands to manipulate the UI DOM structure.
### Command Types
```java
public enum CustomUICommandType {
Append(0), // Append UI document
AppendInline(1), // Append inline XML
InsertBefore(2), // Insert before element
InsertBeforeInline(3),
Remove(4), // Remove element
Set(5), // Set property value
Clear(6) // Clear children
}
```
### Methods
```java
UICommandBuilder cmd = new UICommandBuilder();
// Load UI documents
cmd.append("Pages/MyPage.ui"); // Load to root
cmd.append("#Container", "Common/Button.ui"); // Append to selector
// Clear and remove
cmd.clear("#ItemList"); // Clear children
cmd.remove("#OldElement"); // Remove element
// Set property values
cmd.set("#Title.Text", "Hello World"); // String
cmd.set("#Counter.Value", 42); // Integer
cmd.set("#Progress.Value", 0.75f); // Float
cmd.set("#Panel.Visible", true); // Boolean
cmd.set("#Label.Text", Message.translation("key")); // Localized
// Set with Value references
cmd.set("#Button.Style", Value.ref("Common/Button.ui", "ActiveStyle"));
// Set complex objects
cmd.setObject("#ItemSlot", itemGridSlot); // Single object
cmd.set("#SlotList", itemGridSlotArray); // Array
cmd.set("#DataList", itemList); // List
// Inline XML
cmd.appendInline("#Container", "<Panel Id='NewPanel'/>");
cmd.insertBefore("#Element", "Pages/Header.ui");
```
### Selector Syntax
- `#ElementId` - Select by ID
- `#Parent #Child` - Descendant selector
- `#List[0]` - Index accessor (for lists)
- `#List[0] #Button` - Combined
- `#Element.Property` - Property accessor
## UIEventBuilder
Builds event bindings that trigger server callbacks.
### Event Binding Types
```java
public enum CustomUIEventBindingType {
Activating(0), // Click/press
RightClicking(1), // Right-click
DoubleClicking(2), // Double-click
MouseEntered(3), // Mouse enter
MouseExited(4), // Mouse leave
ValueChanged(5), // Input value changed
ElementReordered(6), // Drag reorder
Validating(7), // Input validation
Dismissing(8), // Page dismiss
FocusGained(9), // Element focused
FocusLost(10), // Element unfocused
KeyDown(11), // Key pressed
MouseButtonReleased(12), // Mouse released
SlotClicking(13), // Inventory slot click
SlotDoubleClicking(14), // Slot double-click
SlotMouseEntered(15), // Slot hover enter
SlotMouseExited(16), // Slot hover exit
DragCancelled(17), // Drag cancelled
Dropped(18), // Item dropped
SlotMouseDragCompleted(19),
SlotMouseDragExited(20),
SlotClickReleaseWhileDragging(21),
SlotClickPressWhileDragging(22),
SelectedTabChanged(23) // Tab selection
}
```
### Methods
```java
UIEventBuilder events = new UIEventBuilder();
// Simple binding
events.addEventBinding(CustomUIEventBindingType.Activating, "#CloseButton");
// With event data
events.addEventBinding(CustomUIEventBindingType.Activating, "#SubmitButton",
EventData.of("Action", "Submit"));
// Without interface lock (for non-blocking events)
events.addEventBinding(CustomUIEventBindingType.MouseEntered, "#HoverArea",
EventData.of("Action", "Hover"), false);
// With value extraction from element
events.addEventBinding(CustomUIEventBindingType.ValueChanged, "#SearchInput",
EventData.of("@Query", "#SearchInput.Value"), false);
```
### EventData
```java
// Simple key-value
EventData.of("Action", "Submit")
// Multiple values
new EventData()
.append("Type", "Update")
.append("Index", "5")
// Value extraction (@ prefix)
EventData.of("@Value", "#InputField.Value") // Extract from element
EventData.of("@Selected", "#Dropdown.Selected") // Extract selection
```
## Value References
The `Value<T>` class references values either directly or from UI documents.
```java
// Direct value
Value<Integer> count = Value.of(42);
Value<String> text = Value.of("Hello");
// Reference from UI document
Value<String> style = Value.ref("Common/Button.ui", "ActiveStyle");
Value<Integer> border = Value.ref("Pages/Dialog.ui", "BorderWidth");
```
### Encoding Format
References are encoded in JSON as:
```json
{
"$Document": "Common/Button.ui",
"@Value": "ActiveStyle"
}
```
## UI Data Types
### PatchStyle (9-Patch Styling)
```java
public class PatchStyle {
Value<String> texturePath;
Value<Integer> border;
Value<Integer> horizontalBorder;
Value<Integer> verticalBorder;
Value<String> color;
Value<Area> area;
}
```
### Area
```java
public class Area {
int x, y, width, height;
}
```
### Anchor
```java
public class Anchor {
Value<Integer> left, right, top, bottom;
Value<Integer> width, height;
Value<Integer> minWidth, maxWidth;
Value<Integer> full, horizontal, vertical;
}
```
### ItemGridSlot
```java
public class ItemGridSlot {
ItemStack itemStack;
Value<PatchStyle> background, overlay, icon;
boolean isItemIncompatible;
boolean isActivatable;
boolean isItemUncraftable;
boolean skipItemQualityBackground;
String name, description;
}
```
### LocalizableString
```java
LocalizableString.fromString("Hello") // Plain string
LocalizableString.fromMessageId("key") // Translation key
LocalizableString.fromMessageId("key", Map.of("name", "Player")) // With params
```
## Complete Page Example
```java
public class ShopPage extends InteractiveCustomUIPage<ShopPage.ShopEventData> {
private final List<ShopItem> items;
public ShopPage(PlayerRef playerRef, List<ShopItem> items) {
super(playerRef, CustomPageLifetime.CanDismiss, ShopEventData.CODEC);
this.items = items;
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder cmd,
UIEventBuilder events, Store<EntityStore> store) {
// Load main layout
cmd.append("Pages/ShopPage.ui");
// Set title
cmd.set("#Title.Text", Message.translation("shop.title"));
// Build item list
cmd.clear("#ItemList");
for (int i = 0; i < items.size(); i++) {
ShopItem item = items.get(i);
String selector = "#ItemList[" + i + "]";
cmd.append("#ItemList", "Pages/ShopElementButton.ui");
cmd.set(selector + " #Name.Text", item.getName());
cmd.set(selector + " #Price.Text", String.valueOf(item.getPrice()));
cmd.setObject(selector + " #Icon", item.toItemGridSlot());
events.addEventBinding(
CustomUIEventBindingType.Activating,
selector,
EventData.of("Action", "Buy").append("Index", String.valueOf(i)),
false
);
}
// Close button
events.addEventBinding(CustomUIEventBindingType.Activating, "#CloseButton",
EventData.of("Action", "Close"));
// Search input
events.addEventBinding(CustomUIEventBindingType.ValueChanged, "#SearchInput",
EventData.of("Action", "Search").append("@Query", "#SearchInput.Value"),
false);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store,
ShopEventData data) {
switch (data.action) {
case "Close" -> close();
case "Buy" -> {
if (data.index >= 0 && data.index < items.size()) {
buyItem(ref, store, items.get(data.index));
// Update UI
UICommandBuilder cmd = new UICommandBuilder();
cmd.set("#Balance.Text", getPlayerBalance(ref, store));
sendUpdate(cmd, false);
}
}
case "Search" -> {
filterItems(data.query);
rebuild();
}
}
}
public static class ShopEventData {
public static final BuilderCodec<ShopEventData> CODEC = BuilderCodec
.builder(ShopEventData.class, ShopEventData::new)
.addField(new KeyedCodec<>("Action", Codec.STRING),
(d, v) -> d.action = v, d -> d.action)
.addField(new KeyedCodec<>("Index", Codec.INTEGER),
(d, v) -> d.index = v, d -> d.index)
.addField(new KeyedCodec<>("Query", Codec.STRING),
(d, v) -> d.query = v, d -> d.query)
.build();
String action;
int index = -1;
String query;
}
}
```
## PageManager
```java
Player player = ...;
PageManager pageManager = player.getPageManager();
// Open custom page
pageManager.openCustomPage(ref, store, new ShopPage(playerRef, items));
// Open built-in page
pageManager.setPage(ref, store, Page.Inventory);
pageManager.setPage(ref, store, Page.None); // Close
// Open page with windows
pageManager.openCustomPageWithWindows(ref, store, customPage, window1, window2);
// Get current custom page
CustomUIPage current = pageManager.getCustomPage();
```
### Built-in Pages
```java
public enum Page {
None(0), // No page
Bench(1), // Crafting bench
Inventory(2), // Player inventory
ToolsSettings(3), // Builder tools
Map(4), // World map
MachinimaEditor(5), // Machinima
ContentCreation(6), // Content creation
Custom(7) // Custom server page
}
```
## Window System
### Window Base Class
```java
public abstract class Window {
public abstract JsonObject getData();
protected abstract boolean onOpen0();
protected abstract void onClose0();
public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action);
public void close();
public void invalidate(); // Mark for update
public void registerCloseEvent(Consumer<Window> callback);
}
```
### Window Types
```java
public enum WindowType {
Container(0),
PocketCrafting(1),
BasicCrafting(2),
DiagramCrafting(3),
StructuralCrafting(4),
Processing(5),
Memories(6)
}
```
### WindowManager
```java
WindowManager wm = player.getWindowManager();
// Open window
OpenWindow packet = wm.openWindow(myWindow);
// Open multiple
List<OpenWindow> packets = wm.openWindows(window1, window2);
// Close
wm.closeWindow(windowId);
wm.closeAllWindows();
// Update
wm.updateWindow(window);
wm.updateWindows(); // Update all dirty
// Get window
Window w = wm.getWindow(windowId);
```
## HUD System
The HUD is a persistent UI layer separate from pages.
### CustomHud Packet
```java
public class CustomHud implements Packet {
boolean clear;
CustomUICommand[] commands;
}
```
### HUD Components
```java
public enum HudComponent {
Hotbar(0), StatusIcons(1), Reticle(2), Chat(3),
Requests(4), Notifications(5), KillFeed(6), InputBindings(7),
PlayerList(8), EventTitle(9), Compass(10), ObjectivePanel(11),
PortalPanel(12), BuilderToolsLegend(13), Speedometer(14),
UtilitySlotSelector(15), BlockVariantSelector(16),
BuilderToolsMaterialSlotSelector(17), Stamina(18),
AmmoIndicator(19), Health(20), Mana(21), Oxygen(22), Sleep(23)
}
```
## Protocol Packets
### Page Packets
| Packet | ID | Direction | Description |
|--------|-----|-----------|-------------|
| `SetPage` | 216 | S->C | Set built-in page |
| `CustomHud` | 217 | S->C | Update HUD |
| `CustomPage` | 218 | S->C | Send custom page |
| `CustomPageEvent` | 219 | C->S | Page event |
### Window Packets
| Packet | ID | Direction | Description |
|--------|-----|-----------|-------------|
| `OpenWindow` | 200 | S->C | Open window |
| `UpdateWindow` | 201 | S->C | Update window |
| `CloseWindow` | 202 | S->C | Close window |
| `ClientOpenWindow` | 203 | C->S | Request window |
| `SendWindowAction` | 204 | C->S | Window action |
## Known UI Documents
| Path | Purpose |
|------|---------|
| `Pages/DialogPage.ui` | Dialog/conversation |
| `Pages/BarterPage.ui` | Trading interface |
| `Pages/ShopPage.ui` | Shop interface |
| `Pages/RespawnPage.ui` | Death/respawn |
| `Pages/ChangeModelPage.ui` | Model selection |
| `Pages/WarpListPage.ui` | Teleport list |
| `Pages/CommandListPage.ui` | Command help |
| `Pages/PluginListPage.ui` | Plugin list |
| `Common/TextButton.ui` | Reusable button |
## Best Practices
1. **Use InteractiveCustomUIPage<T>** - Provides type-safe event handling
2. **Define event data codec** - Create a proper BuilderCodec for your event data
3. **Use sendUpdate for partial updates** - Don't rebuild entire page for small changes
4. **Extract values with @ prefix** - Use `@PropertyName` syntax to extract element values
5. **Set locksInterface appropriately** - Use `false` for non-blocking events like hover
6. **Clear lists before rebuilding** - Always `cmd.clear()` before repopulating lists
7. **Handle dismiss** - Override `onDismiss()` for cleanup
8. **Use Value references** - Reference styles from UI documents for consistency

190
docs/README.md Normal file
View File

@@ -0,0 +1,190 @@
# Hytale Server Modding Documentation
Welcome to the Hytale Server modding documentation. This guide provides comprehensive information for creating mods and extensions for the Hytale Server.
## LLM Reference
**[LLM-Optimized API Reference](00-llm-reference.md)** - Complete API reference with exact class names, method signatures, and JSON structures. Use this for quick lookups.
## Documentation Index
### Getting Started
1. **[Getting Started](01-getting-started.md)** - Introduction and first mod tutorial
### Core Systems
2. **[Plugin Development](02-plugin-development.md)** - Creating and structuring plugins
3. **[Event System](03-event-system.md)** - Handling game events
4. **[Command System](04-command-system.md)** - Creating custom commands
5. **[Entity System (ECS)](05-entity-system.md)** - Working with entities and components
6. **[World Management](06-world-management.md)** - Managing worlds and chunks
### Communication & Data
7. **[Networking](07-networking.md)** - Network protocol and packet handling
8. **[Asset System](08-asset-system.md)** - Managing game assets
9. **[Configuration](10-configuration.md)** - Plugin and server configuration
10. **[Codec/Serialization](11-codec-serialization.md)** - Data serialization framework
### Game Systems
11. **[NPC/AI System](09-npc-ai-system.md)** - Creating and controlling NPCs
12. **[Built-in Modules](12-builtin-modules.md)** - Using server modules
### UI & Presentation
15. **[UI System](15-ui-system.md)** - Pages, windows, HUD, and UI commands
### Reference
13. **[Utilities](13-utilities.md)** - Common utility classes and APIs
14. **[Early Plugin System](14-early-plugin-system.md)** - Advanced bytecode transformation
## Quick Start
### Creating Your First Mod
1. Create a `manifest.json`:
```json
{
"Group": "com.example",
"Name": "MyMod",
"Version": "1.0.0",
"Main": "com.example.MyMod"
}
```
2. Create your main class:
```java
public class MyMod extends JavaPlugin {
public MyMod(JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
getLogger().info("MyMod loaded!");
}
}
```
3. Build and place in `mods/` directory
## Key Concepts
### Plugin Lifecycle
```
NONE -> SETUP -> START -> ENABLED -> SHUTDOWN -> DISABLED
```
- **SETUP**: Register commands, events, entities
- **START**: Initialize plugin logic
- **SHUTDOWN**: Clean up resources
### Available Registries
| Registry | Purpose |
|----------|---------|
| `CommandRegistry` | Custom commands |
| `EventRegistry` | Event listeners |
| `EntityRegistry` | Custom entities |
| `AssetRegistry` | Custom assets |
| `TaskRegistry` | Scheduled tasks |
### Event Priority
| Priority | Value | Use Case |
|----------|-------|----------|
| FIRST | -21844 | Monitoring/logging |
| EARLY | -10922 | Pre-processing |
| NORMAL | 0 | Standard handling |
| LATE | 10922 | Post-processing |
| LAST | 21844 | Final processing |
## Package Structure
```
com.hypixel.hytale/
├── server/core/ # Core server functionality
├── event/ # Event system
├── component/ # Entity Component System
├── codec/ # Serialization
├── protocol/ # Networking
├── common/ # Utilities
├── math/ # Math utilities
└── builtin/ # Built-in modules
```
## API Highlights
### Events
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
Player player = event.getPlayer();
player.sendMessage("Welcome!");
});
```
### Commands
```java
public class MyCommand extends AbstractCommand {
public MyCommand() {
super("mycommand", "Description");
withRequiredArg("player", "Target player");
}
@Override
public void execute(CommandContext ctx, Arguments args) {
String player = args.getString("player");
ctx.sendMessage("Hello, " + player);
}
}
```
### Entities
```java
public class CustomEntity extends Entity {
public CustomEntity(World world) {
super(world);
}
}
// Register
getEntityRegistry().register("custom", CustomEntity.class, CustomEntity::new);
```
### Configuration
```java
public static final Codec<MyConfig> CODEC = BuilderCodec.of(MyConfig::new)
.with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
.with("maxCount", Codec.INTEGER, c -> c.maxCount, 100)
.build();
```
## Best Practices
1. **Register in setup()** - All registrations should happen during setup
2. **Use events** - Prefer events over polling
3. **Respect lifecycle** - Don't access other plugins during setup
4. **Handle errors** - Log and handle exceptions gracefully
5. **Clean up** - Release resources in shutdown()
6. **Use codecs** - Serialize data with the codec system
7. **Namespace assets** - Use your mod ID as namespace
8. **Document** - Document your mod's features and configuration
## Support
For questions and support:
- Check the [Hytale modding forums](#)
- Join the [Discord community](#)
- Report issues on [GitHub](#)
## Contributing
Contributions to this documentation are welcome. Please submit pull requests with improvements or corrections.
---
*This documentation is for Hytale Server modding. For official Hytale information, visit [hytale.com](https://hytale.com).*

View File

@@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HYTALE_SERVER_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT_ROOT="$(cd "$HYTALE_SERVER_ROOT/../.." && pwd)"
DOWNLOADER="$SCRIPT_DIR/hytale-downloader-linux-amd64"
OUTPUT_DIR="$PROJECT_ROOT/repo/hytale-server"
OUTPUT_DIR="$HYTALE_SERVER_ROOT"
if [ ! -f "$DOWNLOADER" ]; then
echo "Error: Downloader not found at $DOWNLOADER"