Update script to write to vendor/hytale-server
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ repo/
|
||||
patcher
|
||||
Assets
|
||||
.hytale-downloader-credentials.json
|
||||
Assets.zip
|
||||
777
docs/00-llm-reference.md
Normal file
777
docs/00-llm-reference.md
Normal 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
142
docs/01-getting-started.md
Normal 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
|
||||
351
docs/02-plugin-development.md
Normal file
351
docs/02-plugin-development.md
Normal 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
335
docs/03-event-system.md
Normal 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
413
docs/04-command-system.md
Normal 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
414
docs/05-entity-system.md
Normal 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
396
docs/06-world-management.md
Normal 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
348
docs/07-networking.md
Normal 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
434
docs/08-asset-system.md
Normal 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
457
docs/10-configuration.md
Normal 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
|
||||
}
|
||||
```
|
||||
402
docs/11-codec-serialization.md
Normal file
402
docs/11-codec-serialization.md
Normal 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
416
docs/13-utilities.md
Normal 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
|
||||
429
docs/14-early-plugin-system.md
Normal file
429
docs/14-early-plugin-system.md
Normal 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
551
docs/15-ui-system.md
Normal 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
190
docs/README.md
Normal 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).*
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user