diff --git a/.gitignore b/.gitignore index 34f33557..430c4f43 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ repo/ patcher Assets .hytale-downloader-credentials.json +Assets.zip \ No newline at end of file diff --git a/docs/00-llm-reference.md b/docs/00-llm-reference.md new file mode 100644 index 00000000..b14c1467 --- /dev/null +++ b/docs/00-llm-reference.md @@ -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` | +| `getChunkStoreRegistry()` | `ComponentRegistryProxy` | +| `getAssetRegistry()` | `AssetRegistry` | + +### Configuration Pattern + +```java +// In constructor (BEFORE setup): +Config config = withConfig(MyConfig.CODEC); +Config 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` | Base event interface | +| `IEvent` | Synchronous event, extends IBaseEvent | +| `IAsyncEvent` | 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 eventClass, Consumer consumer) +EventRegistration register(EventPriority priority, Class eventClass, Consumer consumer) + +// With key +EventRegistration register(Class eventClass, KeyType key, Consumer consumer) +EventRegistration register(EventPriority priority, Class eventClass, KeyType key, Consumer consumer) +``` + +**Global Registration (receives all keys):** +```java +EventRegistration registerGlobal(Class eventClass, Consumer consumer) +EventRegistration registerGlobal(EventPriority priority, Class eventClass, Consumer consumer) +``` + +**Unhandled Registration (when no other handler processed):** +```java +EventRegistration registerUnhandled(Class eventClass, Consumer consumer) +``` + +**Async Registration:** +```java +EventRegistration registerAsync(Class eventClass, Function, CompletableFuture> function) +EventRegistration registerAsyncGlobal(...) +EventRegistration registerAsyncUnhandled(...) +``` + +### Key Event Classes + +**Server Events (`com.hypixel.hytale.server.core.event.events`):** +- `BootEvent` - IEvent +- `ShutdownEvent` - IEvent +- `PrepareUniverseEvent` - IEvent + +**Player Events (`...event.events.player`):** +- `PlayerConnectEvent` - IEvent +- `PlayerSetupConnectEvent` - IEvent, ICancellable +- `PlayerDisconnectEvent` - PlayerRefEvent +- `PlayerChatEvent` - IAsyncEvent, ICancellable +- `PlayerInteractEvent` - PlayerEvent, ICancellable +- `PlayerMouseButtonEvent` - PlayerEvent, ICancellable +- `AddPlayerToWorldEvent` - IEvent +- `DrainPlayerFromWorldEvent` - IEvent + +**World Events (`...universe.world.events`):** +- `AddWorldEvent` - WorldEvent, ICancellable +- `RemoveWorldEvent` - WorldEvent, ICancellable +- `StartWorldEvent` - WorldEvent +- `AllWorldsLoadedEvent` - IEvent + +**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 execute(CommandContext context) + +// Configuration +void requirePermission(String permission) +void addAliases(String... aliases) +void addSubCommand(AbstractCommand command) +void addUsageVariant(AbstractCommand command) + +// Arguments +RequiredArg withRequiredArg(String name, String description, ArgumentType argType) +OptionalArg withOptionalArg(String name, String description, ArgumentType argType) +DefaultArg withDefaultArg(String name, String description, ArgumentType argType, D defaultValue, String defaultValueDescription) +FlagArg withFlagArg(String name, String description) +RequiredArg> withListRequiredArg(String name, String description, ArgumentType argType) +``` + +### CommandContext Key Methods + +```java + DataType get(Argument 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 senderAs(Class type) // Cast sender +Ref 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 enumClass) // Create enum type +``` + +--- + +## ENTITY COMPONENT SYSTEM (ECS) + +### Core Classes (Package: `com.hypixel.hytale.component`) + +| Class | Description | +|-------|-------------| +| `Component` | Base component interface | +| `ComponentRegistry` | Component type registration | +| `ComponentType` | 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 REGISTRY = new ComponentRegistry<>(); + +// Registration +ComponentType registerComponent(Class tClass, Supplier supplier) +ComponentType registerComponent(Class tClass, String id, BuilderCodec codec) +void registerSystem(ISystem 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 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 holder = EntityStore.REGISTRY.newHolder(); +holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(pos, rot)); +Ref 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` | 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 ref) +void setObject(String selector, Object data) +void set(String selector, T[] data) +void set(String selector, List 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 References + +```java +Value.of(directValue) // Direct value +Value.ref("Document.ui", "ValueName") // Reference from UI document +``` + +### Page Example + +```java +public class MyPage extends InteractiveCustomUIPage { + public MyPage(PlayerRef playerRef) { + super(playerRef, CustomPageLifetime.CanDismiss, Data.CODEC); + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, + UIEventBuilder events, Store 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 ref, Store store, Data data) { + if ("Click".equals(data.action)) { close(); } + } + + public static class Data { + public static final BuilderCodec 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` | Base codec interface | +| `BuilderCodec` | Builder-based codec | +| `KeyedCodec` | 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 +Codec.STRING.setOf() // Set +Codec.mapOf(Codec.STRING, Codec.INTEGER) // Map +Codec.arrayOf(Codec.STRING, String[]::new) // String[] +``` + +### BuilderCodec Pattern + +```java +public static final BuilderCodec 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_CODEC = Codec.STRING.xmap(UUID::fromString, UUID::toString); +Codec PORT = Codec.INTEGER.validate(p -> p > 0 && p < 65536, "Invalid port"); +Codec VOLUME = Codec.FLOAT.clamp(0.0f, 1.0f); +Codec ENUM = Codec.enumCodec(MyEnum.class); +``` + +--- + +## ASSET SYSTEM + +### Core Classes + +| Class | Package | +|-------|---------| +| `JsonAsset` | `com.hypixel.hytale.assetstore.JsonAsset` | +| `AssetStore` | `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 worlds = universe.getWorlds(); +World world = universe.getWorld("worldName"); +Collection 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 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 // 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 diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md new file mode 100644 index 00000000..1169f00e --- /dev/null +++ b/docs/01-getting-started.md @@ -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 diff --git a/docs/02-plugin-development.md b/docs/02-plugin-development.md new file mode 100644 index 00000000..024f44ae --- /dev/null +++ b/docs/02-plugin-development.md @@ -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 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 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 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 diff --git a/docs/03-event-system.md b/docs/03-event-system.md new file mode 100644 index 00000000..3bd8c36b --- /dev/null +++ b/docs/03-event-system.md @@ -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 +├── IEvent // Synchronous events +└── IAsyncEvent // Asynchronous events (CompletableFuture) + +ICancellable // Mixin interface for cancellable events +``` + +### Key Classes + +| Class | Description | +|-------|-------------| +| `IEvent` | Synchronous event interface | +| `IAsyncEvent` | 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 { + 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, 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 { + 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 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 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 diff --git a/docs/04-command-system.md b/docs/04-command-system.md new file mode 100644 index 00000000..56481a85 --- /dev/null +++ b/docs/04-command-system.md @@ -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 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 "); + } +} + +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 diff --git a/docs/05-entity-system.md b/docs/05-entity-system.md new file mode 100644 index 00000000..f24565ea --- /dev/null +++ b/docs/05-entity-system.md @@ -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` | Base component interface | +| `ComponentRegistry` | Central registry for component types | +| `Store` | ECS data storage | +| `Ref` | Entity reference (ID wrapper) | +| `Holder` | 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`: + +```java +public class HealthComponent implements Component { + public static final ComponentRegistry.Entry 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 +└── 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 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 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 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 diff --git a/docs/06-world-management.md b/docs/06-world-management.md new file mode 100644 index 00000000..1012cd58 --- /dev/null +++ b/docs/06-world-management.md @@ -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 worlds = universe.getWorlds(); + +// Get a specific world +World world = universe.getWorld("worldName"); + +// Get player count +int playerCount = universe.getPlayerCount(); + +// Get all players +Collection 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 load(int x, int y, int z) { + // Load chunk from storage + return CompletableFuture.supplyAsync(() -> { + return loadFromDatabase(x, y, z); + }); + } + + @Override + public CompletableFuture 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 diff --git a/docs/07-networking.md b/docs/07-networking.md new file mode 100644 index 00000000..5e3d868e --- /dev/null +++ b/docs/07-networking.md @@ -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 // 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 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 diff --git a/docs/08-asset-system.md b/docs/08-asset-system.md new file mode 100644 index 00000000..f5e4565b --- /dev/null +++ b/docs/08-asset-system.md @@ -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 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 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 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 diff --git a/docs/10-configuration.md b/docs/10-configuration.md new file mode 100644 index 00000000..cfbb36b5 --- /dev/null +++ b/docs/10-configuration.md @@ -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 + } + + RateLimitConfig { + enabled: boolean + packetsPerSecond: int + burstCapacity: int + } + + // Dynamic configurations + modules: Map + logLevels: Map + modConfig: Map + + // 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 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 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 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 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 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 allowedPlayers; + public final List spawnPoints; + + private ListConfig(List allowedPlayers, List spawnPoints) { + this.allowedPlayers = List.copyOf(allowedPlayers); + this.spawnPoints = List.copyOf(spawnPoints); + } +} +``` + +### Map Configuration + +```java +public class MapConfig { + + public static final Codec 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 playerHomes; + public final Map warpPoints; + + private MapConfig(Map playerHomes, Map warpPoints) { + this.playerHomes = Map.copyOf(playerHomes); + this.warpPoints = Map.copyOf(warpPoints); + } +} +``` + +## Optional Configuration Values + +### Using Optional + +```java +public class OptionalConfig { + + public static final Codec 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 optionalValue; + + private OptionalConfig(String requiredValue, Optional 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 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 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 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 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 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 +} +``` diff --git a/docs/11-codec-serialization.md b/docs/11-codec-serialization.md new file mode 100644 index 00000000..d035dd42 --- /dev/null +++ b/docs/11-codec-serialization.md @@ -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 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> stringList = Codec.STRING.listOf(); + +// List of integers +Codec> intList = Codec.INTEGER.listOf(); + +// List of custom objects +Codec> objectList = MyObject.CODEC.listOf(); +``` + +### Sets + +```java +// Set of strings +Codec> stringSet = Codec.STRING.setOf(); +``` + +### Maps + +```java +// Map with string keys +Codec> stringToInt = Codec.mapOf(Codec.STRING, Codec.INTEGER); + +// Map with custom key type +Codec> playerMap = Codec.mapOf(UUID_CODEC, PlayerData.CODEC); +``` + +### Arrays + +```java +// Array codec +Codec 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 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 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 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
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 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 KEYED_CODEC = KeyedCodec.of( + "id", + Codec.STRING, + ItemType::getId, + ItemType.CODEC + ); + // ... +} +``` + +## Codec Transformations + +### Mapping Values + +```java +// Transform between types +Codec UUID_CODEC = Codec.STRING.xmap( + UUID::fromString, // decode: String -> UUID + UUID::toString // encode: UUID -> String +); + +// Transform integers to enum +Codec ENUM_CODEC = Codec.INTEGER.xmap( + i -> MyEnum.values()[i], + MyEnum::ordinal +); +``` + +### Validation + +```java +// Validate during decode +Codec PORT_CODEC = Codec.INTEGER.validate( + port -> port > 0 && port < 65536, + "Port must be between 1 and 65535" +); + +// Clamp values +Codec VOLUME_CODEC = Codec.FLOAT.clamp(0.0f, 1.0f); +``` + +### Enum Codecs + +```java +// Automatic enum codec +Codec GAME_MODE_CODEC = Codec.enumCodec(GameMode.class); + +// Custom enum serialization +Codec DIRECTION_CODEC = Codec.STRING.xmap( + Direction::valueOf, + Direction::name +); +``` + +## Complex Examples + +### Polymorphic Types + +```java +// For types with multiple implementations +public interface Shape { + Codec 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 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 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 children; + // ... +} +``` + +### Record Types (Java 16+) + +```java +public record Point(double x, double y, double z) { + public static final Codec 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 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 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 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(); + // ... +} +``` diff --git a/docs/13-utilities.md b/docs/13-utilities.md new file mode 100644 index 00000000..bf89bf06 --- /dev/null +++ b/docs/13-utilities.md @@ -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 shuffled = ListUtil.shuffle(list); +List filtered = ListUtil.filter(list, predicate); +Optional random = ListUtil.random(list); +List> partitioned = ListUtil.partition(list, 10); +``` + +### MapUtil + +```java +// Map operations +Map filtered = MapUtil.filter(map, predicate); +Map merged = MapUtil.merge(map1, map2); + V getOrCreate(Map map, K key, Supplier creator); +``` + +### WeightedMap + +For weighted random selection: + +```java +WeightedMap 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 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 playerHandler = player -> { /* handle */ }; +BiConsumer messageHandler = (player, msg) -> { /* handle */ }; + +// Suppliers +Supplier worldSupplier = () -> Universe.get().getWorld("default"); + +// Predicates +Predicate isPlayer = entity -> entity instanceof Player; +BiPredicate hasPermission = Player::hasPermission; + +// Functions +Function 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 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 diff --git a/docs/14-early-plugin-system.md b/docs/14-early-plugin-system.md new file mode 100644 index 00000000..1e17dceb --- /dev/null +++ b/docs/14-early-plugin-system.md @@ -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 diff --git a/docs/15-ui-system.md b/docs/15-ui-system.md new file mode 100644 index 00000000..b2ee356c --- /dev/null +++ b/docs/15-ui-system.md @@ -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` | 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 ref, UICommandBuilder cmd, + UIEventBuilder events, Store store); + + // Handle event data from client + public void handleDataEvent(Ref ref, Store 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 ref, Store store); +} +``` + +### InteractiveCustomUIPage + +For pages with typed event handling: + +```java +public abstract class InteractiveCustomUIPage extends CustomUIPage { + protected final BuilderCodec eventDataCodec; + + public InteractiveCustomUIPage(PlayerRef playerRef, CustomPageLifetime lifetime, + BuilderCodec eventDataCodec) { + super(playerRef, lifetime); + this.eventDataCodec = eventDataCodec; + } + + // Override this for type-safe event handling + public void handleDataEvent(Ref ref, Store 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", ""); +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` class references values either directly or from UI documents. + +```java +// Direct value +Value count = Value.of(42); +Value text = Value.of("Hello"); + +// Reference from UI document +Value style = Value.ref("Common/Button.ui", "ActiveStyle"); +Value 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 texturePath; + Value border; + Value horizontalBorder; + Value verticalBorder; + Value color; + Value area; +} +``` + +### Area + +```java +public class Area { + int x, y, width, height; +} +``` + +### Anchor + +```java +public class Anchor { + Value left, right, top, bottom; + Value width, height; + Value minWidth, maxWidth; + Value full, horizontal, vertical; +} +``` + +### ItemGridSlot + +```java +public class ItemGridSlot { + ItemStack itemStack; + Value 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 { + + private final List items; + + public ShopPage(PlayerRef playerRef, List items) { + super(playerRef, CustomPageLifetime.CanDismiss, ShopEventData.CODEC); + this.items = items; + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, + UIEventBuilder events, Store 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 ref, Store 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 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 ref, Store store, WindowAction action); + public void close(); + public void invalidate(); // Mark for update + public void registerCloseEvent(Consumer 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 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** - 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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f5bcec0d --- /dev/null +++ b/docs/README.md @@ -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 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).* diff --git a/scripts/update-server.sh b/scripts/update-server.sh index b7c5ee22..6ba24bc4 100755 --- a/scripts/update-server.sh +++ b/scripts/update-server.sh @@ -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"