This commit is contained in:
2026-01-20 20:33:59 +01:00
commit b16a40e431
583 changed files with 87339 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
---
title: Core Concepts
type: docs
weight: 2
---
This section covers the fundamental concepts you need to understand when developing Hytale plugins.
{{< cards >}}
{{< card link="registries" title="Registries" subtitle="How to register commands, events, entities, and more" >}}
{{< card link="assets" title="Assets" subtitle="Working with items, blocks, and game assets" >}}
{{< card link="codecs" title="Codecs" subtitle="Serialization and configuration with BuilderCodec" >}}
{{< card link="threading" title="Threading" subtitle="Thread safety and async operations" >}}
{{< card link="commands" title="Commands" subtitle="Create custom commands with arguments" icon="terminal" >}}
{{< card link="events" title="Events" subtitle="Listen and respond to game events" icon="bell" >}}
{{< card link="tasks" title="Tasks" subtitle="Async operations and scheduling" icon="clock" >}}
{{< /cards >}}

View File

@@ -0,0 +1,17 @@
---
title: Concepts de Base
type: docs
weight: 2
---
Cette section couvre les concepts fondamentaux à comprendre lors du développement de plugins Hytale.
{{< cards >}}
{{< card link="registries" title="Registres" subtitle="Comment enregistrer commandes, événements, entités, et plus" >}}
{{< card link="assets" title="Assets" subtitle="Travailler avec les items, blocs et assets du jeu" >}}
{{< card link="codecs" title="Codecs" subtitle="Sérialisation et configuration avec BuilderCodec" >}}
{{< card link="threading" title="Threading" subtitle="Sécurité des threads et opérations asynchrones" >}}
{{< card link="commands" title="Commandes" subtitle="Créer des commandes avec arguments" icon="terminal" >}}
{{< card link="events" title="Événements" subtitle="Écouter et répondre aux événements" icon="bell" >}}
{{< card link="tasks" title="Tâches" subtitle="Opérations async et planification" icon="clock" >}}
{{< /cards >}}

View File

@@ -0,0 +1,135 @@
---
title: Assets
type: docs
weight: 2
---
Assets in Hytale represent game content like items, blocks, and other resources. Understanding how to work with assets is essential for creating rich plugin experiences.
**Packages:**
- `com.hypixel.hytale.server.core.asset.type.item.config` (Item)
- `com.hypixel.hytale.server.core.asset.type.blocktype.config` (BlockType)
- `com.hypixel.hytale.server.core.inventory` (ItemStack)
## Asset Types
Hytale provides several asset types:
| Asset Type | Description |
|------------|-------------|
| `Item` | Represents an item that can be held or stored |
| `BlockType` | Defines a type of block in the world |
| `EntityType` | Defines an entity type |
## Working with Items
### Getting an Item
```java
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
// Get an item by identifier
Item sword = Item.getAssetMap().getAsset("hytale:iron_sword");
// Check if item exists
if (sword != null) {
// Use the item
}
```
### Creating ItemStacks
```java
import com.hypixel.hytale.server.core.inventory.ItemStack;
// Create an ItemStack with quantity
ItemStack stack = new ItemStack("hytale:iron_sword", 5);
// Or with just the item ID (quantity defaults to 1)
ItemStack singleItem = new ItemStack("hytale:iron_sword");
// Access properties
String itemId = stack.getItemId();
int quantity = stack.getQuantity();
```
## Working with Blocks
### Getting a BlockType
```java
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
BlockType stone = BlockType.getAssetMap().getAsset("hytale:stone");
```
### Block in World
```java
// Get block at position
World world = player.getWorld();
BlockType blockType = world.getBlockType(x, y, z);
// Set block at position
world.setBlock(x, y, z, "hytale:stone");
```
## Asset Registry
Register custom assets through your plugin:
```java
@Override
public void start() {
// Register custom assets
getAssetRegistry().register(myCustomAsset);
}
```
## Asset Identifiers
Assets are identified by namespaced identifiers:
```
namespace:path
Examples:
- hytale:stone
- myplugin:custom_sword
- myplugin:blocks/special_block
```
Your plugin's namespace is typically derived from your manifest's group and name.
## Asset Packs
Plugins can include embedded asset packs. Set `includesAssetPack: true` in your manifest:
```json
{
"name": "MyPlugin",
"version": "1.0.0",
"group": "com.example",
"main": "com.example.myplugin.MyPlugin",
"includesAssetPack": true
}
```
Asset pack files should be structured in your JAR:
```
assets/
└── myplugin/
├── items/
│ └── custom_sword.json
└── blocks/
└── custom_block.json
```
## Best Practices
{{< callout type="tip" >}}
- Cache asset references when possible to avoid repeated lookups
- Always null-check when retrieving assets by identifier
- Use meaningful namespaced identifiers for your custom assets
{{< /callout >}}

View File

@@ -0,0 +1,135 @@
---
title: Assets
type: docs
weight: 2
---
Les assets dans Hytale représentent le contenu du jeu comme les items, blocs et autres ressources. Comprendre comment travailler avec les assets est essentiel pour créer des expériences de plugin riches.
**Packages:**
- `com.hypixel.hytale.server.core.asset.type.item.config` (Item)
- `com.hypixel.hytale.server.core.asset.type.blocktype.config` (BlockType)
- `com.hypixel.hytale.server.core.inventory` (ItemStack)
## Types d'Assets
Hytale fournit plusieurs types d'assets :
| Type d'Asset | Description |
|--------------|-------------|
| `Item` | Représente un item qui peut être tenu ou stocké |
| `BlockType` | Définit un type de bloc dans le monde |
| `EntityType` | Définit un type d'entité |
## Travailler avec les Items
### Obtenir un Item
```java
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
// Obtenir un item par identifiant
Item sword = Item.getAssetMap().getAsset("hytale:iron_sword");
// Vérifier si l'item existe
if (sword != null) {
// Utiliser l'item
}
```
### Créer des ItemStacks
```java
import com.hypixel.hytale.server.core.inventory.ItemStack;
// Créer un ItemStack avec une quantité
ItemStack stack = new ItemStack("hytale:iron_sword", 5);
// Ou juste avec l'ID de l'item (quantité par défaut à 1)
ItemStack singleItem = new ItemStack("hytale:iron_sword");
// Accéder aux propriétés
String itemId = stack.getItemId();
int quantity = stack.getQuantity();
```
## Travailler avec les Blocs
### Obtenir un BlockType
```java
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
BlockType stone = BlockType.getAssetMap().getAsset("hytale:stone");
```
### Bloc dans le Monde
```java
// Obtenir le bloc à une position
World world = player.getWorld();
BlockType blockType = world.getBlockType(x, y, z);
// Définir le bloc à une position
world.setBlock(x, y, z, "hytale:stone");
```
## Registre d'Assets
Enregistrez des assets personnalisés via votre plugin :
```java
@Override
public void start() {
// Enregistrer des assets personnalisés
getAssetRegistry().register(myCustomAsset);
}
```
## Identifiants d'Assets
Les assets sont identifiés par des identifiants avec namespace :
```
namespace:chemin
Exemples:
- hytale:stone
- myplugin:custom_sword
- myplugin:blocks/special_block
```
Le namespace de votre plugin est généralement dérivé du groupe et du nom de votre manifest.
## Packs d'Assets
Les plugins peuvent inclure des packs d'assets embarqués. Définissez `includesAssetPack: true` dans votre manifest :
```json
{
"name": "MyPlugin",
"version": "1.0.0",
"group": "com.example",
"main": "com.example.myplugin.MyPlugin",
"includesAssetPack": true
}
```
Les fichiers du pack d'assets doivent être structurés dans votre JAR :
```
assets/
└── myplugin/
├── items/
│ └── custom_sword.json
└── blocks/
└── custom_block.json
```
## Bonnes Pratiques
{{< callout type="tip" >}}
- Mettez en cache les références d'assets quand c'est possible pour éviter les recherches répétées
- Vérifiez toujours les null lors de la récupération d'assets par identifiant
- Utilisez des identifiants avec namespace significatifs pour vos assets personnalisés
{{< /callout >}}

View File

@@ -0,0 +1,227 @@
---
title: Codecs
type: docs
weight: 3
---
Codecs in Hytale provide a powerful way to serialize and deserialize data. They're used for configuration files, network communication, and data persistence.
**Package:** `com.hypixel.hytale.codec`
## BuilderCodec
The `BuilderCodec` is the primary codec type for complex objects. It uses a builder pattern for construction.
### Defining a Codec
```java
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
public class MyConfig {
private String serverName;
private int maxPlayers = 20; // Default value
private boolean debugMode = false;
public static final BuilderCodec<MyConfig> CODEC = BuilderCodec.builder(MyConfig.class, MyConfig::new)
.append(new KeyedCodec<>("ServerName", Codec.STRING),
(config, val) -> config.serverName = val,
config -> config.serverName)
.add()
.append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER),
(config, val) -> config.maxPlayers = val,
config -> config.maxPlayers)
.add()
.append(new KeyedCodec<>("DebugMode", Codec.BOOLEAN),
(config, val) -> config.debugMode = val,
config -> config.debugMode)
.add()
.build();
// Getters...
public String getServerName() { return serverName; }
public int getMaxPlayers() { return maxPlayers; }
public boolean isDebugMode() { return debugMode; }
}
```
{{< callout type="warning" >}}
**KeyedCodec keys must start with an uppercase letter** (PascalCase). Keys like `"serverName"` will throw an `IllegalArgumentException`.
{{< /callout >}}
### JSON Structure
The codec above corresponds to this JSON:
```json
{
"ServerName": "My Server",
"MaxPlayers": 100,
"DebugMode": true
}
```
## Basic Codecs
Hytale provides codecs for primitive types:
| Codec | Type |
|-------|------|
| `Codec.STRING` | String |
| `Codec.INTEGER` | Integer |
| `Codec.LONG` | Long |
| `Codec.FLOAT` | Float |
| `Codec.DOUBLE` | Double |
| `Codec.BOOLEAN` | Boolean |
| `Codec.BYTE` | Byte |
| `Codec.SHORT` | Short |
## Using Config Files
Register configuration files in `setup()`:
```java
import com.hypixel.hytale.server.core.util.Config;
import java.util.logging.Level;
private Config<MyConfig> config;
@Override
public void setup() {
config = withConfig(MyConfig.CODEC);
}
@Override
public void start() {
MyConfig cfg = config.get();
getLogger().at(Level.INFO).log("Server name: " + cfg.getServerName());
}
```
### Config File Location
Config files are automatically stored in your plugin's data directory:
```
Plugins/
└── your-plugin/
└── config.json
```
### Named Configs
You can have multiple config files:
```java
Config<MyConfig> mainConfig = withConfig("main", MyConfig.CODEC);
Config<DatabaseConfig> dbConfig = withConfig("database", DatabaseConfig.CODEC);
```
## Required vs Optional Fields
By default, all fields are **optional**. To make a field required, add a validator:
```java
import com.hypixel.hytale.codec.validation.Validators;
BuilderCodec.builder(MyConfig.class, MyConfig::new)
.append(new KeyedCodec<>("ServerName", Codec.STRING),
(config, val) -> config.serverName = val,
config -> config.serverName)
.addValidator(Validators.nonNull()) // Makes field required
.add()
.append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER),
(config, val) -> config.maxPlayers = val,
config -> config.maxPlayers)
.add() // Optional - uses default value from class
.build();
```
## List and Map Codecs
For collections:
```java
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import com.hypixel.hytale.codec.codecs.map.ObjectMapCodec;
// Array of strings
Codec<String[]> stringArray = new ArrayCodec<>(Codec.STRING, String[]::new);
// Map with string keys
Codec<Map<String, Integer>> stringIntMap = new ObjectMapCodec<>(
Codec.INTEGER,
LinkedHashMap::new,
key -> key, // Key to string
str -> str // String to key
);
```
## Enum Codecs
For enum types:
```java
import com.hypixel.hytale.codec.codecs.EnumCodec;
public enum GameMode {
SURVIVAL, CREATIVE, ADVENTURE
}
Codec<GameMode> gameModeCodec = new EnumCodec<>(GameMode.class);
```
## Nested Codecs
Codecs can be nested for complex structures:
```java
public class ServerSettings {
private GeneralSettings general;
private DatabaseSettings database;
public static final BuilderCodec<ServerSettings> CODEC = BuilderCodec.builder(ServerSettings.class, ServerSettings::new)
.append(new KeyedCodec<>("General", GeneralSettings.CODEC),
(s, val) -> s.general = val,
s -> s.general)
.add()
.append(new KeyedCodec<>("Database", DatabaseSettings.CODEC),
(s, val) -> s.database = val,
s -> s.database)
.add()
.build();
}
```
## Inheritance
Use parent codecs for class hierarchies:
```java
// Base class codec
public static final BuilderCodec<BaseEntity> BASE_CODEC = BuilderCodec.builder(BaseEntity.class, BaseEntity::new)
.append(new KeyedCodec<>("Id", Codec.STRING),
(e, val) -> e.id = val,
e -> e.id)
.add()
.build();
// Child class codec inherits from parent
public static final BuilderCodec<PlayerEntity> CODEC = BuilderCodec.builder(PlayerEntity.class, PlayerEntity::new, BASE_CODEC)
.append(new KeyedCodec<>("Username", Codec.STRING),
(e, val) -> e.username = val,
e -> e.username)
.add()
.build();
```
## Best Practices
{{< callout type="tip" >}}
- Define codecs as `public static final` fields
- Use PascalCase for JSON keys (e.g., `"ServerName"`, not `"serverName"`)
- Set default values in class field declarations
- Use `Validators.nonNull()` for required fields
- Keep configuration classes with simple setters for codec compatibility
{{< /callout >}}

View File

@@ -0,0 +1,227 @@
---
title: Codecs
type: docs
weight: 3
---
Les codecs dans Hytale fournissent un moyen puissant de sérialiser et désérialiser des données. Ils sont utilisés pour les fichiers de configuration, la communication réseau et la persistance des données.
**Package:** `com.hypixel.hytale.codec`
## BuilderCodec
Le `BuilderCodec` est le type de codec principal pour les objets complexes. Il utilise un pattern builder pour la construction.
### Définir un Codec
```java
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
public class MyConfig {
private String serverName;
private int maxPlayers = 20; // Valeur par défaut
private boolean debugMode = false;
public static final BuilderCodec<MyConfig> CODEC = BuilderCodec.builder(MyConfig.class, MyConfig::new)
.append(new KeyedCodec<>("ServerName", Codec.STRING),
(config, val) -> config.serverName = val,
config -> config.serverName)
.add()
.append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER),
(config, val) -> config.maxPlayers = val,
config -> config.maxPlayers)
.add()
.append(new KeyedCodec<>("DebugMode", Codec.BOOLEAN),
(config, val) -> config.debugMode = val,
config -> config.debugMode)
.add()
.build();
// Getters...
public String getServerName() { return serverName; }
public int getMaxPlayers() { return maxPlayers; }
public boolean isDebugMode() { return debugMode; }
}
```
{{< callout type="warning" >}}
**Les clés KeyedCodec doivent commencer par une majuscule** (PascalCase). Les clés comme `"serverName"` lanceront une `IllegalArgumentException`.
{{< /callout >}}
### Structure JSON
Le codec ci-dessus correspond à ce JSON :
```json
{
"ServerName": "Mon Serveur",
"MaxPlayers": 100,
"DebugMode": true
}
```
## Codecs de Base
Hytale fournit des codecs pour les types primitifs :
| Codec | Type |
|-------|------|
| `Codec.STRING` | String |
| `Codec.INTEGER` | Integer |
| `Codec.LONG` | Long |
| `Codec.FLOAT` | Float |
| `Codec.DOUBLE` | Double |
| `Codec.BOOLEAN` | Boolean |
| `Codec.BYTE` | Byte |
| `Codec.SHORT` | Short |
## Utilisation des Fichiers de Config
Enregistrez les fichiers de configuration dans `setup()` :
```java
import com.hypixel.hytale.server.core.util.Config;
import java.util.logging.Level;
private Config<MyConfig> config;
@Override
public void setup() {
config = withConfig(MyConfig.CODEC);
}
@Override
public void start() {
MyConfig cfg = config.get();
getLogger().at(Level.INFO).log("Nom du serveur : " + cfg.getServerName());
}
```
### Emplacement du Fichier Config
Les fichiers de config sont automatiquement stockés dans le répertoire de données de votre plugin :
```
Plugins/
└── your-plugin/
└── config.json
```
### Configs Nommées
Vous pouvez avoir plusieurs fichiers de configuration :
```java
Config<MyConfig> mainConfig = withConfig("main", MyConfig.CODEC);
Config<DatabaseConfig> dbConfig = withConfig("database", DatabaseConfig.CODEC);
```
## Champs Requis vs Optionnels
Par défaut, tous les champs sont **optionnels**. Pour rendre un champ requis, ajoutez un validateur :
```java
import com.hypixel.hytale.codec.validation.Validators;
BuilderCodec.builder(MyConfig.class, MyConfig::new)
.append(new KeyedCodec<>("ServerName", Codec.STRING),
(config, val) -> config.serverName = val,
config -> config.serverName)
.addValidator(Validators.nonNull()) // Rend le champ requis
.add()
.append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER),
(config, val) -> config.maxPlayers = val,
config -> config.maxPlayers)
.add() // Optionnel - utilise la valeur par défaut de la classe
.build();
```
## Codecs List et Map
Pour les collections :
```java
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import com.hypixel.hytale.codec.codecs.map.ObjectMapCodec;
// Tableau de strings
Codec<String[]> stringArray = new ArrayCodec<>(Codec.STRING, String[]::new);
// Map avec clés string
Codec<Map<String, Integer>> stringIntMap = new ObjectMapCodec<>(
Codec.INTEGER,
LinkedHashMap::new,
key -> key, // Clé vers string
str -> str // String vers clé
);
```
## Codecs Enum
Pour les types enum :
```java
import com.hypixel.hytale.codec.codecs.EnumCodec;
public enum GameMode {
SURVIVAL, CREATIVE, ADVENTURE
}
Codec<GameMode> gameModeCodec = new EnumCodec<>(GameMode.class);
```
## Codecs Imbriqués
Les codecs peuvent être imbriqués pour des structures complexes :
```java
public class ServerSettings {
private GeneralSettings general;
private DatabaseSettings database;
public static final BuilderCodec<ServerSettings> CODEC = BuilderCodec.builder(ServerSettings.class, ServerSettings::new)
.append(new KeyedCodec<>("General", GeneralSettings.CODEC),
(s, val) -> s.general = val,
s -> s.general)
.add()
.append(new KeyedCodec<>("Database", DatabaseSettings.CODEC),
(s, val) -> s.database = val,
s -> s.database)
.add()
.build();
}
```
## Héritage
Utilisez des codecs parents pour les hiérarchies de classes :
```java
// Codec classe de base
public static final BuilderCodec<BaseEntity> BASE_CODEC = BuilderCodec.builder(BaseEntity.class, BaseEntity::new)
.append(new KeyedCodec<>("Id", Codec.STRING),
(e, val) -> e.id = val,
e -> e.id)
.add()
.build();
// Le codec classe enfant hérite du parent
public static final BuilderCodec<PlayerEntity> CODEC = BuilderCodec.builder(PlayerEntity.class, PlayerEntity::new, BASE_CODEC)
.append(new KeyedCodec<>("Username", Codec.STRING),
(e, val) -> e.username = val,
e -> e.username)
.add()
.build();
```
## Bonnes Pratiques
{{< callout type="tip" >}}
- Définissez les codecs comme champs `public static final`
- Utilisez PascalCase pour les clés JSON (ex : `"ServerName"`, pas `"serverName"`)
- Définissez les valeurs par défaut dans les déclarations de champs de classe
- Utilisez `Validators.nonNull()` pour les champs requis
- Gardez les classes de configuration avec des setters simples pour la compatibilité codec
{{< /callout >}}

View File

@@ -0,0 +1,14 @@
---
title: Commands
type: docs
weight: 1
---
Commands allow players and operators to interact with your plugin through the chat or console. This section covers how to create and register commands.
{{< cards >}}
{{< card link="creating-commands" title="Creating Commands" subtitle="Implement AbstractCommand for custom commands" >}}
{{< card link="argument-types" title="Argument Types" subtitle="All available ArgTypes and how to use them" >}}
{{< card link="subcommands" title="Subcommands" subtitle="Create command hierarchies" >}}
{{< card link="command-context" title="Command Context" subtitle="Access sender, arguments, and more" >}}
{{< /cards >}}

View File

@@ -0,0 +1,14 @@
---
title: Commandes
type: docs
weight: 1
---
Les commandes permettent aux joueurs et opérateurs d'interagir avec votre plugin via le chat ou la console. Cette section explique comment créer et enregistrer des commandes.
{{< cards >}}
{{< card link="creating-commands" title="Créer des Commandes" subtitle="Implémenter AbstractCommand pour des commandes personnalisées" >}}
{{< card link="argument-types" title="Types d'Arguments" subtitle="Tous les ArgTypes disponibles et comment les utiliser" >}}
{{< card link="subcommands" title="Sous-commandes" subtitle="Créer des hiérarchies de commandes" >}}
{{< card link="command-context" title="Contexte de Commande" subtitle="Accéder à l'expéditeur, arguments, et plus" >}}
{{< /cards >}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
---
title: Command Context
type: docs
weight: 4
---
The `CommandContext` provides access to command execution information, including the sender, parsed arguments, and the original input string.
## Accessing the Context
The context is passed to your `execute()` method:
```java
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Use context here
return null;
}
```
## Command Sender
Get the entity or console that executed the command:
```java
CommandSender sender = context.sender();
// Send messages (must use Message class)
sender.sendMessage(Message.raw("Hello!"));
sender.sendMessage(Message.translation("my.translation.key"));
// Check permissions
if (sender.hasPermission("myplugin.admin")) {
// ...
}
```
### Checking Sender Type
```java
if (context.sender() instanceof Player) {
Player player = (Player) context.sender();
// Player-specific logic
} else {
// Console or other sender
context.sender().sendMessage("This command requires a player!");
}
```
## Getting Arguments
### Required Arguments
```java
private final RequiredArg<PlayerRef> playerArg;
private final RequiredArg<Integer> countArg;
// In execute():
PlayerRef player = context.get(playerArg); // Never null for required args
int count = context.get(countArg); // Never null for required args
```
### Optional Arguments
```java
private final OptionalArg<String> reasonArg;
// Get value (may be null if not provided)
String reason = context.get(reasonArg);
// Check if provided before using
if (context.provided(reasonArg)) {
String reason = context.get(reasonArg);
}
```
### Default Arguments
```java
private final DefaultArg<Integer> countArg; // Default: 1
// Always returns a value (never null)
int count = context.get(countArg); // Returns default if not specified
```
### Flag Arguments
```java
private final FlagArg silentFlag;
// Check if flag was provided
boolean isSilent = context.provided(silentFlag);
```
## Input String
Access the original command input:
```java
String input = context.getInputString();
// For "/give player123 sword 5" -> "give player123 sword 5"
```
## The Command
Access the command being executed:
```java
AbstractCommand command = context.getCalledCommand();
String commandName = command.getName();
String fullName = command.getFullyQualifiedName(); // e.g., "admin kick"
```
## Argument Type Fallback Behavior
Some argument types have special processing with fallback behavior. This is handled by the argument type's `processedGet()` method:
### PLAYER_REF Fallback
When using `PLAYER_REF` with optional arguments:
- If argument is not provided and sender is a player, the argument type can return the sender's PlayerRef
```java
// Note: This fallback behavior is in the argument type, not CommandContext
private final OptionalArg<PlayerRef> targetArg;
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef target = context.get(targetArg);
// Manual fallback if null
if (target == null && context.sender() instanceof Player player) {
target = player.getPlayerRef();
}
// ...
}
```
### World Argument Fallback
When using `WORLD` with optional arguments:
- If not specified and sender is player, returns player's world
- If only one world exists, returns that world
```java
private final OptionalArg<World> worldArg;
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
World world = context.get(worldArg);
// Manual fallback if null
if (world == null && context.sender() instanceof Player player) {
world = player.getWorld();
}
// ...
}
```
## Error Handling
Commands can throw exceptions for error cases:
```java
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef playerRef = context.get(playerArg);
// Check if player is online via ECS reference
Ref<EntityStore> ref = playerRef.getReference();
if (ref == null || !ref.isValid()) {
throw new GeneralCommandException(
Message.translation("error.player.offline")
.param("player", playerRef.getUsername())
);
}
// Continue execution with valid reference
return null;
}
```
## Asynchronous Commands
Return a `CompletableFuture` for async operations:
```java
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
if (!(context.sender() instanceof Player player)) {
return null;
}
PlayerRef playerRef = player.getPlayerRef(); // Note: deprecated
World world = player.getWorld();
return CompletableFuture.runAsync(() -> {
// Async operation (e.g., database query)
// PlayerData data = database.loadData(playerRef.getUuid());
// Return to world thread for game logic
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && ref.isValid()) {
// applyData(ref, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
});
});
}
```
## Complete Example
```java
public class GiveCommand extends AbstractCommand {
private final RequiredArg<Item> itemArg;
private final OptionalArg<PlayerRef> targetArg;
private final DefaultArg<Integer> countArg;
private final FlagArg silentFlag;
public GiveCommand() {
super("give", "Give items to a player");
itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET);
targetArg = withOptionalArg("target", "Target player", ArgTypes.PLAYER_REF);
countArg = withDefaultArg("count", "Amount", ArgTypes.INTEGER, 1, "1");
silentFlag = withFlagArg("silent", "Don't broadcast");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Get arguments
Item item = context.get(itemArg);
int count = context.get(countArg); // Uses default if not specified
boolean silent = context.provided(silentFlag);
// Get target with fallback to sender
PlayerRef targetRef = context.get(targetArg);
if (targetRef == null && context.sender() instanceof Player senderPlayer) {
targetRef = senderPlayer.getPlayerRef(); // Note: deprecated
}
if (targetRef == null) {
throw new GeneralCommandException(
Message.raw("Must specify a target player!")
);
}
// Validate target is online via ECS reference
Ref<EntityStore> ref = targetRef.getReference();
if (ref == null || !ref.isValid()) {
throw new GeneralCommandException(
Message.raw("Player is not online!")
);
}
// Execute - give item to player via ECS
// ...
// Feedback
if (!silent) {
context.sendMessage(Message.raw(
"Gave " + count + "x " + item.getId() + " to " + targetRef.getUsername()
));
}
return null;
}
}
```

View File

@@ -0,0 +1,272 @@
---
title: Command Context
type: docs
weight: 4
---
The `CommandContext` provides access to command execution information, including the sender, parsed arguments, and the original input string.
## Accessing the Context
The context is passed to your `execute()` method:
```java
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Use context here
return null;
}
```
## Command Sender
Get the entity or console that executed the command:
```java
CommandSender sender = context.sender();
// Send messages (must use Message class)
sender.sendMessage(Message.raw("Hello!"));
sender.sendMessage(Message.translation("my.translation.key"));
// Check permissions
if (sender.hasPermission("myplugin.admin")) {
// ...
}
```
### Checking Sender Type
```java
if (context.sender() instanceof Player) {
Player player = (Player) context.sender();
// Player-specific logic
} else {
// Console or other sender
context.sender().sendMessage("This command requires a player!");
}
```
## Getting Arguments
### Required Arguments
```java
private final RequiredArg<PlayerRef> playerArg;
private final RequiredArg<Integer> countArg;
// In execute():
PlayerRef player = context.get(playerArg); // Never null for required args
int count = context.get(countArg); // Never null for required args
```
### Optional Arguments
```java
private final OptionalArg<String> reasonArg;
// Get value (may be null if not provided)
String reason = context.get(reasonArg);
// Check if provided before using
if (context.provided(reasonArg)) {
String reason = context.get(reasonArg);
}
```
### Default Arguments
```java
private final DefaultArg<Integer> countArg; // Default: 1
// Always returns a value (never null)
int count = context.get(countArg); // Returns default if not specified
```
### Flag Arguments
```java
private final FlagArg silentFlag;
// Check if flag was provided
boolean isSilent = context.provided(silentFlag);
```
## Input String
Access the original command input:
```java
String input = context.getInputString();
// For "/give player123 sword 5" -> "give player123 sword 5"
```
## The Command
Access the command being executed:
```java
AbstractCommand command = context.getCalledCommand();
String commandName = command.getName();
String fullName = command.getFullyQualifiedName(); // e.g., "admin kick"
```
## Argument Type Fallback Behavior
Some argument types have special processing with fallback behavior. This is handled by the argument type's `processedGet()` method:
### PLAYER_REF Fallback
When using `PLAYER_REF` with optional arguments:
- If argument is not provided and sender is a player, the argument type can return the sender's PlayerRef
```java
// Note: This fallback behavior is in the argument type, not CommandContext
private final OptionalArg<PlayerRef> targetArg;
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef target = context.get(targetArg);
// Manual fallback if null
if (target == null && context.sender() instanceof Player player) {
target = player.getPlayerRef();
}
// ...
}
```
### World Argument Fallback
When using `WORLD` with optional arguments:
- If not specified and sender is player, returns player's world
- If only one world exists, returns that world
```java
private final OptionalArg<World> worldArg;
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
World world = context.get(worldArg);
// Manual fallback if null
if (world == null && context.sender() instanceof Player player) {
world = player.getWorld();
}
// ...
}
```
## Error Handling
Commands can throw exceptions for error cases:
```java
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef playerRef = context.get(playerArg);
// Check if player is online via ECS reference
Ref<EntityStore> ref = playerRef.getReference();
if (ref == null || !ref.isValid()) {
throw new GeneralCommandException(
Message.translation("error.player.offline")
.param("player", playerRef.getUsername())
);
}
// Continue execution with valid reference
return null;
}
```
## Asynchronous Commands
Return a `CompletableFuture` for async operations:
```java
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
if (!(context.sender() instanceof Player player)) {
return null;
}
PlayerRef playerRef = player.getPlayerRef(); // Note: deprecated
World world = player.getWorld();
return CompletableFuture.runAsync(() -> {
// Async operation (e.g., database query)
// PlayerData data = database.loadData(playerRef.getUuid());
// Return to world thread for game logic
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && ref.isValid()) {
// applyData(ref, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
});
});
}
```
## Complete Example
```java
public class GiveCommand extends AbstractCommand {
private final RequiredArg<Item> itemArg;
private final OptionalArg<PlayerRef> targetArg;
private final DefaultArg<Integer> countArg;
private final FlagArg silentFlag;
public GiveCommand() {
super("give", "Give items to a player");
itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET);
targetArg = withOptionalArg("target", "Target player", ArgTypes.PLAYER_REF);
countArg = withDefaultArg("count", "Amount", ArgTypes.INTEGER, 1, "1");
silentFlag = withFlagArg("silent", "Don't broadcast");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Get arguments
Item item = context.get(itemArg);
int count = context.get(countArg); // Uses default if not specified
boolean silent = context.provided(silentFlag);
// Get target with fallback to sender
PlayerRef targetRef = context.get(targetArg);
if (targetRef == null && context.sender() instanceof Player senderPlayer) {
targetRef = senderPlayer.getPlayerRef(); // Note: deprecated
}
if (targetRef == null) {
throw new GeneralCommandException(
Message.raw("Must specify a target player!")
);
}
// Validate target is online via ECS reference
Ref<EntityStore> ref = targetRef.getReference();
if (ref == null || !ref.isValid()) {
throw new GeneralCommandException(
Message.raw("Player is not online!")
);
}
// Execute - give item to player via ECS
// ...
// Feedback
if (!silent) {
context.sendMessage(Message.raw(
"Gave " + count + "x " + item.getId() + " to " + targetRef.getUsername()
));
}
return null;
}
}
```

View File

@@ -0,0 +1,264 @@
---
title: Creating Commands
type: docs
weight: 1
---
Commands in Hytale are created by extending the `AbstractCommand` class. This page covers the basics of creating and registering commands.
## Basic Command Structure
```java
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.Message;
import java.util.concurrent.CompletableFuture;
public class HelloCommand extends AbstractCommand {
public HelloCommand() {
super("hello", "Sends a greeting message");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
context.sendMessage(Message.raw("Hello, World!"));
return null;
}
}
```
## Constructors
`AbstractCommand` provides several constructors:
```java
// Name and description
protected AbstractCommand(String name, String description)
// Name, description, and requires confirmation flag
protected AbstractCommand(String name, String description, boolean requiresConfirmation)
// Description only (for variant commands)
protected AbstractCommand(String description)
```
## Registering Commands
Register commands in your plugin's `start()` method:
```java
@Override
public void start() {
getCommandRegistry().registerCommand(new HelloCommand());
}
```
## Adding Aliases
Commands can have multiple names:
```java
public class TeleportCommand extends AbstractCommand {
public TeleportCommand() {
super("teleport", "Teleport to a location");
addAliases("tp", "warp");
}
// ...
}
```
Players can now use `/teleport`, `/tp`, or `/warp`.
## Command with Arguments
Add required arguments using `withRequiredArg`:
```java
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
// For ECS operations: import com.hypixel.hytale.component.Ref;
// For ECS operations: import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
import com.hypixel.hytale.server.core.Message;
import java.util.concurrent.CompletableFuture;
public class GiveCommand extends AbstractCommand {
private final RequiredArg<PlayerRef> targetArg;
private final RequiredArg<Item> itemArg;
public GiveCommand() {
super("give", "Give an item to a player");
targetArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF);
itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef targetRef = context.get(targetArg);
Item item = context.get(itemArg);
// PlayerRef provides direct access to player info
String username = targetRef.getUsername();
// For ECS operations, use getReference() to access the EntityStore
// Ref<EntityStore> entityRef = targetRef.getReference();
// Give item to player...
context.sendMessage(Message.raw("Gave item to " + username));
return null;
}
}
```
## Optional Arguments
Add optional arguments that don't need to be specified:
```java
private final OptionalArg<Integer> countArg;
public GiveCommand() {
super("give", "Give items to a player");
targetArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF);
itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET);
countArg = withOptionalArg("count", "Number of items", ArgTypes.INTEGER);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef targetRef = context.get(targetArg);
Item item = context.get(itemArg);
Integer count = context.get(countArg); // May be null
int amount = count != null ? count : 1;
// Give items to player...
return null;
}
```
Usage: `/give player123 iron_sword` or `/give player123 iron_sword --count 5`
## Default Arguments
Arguments with default values:
```java
private final DefaultArg<Integer> countArg;
public GiveCommand() {
super("give", "Give items to a player");
countArg = withDefaultArg("count", "Number of items",
ArgTypes.INTEGER, 1, "defaults to 1");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
int count = context.get(countArg); // Never null, uses default
// ...
}
```
## Flag Arguments
Boolean flags that can be toggled:
```java
private final FlagArg silentFlag;
public BroadcastCommand() {
super("broadcast", "Send a message to all players");
silentFlag = withFlagArg("silent", "Don't show sender name");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
boolean silent = context.provided(silentFlag);
// ...
}
```
Usage: `/broadcast Hello everyone! --silent`
## List Arguments
For arguments that accept multiple values:
```java
private final RequiredArg<List<String>> playersArg;
public KickAllCommand() {
super("kickall", "Kick multiple players");
playersArg = withListRequiredArg("players", "Players to kick", ArgTypes.STRING);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
List<String> players = context.get(playersArg);
for (String player : players) {
// Kick each player
}
return null;
}
```
Usage: `/kickall player1 player2 player3`
Available list argument methods:
- `withListRequiredArg(name, description, argType)` - Required list
- `withListOptionalArg(name, description, argType)` - Optional list
- `withListDefaultArg(name, description, argType, defaultValue, defaultDesc)` - List with default
## Requiring Confirmation
For dangerous commands, require explicit confirmation:
```java
public class ResetCommand extends AbstractCommand {
public ResetCommand() {
super("reset", "Reset all player data", true); // requiresConfirmation = true
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// This only runs if --confirm was provided
resetAllData();
return null;
}
}
```
Usage: `/reset --confirm`
## Permissions
Commands automatically generate permissions based on your plugin name:
```
{group}.{plugin}.command.{commandname}
```
For example: `com.example.myplugin.command.give`
You can also set a custom permission:
```java
public GiveCommand() {
super("give", "Give items");
requirePermission("myplugin.admin.give");
}
```

View File

@@ -0,0 +1,264 @@
---
title: Créer des Commandes
type: docs
weight: 1
---
Les commandes dans Hytale sont créées en étendant la classe `AbstractCommand`. Cette page couvre les bases de la création et de l'enregistrement des commandes.
## Structure de Base d'une Commande
```java
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.Message;
import java.util.concurrent.CompletableFuture;
public class HelloCommand extends AbstractCommand {
public HelloCommand() {
super("hello", "Envoie un message de salutation");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
context.sendMessage(Message.raw("Bonjour, le monde !"));
return null;
}
}
```
## Constructeurs
`AbstractCommand` fournit plusieurs constructeurs :
```java
// Nom et description
protected AbstractCommand(String name, String description)
// Nom, description, et flag de confirmation requise
protected AbstractCommand(String name, String description, boolean requiresConfirmation)
// Description seule (pour les commandes variantes)
protected AbstractCommand(String description)
```
## Enregistrer des Commandes
Enregistrez les commandes dans la méthode `start()` de votre plugin :
```java
@Override
public void start() {
getCommandRegistry().registerCommand(new HelloCommand());
}
```
## Ajouter des Alias
Les commandes peuvent avoir plusieurs noms :
```java
public class TeleportCommand extends AbstractCommand {
public TeleportCommand() {
super("teleport", "Téléporter vers un emplacement");
addAliases("tp", "warp");
}
// ...
}
```
Les joueurs peuvent maintenant utiliser `/teleport`, `/tp`, ou `/warp`.
## Commande avec Arguments
Ajoutez des arguments requis avec `withRequiredArg` :
```java
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
// Pour les opérations ECS: import com.hypixel.hytale.component.Ref;
// Pour les opérations ECS: import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
import com.hypixel.hytale.server.core.Message;
import java.util.concurrent.CompletableFuture;
public class GiveCommand extends AbstractCommand {
private final RequiredArg<PlayerRef> targetArg;
private final RequiredArg<Item> itemArg;
public GiveCommand() {
super("give", "Donner un item à un joueur");
targetArg = withRequiredArg("player", "Joueur cible", ArgTypes.PLAYER_REF);
itemArg = withRequiredArg("item", "Item à donner", ArgTypes.ITEM_ASSET);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef targetRef = context.get(targetArg);
Item item = context.get(itemArg);
// PlayerRef fournit un accès direct aux infos du joueur
String username = targetRef.getUsername();
// Pour les opérations ECS, utiliser getReference() pour accéder à l'EntityStore
// Ref<EntityStore> entityRef = targetRef.getReference();
// Donner l'item au joueur...
context.sendMessage(Message.raw("Item donné à " + username));
return null;
}
}
```
## Arguments Optionnels
Ajoutez des arguments optionnels qui n'ont pas besoin d'être spécifiés :
```java
private final OptionalArg<Integer> countArg;
public GiveCommand() {
super("give", "Donner des items à un joueur");
targetArg = withRequiredArg("player", "Joueur cible", ArgTypes.PLAYER_REF);
itemArg = withRequiredArg("item", "Item à donner", ArgTypes.ITEM_ASSET);
countArg = withOptionalArg("count", "Nombre d'items", ArgTypes.INTEGER);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef targetRef = context.get(targetArg);
Item item = context.get(itemArg);
Integer count = context.get(countArg); // Peut être null
int amount = count != null ? count : 1;
// Donner les items au joueur...
return null;
}
```
Utilisation : `/give player123 iron_sword` ou `/give player123 iron_sword --count 5`
## Arguments par Défaut
Arguments avec valeurs par défaut :
```java
private final DefaultArg<Integer> countArg;
public GiveCommand() {
super("give", "Donner des items à un joueur");
countArg = withDefaultArg("count", "Nombre d'items",
ArgTypes.INTEGER, 1, "par défaut 1");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
int count = context.get(countArg); // Jamais null, utilise la valeur par défaut
// ...
}
```
## Arguments Flag
Flags booléens qui peuvent être activés :
```java
private final FlagArg silentFlag;
public BroadcastCommand() {
super("broadcast", "Envoyer un message à tous les joueurs");
silentFlag = withFlagArg("silent", "Ne pas afficher le nom de l'expéditeur");
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
boolean silent = context.provided(silentFlag);
// ...
}
```
Utilisation : `/broadcast Bonjour à tous ! --silent`
## Arguments Liste
Pour les arguments qui acceptent plusieurs valeurs :
```java
private final RequiredArg<List<String>> playersArg;
public KickAllCommand() {
super("kickall", "Expulser plusieurs joueurs");
playersArg = withListRequiredArg("players", "Joueurs à expulser", ArgTypes.STRING);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
List<String> players = context.get(playersArg);
for (String player : players) {
// Expulser chaque joueur
}
return null;
}
```
Utilisation : `/kickall player1 player2 player3`
Méthodes d'arguments liste disponibles :
- `withListRequiredArg(name, description, argType)` - Liste requise
- `withListOptionalArg(name, description, argType)` - Liste optionnelle
- `withListDefaultArg(name, description, argType, defaultValue, defaultDesc)` - Liste avec défaut
## Exiger une Confirmation
Pour les commandes dangereuses, exigez une confirmation explicite :
```java
public class ResetCommand extends AbstractCommand {
public ResetCommand() {
super("reset", "Réinitialiser toutes les données joueur", true); // requiresConfirmation = true
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Ceci ne s'exécute que si --confirm a été fourni
resetAllData();
return null;
}
}
```
Utilisation : `/reset --confirm`
## Permissions
Les commandes génèrent automatiquement des permissions basées sur le nom de votre plugin :
```
{group}.{plugin}.command.{commandname}
```
Par exemple : `com.example.myplugin.command.give`
Vous pouvez aussi définir une permission personnalisée :
```java
public GiveCommand() {
super("give", "Donner des items");
requirePermission("myplugin.admin.give");
}
```

View File

@@ -0,0 +1,224 @@
---
title: Subcommands
type: docs
weight: 3
---
Subcommands allow you to create hierarchical command structures, organizing related functionality under a single parent command.
## Creating Subcommands
### Basic Structure
```java
public class AdminCommand extends AbstractCommand {
public AdminCommand() {
super("admin", "Administration commands");
// Add subcommands
addSubCommand(new KickSubCommand());
addSubCommand(new BanSubCommand());
addSubCommand(new MuteSubCommand());
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// This runs when no subcommand is specified
context.sender().sendMessage(Message.raw("Usage: /admin <kick|ban|mute>"));
return null;
}
}
```
### Subcommand Implementation
```java
public class KickSubCommand extends AbstractCommand {
private final RequiredArg<PlayerRef> playerArg;
private final OptionalArg<String> reasonArg;
public KickSubCommand() {
super("kick", "Kick a player from the server");
playerArg = withRequiredArg("player", "Player to kick", ArgTypes.PLAYER_REF);
reasonArg = withOptionalArg("reason", "Kick reason", ArgTypes.STRING);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef target = context.get(playerArg);
String reason = context.get(reasonArg);
// Check if player is online via ECS reference
Ref<EntityStore> ref = target.getReference();
if (ref != null && ref.isValid()) {
// Disconnect via PacketHandler
String kickReason = reason != null ? reason : "Kicked by admin";
target.getPacketHandler().disconnect(kickReason);
context.sendMessage(Message.raw("Kicked " + target.getUsername()));
}
return null;
}
}
```
Usage: `/admin kick player123 --reason "Breaking rules"`
## Command Collections
For commands that only contain subcommands (no direct execution), extend `AbstractCommandCollection`:
```java
public class ManageCommand extends AbstractCommandCollection {
public ManageCommand() {
super("manage", "Management commands");
addSubCommand(new ManageUsersCommand());
addSubCommand(new ManageWorldsCommand());
addSubCommand(new ManagePluginsCommand());
}
}
```
With `AbstractCommandCollection`, running `/manage` without a subcommand will show available subcommands automatically.
## Nested Subcommands
Subcommands can have their own subcommands:
```java
public class ManageUsersCommand extends AbstractCommand {
public ManageUsersCommand() {
super("users", "User management");
addSubCommand(new ListUsersCommand()); // /manage users list
addSubCommand(new AddUserCommand()); // /manage users add
addSubCommand(new RemoveUserCommand()); // /manage users remove
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Show usage for /manage users
return null;
}
}
```
## Subcommand Aliases
Subcommands can have aliases just like regular commands:
```java
public class TeleportCommand extends AbstractCommand {
public TeleportCommand() {
super("teleport", "Teleport commands");
addAliases("tp");
addSubCommand(new TeleportHereCommand());
addSubCommand(new TeleportAllCommand());
}
}
public class TeleportHereCommand extends AbstractCommand {
public TeleportHereCommand() {
super("here", "Teleport player to you");
addAliases("h", "tome");
}
// ...
}
```
Now players can use:
- `/teleport here player1`
- `/tp here player1`
- `/tp h player1`
- `/tp tome player1`
## Command Variants
Variants allow the same command to accept different argument patterns:
```java
public class TpCommand extends AbstractCommand {
private final RequiredArg<PlayerRef> targetArg;
public TpCommand() {
super("tp", "Teleport command");
// Main variant: /tp <player>
targetArg = withRequiredArg("target", "Player to teleport to", ArgTypes.PLAYER_REF);
// Add variant: /tp <player> <destination>
addUsageVariant(new TpToPlayerVariant());
// Add variant: /tp <x> <y> <z>
addUsageVariant(new TpToPositionVariant());
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Teleport sender to target player
PlayerRef target = context.get(targetArg);
// ...
return null;
}
}
```
### Variant Implementation
```java
public class TpToPlayerVariant extends AbstractCommand {
private final RequiredArg<PlayerRef> playerArg;
private final RequiredArg<PlayerRef> destinationArg;
public TpToPlayerVariant() {
// No name for variants - use description only
super("Teleport one player to another");
playerArg = withRequiredArg("player", "Player to teleport", ArgTypes.PLAYER_REF);
destinationArg = withRequiredArg("destination", "Destination player", ArgTypes.PLAYER_REF);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef player = context.get(playerArg);
PlayerRef destination = context.get(destinationArg);
// Teleport player to destination
return null;
}
}
```
{{< callout type="info" >}}
Variants are distinguished by the number of required parameters. Each variant must have a different number of required arguments.
{{< /callout >}}
## Permission Inheritance
Subcommand permissions are automatically built from the parent:
```
/admin -> myplugin.command.admin
/admin kick -> myplugin.command.admin.kick
/admin ban -> myplugin.command.admin.ban
```
You can also set custom permissions:
```java
public KickSubCommand() {
super("kick", "Kick a player");
requirePermission("myplugin.admin.kick");
}
```

View File

@@ -0,0 +1,224 @@
---
title: Subcommands
type: docs
weight: 3
---
Subcommands allow you to create hierarchical command structures, organizing related functionality under a single parent command.
## Creating Subcommands
### Basic Structure
```java
public class AdminCommand extends AbstractCommand {
public AdminCommand() {
super("admin", "Administration commands");
// Add subcommands
addSubCommand(new KickSubCommand());
addSubCommand(new BanSubCommand());
addSubCommand(new MuteSubCommand());
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// This runs when no subcommand is specified
context.sender().sendMessage(Message.raw("Usage: /admin <kick|ban|mute>"));
return null;
}
}
```
### Subcommand Implementation
```java
public class KickSubCommand extends AbstractCommand {
private final RequiredArg<PlayerRef> playerArg;
private final OptionalArg<String> reasonArg;
public KickSubCommand() {
super("kick", "Kick a player from the server");
playerArg = withRequiredArg("player", "Player to kick", ArgTypes.PLAYER_REF);
reasonArg = withOptionalArg("reason", "Kick reason", ArgTypes.STRING);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef target = context.get(playerArg);
String reason = context.get(reasonArg);
// Check if player is online via ECS reference
Ref<EntityStore> ref = target.getReference();
if (ref != null && ref.isValid()) {
// Disconnect via PacketHandler
String kickReason = reason != null ? reason : "Kicked by admin";
target.getPacketHandler().disconnect(kickReason);
context.sendMessage(Message.raw("Kicked " + target.getUsername()));
}
return null;
}
}
```
Usage: `/admin kick player123 --reason "Breaking rules"`
## Command Collections
For commands that only contain subcommands (no direct execution), extend `AbstractCommandCollection`:
```java
public class ManageCommand extends AbstractCommandCollection {
public ManageCommand() {
super("manage", "Management commands");
addSubCommand(new ManageUsersCommand());
addSubCommand(new ManageWorldsCommand());
addSubCommand(new ManagePluginsCommand());
}
}
```
With `AbstractCommandCollection`, running `/manage` without a subcommand will show available subcommands automatically.
## Nested Subcommands
Subcommands can have their own subcommands:
```java
public class ManageUsersCommand extends AbstractCommand {
public ManageUsersCommand() {
super("users", "User management");
addSubCommand(new ListUsersCommand()); // /manage users list
addSubCommand(new AddUserCommand()); // /manage users add
addSubCommand(new RemoveUserCommand()); // /manage users remove
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Show usage for /manage users
return null;
}
}
```
## Subcommand Aliases
Subcommands can have aliases just like regular commands:
```java
public class TeleportCommand extends AbstractCommand {
public TeleportCommand() {
super("teleport", "Teleport commands");
addAliases("tp");
addSubCommand(new TeleportHereCommand());
addSubCommand(new TeleportAllCommand());
}
}
public class TeleportHereCommand extends AbstractCommand {
public TeleportHereCommand() {
super("here", "Teleport player to you");
addAliases("h", "tome");
}
// ...
}
```
Now players can use:
- `/teleport here player1`
- `/tp here player1`
- `/tp h player1`
- `/tp tome player1`
## Command Variants
Variants allow the same command to accept different argument patterns:
```java
public class TpCommand extends AbstractCommand {
private final RequiredArg<PlayerRef> targetArg;
public TpCommand() {
super("tp", "Teleport command");
// Main variant: /tp <player>
targetArg = withRequiredArg("target", "Player to teleport to", ArgTypes.PLAYER_REF);
// Add variant: /tp <player> <destination>
addUsageVariant(new TpToPlayerVariant());
// Add variant: /tp <x> <y> <z>
addUsageVariant(new TpToPositionVariant());
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
// Teleport sender to target player
PlayerRef target = context.get(targetArg);
// ...
return null;
}
}
```
### Variant Implementation
```java
public class TpToPlayerVariant extends AbstractCommand {
private final RequiredArg<PlayerRef> playerArg;
private final RequiredArg<PlayerRef> destinationArg;
public TpToPlayerVariant() {
// No name for variants - use description only
super("Teleport one player to another");
playerArg = withRequiredArg("player", "Player to teleport", ArgTypes.PLAYER_REF);
destinationArg = withRequiredArg("destination", "Destination player", ArgTypes.PLAYER_REF);
}
@Override
protected CompletableFuture<Void> execute(CommandContext context) {
PlayerRef player = context.get(playerArg);
PlayerRef destination = context.get(destinationArg);
// Teleport player to destination
return null;
}
}
```
{{< callout type="info" >}}
Variants are distinguished by the number of required parameters. Each variant must have a different number of required arguments.
{{< /callout >}}
## Permission Inheritance
Subcommand permissions are automatically built from the parent:
```
/admin -> myplugin.command.admin
/admin kick -> myplugin.command.admin.kick
/admin ban -> myplugin.command.admin.ban
```
You can also set custom permissions:
```java
public KickSubCommand() {
super("kick", "Kick a player");
requirePermission("myplugin.admin.kick");
}
```

View File

@@ -0,0 +1,15 @@
---
title: Events
type: docs
weight: 2
---
The event system allows your plugin to react to game events. You can listen for player actions, entity behaviors, world changes, and more.
{{< cards >}}
{{< card link="event-system" title="Event System" subtitle="How EventBus and registration work" >}}
{{< card link="event-priorities" title="Event Priorities" subtitle="Control the order of event handling" >}}
{{< card link="cancellable-events" title="Cancellable Events" subtitle="Prevent default game behavior" >}}
{{< card link="async-events" title="Async Events" subtitle="Handle events asynchronously" >}}
{{< card link="event-reference" title="Event Reference" subtitle="Complete list of available events" >}}
{{< /cards >}}

View File

@@ -0,0 +1,15 @@
---
title: Événements
type: docs
weight: 2
---
Le système d'événements permet à votre plugin de réagir aux événements du jeu. Vous pouvez écouter les actions des joueurs, comportements des entités, changements du monde, et plus.
{{< cards >}}
{{< card link="event-system" title="Système d'Événements" subtitle="Comment fonctionnent EventBus et l'enregistrement" >}}
{{< card link="event-priorities" title="Priorités d'Événements" subtitle="Contrôler l'ordre de gestion des événements" >}}
{{< card link="cancellable-events" title="Événements Annulables" subtitle="Empêcher le comportement par défaut du jeu" >}}
{{< card link="async-events" title="Événements Async" subtitle="Gérer les événements de façon asynchrone" >}}
{{< card link="event-reference" title="Référence des Événements" subtitle="Liste complète des événements disponibles" >}}
{{< /cards >}}

View File

@@ -0,0 +1,361 @@
---
title: Async Events
type: docs
weight: 4
---
Async events allow you to perform asynchronous operations during event handling. They use `CompletableFuture` for non-blocking execution.
## IAsyncEvent Interface
Events implementing `IAsyncEvent<K>` support asynchronous handling:
```java
public interface IAsyncEvent<K> extends IBaseEvent<K> {
// Async events use CompletableFuture in handlers
}
```
## Registering Async Handlers
Use `registerAsync()` for async events with `Void` key, or `registerAsyncGlobal()` for keyed events:
{{< callout type="warning" >}}
`PlayerChatEvent` implements `IAsyncEvent<String>` - it has a `String` key type. Therefore, you must use `registerAsyncGlobal()` instead of `registerAsync()`. The simple `registerAsync(Class, handler)` method only works for events with `Void` key type.
{{< /callout >}}
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// This runs asynchronously
String filtered = filterContent(event.getContent());
event.setContent(filtered);
return event;
});
});
```
## Async Registration Methods
### Basic Async Registration
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async processing
return event;
});
});
```
### With Priority
```java
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Runs early in async chain
return event;
})
);
```
### With Key
```java
getEventRegistry().registerAsync(
KeyedAsyncEvent.class,
myKey,
future -> future.thenApply(event -> {
// Only for events matching myKey
return event;
})
);
```
### Global Async
```java
getEventRegistry().registerAsyncGlobal(
KeyedAsyncEvent.class,
future -> future.thenApply(event -> {
// All events of this type
return event;
})
);
```
### Unhandled Async
```java
getEventRegistry().registerAsyncUnhandled(
KeyedAsyncEvent.class,
future -> future.thenApply(event -> {
// Events not handled by keyed handlers
return event;
})
);
```
## Working with CompletableFuture
### Sequential Operations
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future
.thenApply(event -> {
// Step 1: Filter content
event.setContent(filterProfanity(event.getContent()));
return event;
})
.thenApply(event -> {
// Step 2: Add formatting
event.setContent(addChatFormatting(event.getContent()));
return event;
})
.thenApply(event -> {
// Step 3: Log
logChatMessage(event.getSender(), event.getContent());
return event;
});
});
```
### Parallel Operations
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenCompose(event -> {
// Start multiple async operations in parallel
CompletableFuture<Boolean> spamCheckFuture =
CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent()));
CompletableFuture<Boolean> linkCheckFuture =
CompletableFuture.supplyAsync(() -> checkForLinks(event.getContent()));
// Combine results
return spamCheckFuture.thenCombine(linkCheckFuture, (isSpam, hasLinks) -> {
if (isSpam || hasLinks) {
event.setCancelled(true);
}
return event;
});
});
});
```
### Error Handling
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future
.thenApply(event -> {
// May throw exception
riskyOperation(event);
return event;
})
.exceptionally(throwable -> {
// Handle error
getLogger().severe("Async event failed: " + throwable.getMessage());
return null; // Event will be skipped
});
});
```
## Switching to Main Thread
If you need to perform game operations after async work:
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async: Check external database
boolean isMuted = checkMuteStatus(event.getSender().getUuid());
if (isMuted) {
event.setCancelled(true);
// Schedule world thread work for player notification
World world = Universe.get().getWorld(event.getSender().getWorldUuid());
if (world != null) {
world.execute(() -> {
event.getSender().sendMessage(Message.raw("You are muted!"));
});
}
}
return event;
});
});
```
## PlayerChatEvent - The Main Async Event
`PlayerChatEvent` is the primary async event in Hytale. It implements both `IAsyncEvent<String>` and `ICancellable`:
```java
public class PlayerChatEvent implements IAsyncEvent<String>, ICancellable {
// sender: PlayerRef
// targets: List<PlayerRef>
// content: String
// formatter: Formatter
// cancelled: boolean
}
```
### Complete Chat Handler Example
```java
public class ChatPlugin extends JavaPlugin {
@Override
public void start() {
// Early priority: Content filtering
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
this::filterContent
);
// Normal priority: Standard processing
getEventRegistry().registerAsync(
PlayerChatEvent.class,
this::processChat
);
// Late priority: Logging
getEventRegistry().registerAsync(
EventPriority.LATE,
PlayerChatEvent.class,
this::logChat
);
}
private CompletableFuture<PlayerChatEvent> filterContent(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
String content = event.getContent();
// Filter profanity
String filtered = filterProfanity(content);
if (!filtered.equals(content)) {
event.setContent(filtered);
}
return event;
});
}
private CompletableFuture<PlayerChatEvent> processChat(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
if (event.isCancelled()) {
return event;
}
PlayerRef sender = event.getSender();
// Custom formatter with rank prefix
event.setFormatter((playerRef, msg) -> {
String prefix = getRankPrefix(playerRef);
return Message.raw(prefix + playerRef.getUsername() + ": " + msg);
});
return event;
});
}
private CompletableFuture<PlayerChatEvent> logChat(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
if (!event.isCancelled()) {
logToDatabase(
event.getSender().getUuid(),
event.getContent(),
System.currentTimeMillis()
);
}
return event;
});
}
}
```
## Database Integration Example
```java
public class ChatDatabasePlugin extends JavaPlugin {
private final ExecutorService dbExecutor = Executors.newFixedThreadPool(4);
@Override
public void start() {
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenCompose(event -> {
// Run database check on dedicated thread pool
return CompletableFuture.supplyAsync(() -> {
try {
// Check if player is muted in database
boolean muted = database.isPlayerMuted(event.getSender().getUuid());
if (muted) {
event.setCancelled(true);
}
return event;
} catch (Exception e) {
getLogger().severe("Database error: " + e.getMessage());
return event; // Allow message on DB error
}
}, dbExecutor);
});
});
}
@Override
public void shutdown() {
dbExecutor.shutdown();
}
}
```
## Best Practices
{{< callout type="tip" >}}
**Async Event Tips:**
- Never block the async chain with `.get()` or `.join()` - use `.thenApply()` or `.thenCompose()`
- Use `world.execute()` to return to world thread for game operations
- Handle exceptions with `.exceptionally()` or `.handle()`
- Keep async handlers lightweight for better performance
- Check `isCancelled()` before doing expensive operations
{{< /callout >}}
{{< callout type="warning" >}}
**Thread Safety:**
- PlayerRef's `getReference()` may return null or invalid reference if player disconnected
- Always check `ref != null && ref.isValid()` before accessing ECS data
- Avoid modifying game state directly in async handlers
- Use `world.execute()` to safely access game state from the world thread
{{< /callout >}}
```java
// Safe pattern for async player operations
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async work here...
String processed = processMessage(event.getContent());
event.setContent(processed);
// Safe player notification via PlayerRef
PlayerRef sender = event.getSender();
World world = Universe.get().getWorld(sender.getWorldUuid());
if (world != null) {
world.execute(() -> {
sender.sendMessage(Message.raw("Message processed"));
});
}
return event;
});
});
```

View File

@@ -0,0 +1,361 @@
---
title: Async Events
type: docs
weight: 4
---
Async events allow you to perform asynchronous operations during event handling. They use `CompletableFuture` for non-blocking execution.
## IAsyncEvent Interface
Events implementing `IAsyncEvent<K>` support asynchronous handling:
```java
public interface IAsyncEvent<K> extends IBaseEvent<K> {
// Async events use CompletableFuture in handlers
}
```
## Registering Async Handlers
Use `registerAsync()` for async events with `Void` key, or `registerAsyncGlobal()` for keyed events:
{{< callout type="warning" >}}
`PlayerChatEvent` implements `IAsyncEvent<String>` - it has a `String` key type. Therefore, you must use `registerAsyncGlobal()` instead of `registerAsync()`. The simple `registerAsync(Class, handler)` method only works for events with `Void` key type.
{{< /callout >}}
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// This runs asynchronously
String filtered = filterContent(event.getContent());
event.setContent(filtered);
return event;
});
});
```
## Async Registration Methods
### Basic Async Registration
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async processing
return event;
});
});
```
### With Priority
```java
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Runs early in async chain
return event;
})
);
```
### With Key
```java
getEventRegistry().registerAsync(
KeyedAsyncEvent.class,
myKey,
future -> future.thenApply(event -> {
// Only for events matching myKey
return event;
})
);
```
### Global Async
```java
getEventRegistry().registerAsyncGlobal(
KeyedAsyncEvent.class,
future -> future.thenApply(event -> {
// All events of this type
return event;
})
);
```
### Unhandled Async
```java
getEventRegistry().registerAsyncUnhandled(
KeyedAsyncEvent.class,
future -> future.thenApply(event -> {
// Events not handled by keyed handlers
return event;
})
);
```
## Working with CompletableFuture
### Sequential Operations
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future
.thenApply(event -> {
// Step 1: Filter content
event.setContent(filterProfanity(event.getContent()));
return event;
})
.thenApply(event -> {
// Step 2: Add formatting
event.setContent(addChatFormatting(event.getContent()));
return event;
})
.thenApply(event -> {
// Step 3: Log
logChatMessage(event.getSender(), event.getContent());
return event;
});
});
```
### Parallel Operations
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenCompose(event -> {
// Start multiple async operations in parallel
CompletableFuture<Boolean> spamCheckFuture =
CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent()));
CompletableFuture<Boolean> linkCheckFuture =
CompletableFuture.supplyAsync(() -> checkForLinks(event.getContent()));
// Combine results
return spamCheckFuture.thenCombine(linkCheckFuture, (isSpam, hasLinks) -> {
if (isSpam || hasLinks) {
event.setCancelled(true);
}
return event;
});
});
});
```
### Error Handling
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future
.thenApply(event -> {
// May throw exception
riskyOperation(event);
return event;
})
.exceptionally(throwable -> {
// Handle error
getLogger().severe("Async event failed: " + throwable.getMessage());
return null; // Event will be skipped
});
});
```
## Switching to Main Thread
If you need to perform game operations after async work:
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async: Check external database
boolean isMuted = checkMuteStatus(event.getSender().getUuid());
if (isMuted) {
event.setCancelled(true);
// Schedule world thread work for player notification
World world = Universe.get().getWorld(event.getSender().getWorldUuid());
if (world != null) {
world.execute(() -> {
event.getSender().sendMessage(Message.raw("You are muted!"));
});
}
}
return event;
});
});
```
## PlayerChatEvent - The Main Async Event
`PlayerChatEvent` is the primary async event in Hytale. It implements both `IAsyncEvent<String>` and `ICancellable`:
```java
public class PlayerChatEvent implements IAsyncEvent<String>, ICancellable {
// sender: PlayerRef
// targets: List<PlayerRef>
// content: String
// formatter: Formatter
// cancelled: boolean
}
```
### Complete Chat Handler Example
```java
public class ChatPlugin extends JavaPlugin {
@Override
public void start() {
// Early priority: Content filtering
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
this::filterContent
);
// Normal priority: Standard processing
getEventRegistry().registerAsync(
PlayerChatEvent.class,
this::processChat
);
// Late priority: Logging
getEventRegistry().registerAsync(
EventPriority.LATE,
PlayerChatEvent.class,
this::logChat
);
}
private CompletableFuture<PlayerChatEvent> filterContent(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
String content = event.getContent();
// Filter profanity
String filtered = filterProfanity(content);
if (!filtered.equals(content)) {
event.setContent(filtered);
}
return event;
});
}
private CompletableFuture<PlayerChatEvent> processChat(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
if (event.isCancelled()) {
return event;
}
PlayerRef sender = event.getSender();
// Custom formatter with rank prefix
event.setFormatter((playerRef, msg) -> {
String prefix = getRankPrefix(playerRef);
return Message.raw(prefix + playerRef.getUsername() + ": " + msg);
});
return event;
});
}
private CompletableFuture<PlayerChatEvent> logChat(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
if (!event.isCancelled()) {
logToDatabase(
event.getSender().getUuid(),
event.getContent(),
System.currentTimeMillis()
);
}
return event;
});
}
}
```
## Database Integration Example
```java
public class ChatDatabasePlugin extends JavaPlugin {
private final ExecutorService dbExecutor = Executors.newFixedThreadPool(4);
@Override
public void start() {
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenCompose(event -> {
// Run database check on dedicated thread pool
return CompletableFuture.supplyAsync(() -> {
try {
// Check if player is muted in database
boolean muted = database.isPlayerMuted(event.getSender().getUuid());
if (muted) {
event.setCancelled(true);
}
return event;
} catch (Exception e) {
getLogger().severe("Database error: " + e.getMessage());
return event; // Allow message on DB error
}
}, dbExecutor);
});
});
}
@Override
public void shutdown() {
dbExecutor.shutdown();
}
}
```
## Best Practices
{{< callout type="tip" >}}
**Async Event Tips:**
- Never block the async chain with `.get()` or `.join()` - use `.thenApply()` or `.thenCompose()`
- Use `world.execute()` to return to world thread for game operations
- Handle exceptions with `.exceptionally()` or `.handle()`
- Keep async handlers lightweight for better performance
- Check `isCancelled()` before doing expensive operations
{{< /callout >}}
{{< callout type="warning" >}}
**Thread Safety:**
- PlayerRef's `getReference()` may return null or invalid reference if player disconnected
- Always check `ref != null && ref.isValid()` before accessing ECS data
- Avoid modifying game state directly in async handlers
- Use `world.execute()` to safely access game state from the world thread
{{< /callout >}}
```java
// Safe pattern for async player operations
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async work here...
String processed = processMessage(event.getContent());
event.setContent(processed);
// Safe player notification via PlayerRef
PlayerRef sender = event.getSender();
World world = Universe.get().getWorld(sender.getWorldUuid());
if (world != null) {
world.execute(() -> {
sender.sendMessage(Message.raw("Message processed"));
});
}
return event;
});
});
```

View File

@@ -0,0 +1,250 @@
---
title: Cancellable Events
type: docs
weight: 3
---
Some events implement the `ICancellable` interface, allowing you to prevent the default game behavior.
## The ICancellable Interface
```java
public interface ICancellable {
boolean isCancelled();
void setCancelled(boolean cancelled);
}
```
## ECS Cancellable Events
ECS events use a similar interface `ICancellableEcsEvent`:
```java
public interface ICancellableEcsEvent {
boolean isCancelled();
void setCancelled(boolean cancelled);
}
```
## Cancelling Events
```java
getEventRegistry().register(BreakBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
if (isProtectedArea(position)) {
event.setCancelled(true);
// Note: BreakBlockEvent doesn't have direct player access
// Use ECS components if you need to message the player
}
});
```
## Checking Cancellation Status
```java
getEventRegistry().register(EventPriority.LATE, BreakBlockEvent.class, event -> {
if (event.isCancelled()) {
// Another handler cancelled this event
return;
}
// Process the event
recordBlockBreak(event.getTargetBlock(), event.getBlockType());
});
```
## Common Cancellable Events
### Player Events
| Event | Cancel Effect |
|-------|---------------|
| `PlayerSetupConnectEvent` | Prevents player from connecting |
| `PlayerChatEvent` | Prevents message from being sent |
| `PlayerMouseButtonEvent` | Prevents mouse button action |
| `PlayerInteractEvent` | Prevents interaction (deprecated) |
### ECS Block Events
| Event | Cancel Effect |
|-------|---------------|
| `BreakBlockEvent` | Prevents block from being broken |
| `PlaceBlockEvent` | Prevents block from being placed |
| `DamageBlockEvent` | Prevents damage to block |
| `UseBlockEvent.Pre` | Prevents block use action |
### Other ECS Events
| Event | Cancel Effect |
|-------|---------------|
| `DropItemEvent` | Prevents item from being dropped |
| `CraftRecipeEvent` | Prevents crafting |
## Best Practices
### Check Before Acting
Always check `isCancelled()` before performing actions:
```java
getEventRegistry().register(BreakBlockEvent.class, event -> {
// Check if already cancelled
if (event.isCancelled()) {
return;
}
// Your logic here
if (shouldPrevent(event.getTargetBlock())) {
event.setCancelled(true);
}
});
```
### Use Appropriate Priority
```java
// Protection plugins should use EARLY
getEventRegistry().register(EventPriority.EARLY, BreakBlockEvent.class, event -> {
if (isProtected(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Feature plugins should respect cancellation
getEventRegistry().register(EventPriority.NORMAL, BreakBlockEvent.class, event -> {
if (!event.isCancelled()) {
giveBlockReward(event.getTargetBlock());
}
});
// Logging should use LATE
getEventRegistry().register(EventPriority.LATE, BreakBlockEvent.class, event -> {
logBlockBreakAttempt(event.getTargetBlock(), event.isCancelled());
});
```
### Provide Feedback with Chat Events
When cancelling chat events, you can notify the player:
```java
// Note: PlayerChatEvent has String key, use registerAsyncGlobal()
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
if (containsBadWord(event.getContent())) {
event.setCancelled(true);
// Send message via PlayerRef (PlayerRef.sendMessage() works)
event.getSender().sendMessage(Message.raw("Please don't use that word!"));
}
return event;
});
});
```
## Uncancelling Events
You can also uncancel an event that was previously cancelled:
```java
getEventRegistry().register(EventPriority.LATE, BreakBlockEvent.class, event -> {
// Override cancellation for certain block types
if (event.isCancelled() && event.getBlockType().getId().equals("temporary_block")) {
event.setCancelled(false);
}
});
```
{{< callout type="warning" >}}
Uncancelling events can cause conflicts with other plugins. Use sparingly and document this behavior.
{{< /callout >}}
## Example: Connection Whitelist
```java
public class WhitelistPlugin extends JavaPlugin {
private final Set<UUID> whitelist = new HashSet<>();
@Override
public void start() {
// Cancel connection for non-whitelisted players
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
if (!whitelist.contains(event.getUuid())) {
event.setCancelled(true);
event.setReason("You are not whitelisted on this server!");
}
});
}
public void addToWhitelist(UUID uuid) {
whitelist.add(uuid);
}
public void removeFromWhitelist(UUID uuid) {
whitelist.remove(uuid);
}
}
```
## Example: Chat Filter
```java
public class ChatFilterPlugin extends JavaPlugin {
private final Set<String> bannedWords = new HashSet<>();
@Override
public void start() {
loadBannedWords();
// Note: PlayerChatEvent has String key, use registerAsyncGlobal()
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
String message = event.getContent().toLowerCase();
for (String banned : bannedWords) {
if (message.contains(banned)) {
event.setCancelled(true);
// Send message via PlayerRef
event.getSender().sendMessage(Message.raw("Your message was blocked!"));
break;
}
}
return event;
});
});
}
}
```
## Example: Block Protection
```java
public class ProtectionPlugin extends JavaPlugin {
private final Set<Vector3i> protectedBlocks = new HashSet<>();
@Override
public void start() {
// Register at EARLY to cancel before other plugins process
getEventRegistry().register(EventPriority.EARLY, BreakBlockEvent.class, event -> {
if (protectedBlocks.contains(event.getTargetBlock())) {
event.setCancelled(true);
}
});
getEventRegistry().register(EventPriority.EARLY, PlaceBlockEvent.class, event -> {
if (isProtectedArea(event.getTargetBlock())) {
event.setCancelled(true);
}
});
}
private boolean isProtectedArea(Vector3i pos) {
// Check if position is in a protected region
return false;
}
}
```

View File

@@ -0,0 +1,250 @@
---
title: Cancellable Events
type: docs
weight: 3
---
Some events implement the `ICancellable` interface, allowing you to prevent the default game behavior.
## The ICancellable Interface
```java
public interface ICancellable {
boolean isCancelled();
void setCancelled(boolean cancelled);
}
```
## ECS Cancellable Events
ECS events use a similar interface `ICancellableEcsEvent`:
```java
public interface ICancellableEcsEvent {
boolean isCancelled();
void setCancelled(boolean cancelled);
}
```
## Cancelling Events
```java
getEventRegistry().register(BreakBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
if (isProtectedArea(position)) {
event.setCancelled(true);
// Note: BreakBlockEvent doesn't have direct player access
// Use ECS components if you need to message the player
}
});
```
## Checking Cancellation Status
```java
getEventRegistry().register(EventPriority.LATE, BreakBlockEvent.class, event -> {
if (event.isCancelled()) {
// Another handler cancelled this event
return;
}
// Process the event
recordBlockBreak(event.getTargetBlock(), event.getBlockType());
});
```
## Common Cancellable Events
### Player Events
| Event | Cancel Effect |
|-------|---------------|
| `PlayerSetupConnectEvent` | Prevents player from connecting |
| `PlayerChatEvent` | Prevents message from being sent |
| `PlayerMouseButtonEvent` | Prevents mouse button action |
| `PlayerInteractEvent` | Prevents interaction (deprecated) |
### ECS Block Events
| Event | Cancel Effect |
|-------|---------------|
| `BreakBlockEvent` | Prevents block from being broken |
| `PlaceBlockEvent` | Prevents block from being placed |
| `DamageBlockEvent` | Prevents damage to block |
| `UseBlockEvent.Pre` | Prevents block use action |
### Other ECS Events
| Event | Cancel Effect |
|-------|---------------|
| `DropItemEvent` | Prevents item from being dropped |
| `CraftRecipeEvent` | Prevents crafting |
## Best Practices
### Check Before Acting
Always check `isCancelled()` before performing actions:
```java
getEventRegistry().register(BreakBlockEvent.class, event -> {
// Check if already cancelled
if (event.isCancelled()) {
return;
}
// Your logic here
if (shouldPrevent(event.getTargetBlock())) {
event.setCancelled(true);
}
});
```
### Use Appropriate Priority
```java
// Protection plugins should use EARLY
getEventRegistry().register(EventPriority.EARLY, BreakBlockEvent.class, event -> {
if (isProtected(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Feature plugins should respect cancellation
getEventRegistry().register(EventPriority.NORMAL, BreakBlockEvent.class, event -> {
if (!event.isCancelled()) {
giveBlockReward(event.getTargetBlock());
}
});
// Logging should use LATE
getEventRegistry().register(EventPriority.LATE, BreakBlockEvent.class, event -> {
logBlockBreakAttempt(event.getTargetBlock(), event.isCancelled());
});
```
### Provide Feedback with Chat Events
When cancelling chat events, you can notify the player:
```java
// Note: PlayerChatEvent has String key, use registerAsyncGlobal()
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
if (containsBadWord(event.getContent())) {
event.setCancelled(true);
// Send message via PlayerRef (PlayerRef.sendMessage() works)
event.getSender().sendMessage(Message.raw("Please don't use that word!"));
}
return event;
});
});
```
## Uncancelling Events
You can also uncancel an event that was previously cancelled:
```java
getEventRegistry().register(EventPriority.LATE, BreakBlockEvent.class, event -> {
// Override cancellation for certain block types
if (event.isCancelled() && event.getBlockType().getId().equals("temporary_block")) {
event.setCancelled(false);
}
});
```
{{< callout type="warning" >}}
Uncancelling events can cause conflicts with other plugins. Use sparingly and document this behavior.
{{< /callout >}}
## Example: Connection Whitelist
```java
public class WhitelistPlugin extends JavaPlugin {
private final Set<UUID> whitelist = new HashSet<>();
@Override
public void start() {
// Cancel connection for non-whitelisted players
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
if (!whitelist.contains(event.getUuid())) {
event.setCancelled(true);
event.setReason("You are not whitelisted on this server!");
}
});
}
public void addToWhitelist(UUID uuid) {
whitelist.add(uuid);
}
public void removeFromWhitelist(UUID uuid) {
whitelist.remove(uuid);
}
}
```
## Example: Chat Filter
```java
public class ChatFilterPlugin extends JavaPlugin {
private final Set<String> bannedWords = new HashSet<>();
@Override
public void start() {
loadBannedWords();
// Note: PlayerChatEvent has String key, use registerAsyncGlobal()
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
String message = event.getContent().toLowerCase();
for (String banned : bannedWords) {
if (message.contains(banned)) {
event.setCancelled(true);
// Send message via PlayerRef
event.getSender().sendMessage(Message.raw("Your message was blocked!"));
break;
}
}
return event;
});
});
}
}
```
## Example: Block Protection
```java
public class ProtectionPlugin extends JavaPlugin {
private final Set<Vector3i> protectedBlocks = new HashSet<>();
@Override
public void start() {
// Register at EARLY to cancel before other plugins process
getEventRegistry().register(EventPriority.EARLY, BreakBlockEvent.class, event -> {
if (protectedBlocks.contains(event.getTargetBlock())) {
event.setCancelled(true);
}
});
getEventRegistry().register(EventPriority.EARLY, PlaceBlockEvent.class, event -> {
if (isProtectedArea(event.getTargetBlock())) {
event.setCancelled(true);
}
});
}
private boolean isProtectedArea(Vector3i pos) {
// Check if position is in a protected region
return false;
}
}
```

View File

@@ -0,0 +1,246 @@
---
title: Event Priorities
type: docs
weight: 2
---
Event priorities control the order in which event listeners are called. This is important when multiple plugins or handlers need to react to the same event.
## Priority Values
Hytale provides five priority levels:
| 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 |
{{< callout type="info" >}}
Lower values run first. The values are spread across the `short` range to allow for custom priorities between the standard levels.
{{< /callout >}}
## Using EventPriority
### With Enum
```java
getEventRegistry().register(
EventPriority.EARLY,
PlayerConnectEvent.class,
event -> {
// This runs before NORMAL handlers
}
);
getEventRegistry().register(
EventPriority.LATE,
PlayerConnectEvent.class,
event -> {
// This runs after NORMAL handlers
}
);
```
### With Custom Value
```java
// Use a custom priority value (between -32768 and 32767)
short customPriority = -5000; // Between EARLY and NORMAL
getEventRegistry().register(
customPriority,
PlayerConnectEvent.class,
event -> {
// Custom priority handler
}
);
```
## Execution Order
When an event is fired, handlers are called in priority order:
```
FIRST (-21844)
EARLY (-10922)
NORMAL (0) ← Default
LATE (10922)
LAST (21844)
```
### Example
```java
@Override
public void start() {
getEventRegistry().register(EventPriority.LAST, PlayerReadyEvent.class, event -> {
getLogger().at(Level.INFO).log("3. LAST handler");
});
getEventRegistry().register(EventPriority.FIRST, PlayerReadyEvent.class, event -> {
getLogger().at(Level.INFO).log("1. FIRST handler");
});
getEventRegistry().register(PlayerReadyEvent.class, event -> {
getLogger().at(Level.INFO).log("2. NORMAL handler (default)");
});
}
// Output when player is ready:
// 1. FIRST handler
// 2. NORMAL handler (default)
// 3. LAST handler
```
## When to Use Each Priority
### FIRST
Use for:
- Setting up event data before other handlers
- Logging/debugging that should see the original event
- Critical validation that must run before anything else
```java
getEventRegistry().register(EventPriority.FIRST, PlayerChatEvent.class, event -> {
// Log original message before any filtering
logChat(event.getSender(), event.getContent());
});
```
### EARLY
Use for:
- Modifications that other plugins should see
- Permission checks before regular handlers
- Data preprocessing
```java
getEventRegistry().register(EventPriority.EARLY, PlayerChatEvent.class, event -> {
// Filter profanity before other plugins process the message
String filtered = filterProfanity(event.getContent());
event.setContent(filtered);
});
```
### NORMAL
Use for:
- Standard event handling
- Most plugin functionality
- Default when priority doesn't matter
```java
getEventRegistry().register(PlayerChatEvent.class, event -> {
// Standard chat processing
processMessage(event.getSender(), event.getContent());
});
```
### LATE
Use for:
- Reactions to modifications made by other handlers
- Analytics/statistics gathering
- Secondary effects
```java
getEventRegistry().register(EventPriority.LATE, PlayerChatEvent.class, event -> {
// Record the final message after all modifications
recordChatHistory(event.getSender(), event.getContent());
});
```
### LAST
Use for:
- Final cleanup or overrides
- Monitoring cancellation status
- Logging final event state
```java
getEventRegistry().register(EventPriority.LAST, PlayerChatEvent.class, event -> {
if (event.isCancelled()) {
getLogger().at(Level.INFO).log("Chat was cancelled by another plugin");
}
});
```
## With Cancellable Events
Priority is especially important with cancellable events:
```java
// FIRST handler might cancel
getEventRegistry().register(EventPriority.FIRST, BreakBlockEvent.class, event -> {
if (isProtectedArea(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// NORMAL handler should check cancellation
getEventRegistry().register(BreakBlockEvent.class, event -> {
if (!event.isCancelled()) {
// Process block break
trackBlockBreak(event.getTargetBlock(), event.getBlockType());
}
});
// LAST handler for monitoring
getEventRegistry().register(EventPriority.LAST, BreakBlockEvent.class, event -> {
if (event.isCancelled()) {
logProtectedBlockAttempt(event.getTargetBlock());
}
});
```
## Async Events and Priority
Async events also support priorities:
```java
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Early async processing
event.setContent(filterContent(event.getContent()));
return event;
})
);
getEventRegistry().registerAsync(
EventPriority.LATE,
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Late async processing - sees modified content
logFinalMessage(event.getSender(), event.getContent());
return event;
})
);
```
## Best Practices
{{< callout type="tip" >}}
**Priority Guidelines:**
- Use `FIRST` sparingly - only for logging or critical checks
- Protection/permission plugins should use `EARLY`
- Feature plugins should use `NORMAL` (default)
- Analytics/logging should use `LATE`
- Use `LAST` only when you need to see the final event state
{{< /callout >}}
{{< callout type="warning" >}}
**Avoid Priority Conflicts:**
- Document your plugin's priority choices
- Check cancellation status at NORMAL and later priorities
- Don't rely on running "after" specific plugins
{{< /callout >}}

View File

@@ -0,0 +1,201 @@
---
title: Priorités d'Événements
type: docs
weight: 2
---
Les priorités d'événements contrôlent l'ordre dans lequel les écouteurs sont appelés. C'est important quand plusieurs plugins ou gestionnaires doivent réagir au même événement.
## Valeurs de Priorité
Hytale fournit cinq niveaux de priorité :
| Priorité | Valeur | Description |
|----------|--------|-------------|
| `FIRST` | -21844 | S'exécute en premier, avant tous les autres |
| `EARLY` | -10922 | S'exécute tôt, après FIRST |
| `NORMAL` | 0 | Priorité par défaut |
| `LATE` | 10922 | S'exécute tard, après NORMAL |
| `LAST` | 21844 | S'exécute en dernier, après tous les autres |
{{< callout type="info" >}}
Les valeurs plus basses s'exécutent en premier. Les valeurs sont réparties sur la plage `short` pour permettre des priorités personnalisées entre les niveaux standards.
{{< /callout >}}
## Utiliser EventPriority
### Avec Enum
```java
getEventRegistry().register(
EventPriority.EARLY,
PlayerConnectEvent.class,
event -> {
// Ceci s'exécute avant les gestionnaires NORMAL
}
);
getEventRegistry().register(
EventPriority.LATE,
PlayerConnectEvent.class,
event -> {
// Ceci s'exécute après les gestionnaires NORMAL
}
);
```
### Avec Valeur Personnalisée
```java
// Utiliser une valeur de priorité personnalisée (entre -32768 et 32767)
short customPriority = -5000; // Entre EARLY et NORMAL
getEventRegistry().register(
customPriority,
PlayerConnectEvent.class,
event -> {
// Gestionnaire priorité personnalisée
}
);
```
## Ordre d'Exécution
Quand un événement est déclenché, les gestionnaires sont appelés dans l'ordre de priorité :
```
FIRST (-21844)
EARLY (-10922)
NORMAL (0) ← Par défaut
LATE (10922)
LAST (21844)
```
### Exemple
```java
@Override
public void start() {
getEventRegistry().register(EventPriority.LAST, PlayerConnectEvent.class, event -> {
getLogger().at(Level.INFO).log("3. Gestionnaire LAST");
});
getEventRegistry().register(EventPriority.FIRST, PlayerConnectEvent.class, event -> {
getLogger().at(Level.INFO).log("1. Gestionnaire FIRST");
});
getEventRegistry().register(PlayerConnectEvent.class, event -> {
getLogger().at(Level.INFO).log("2. Gestionnaire NORMAL (défaut)");
});
}
// Sortie quand un joueur rejoint :
// 1. Gestionnaire FIRST
// 2. Gestionnaire NORMAL (défaut)
// 3. Gestionnaire LAST
```
## Quand Utiliser Chaque Priorité
### FIRST
Utiliser pour :
- Configurer les données de l'événement avant les autres gestionnaires
- Logging/débogage qui doit voir l'événement original
- Validation critique qui doit s'exécuter avant tout le reste
```java
getEventRegistry().register(EventPriority.FIRST, PlayerChatEvent.class, event -> {
// Logger le message original avant tout filtrage
logChat(event.getSender(), event.getContent());
});
```
### EARLY
Utiliser pour :
- Modifications que les autres plugins devraient voir
- Vérifications de permissions avant les gestionnaires normaux
- Prétraitement des données
```java
getEventRegistry().register(EventPriority.EARLY, PlayerChatEvent.class, event -> {
// Filtrer les grossièretés avant que les autres plugins traitent le message
event.setContent(filterProfanity(event.getContent()));
});
```
### NORMAL
Utiliser pour :
- Gestion standard des événements
- La plupart des fonctionnalités de plugin
- Par défaut quand la priorité n'importe pas
```java
getEventRegistry().register(PlayerChatEvent.class, event -> {
// Traitement standard du chat
processMessage(event.getSender(), event.getContent());
});
```
### LATE
Utiliser pour :
- Réactions aux modifications faites par d'autres gestionnaires
- Collecte d'analytics/statistiques
- Effets secondaires
```java
getEventRegistry().register(EventPriority.LATE, PlayerChatEvent.class, event -> {
// Enregistrer le message final après toutes les modifications
recordChatHistory(event.getSender(), event.getContent());
});
```
### LAST
Utiliser pour :
- Nettoyage final ou overrides
- Surveillance du statut d'annulation
- Logging de l'état final de l'événement
```java
getEventRegistry().register(EventPriority.LAST, PlayerChatEvent.class, event -> {
if (event.isCancelled()) {
getLogger().at(Level.INFO).log("Le chat a été annulé par un autre plugin");
}
});
```
## Avec les Événements Annulables
La priorité est particulièrement importante avec les événements annulables :
```java
// Le gestionnaire FIRST peut annuler
getEventRegistry().register(EventPriority.FIRST, BreakBlockEvent.class, event -> {
if (isProtectedArea(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Le gestionnaire NORMAL devrait vérifier l'annulation
getEventRegistry().register(BreakBlockEvent.class, event -> {
if (!event.isCancelled()) {
// Traiter la destruction du bloc
trackBlockBreak(event.getTargetBlock(), event.getBlockType());
}
});
// Le gestionnaire LAST pour la surveillance
getEventRegistry().register(EventPriority.LAST, BreakBlockEvent.class, event -> {
if (event.isCancelled()) {
logProtectedBlockAttempt(event.getTargetBlock());
}
});
```

View File

@@ -0,0 +1,50 @@
---
title: Event Reference
type: docs
weight: 5
---
This section provides a comprehensive reference of all available events in Hytale.
{{< cards >}}
{{< card link="player-events" title="Player Events" subtitle="Join, quit, chat, and player actions" >}}
{{< card link="entity-events" title="Entity Events" subtitle="Spawn, damage, death, and movement" >}}
{{< card link="block-events" title="Block Events" subtitle="Break, place, and interact with blocks" >}}
{{< card link="permission-events" title="Permission Events" subtitle="Permission checking and management" >}}
{{< /cards >}}
## Event Categories
### Player Events
Events related to player actions and state changes:
- `PlayerConnectEvent` - Player joins the server
- `PlayerDisconnectEvent` - Player leaves the server
- `PlayerChatEvent` - Player sends a chat message
- `PlayerMoveEvent` - Player moves
- `PlayerInteractEvent` - Player interacts with the world
- `PlayerDamageEvent` - Player takes damage
- `PlayerDeathEvent` - Player dies
### Entity Events
Events related to all entities:
- `EntitySpawnEvent` - Entity is spawned
- `EntityDamageEvent` - Entity takes damage
- `EntityDeathEvent` - Entity dies
- `EntityMoveEvent` - Entity moves
### Block Events
Events related to world blocks:
- `BlockBreakEvent` - Block is broken
- `BlockPlaceEvent` - Block is placed
- `BlockInteractEvent` - Block is interacted with
### World Events
Events related to world state:
- `WorldLoadEvent` - World is loaded
- `WorldUnloadEvent` - World is unloaded
- `ChunkLoadEvent` - Chunk is loaded
- `ChunkUnloadEvent` - Chunk is unloaded
### Permission Events
Events related to permissions:
- `PermissionCheckEvent` - Permission is checked

View File

@@ -0,0 +1,50 @@
---
title: Référence des Événements
type: docs
weight: 5
---
Cette section fournit une référence complète de tous les événements disponibles dans Hytale.
{{< cards >}}
{{< card link="player-events" title="Événements Joueur" subtitle="Connexion, déconnexion, chat et actions joueur" >}}
{{< card link="entity-events" title="Événements Entité" subtitle="Spawn, dégâts, mort et mouvement" >}}
{{< card link="block-events" title="Événements Bloc" subtitle="Casser, placer et interagir avec les blocs" >}}
{{< card link="permission-events" title="Événements Permission" subtitle="Vérification et gestion des permissions" >}}
{{< /cards >}}
## Catégories d'Événements
### Événements Joueur
Événements liés aux actions et changements d'état des joueurs :
- `PlayerConnectEvent` - Un joueur rejoint le serveur
- `PlayerDisconnectEvent` - Un joueur quitte le serveur
- `PlayerChatEvent` - Un joueur envoie un message chat
- `PlayerMoveEvent` - Un joueur se déplace
- `PlayerInteractEvent` - Un joueur interagit avec le monde
- `PlayerDamageEvent` - Un joueur subit des dégâts
- `PlayerDeathEvent` - Un joueur meurt
### Événements Entité
Événements liés à toutes les entités :
- `EntitySpawnEvent` - Une entité apparaît
- `EntityDamageEvent` - Une entité subit des dégâts
- `EntityDeathEvent` - Une entité meurt
- `EntityMoveEvent` - Une entité se déplace
### Événements Bloc
Événements liés aux blocs du monde :
- `BlockBreakEvent` - Un bloc est cassé
- `BlockPlaceEvent` - Un bloc est placé
- `BlockInteractEvent` - Un bloc subit une interaction
### Événements Monde
Événements liés à l'état du monde :
- `WorldLoadEvent` - Un monde est chargé
- `WorldUnloadEvent` - Un monde est déchargé
- `ChunkLoadEvent` - Un chunk est chargé
- `ChunkUnloadEvent` - Un chunk est déchargé
### Événements Permission
Événements liés aux permissions :
- `PermissionCheckEvent` - Une permission est vérifiée

View File

@@ -0,0 +1,721 @@
---
title: Block Events
type: docs
weight: 3
---
Events triggered by block interactions and changes. These events use the ECS (Entity Component System) pattern.
## Breaking & Placing Events
### BreakBlockEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when a block is being broken.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| targetBlock | `Vector3i` | Block position |
| blockType | `BlockType` | Type of block |
| itemInHand | `ItemStack` | Tool being used |
{{< /tab >}}
{{< tab >}}
- `getTargetBlock()` - Returns block position
- `getBlockType()` - Returns block type
- `getItemInHand()` - Returns tool being used
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel breaking
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(BreakBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
ItemStack tool = event.getItemInHand();
// Protect certain blocks
if (blockType.getId().equals("special_ore")) {
event.setCancelled(true);
return;
}
// Log block break
getLogger().at(Level.INFO).log("Block broken at " + position + ": " + blockType.getId());
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlaceBlockEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when a block is placed.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| targetBlock | `Vector3i` | Position where block will be placed |
| rotation | `RotationTuple` | Block rotation/orientation |
| itemInHand | `ItemStack` | Block item being placed |
{{< /tab >}}
{{< tab >}}
- `getTargetBlock()` - Returns target position
- `setTargetBlock(Vector3i)` - Change placement position
- `getRotation()` - Returns block rotation
- `setRotation(RotationTuple)` - Change rotation
- `getItemInHand()` - Returns block item
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel placement
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlaceBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
RotationTuple rotation = event.getRotation();
ItemStack item = event.getItemInHand();
// Check build permissions
if (isProtectedArea(position)) {
event.setCancelled(true);
return;
}
// Force certain rotation
event.setRotation(RotationTuple.of(Rotation.North, Rotation.None, Rotation.None));
});
```
{{< /tab >}}
{{< /tabs >}}
---
### DamageBlockEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when damage is applied to a block during breaking (before the block is fully broken).
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| targetBlock | `Vector3i` | Block position |
| blockType | `BlockType` | Type of block |
| itemInHand | `ItemStack` | Tool being used (may be null) |
| currentDamage | `float` | Damage already applied to block |
| damage | `float` | Damage being applied this tick |
{{< /tab >}}
{{< tab >}}
- `getTargetBlock()` - Returns block position
- `setTargetBlock(Vector3i)` - Change target block
- `getBlockType()` - Returns block type
- `getItemInHand()` - Returns tool being used
- `getCurrentDamage()` - Returns accumulated damage
- `getDamage()` - Returns damage this tick
- `setDamage(float)` - Modify damage amount
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel damage
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DamageBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
// Make certain blocks indestructible
if (blockType.getId().equals("bedrock")) {
event.setCancelled(true);
return;
}
// Reduce damage to hardened blocks
if (blockType.getId().startsWith("hardened_")) {
event.setDamage(event.getDamage() * 0.5f);
}
// Log mining progress
float progress = event.getCurrentDamage() / 100f;
getLogger().at(Level.INFO).log("Block " + blockType.getId() + " at " + progress + "% damage");
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Interaction Events
### UseBlockEvent
Base class for block use events. Has Pre and Post variants.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
#### UseBlockEvent.Pre
{{< badge "Cancellable" >}}
Fired before a block use action occurs (right-click on block).
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| interactionType | `InteractionType` | Type of interaction |
| context | `InteractionContext` | Interaction context |
| targetBlock | `Vector3i` | Block position |
| blockType | `BlockType` | The block type |
{{< /tab >}}
{{< tab >}}
- `getInteractionType()` - Returns interaction type
- `getContext()` - Returns interaction context
- `getTargetBlock()` - Returns block position
- `getBlockType()` - Returns block type
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel use action
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(UseBlockEvent.Pre.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
InteractionContext context = event.getContext();
// Prevent opening locked containers
if (isLocked(position)) {
event.setCancelled(true);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
#### UseBlockEvent.Post
Fired after a block use action completes successfully.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| interactionType | `InteractionType` | Type of interaction |
| context | `InteractionContext` | Interaction context |
| targetBlock | `Vector3i` | Block position |
| blockType | `BlockType` | The block type |
{{< /tab >}}
{{< tab >}}
- `getInteractionType()` - Returns interaction type
- `getContext()` - Returns interaction context
- `getTargetBlock()` - Returns block position
- `getBlockType()` - Returns block type
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(UseBlockEvent.Post.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
// Track interactions for quests
if (isQuestBlock(blockType)) {
completeQuestObjective("interact_with_" + blockType.getId());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Item Events
### DropItemEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when an item is dropped. Has two variants: `Drop` and `PlayerRequest`.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
#### DropItemEvent.Drop
The actual drop event with item details.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| itemStack | `ItemStack` | Item being dropped |
| throwSpeed | `float` | Speed of the throw |
{{< /tab >}}
{{< tab >}}
- `getItemStack()` - Returns the item stack
- `setItemStack(ItemStack)` - Change the dropped item
- `getThrowSpeed()` - Returns throw speed
- `setThrowSpeed(float)` - Change throw speed
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel drop
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DropItemEvent.Drop.class, event -> {
ItemStack item = event.getItemStack();
// Prevent dropping certain items
if (item.getId().equals("quest_item")) {
event.setCancelled(true);
return;
}
// Modify throw speed
event.setThrowSpeed(event.getThrowSpeed() * 1.5f);
});
```
{{< /tab >}}
{{< /tabs >}}
---
#### DropItemEvent.PlayerRequest
Fired when a player requests to drop an item (before the actual drop).
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| inventorySectionId | `int` | Inventory section ID |
| slotId | `short` | Slot being dropped from |
{{< /tab >}}
{{< tab >}}
- `getInventorySectionId()` - Returns inventory section
- `getSlotId()` - Returns slot index
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel request
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DropItemEvent.PlayerRequest.class, event -> {
int sectionId = event.getInventorySectionId();
short slot = event.getSlotId();
// Prevent dropping from certain slots
if (sectionId == Inventory.HOTBAR && slot == 0) {
event.setCancelled(true); // Can't drop first hotbar item
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### InteractivelyPickupItemEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when a player picks up an item interactively (manual pickup, not auto-pickup).
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| itemStack | `ItemStack` | The item being picked up |
{{< /tab >}}
{{< tab >}}
- `getItemStack()` - Returns the item stack
- `setItemStack(ItemStack)` - Change the item being picked up
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel pickup
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(InteractivelyPickupItemEvent.class, event -> {
ItemStack item = event.getItemStack();
// Prevent picking up quest items that don't belong to player
if (item.getId().startsWith("quest_") && !canPickupQuestItem(player, item)) {
event.setCancelled(true);
return;
}
// Transform items when picking up
if (item.getId().equals("raw_ore")) {
event.setItemStack(item.withCount(item.getCount() * 2));
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Crafting Events
### CraftRecipeEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when a crafting recipe is executed. Has Pre and Post variants.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| craftedRecipe | `CraftingRecipe` | The recipe being crafted |
| quantity | `int` | Number of items being crafted |
{{< /tab >}}
{{< tab >}}
- `getCraftedRecipe()` - Returns the crafting recipe
- `getQuantity()` - Returns craft quantity
- `isCancelled()` - Check if cancelled (Pre only)
- `setCancelled(boolean)` - Cancel crafting (Pre only)
{{< /tab >}}
{{< tab >}}
```java
// Pre-craft check
getEventRegistry().register(CraftRecipeEvent.Pre.class, event -> {
CraftingRecipe recipe = event.getCraftedRecipe();
int quantity = event.getQuantity();
// Block certain recipes
if (isRestrictedRecipe(recipe)) {
event.setCancelled(true);
}
});
// Post-craft tracking
getEventRegistry().register(CraftRecipeEvent.Post.class, event -> {
CraftingRecipe recipe = event.getCraftedRecipe();
int quantity = event.getQuantity();
// Track crafting statistics
incrementCraftCount(recipe.getId(), quantity);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Other Events
### SwitchActiveSlotEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when a player switches their active hotbar slot.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| inventorySectionId | `int` | The inventory section ID |
| previousSlot | `int` | The slot before switching |
| newSlot | `byte` | The target slot |
| serverRequest | `boolean` | If server initiated the switch |
{{< /tab >}}
{{< tab >}}
- `getInventorySectionId()` - Returns inventory section
- `getPreviousSlot()` - Returns previous slot index
- `getNewSlot()` - Returns new slot index
- `setNewSlot(byte)` - Change the target slot
- `isServerRequest()` - Check if server initiated
- `isClientRequest()` - Check if client initiated
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel slot switch
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(SwitchActiveSlotEvent.class, event -> {
int fromSlot = event.getPreviousSlot();
int toSlot = event.getNewSlot();
// Prevent switching during cooldown
if (isOnCooldown(player)) {
event.setCancelled(true);
return;
}
// Track slot usage for analytics
if (event.isClientRequest()) {
trackSlotSwitch(player, fromSlot, toSlot);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### DiscoverZoneEvent
{{< badge "ECS" >}}
Fired when a player discovers a new zone. Has a `Display` variant that is cancellable.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| discoveryInfo | `ZoneDiscoveryInfo` | Information about the discovered zone |
{{< /tab >}}
{{< tab >}}
- `getDiscoveryInfo()` - Returns zone discovery info
- `isCancelled()` - Check if cancelled (Display only)
- `setCancelled(boolean)` - Cancel display (Display only)
{{< /tab >}}
{{< tab >}}
```java
// Cancel zone discovery notification display
getEventRegistry().register(DiscoverZoneEvent.Display.class, event -> {
WorldMapTracker.ZoneDiscoveryInfo info = event.getDiscoveryInfo();
// Hide certain zones from discovery UI
if (isHiddenZone(info)) {
event.setCancelled(true);
return;
}
// Log discovery for achievements
recordZoneDiscovery(player, info);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChangeGameModeEvent
{{< badge "Cancellable" >}} {{< badge "ECS" >}}
Fired when an entity's game mode changes.
**Package:** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| gameMode | `GameMode` | The new game mode |
{{< /tab >}}
{{< tab >}}
- `getGameMode()` - Returns the new game mode
- `setGameMode(GameMode)` - Change the target game mode
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel mode change
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChangeGameModeEvent.class, event -> {
GameMode newMode = event.getGameMode();
// Prevent creative mode in certain worlds
if (newMode == GameMode.CREATIVE && isRestrictedWorld(player.getWorld())) {
event.setCancelled(true);
player.sendMessage(Message.raw("Creative mode not allowed here!"));
return;
}
// Force adventure mode override
if (shouldForceAdventure(player)) {
event.setGameMode(GameMode.ADVENTURE);
}
getLogger().at(Level.INFO).log(player.getDisplayName() + " changed to " + newMode);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Practical Examples
### Block Logger Plugin
```java
public class BlockLoggerPlugin extends JavaPlugin {
@Override
public void start() {
getEventRegistry().register(PlaceBlockEvent.class, event -> {
logAction("PLACE", event.getTargetBlock(), event.getItemInHand());
});
getEventRegistry().register(BreakBlockEvent.class, event -> {
logAction("BREAK", event.getTargetBlock(), event.getBlockType());
});
}
private void logAction(String action, Vector3i pos, Object data) {
getLogger().at(Level.INFO).log(String.format("%s: %d,%d,%d - %s",
action, pos.x(), pos.y(), pos.z(), data));
}
}
```
### Region Protection System
```java
public class RegionPlugin extends JavaPlugin {
private final Set<Region> regions = new HashSet<>();
@Override
public void start() {
// Protect against breaking
getEventRegistry().register(EventPriority.EARLY, BreakBlockEvent.class, event -> {
if (isInProtectedRegion(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Protect against placing
getEventRegistry().register(EventPriority.EARLY, PlaceBlockEvent.class, event -> {
if (isInProtectedRegion(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Protect against use
getEventRegistry().register(EventPriority.EARLY, UseBlockEvent.Pre.class, event -> {
if (isInProtectedRegion(event.getTargetBlock())) {
event.setCancelled(true);
}
});
}
private boolean isInProtectedRegion(Vector3i pos) {
return regions.stream().anyMatch(r -> r.contains(pos));
}
}
```
### Custom Ore System
```java
public class CustomOrePlugin extends JavaPlugin {
private final Random random = new Random();
@Override
public void start() {
getEventRegistry().register(BreakBlockEvent.class, event -> {
BlockType blockType = event.getBlockType();
if (isCustomOre(blockType)) {
// Apply fortune multiplier
ItemStack tool = event.getItemInHand();
int fortuneLevel = getFortuneLevel(tool);
int dropMultiplier = 1 + random.nextInt(fortuneLevel + 1);
// Schedule custom drops
Vector3i pos = event.getTargetBlock();
scheduleOreDrops(pos, blockType, dropMultiplier);
}
});
}
private boolean isCustomOre(BlockType blockType) {
return blockType.getId().startsWith("custom_ore_");
}
}
```
### Anti-Grief System
```java
public class AntiGriefPlugin extends JavaPlugin {
private final Map<Vector3i, BlockAction> recentActions = new HashMap<>();
@Override
public void start() {
// Track block breaks for rollback
getEventRegistry().register(BreakBlockEvent.class, event -> {
Vector3i pos = event.getTargetBlock();
BlockType blockType = event.getBlockType();
recentActions.put(pos, new BlockAction(
ActionType.BREAK,
blockType,
System.currentTimeMillis()
));
});
// Track block places for rollback
getEventRegistry().register(PlaceBlockEvent.class, event -> {
Vector3i pos = event.getTargetBlock();
recentActions.put(pos, new BlockAction(
ActionType.PLACE,
null,
System.currentTimeMillis()
));
});
}
public void rollback(World world, int seconds) {
long cutoff = System.currentTimeMillis() - (seconds * 1000L);
recentActions.entrySet().stream()
.filter(e -> e.getValue().timestamp() >= cutoff)
.forEach(e -> {
Vector3i pos = e.getKey();
BlockAction action = e.getValue();
if (action.type() == ActionType.PLACE) {
// Remove placed blocks
world.breakBlock(pos.x(), pos.y(), pos.z());
} else if (action.blockType() != null) {
// Restore broken blocks
world.setBlock(pos.x(), pos.y(), pos.z(), action.blockType());
}
});
recentActions.clear();
}
record BlockAction(ActionType type, BlockType blockType, long timestamp) {}
enum ActionType { BREAK, PLACE }
}
```
## Best Practices
{{< callout type="info" >}}
**Block Event Guidelines:**
- Use `EventPriority.EARLY` for protection systems
- ECS events provide detailed control over block operations
- Always validate positions before modifying blocks
- Consider chunk load state when working with distant blocks
- Clean up tracking data when no longer needed
{{< /callout >}}
{{< callout type="warning" >}}
**Performance Note:** Block events can fire very frequently. Avoid expensive operations in handlers and cache results where possible.
{{< /callout >}}

View File

@@ -0,0 +1,721 @@
---
title: Événements Bloc
type: docs
weight: 3
---
Événements déclenchés par les interactions et modifications de blocs. Ces événements utilisent le pattern ECS (Entity Component System).
## Événements de Destruction & Placement
### BreakBlockEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand un bloc est en train d'être cassé.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| targetBlock | `Vector3i` | Position du bloc |
| blockType | `BlockType` | Type de bloc |
| itemInHand | `ItemStack` | Outil utilisé |
{{< /tab >}}
{{< tab >}}
- `getTargetBlock()` - Retourne la position du bloc
- `getBlockType()` - Retourne le type de bloc
- `getItemInHand()` - Retourne l'outil utilisé
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le cassage
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(BreakBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
ItemStack tool = event.getItemInHand();
// Protéger certains blocs
if (blockType.getId().equals("special_ore")) {
event.setCancelled(true);
return;
}
// Logger le cassage
getLogger().at(Level.INFO).log("Bloc cassé à " + position + ": " + blockType.getId());
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlaceBlockEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand un bloc est placé.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| targetBlock | `Vector3i` | Position où le bloc sera placé |
| rotation | `RotationTuple` | Rotation/orientation du bloc |
| itemInHand | `ItemStack` | Item bloc placé |
{{< /tab >}}
{{< tab >}}
- `getTargetBlock()` - Retourne la position cible
- `setTargetBlock(Vector3i)` - Change la position de placement
- `getRotation()` - Retourne la rotation du bloc
- `setRotation(RotationTuple)` - Change la rotation
- `getItemInHand()` - Retourne l'item bloc
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le placement
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlaceBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
RotationTuple rotation = event.getRotation();
ItemStack item = event.getItemInHand();
// Vérifier les permissions de construction
if (isProtectedArea(position)) {
event.setCancelled(true);
return;
}
// Forcer une certaine rotation
event.setRotation(RotationTuple.of(Rotation.North, Rotation.None, Rotation.None));
});
```
{{< /tab >}}
{{< /tabs >}}
---
### DamageBlockEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand des dégâts sont appliqués à un bloc pendant le cassage (avant que le bloc ne soit complètement cassé).
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| targetBlock | `Vector3i` | Position du bloc |
| blockType | `BlockType` | Type de bloc |
| itemInHand | `ItemStack` | Outil utilisé (peut être null) |
| currentDamage | `float` | Dégâts déjà appliqués au bloc |
| damage | `float` | Dégâts appliqués ce tick |
{{< /tab >}}
{{< tab >}}
- `getTargetBlock()` - Retourne la position du bloc
- `setTargetBlock(Vector3i)` - Change le bloc cible
- `getBlockType()` - Retourne le type de bloc
- `getItemInHand()` - Retourne l'outil utilisé
- `getCurrentDamage()` - Retourne les dégâts accumulés
- `getDamage()` - Retourne les dégâts ce tick
- `setDamage(float)` - Modifie le montant de dégâts
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule les dégâts
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DamageBlockEvent.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
// Rendre certains blocs indestructibles
if (blockType.getId().equals("bedrock")) {
event.setCancelled(true);
return;
}
// Réduire les dégâts sur les blocs renforcés
if (blockType.getId().startsWith("hardened_")) {
event.setDamage(event.getDamage() * 0.5f);
}
// Logger la progression du minage
float progress = event.getCurrentDamage() / 100f;
getLogger().at(Level.INFO).log("Bloc " + blockType.getId() + " à " + progress + "% de dégâts");
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements d'Interaction
### UseBlockEvent
Classe de base pour les événements d'utilisation de bloc. A des variantes Pre et Post.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
#### UseBlockEvent.Pre
{{< badge "Annulable" >}}
Déclenché avant qu'une action d'utilisation de bloc ne se produise (clic droit sur bloc).
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| interactionType | `InteractionType` | Type d'interaction |
| context | `InteractionContext` | Contexte d'interaction |
| targetBlock | `Vector3i` | Position du bloc |
| blockType | `BlockType` | Le type de bloc |
{{< /tab >}}
{{< tab >}}
- `getInteractionType()` - Retourne le type d'interaction
- `getContext()` - Retourne le contexte d'interaction
- `getTargetBlock()` - Retourne la position du bloc
- `getBlockType()` - Retourne le type de bloc
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule l'action d'utilisation
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(UseBlockEvent.Pre.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
InteractionContext context = event.getContext();
// Empêcher l'ouverture de conteneurs verrouillés
if (isLocked(position)) {
event.setCancelled(true);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
#### UseBlockEvent.Post
Déclenché après qu'une action d'utilisation de bloc s'est terminée avec succès.
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| interactionType | `InteractionType` | Type d'interaction |
| context | `InteractionContext` | Contexte d'interaction |
| targetBlock | `Vector3i` | Position du bloc |
| blockType | `BlockType` | Le type de bloc |
{{< /tab >}}
{{< tab >}}
- `getInteractionType()` - Retourne le type d'interaction
- `getContext()` - Retourne le contexte d'interaction
- `getTargetBlock()` - Retourne la position du bloc
- `getBlockType()` - Retourne le type de bloc
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(UseBlockEvent.Post.class, event -> {
Vector3i position = event.getTargetBlock();
BlockType blockType = event.getBlockType();
// Suivre les interactions pour les quêtes
if (isQuestBlock(blockType)) {
completeQuestObjective("interact_with_" + blockType.getId());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements d'Items
### DropItemEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand un item est lâché. A deux variantes : `Drop` et `PlayerRequest`.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
#### DropItemEvent.Drop
L'événement de drop réel avec les détails de l'item.
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| itemStack | `ItemStack` | Item lâché |
| throwSpeed | `float` | Vitesse du lancer |
{{< /tab >}}
{{< tab >}}
- `getItemStack()` - Retourne le stack d'items
- `setItemStack(ItemStack)` - Change l'item lâché
- `getThrowSpeed()` - Retourne la vitesse de lancer
- `setThrowSpeed(float)` - Change la vitesse de lancer
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le drop
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DropItemEvent.Drop.class, event -> {
ItemStack item = event.getItemStack();
// Empêcher de lâcher certains items
if (item.getId().equals("quest_item")) {
event.setCancelled(true);
return;
}
// Modifier la vitesse de lancer
event.setThrowSpeed(event.getThrowSpeed() * 1.5f);
});
```
{{< /tab >}}
{{< /tabs >}}
---
#### DropItemEvent.PlayerRequest
Déclenché quand un joueur demande à lâcher un item (avant le drop réel).
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| inventorySectionId | `int` | ID de la section d'inventaire |
| slotId | `short` | Slot depuis lequel on drop |
{{< /tab >}}
{{< tab >}}
- `getInventorySectionId()` - Retourne la section d'inventaire
- `getSlotId()` - Retourne l'index du slot
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule la requête
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DropItemEvent.PlayerRequest.class, event -> {
int sectionId = event.getInventorySectionId();
short slot = event.getSlotId();
// Empêcher de drop depuis certains slots
if (sectionId == Inventory.HOTBAR && slot == 0) {
event.setCancelled(true); // Ne peut pas drop le premier slot hotbar
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### InteractivelyPickupItemEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand un joueur ramasse un item de manière interactive (ramassage manuel, pas auto-pickup).
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| itemStack | `ItemStack` | L'item ramassé |
{{< /tab >}}
{{< tab >}}
- `getItemStack()` - Retourne le stack d'items
- `setItemStack(ItemStack)` - Change l'item ramassé
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le ramassage
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(InteractivelyPickupItemEvent.class, event -> {
ItemStack item = event.getItemStack();
// Empêcher de ramasser des items de quête qui n'appartiennent pas au joueur
if (item.getId().startsWith("quest_") && !canPickupQuestItem(player, item)) {
event.setCancelled(true);
return;
}
// Transformer les items au ramassage
if (item.getId().equals("raw_ore")) {
event.setItemStack(item.withCount(item.getCount() * 2));
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements de Craft
### CraftRecipeEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand une recette de craft est exécutée. A des variantes Pre et Post.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| craftedRecipe | `CraftingRecipe` | La recette craftée |
| quantity | `int` | Nombre d'items craftés |
{{< /tab >}}
{{< tab >}}
- `getCraftedRecipe()` - Retourne la recette de craft
- `getQuantity()` - Retourne la quantité craftée
- `isCancelled()` - Vérifie si annulé (Pre uniquement)
- `setCancelled(boolean)` - Annule le craft (Pre uniquement)
{{< /tab >}}
{{< tab >}}
```java
// Vérification pré-craft
getEventRegistry().register(CraftRecipeEvent.Pre.class, event -> {
CraftingRecipe recipe = event.getCraftedRecipe();
int quantity = event.getQuantity();
// Bloquer certaines recettes
if (isRestrictedRecipe(recipe)) {
event.setCancelled(true);
}
});
// Suivi post-craft
getEventRegistry().register(CraftRecipeEvent.Post.class, event -> {
CraftingRecipe recipe = event.getCraftedRecipe();
int quantity = event.getQuantity();
// Suivre les statistiques de craft
incrementCraftCount(recipe.getId(), quantity);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Autres Événements
### SwitchActiveSlotEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand un joueur change son slot actif de hotbar.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| inventorySectionId | `int` | ID de la section d'inventaire |
| previousSlot | `int` | Le slot avant le changement |
| newSlot | `byte` | Le slot cible |
| serverRequest | `boolean` | Si le serveur a initié le changement |
{{< /tab >}}
{{< tab >}}
- `getInventorySectionId()` - Retourne la section d'inventaire
- `getPreviousSlot()` - Retourne l'index du slot précédent
- `getNewSlot()` - Retourne l'index du nouveau slot
- `setNewSlot(byte)` - Change le slot cible
- `isServerRequest()` - Vérifie si initié par le serveur
- `isClientRequest()` - Vérifie si initié par le client
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le changement de slot
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(SwitchActiveSlotEvent.class, event -> {
int fromSlot = event.getPreviousSlot();
int toSlot = event.getNewSlot();
// Empêcher le changement pendant un cooldown
if (isOnCooldown(player)) {
event.setCancelled(true);
return;
}
// Suivre l'usage des slots pour analytics
if (event.isClientRequest()) {
trackSlotSwitch(player, fromSlot, toSlot);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### DiscoverZoneEvent
{{< badge "ECS" >}}
Déclenché quand un joueur découvre une nouvelle zone. A une variante `Display` qui est annulable.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| discoveryInfo | `ZoneDiscoveryInfo` | Informations sur la zone découverte |
{{< /tab >}}
{{< tab >}}
- `getDiscoveryInfo()` - Retourne les infos de découverte de zone
- `isCancelled()` - Vérifie si annulé (Display uniquement)
- `setCancelled(boolean)` - Annule l'affichage (Display uniquement)
{{< /tab >}}
{{< tab >}}
```java
// Annuler l'affichage de notification de découverte de zone
getEventRegistry().register(DiscoverZoneEvent.Display.class, event -> {
WorldMapTracker.ZoneDiscoveryInfo info = event.getDiscoveryInfo();
// Cacher certaines zones de l'UI de découverte
if (isHiddenZone(info)) {
event.setCancelled(true);
return;
}
// Logger la découverte pour les succès
recordZoneDiscovery(player, info);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChangeGameModeEvent
{{< badge "Annulable" >}} {{< badge "ECS" >}}
Déclenché quand le mode de jeu d'une entité change.
**Package :** `com.hypixel.hytale.server.core.event.events.ecs`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| gameMode | `GameMode` | Le nouveau mode de jeu |
{{< /tab >}}
{{< tab >}}
- `getGameMode()` - Retourne le nouveau mode de jeu
- `setGameMode(GameMode)` - Change le mode de jeu cible
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le changement de mode
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChangeGameModeEvent.class, event -> {
GameMode newMode = event.getGameMode();
// Empêcher le mode créatif dans certains mondes
if (newMode == GameMode.CREATIVE && isRestrictedWorld(player.getWorld())) {
event.setCancelled(true);
player.sendMessage(Message.raw("Mode créatif non autorisé ici !"));
return;
}
// Forcer le mode aventure
if (shouldForceAdventure(player)) {
event.setGameMode(GameMode.ADVENTURE);
}
getLogger().at(Level.INFO).log(player.getDisplayName() + " changé en " + newMode);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Exemples Pratiques
### Plugin Logger de Blocs
```java
public class BlockLoggerPlugin extends JavaPlugin {
@Override
public void start() {
getEventRegistry().register(PlaceBlockEvent.class, event -> {
logAction("PLACE", event.getTargetBlock(), event.getItemInHand());
});
getEventRegistry().register(BreakBlockEvent.class, event -> {
logAction("BREAK", event.getTargetBlock(), event.getBlockType());
});
}
private void logAction(String action, Vector3i pos, Object data) {
getLogger().at(Level.INFO).log(String.format("%s: %d,%d,%d - %s",
action, pos.x(), pos.y(), pos.z(), data));
}
}
```
### Système de Protection de Région
```java
public class RegionPlugin extends JavaPlugin {
private final Set<Region> regions = new HashSet<>();
@Override
public void start() {
// Protéger contre le cassage
getEventRegistry().register(EventPriority.EARLY, BreakBlockEvent.class, event -> {
if (isInProtectedRegion(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Protéger contre le placement
getEventRegistry().register(EventPriority.EARLY, PlaceBlockEvent.class, event -> {
if (isInProtectedRegion(event.getTargetBlock())) {
event.setCancelled(true);
}
});
// Protéger contre l'utilisation
getEventRegistry().register(EventPriority.EARLY, UseBlockEvent.Pre.class, event -> {
if (isInProtectedRegion(event.getTargetBlock())) {
event.setCancelled(true);
}
});
}
private boolean isInProtectedRegion(Vector3i pos) {
return regions.stream().anyMatch(r -> r.contains(pos));
}
}
```
### Système de Minerais Custom
```java
public class CustomOrePlugin extends JavaPlugin {
private final Random random = new Random();
@Override
public void start() {
getEventRegistry().register(BreakBlockEvent.class, event -> {
BlockType blockType = event.getBlockType();
if (isCustomOre(blockType)) {
// Appliquer le multiplicateur fortune
ItemStack tool = event.getItemInHand();
int fortuneLevel = getFortuneLevel(tool);
int dropMultiplier = 1 + random.nextInt(fortuneLevel + 1);
// Programmer les drops custom
Vector3i pos = event.getTargetBlock();
scheduleOreDrops(pos, blockType, dropMultiplier);
}
});
}
private boolean isCustomOre(BlockType blockType) {
return blockType.getId().startsWith("custom_ore_");
}
}
```
### Système Anti-Grief
```java
public class AntiGriefPlugin extends JavaPlugin {
private final Map<Vector3i, BlockAction> recentActions = new HashMap<>();
@Override
public void start() {
// Suivre les cassages de blocs pour rollback
getEventRegistry().register(BreakBlockEvent.class, event -> {
Vector3i pos = event.getTargetBlock();
BlockType blockType = event.getBlockType();
recentActions.put(pos, new BlockAction(
ActionType.BREAK,
blockType,
System.currentTimeMillis()
));
});
// Suivre les placements de blocs pour rollback
getEventRegistry().register(PlaceBlockEvent.class, event -> {
Vector3i pos = event.getTargetBlock();
recentActions.put(pos, new BlockAction(
ActionType.PLACE,
null,
System.currentTimeMillis()
));
});
}
public void rollback(World world, int seconds) {
long cutoff = System.currentTimeMillis() - (seconds * 1000L);
recentActions.entrySet().stream()
.filter(e -> e.getValue().timestamp() >= cutoff)
.forEach(e -> {
Vector3i pos = e.getKey();
BlockAction action = e.getValue();
if (action.type() == ActionType.PLACE) {
// Supprimer les blocs placés
world.breakBlock(pos.x(), pos.y(), pos.z());
} else if (action.blockType() != null) {
// Restaurer les blocs cassés
world.setBlock(pos.x(), pos.y(), pos.z(), action.blockType());
}
});
recentActions.clear();
}
record BlockAction(ActionType type, BlockType blockType, long timestamp) {}
enum ActionType { BREAK, PLACE }
}
```
## Bonnes Pratiques
{{< callout type="info" >}}
**Directives pour les Événements de Bloc :**
- Utiliser `EventPriority.EARLY` pour les systèmes de protection
- Les événements ECS fournissent un contrôle détaillé sur les opérations de bloc
- Toujours valider les positions avant de modifier les blocs
- Considérer l'état de chargement des chunks pour les blocs distants
- Nettoyer les données de suivi quand elles ne sont plus nécessaires
{{< /callout >}}
{{< callout type="warning" >}}
**Note de Performance :** Les événements de bloc peuvent se déclencher très fréquemment. Évitez les opérations coûteuses dans les handlers et mettez en cache les résultats si possible.
{{< /callout >}}

View File

@@ -0,0 +1,193 @@
---
title: Entity Events
type: docs
weight: 2
---
Events triggered by entity actions and state changes. These events apply to entities in the game world.
{{< callout type="warning" >}}
**Note:** Hytale's entity event system is minimal compared to other game APIs. Most entity-related logic is handled through the ECS (Entity Component System) rather than traditional events. For block interactions, item drops, and crafting, see [Block Events](../block-events).
{{< /callout >}}
## Entity Lifecycle Events
### EntityRemoveEvent
Fired when an entity is removed from the world.
**Package:** `com.hypixel.hytale.server.core.event.events.entity`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| entity | `Entity` | The entity being removed |
{{< /tab >}}
{{< tab >}}
- `getEntity()` - Returns the Entity object being removed
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(EntityRemoveEvent.class, event -> {
Entity entity = event.getEntity();
// Clean up any custom data associated with this entity
if (trackedEntities.contains(entity.getUuid())) {
trackedEntities.remove(entity.getUuid());
getLogger().at(Level.INFO).log("Tracked entity removed: " + entity.getType().getId());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Living Entity Events
### LivingEntityInventoryChangeEvent
Fired when a living entity's inventory changes.
**Package:** `com.hypixel.hytale.server.core.event.events.entity`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| entity | `LivingEntity` | The entity whose inventory changed |
| itemContainer | `ItemContainer` | The item container that changed |
| transaction | `Transaction` | The transaction that occurred |
{{< /tab >}}
{{< tab >}}
- `getEntity()` - Returns the LivingEntity
- `getItemContainer()` - Returns the ItemContainer that changed
- `getTransaction()` - Returns the Transaction details
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(LivingEntityInventoryChangeEvent.class, event -> {
LivingEntity entity = event.getEntity();
ItemContainer container = event.getItemContainer();
Transaction transaction = event.getTransaction();
// Log inventory changes for debugging
getLogger().at(Level.INFO).log("Inventory changed for: " + entity.getType().getId());
getLogger().at(Level.INFO).log("Container: " + container);
getLogger().at(Level.INFO).log("Transaction: " + transaction);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### LivingEntityUseBlockEvent
{{< badge "Deprecated" >}}
Fired when a living entity uses a block. This event is deprecated and marked for removal.
**Package:** `com.hypixel.hytale.server.core.event.events.entity`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| ref | `Ref<EntityStore>` | Reference to the entity store |
| blockType | `String` | The type of block being used |
{{< /tab >}}
{{< tab >}}
- `getRef()` - Returns the entity store reference
- `getBlockType()` - Returns the block type string
{{< /tab >}}
{{< tab >}}
```java
// Note: This event is deprecated. Use UseBlockEvent instead.
getEventRegistry().register(LivingEntityUseBlockEvent.class, event -> {
String blockType = event.getBlockType();
Ref<EntityStore> ref = event.getRef();
getLogger().at(Level.INFO).log("Entity used block: " + blockType);
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
**Deprecated:** This event is marked for removal. Use `UseBlockEvent.Pre` and `UseBlockEvent.Post` from the ECS event system instead. See [Block Events](../block-events#useblockevent) for the modern alternative.
{{< /callout >}}
---
## Working with Entities
### Entity Component System (ECS)
Hytale uses an Entity Component System architecture. Instead of traditional entity events, much of the entity behavior is handled through components and the ECS event system.
For entity-related operations, consider using:
- **ECS Events** for block interactions, item management, and game mode changes
- **Entity Components** for entity state and behavior
- **EntityStore** for entity data persistence
### Example: Tracking Entities
```java
public class EntityTrackerPlugin extends JavaPlugin {
private final Set<UUID> trackedEntities = new HashSet<>();
@Override
public void start() {
// Track entity removal
getEventRegistry().register(EntityRemoveEvent.class, event -> {
Entity entity = event.getEntity();
if (trackedEntities.remove(entity.getUuid())) {
getLogger().at(Level.INFO).log("Tracked entity removed: " + entity.getUuid());
onEntityRemoved(entity);
}
});
// Track inventory changes
getEventRegistry().register(LivingEntityInventoryChangeEvent.class, event -> {
LivingEntity entity = event.getEntity();
if (trackedEntities.contains(entity.getUuid())) {
onInventoryChanged(entity, event.getTransaction());
}
});
}
public void trackEntity(Entity entity) {
trackedEntities.add(entity.getUuid());
getLogger().at(Level.INFO).log("Now tracking entity: " + entity.getUuid());
}
private void onEntityRemoved(Entity entity) {
// Custom cleanup logic
}
private void onInventoryChanged(LivingEntity entity, Transaction transaction) {
// React to inventory changes
}
}
```
## Best Practices
{{< callout type="info" >}}
**Entity Event Guidelines:**
- Entity events in Hytale are minimal - prefer ECS patterns for complex behavior
- Use `EntityRemoveEvent` for cleanup when entities are removed
- `LivingEntityUseBlockEvent` is deprecated - migrate to `UseBlockEvent`
- For player-specific entity events, see [Player Events](../player-events)
- For block and item interactions, see [Block Events](../block-events)
{{< /callout >}}
{{< callout type="tip" >}}
**Migration Note:** If you're coming from Minecraft/Bukkit development, note that Hytale doesn't have traditional `EntityDamageEvent`, `EntityDeathEvent`, or `EntitySpawnEvent`. Entity lifecycle and combat are handled differently through the ECS architecture.
{{< /callout >}}

View File

@@ -0,0 +1,193 @@
---
title: Événements Entité
type: docs
weight: 2
---
Événements déclenchés par les actions et changements d'état des entités dans le monde du jeu.
{{< callout type="warning" >}}
**Note :** Le système d'événements d'entité de Hytale est minimal comparé à d'autres APIs de jeu. La plupart de la logique liée aux entités est gérée via l'ECS (Entity Component System) plutôt que par des événements traditionnels. Pour les interactions de blocs, drops d'items et craft, voir [Événements Bloc](../block-events).
{{< /callout >}}
## Événements du Cycle de Vie
### EntityRemoveEvent
Déclenché quand une entité est supprimée du monde.
**Package :** `com.hypixel.hytale.server.core.event.events.entity`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| entity | `Entity` | L'entité supprimée |
{{< /tab >}}
{{< tab >}}
- `getEntity()` - Retourne l'objet Entity supprimé
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(EntityRemoveEvent.class, event -> {
Entity entity = event.getEntity();
// Nettoyer les données personnalisées associées à cette entité
if (trackedEntities.contains(entity.getUuid())) {
trackedEntities.remove(entity.getUuid());
getLogger().at(Level.INFO).log("Entité suivie supprimée : " + entity.getType().getId());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements d'Entités Vivantes
### LivingEntityInventoryChangeEvent
Déclenché quand l'inventaire d'une entité vivante change.
**Package :** `com.hypixel.hytale.server.core.event.events.entity`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| entity | `LivingEntity` | L'entité dont l'inventaire a changé |
| itemContainer | `ItemContainer` | Le conteneur d'items modifié |
| transaction | `Transaction` | La transaction effectuée |
{{< /tab >}}
{{< tab >}}
- `getEntity()` - Retourne le LivingEntity
- `getItemContainer()` - Retourne l'ItemContainer modifié
- `getTransaction()` - Retourne les détails de la Transaction
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(LivingEntityInventoryChangeEvent.class, event -> {
LivingEntity entity = event.getEntity();
ItemContainer container = event.getItemContainer();
Transaction transaction = event.getTransaction();
// Logger les changements d'inventaire pour debug
getLogger().at(Level.INFO).log("Inventaire changé pour : " + entity.getType().getId());
getLogger().at(Level.INFO).log("Conteneur : " + container);
getLogger().at(Level.INFO).log("Transaction : " + transaction);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### LivingEntityUseBlockEvent
{{< badge "Déprécié" >}}
Déclenché quand une entité vivante utilise un bloc. Cet événement est déprécié et marqué pour suppression.
**Package :** `com.hypixel.hytale.server.core.event.events.entity`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| ref | `Ref<EntityStore>` | Référence au store d'entité |
| blockType | `String` | Le type de bloc utilisé |
{{< /tab >}}
{{< tab >}}
- `getRef()` - Retourne la référence du store d'entité
- `getBlockType()` - Retourne la chaîne du type de bloc
{{< /tab >}}
{{< tab >}}
```java
// Note : Cet événement est déprécié. Utilisez UseBlockEvent à la place.
getEventRegistry().register(LivingEntityUseBlockEvent.class, event -> {
String blockType = event.getBlockType();
Ref<EntityStore> ref = event.getRef();
getLogger().at(Level.INFO).log("Entité a utilisé le bloc : " + blockType);
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
**Déprécié :** Cet événement est marqué pour suppression. Utilisez `UseBlockEvent.Pre` et `UseBlockEvent.Post` du système d'événements ECS à la place. Voir [Événements Bloc](../block-events#useblockevent) pour l'alternative moderne.
{{< /callout >}}
---
## Travailler avec les Entités
### Entity Component System (ECS)
Hytale utilise une architecture Entity Component System. Au lieu d'événements d'entité traditionnels, la plupart du comportement des entités est géré via des composants et le système d'événements ECS.
Pour les opérations liées aux entités, considérez utiliser :
- **Événements ECS** pour les interactions de blocs, gestion d'items et changements de mode de jeu
- **Composants d'Entité** pour l'état et le comportement des entités
- **EntityStore** pour la persistance des données d'entité
### Exemple : Suivi d'Entités
```java
public class EntityTrackerPlugin extends JavaPlugin {
private final Set<UUID> trackedEntities = new HashSet<>();
@Override
public void start() {
// Suivre la suppression d'entités
getEventRegistry().register(EntityRemoveEvent.class, event -> {
Entity entity = event.getEntity();
if (trackedEntities.remove(entity.getUuid())) {
getLogger().at(Level.INFO).log("Entité suivie supprimée : " + entity.getUuid());
onEntityRemoved(entity);
}
});
// Suivre les changements d'inventaire
getEventRegistry().register(LivingEntityInventoryChangeEvent.class, event -> {
LivingEntity entity = event.getEntity();
if (trackedEntities.contains(entity.getUuid())) {
onInventoryChanged(entity, event.getTransaction());
}
});
}
public void trackEntity(Entity entity) {
trackedEntities.add(entity.getUuid());
getLogger().at(Level.INFO).log("Suivi de l'entité : " + entity.getUuid());
}
private void onEntityRemoved(Entity entity) {
// Logique de nettoyage personnalisée
}
private void onInventoryChanged(LivingEntity entity, Transaction transaction) {
// Réagir aux changements d'inventaire
}
}
```
## Bonnes Pratiques
{{< callout type="info" >}}
**Directives pour les Événements d'Entité :**
- Les événements d'entité dans Hytale sont minimaux - préférez les patterns ECS pour les comportements complexes
- Utilisez `EntityRemoveEvent` pour le nettoyage quand les entités sont supprimées
- `LivingEntityUseBlockEvent` est déprécié - migrez vers `UseBlockEvent`
- Pour les événements d'entité spécifiques aux joueurs, voir [Événements Joueur](../player-events)
- Pour les interactions de blocs et d'items, voir [Événements Bloc](../block-events)
{{< /callout >}}
{{< callout type="tip" >}}
**Note de Migration :** Si vous venez du développement Minecraft/Bukkit, notez que Hytale n'a pas de `EntityDamageEvent`, `EntityDeathEvent`, ou `EntitySpawnEvent` traditionnels. Le cycle de vie des entités et le combat sont gérés différemment via l'architecture ECS.
{{< /callout >}}

View File

@@ -0,0 +1,425 @@
---
title: Permission Events
type: docs
weight: 4
---
Events triggered when permissions or group memberships change. These events allow plugins to react to permission system modifications.
{{< callout type="info" >}}
**Note:** These events fire when permissions change, not when they are checked. They are useful for synchronization, logging, and reacting to permission modifications.
{{< /callout >}}
## Player Permission Events
### PlayerPermissionChangeEvent
Abstract base class for all player permission change events.
**Package:** `com.hypixel.hytale.server.core.event.events.permissions`
{{< tabs items="Fields,Methods" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the UUID of the player whose permissions changed
{{< /tab >}}
{{< /tabs >}}
---
### PlayerPermissionChangeEvent.PermissionsAdded
Fired when permissions are directly added to a player.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| addedPermissions | `Set<String>` | Set of permission nodes added |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getAddedPermissions()` - Returns unmodifiable set of added permissions
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsAdded.class, event -> {
UUID playerUuid = event.getPlayerUuid();
Set<String> addedPerms = event.getAddedPermissions();
getLogger().at(Level.INFO).log("Player " + playerUuid + " gained permissions: " + addedPerms);
// Notify online player
Player player = Universe.get().getPlayer(playerUuid);
if (player != null) {
player.sendMessage(Message.raw("You gained new permissions!"));
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerPermissionChangeEvent.PermissionsRemoved
Fired when permissions are directly removed from a player.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| removedPermissions | `Set<String>` | Set of permission nodes removed |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getRemovedPermissions()` - Returns unmodifiable set of removed permissions
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsRemoved.class, event -> {
UUID playerUuid = event.getPlayerUuid();
Set<String> removedPerms = event.getRemovedPermissions();
getLogger().at(Level.INFO).log("Player " + playerUuid + " lost permissions: " + removedPerms);
// Check if player lost admin permission
if (removedPerms.contains("admin.*")) {
notifyAdmins("Player " + playerUuid + " is no longer an admin");
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerPermissionChangeEvent.GroupAdded
Fired when a player is added to a permission group (via PlayerPermissionChangeEvent).
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| groupName | `String` | Name of the group added |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getGroupName()` - Returns the group name
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerPermissionChangeEvent.GroupAdded.class, event -> {
UUID playerUuid = event.getPlayerUuid();
String group = event.getGroupName();
getLogger().at(Level.INFO).log("Player " + playerUuid + " added to group: " + group);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerPermissionChangeEvent.GroupRemoved
Fired when a player is removed from a permission group (via PlayerPermissionChangeEvent).
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| groupName | `String` | Name of the group removed |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getGroupName()` - Returns the group name
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerPermissionChangeEvent.GroupRemoved.class, event -> {
UUID playerUuid = event.getPlayerUuid();
String group = event.getGroupName();
getLogger().at(Level.INFO).log("Player " + playerUuid + " removed from group: " + group);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Group Permission Events
### GroupPermissionChangeEvent
Abstract base class for group permission change events.
**Package:** `com.hypixel.hytale.server.core.event.events.permissions`
{{< tabs items="Fields,Methods" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| groupName | `String` | Name of the affected group |
{{< /tab >}}
{{< tab >}}
- `getGroupName()` - Returns the name of the group whose permissions changed
{{< /tab >}}
{{< /tabs >}}
---
### GroupPermissionChangeEvent.Added
Fired when permissions are added to a group.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| groupName | `String` | Name of the affected group |
| addedPermissions | `Set<String>` | Set of permission nodes added |
{{< /tab >}}
{{< tab >}}
- `getGroupName()` - Returns the group name
- `getAddedPermissions()` - Returns unmodifiable set of added permissions
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(GroupPermissionChangeEvent.Added.class, event -> {
String group = event.getGroupName();
Set<String> addedPerms = event.getAddedPermissions();
getLogger().at(Level.INFO).log("Group " + group + " gained permissions: " + addedPerms);
// Notify all online players in this group
notifyGroupMembers(group, "Your group gained new permissions!");
});
```
{{< /tab >}}
{{< /tabs >}}
---
### GroupPermissionChangeEvent.Removed
Fired when permissions are removed from a group.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| groupName | `String` | Name of the affected group |
| removedPermissions | `Set<String>` | Set of permission nodes removed |
{{< /tab >}}
{{< tab >}}
- `getGroupName()` - Returns the group name
- `getRemovedPermissions()` - Returns unmodifiable set of removed permissions
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(GroupPermissionChangeEvent.Removed.class, event -> {
String group = event.getGroupName();
Set<String> removedPerms = event.getRemovedPermissions();
getLogger().at(Level.INFO).log("Group " + group + " lost permissions: " + removedPerms);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Player Group Events
### PlayerGroupEvent
Fired when a player's group membership changes. Extends `PlayerPermissionChangeEvent`.
**Package:** `com.hypixel.hytale.server.core.event.events.permissions`
{{< tabs items="Fields,Methods" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| groupName | `String` | Name of the group |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getGroupName()` - Returns the group name
{{< /tab >}}
{{< /tabs >}}
---
### PlayerGroupEvent.Added
Fired when a player is added to a group.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| groupName | `String` | Name of the group joined |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getGroupName()` - Returns the group name
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerGroupEvent.Added.class, event -> {
UUID playerUuid = event.getPlayerUuid();
String group = event.getGroupName();
getLogger().at(Level.INFO).log("Player " + playerUuid + " joined group: " + group);
// Welcome message for VIP group
if (group.equals("vip")) {
Player player = Universe.get().getPlayer(playerUuid);
if (player != null) {
player.sendMessage(Message.raw("Welcome to the VIP group!"));
}
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerGroupEvent.Removed
Fired when a player is removed from a group.
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerUuid | `UUID` | UUID of the affected player |
| groupName | `String` | Name of the group left |
{{< /tab >}}
{{< tab >}}
- `getPlayerUuid()` - Returns the player's UUID
- `getGroupName()` - Returns the group name
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerGroupEvent.Removed.class, event -> {
UUID playerUuid = event.getPlayerUuid();
String group = event.getGroupName();
getLogger().at(Level.INFO).log("Player " + playerUuid + " left group: " + group);
// Log staff changes
if (group.equals("staff") || group.equals("admin")) {
logStaffChange(playerUuid, group, "removed");
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Practical Examples
### Permission Audit Logger
```java
public class PermissionAuditPlugin extends JavaPlugin {
@Override
public void start() {
// Log all player permission changes
getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsAdded.class, event -> {
logAudit("PERM_ADD", event.getPlayerUuid(), event.getAddedPermissions());
});
getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsRemoved.class, event -> {
logAudit("PERM_REMOVE", event.getPlayerUuid(), event.getRemovedPermissions());
});
// Log all group membership changes
getEventRegistry().register(PlayerGroupEvent.Added.class, event -> {
logAudit("GROUP_JOIN", event.getPlayerUuid(), Set.of(event.getGroupName()));
});
getEventRegistry().register(PlayerGroupEvent.Removed.class, event -> {
logAudit("GROUP_LEAVE", event.getPlayerUuid(), Set.of(event.getGroupName()));
});
// Log group permission changes
getEventRegistry().register(GroupPermissionChangeEvent.Added.class, event -> {
logGroupAudit("GROUP_PERM_ADD", event.getGroupName(), event.getAddedPermissions());
});
getEventRegistry().register(GroupPermissionChangeEvent.Removed.class, event -> {
logGroupAudit("GROUP_PERM_REMOVE", event.getGroupName(), event.getRemovedPermissions());
});
}
private void logAudit(String action, UUID player, Set<String> items) {
getLogger().at(Level.INFO).log(String.format("[AUDIT] %s: player=%s items=%s", action, player, items));
}
private void logGroupAudit(String action, String group, Set<String> items) {
getLogger().at(Level.INFO).log(String.format("[AUDIT] %s: group=%s items=%s", action, group, items));
}
}
```
### Permission Synchronization
```java
public class PermissionSyncPlugin extends JavaPlugin {
private final Map<UUID, Set<String>> cachedPermissions = new HashMap<>();
@Override
public void start() {
// Keep cache synchronized with permission changes
getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsAdded.class, event -> {
cachedPermissions.computeIfAbsent(event.getPlayerUuid(), k -> new HashSet<>())
.addAll(event.getAddedPermissions());
});
getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsRemoved.class, event -> {
Set<String> perms = cachedPermissions.get(event.getPlayerUuid());
if (perms != null) {
perms.removeAll(event.getRemovedPermissions());
}
});
}
public Set<String> getCachedPermissions(UUID player) {
return cachedPermissions.getOrDefault(player, Collections.emptySet());
}
}
```
## Best Practices
{{< callout type="info" >}}
**Permission Event Guidelines:**
- These events fire after permissions have changed, not before
- Use for logging, synchronization, and notifications
- The returned permission sets are unmodifiable - do not try to modify them
- Player may be offline when permission changes occur - check for null
{{< /callout >}}
{{< callout type="warning" >}}
**Important:** These events do not allow you to intercept or modify permission checks. They only notify you when permissions have been modified through the permission system.
{{< /callout >}}

View File

@@ -0,0 +1,98 @@
---
title: Événements Permission
type: docs
weight: 4
---
Événements liés à la vérification et gestion des permissions.
## PermissionCheckEvent
Déclenché quand une permission est vérifiée pour un joueur.
```java
getEventRegistry().register(PermissionCheckEvent.class, event -> {
String permission = event.getPermission();
Player player = event.getPlayer();
// Overrider le résultat de permission
if (permission.startsWith("vip.") && isVIP(player)) {
event.setResult(true);
}
});
```
| Méthode | Retourne | Description |
|---------|----------|-------------|
| `getPlayer()` | `Player` | Joueur vérifié |
| `getPermission()` | `String` | Noeud de permission |
| `getResult()` | `boolean` | Résultat actuel |
| `setResult(boolean)` | `void` | Overrider le résultat |
## Cas d'Utilisation
### Logique de Permission Personnalisée
```java
getEventRegistry().register(PermissionCheckEvent.class, event -> {
String permission = event.getPermission();
Player player = event.getPlayer();
// Accorder toutes les permissions aux admins
if (isServerAdmin(player)) {
event.setResult(true);
return;
}
// Vérifier une source de permissions personnalisée
if (customPermissions.hasPermission(player.getUuid(), permission)) {
event.setResult(true);
}
});
```
### Logging des Permissions
```java
getEventRegistry().register(PermissionCheckEvent.class, event -> {
getLogger().fine(String.format(
"Vérification permission : %s pour %s = %s",
event.getPermission(),
event.getPlayer().getName(),
event.getResult()
));
});
```
### Permissions Temporaires
```java
public class TempPermPlugin extends JavaPlugin {
private final Map<UUID, Set<String>> tempPerms = new HashMap<>();
@Override
public void start() {
getEventRegistry().register(PermissionCheckEvent.class, event -> {
UUID uuid = event.getPlayer().getUuid();
Set<String> perms = tempPerms.get(uuid);
if (perms != null && perms.contains(event.getPermission())) {
event.setResult(true);
}
});
}
public void grantTemp(UUID player, String permission) {
tempPerms.computeIfAbsent(player, k -> new HashSet<>())
.add(permission);
}
public void revokeTemp(UUID player, String permission) {
Set<String> perms = tempPerms.get(player);
if (perms != null) {
perms.remove(permission);
}
}
}
```

View File

@@ -0,0 +1,675 @@
---
title: Player Events
type: docs
weight: 1
---
Events triggered by player actions and state changes.
## Connection Events
### PlayerSetupConnectEvent
{{< badge "Cancellable" >}}
Fired during player connection setup. Can be cancelled to prevent connection or redirect to another server.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| uuid | `UUID` | Player's unique identifier |
| username | `String` | Player's username |
| auth | `PlayerAuthentication` | Authentication information |
| referralData | `byte[]` | Data from referral (if redirected) |
| referralSource | `HostAddress` | Server that referred player |
{{< /tab >}}
{{< tab >}}
- `getUuid()` - Returns player UUID
- `getUsername()` - Returns player username
- `getAuth()` - Returns authentication data
- `getReferralData()` - Returns referral data (may be null)
- `isReferralConnection()` - Check if redirected from another server
- `getReferralSource()` - Returns source server address
- `referToServer(host, port)` - Redirect player to another server
- `referToServer(host, port, data)` - Redirect with data
- `getReason()` - Get disconnect reason message
- `setReason(String)` - Set disconnect reason message
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel connection
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
String username = event.getUsername();
UUID uuid = event.getUuid();
// Ban check
if (isBanned(uuid)) {
event.setCancelled(true);
event.setReason("You are banned from this server!");
return;
}
// Redirect to different server based on condition
if (shouldRedirect(uuid)) {
event.referToServer("lobby.example.com", 25565);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerConnectEvent
Fired when a player connects to the server. Use this to set the spawn world.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| holder | `Holder<EntityStore>` | Entity store holder |
| playerRef | `PlayerRef` | Reference to the connecting player |
| world | `World` | The world player will spawn in |
{{< /tab >}}
{{< tab >}}
- `getHolder()` - Returns the entity store holder
- `getPlayerRef()` - Returns the player reference
- `getPlayer()` - Returns the Player object (deprecated)
- `getWorld()` - Returns current spawn world (may be null)
- `setWorld(World)` - Set the world player will spawn in
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername());
// Set spawn world
World lobbyWorld = Universe.get().getWorld("lobby");
if (lobbyWorld != null) {
event.setWorld(lobbyWorld);
}
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
The `getPlayer()` method is deprecated. Prefer using `getPlayerRef()` or `getHolder()` to access player data.
{{< /callout >}}
---
### PlayerSetupDisconnectEvent
Fired when a player disconnects during the setup phase (before fully connecting).
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| uuid | `UUID` | Player's unique identifier |
| username | `String` | Player's username |
| auth | `PlayerAuthentication` | Authentication information |
| disconnectReason | `DisconnectReason` | Why the player disconnected |
{{< /tab >}}
{{< tab >}}
- `getUuid()` - Returns player UUID
- `getUsername()` - Returns player username
- `getAuth()` - Returns authentication data
- `getDisconnectReason()` - Returns disconnect reason
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerSetupDisconnectEvent.class, event -> {
String username = event.getUsername();
PacketHandler.DisconnectReason reason = event.getDisconnectReason();
getLogger().at(Level.INFO).log("Player " + username + " disconnected during setup: " + reason);
// Cleanup any pre-connection data
cleanupPendingData(event.getUuid());
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerReadyEvent
Fired when a player is fully ready and loaded into the game. This is the safe point to interact with the player.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `Ref<EntityStore>` | Entity store reference |
| player | `Player` | The player object |
| readyId | `int` | Ready event identifier |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns the entity store reference
- `getReadyId()` - Returns the ready event ID
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
// Player is fully loaded, safe to send complex data
player.sendMessage(Message.raw("Welcome to the server!"));
loadPlayerData(player);
sendWelcomeScreen(player);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerDisconnectEvent
Fired when a player disconnects from the server.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `PlayerRef` | Reference to the disconnecting player |
{{< /tab >}}
{{< tab >}}
- `getPlayerRef()` - Returns the player reference
- `getDisconnectReason()` - Returns why the player disconnected
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
savePlayerData(playerRef.getUuid());
getLogger().at(Level.INFO).log(playerRef.getUsername() + " has left the server");
// Check disconnect reason
PacketHandler.DisconnectReason reason = event.getDisconnectReason();
getLogger().at(Level.INFO).log("Reason: " + reason);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## World Events
### AddPlayerToWorldEvent
Fired when a player is added to a world.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| holder | `Holder<EntityStore>` | Entity store holder |
| world | `World` | The world being entered |
| broadcastJoinMessage | `boolean` | Whether to broadcast join message |
{{< /tab >}}
{{< tab >}}
- `getHolder()` - Returns the entity store holder
- `getWorld()` - Returns the world
- `shouldBroadcastJoinMessage()` - Check if join message will be sent
- `setBroadcastJoinMessage(boolean)` - Control join message broadcast
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(AddPlayerToWorldEvent.class, event -> {
World world = event.getWorld();
// Disable default join message for silent joins
if (isSilentJoin(event.getHolder())) {
event.setBroadcastJoinMessage(false);
}
applyWorldEffects(event.getHolder(), world);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### DrainPlayerFromWorldEvent
Fired when a player is removed from a world (before teleporting to another or disconnecting).
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| holder | `Holder<EntityStore>` | Entity store holder |
| world | `World` | The world being left |
| transform | `Transform` | Player's position/rotation |
{{< /tab >}}
{{< tab >}}
- `getHolder()` - Returns the entity store holder
- `getWorld()` - Returns the world being left
- `setWorld(World)` - Change destination world
- `getTransform()` - Returns player's transform
- `setTransform(Transform)` - Set spawn transform in new world
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DrainPlayerFromWorldEvent.class, event -> {
World world = event.getWorld();
removeWorldEffects(event.getHolder());
saveWorldProgress(event.getHolder(), world);
// Optionally redirect to different world
World newWorld = Universe.get().getWorld("hub");
if (newWorld != null) {
event.setWorld(newWorld);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Chat Events
### PlayerChatEvent
{{< badge "Cancellable" >}} {{< badge "Async" >}}
Fired when a player sends a chat message. This event is asynchronous.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| sender | `PlayerRef` | The player sending the message |
| targets | `List<PlayerRef>` | Players who will receive the message |
| content | `String` | The message content |
| formatter | `Formatter` | Message formatting handler |
{{< /tab >}}
{{< tab >}}
- `getSender()` - Returns the sending player reference
- `setSender(PlayerRef)` - Change the sender
- `getContent()` - Returns the message content
- `setContent(String)` - Modify the message
- `getTargets()` - Returns message recipients
- `setTargets(List<PlayerRef>)` - Change recipients
- `getFormatter()` - Returns the message formatter
- `setFormatter(Formatter)` - Set custom formatter
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel the message
{{< /tab >}}
{{< tab >}}
```java
// PlayerChatEvent has String key and is async - use registerAsyncGlobal()
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
PlayerRef sender = event.getSender();
String message = event.getContent();
// Filter bad words
if (containsBadWord(message)) {
event.setCancelled(true);
// Use PlayerRef.sendMessage() directly
sender.sendMessage(Message.raw("Please don't use that word!"));
return event;
}
// Add prefix based on rank
String prefix = getPlayerPrefix(sender);
event.setContent(prefix + message);
// Custom formatter
event.setFormatter((playerRef, msg) ->
Message.translation("custom.chat.format")
.param("name", playerRef.getUsername())
.param("message", msg)
);
return event;
});
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="info" >}}
**Async Event:** This event runs asynchronously. Use `sender.getReference()` to check if the player is still online (returns null or invalid reference if disconnected).
{{< /callout >}}
---
## Crafting Events
### PlayerCraftEvent
{{< badge "Deprecated" >}}
Fired when a player crafts an item.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< callout type="warning" >}}
**Deprecated:** This event is marked for removal. Use `CraftRecipeEvent` from the ECS events instead.
{{< /callout >}}
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| ref | `Ref<EntityStore>` | Entity store reference |
| player | `Player` | The crafting player |
| craftedRecipe | `CraftingRecipe` | The recipe being crafted |
| quantity | `int` | Number of items crafted |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns entity store reference
- `getCraftedRecipe()` - Returns the crafting recipe
- `getQuantity()` - Returns number crafted
{{< /tab >}}
{{< tab >}}
```java
// Deprecated - prefer CraftRecipeEvent
getEventRegistry().register(PlayerCraftEvent.class, event -> {
Player player = event.getPlayer();
CraftingRecipe recipe = event.getCraftedRecipe();
int quantity = event.getQuantity();
getLogger().at(Level.INFO).log(player.getDisplayName() + " crafted " + quantity + " items");
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Input Events
### PlayerMouseButtonEvent
{{< badge "Cancellable" >}}
Fired when a player presses a mouse button.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `PlayerRef` | Reference to the player |
| clientUseTime | `long` | Client-side timestamp |
| itemInHand | `Item` | The item being held |
| targetBlock | `Vector3i` | Block being targeted |
| targetEntity | `Entity` | Entity being targeted |
| screenPoint | `Vector2f` | Screen coordinates |
| mouseButton | `MouseButtonEvent` | Which button was pressed |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns entity store reference
- `getPlayerRefComponent()` - Returns PlayerRef component
- `getClientUseTime()` - Returns client timestamp
- `getItemInHand()` - Returns held item
- `getTargetBlock()` - Returns targeted block position
- `getTargetEntity()` - Returns targeted entity
- `getScreenPoint()` - Returns screen coordinates
- `getMouseButton()` - Returns mouse button info
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel the input
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerMouseButtonEvent.class, event -> {
Player player = event.getPlayer();
// Prevent input during cutscene
if (isInCutscene(player)) {
event.setCancelled(true);
return;
}
// Custom item interaction
Item item = event.getItemInHand();
if (item != null && item.getId().equals("magic_wand")) {
handleMagicWandUse(player, event.getTargetBlock());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerMouseMotionEvent
{{< badge "Cancellable" >}}
Fired when a player moves their mouse (camera rotation, aiming).
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `Ref<EntityStore>` | Entity store reference |
| player | `Player` | The player |
| clientUseTime | `long` | Client-side timestamp |
| itemInHand | `Item` | The item being held |
| targetBlock | `Vector3i` | Block being targeted |
| targetEntity | `Entity` | Entity being targeted |
| screenPoint | `Vector2f` | Screen coordinates |
| mouseMotion | `MouseMotionEvent` | Mouse motion data |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns entity store reference
- `getClientUseTime()` - Returns client timestamp
- `getItemInHand()` - Returns held item
- `getTargetBlock()` - Returns targeted block position
- `getTargetEntity()` - Returns targeted entity
- `getScreenPoint()` - Returns screen coordinates
- `getMouseMotion()` - Returns mouse motion data
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel the motion event
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerMouseMotionEvent.class, event -> {
Player player = event.getPlayer();
// Track what the player is looking at
Entity target = event.getTargetEntity();
if (target != null) {
updatePlayerTarget(player, target);
}
// Track camera movement for analytics
trackCameraMovement(player, event.getMouseMotion());
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Interaction Events
### PlayerInteractEvent
{{< badge "Deprecated" >}} {{< badge "Cancellable" >}}
Fired when a player interacts with the world.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< callout type="warning" >}}
**Deprecated:** This event is deprecated. Use more specific events like `UseBlockEvent`, `PlayerMouseButtonEvent`, or ECS interaction events instead.
{{< /callout >}}
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| actionType | `InteractionType` | Type of interaction |
| clientUseTime | `long` | Client timestamp |
| itemInHand | `ItemStack` | Item being used |
| targetBlock | `Vector3i` | Block being targeted |
| targetEntity | `Entity` | Entity being targeted |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getActionType()` - Returns interaction type
- `getClientUseTime()` - Returns client timestamp
- `getItemInHand()` - Returns item in hand
- `getTargetBlock()` - Returns targeted block
- `getTargetEntity()` - Returns targeted entity
- `getTargetRef()` - Returns target entity reference
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel interaction
{{< /tab >}}
{{< tab >}}
```java
// Deprecated - prefer UseBlockEvent or PlayerMouseButtonEvent
getEventRegistry().register(PlayerInteractEvent.class, event -> {
Player player = event.getPlayer();
InteractionType action = event.getActionType();
// Handle interaction
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Practical Examples
### Complete Welcome System
```java
public class WelcomePlugin extends JavaPlugin {
private final Set<UUID> firstJoinPlayers = new HashSet<>();
@Override
public void start() {
// Track setup phase
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
if (isFirstJoin(event.getUuid())) {
firstJoinPlayers.add(event.getUuid());
}
});
// Welcome when fully ready
getEventRegistry().register(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
if (firstJoinPlayers.remove(player.getUuid())) {
// First time player
player.sendMessage(Message.raw("Welcome to the server for the first time!"));
giveStarterKit(player);
} else {
// Returning player
player.sendMessage(Message.raw("Welcome back, " + player.getDisplayName() + "!"));
}
});
// Save on disconnect
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
savePlayerData(event.getPlayerRef().getUuid());
});
}
}
```
### Server Referral System
```java
public class ReferralPlugin extends JavaPlugin {
@Override
public void start() {
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
// Check if player came from referral
if (event.isReferralConnection()) {
byte[] data = event.getReferralData();
HostAddress source = event.getReferralSource();
getLogger().at(Level.INFO).log("Player referred from " + source.host);
}
// Redirect based on load balancing
String targetServer = getOptimalServer();
if (!isThisServer(targetServer)) {
event.referToServer(targetServer, 25565, createReferralData());
}
});
}
}
```
### Chat Filter System
```java
public class ChatFilterPlugin extends JavaPlugin {
private final Set<String> bannedWords = new HashSet<>();
private final Map<UUID, Integer> warnings = new HashMap<>();
@Override
public void start() {
loadBannedWords();
getEventRegistry().register(PlayerChatEvent.class, event -> {
PlayerRef sender = event.getSender();
String message = event.getContent().toLowerCase();
for (String banned : bannedWords) {
if (message.contains(banned)) {
event.setCancelled(true);
Player player = sender.getPlayer();
if (player != null) {
int count = warnings.merge(sender.getUuid(), 1, Integer::sum);
player.sendMessage(Message.raw("Warning " + count + "/3: Watch your language!"));
if (count >= 3) {
player.kick("Too many chat violations");
}
}
return;
}
}
});
}
}
```

View File

@@ -0,0 +1,675 @@
---
title: Player Events
type: docs
weight: 1
---
Events triggered by player actions and state changes.
## Connection Events
### PlayerSetupConnectEvent
{{< badge "Cancellable" >}}
Fired during player connection setup. Can be cancelled to prevent connection or redirect to another server.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| uuid | `UUID` | Player's unique identifier |
| username | `String` | Player's username |
| auth | `PlayerAuthentication` | Authentication information |
| referralData | `byte[]` | Data from referral (if redirected) |
| referralSource | `HostAddress` | Server that referred player |
{{< /tab >}}
{{< tab >}}
- `getUuid()` - Returns player UUID
- `getUsername()` - Returns player username
- `getAuth()` - Returns authentication data
- `getReferralData()` - Returns referral data (may be null)
- `isReferralConnection()` - Check if redirected from another server
- `getReferralSource()` - Returns source server address
- `referToServer(host, port)` - Redirect player to another server
- `referToServer(host, port, data)` - Redirect with data
- `getReason()` - Get disconnect reason message
- `setReason(String)` - Set disconnect reason message
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel connection
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
String username = event.getUsername();
UUID uuid = event.getUuid();
// Ban check
if (isBanned(uuid)) {
event.setCancelled(true);
event.setReason("You are banned from this server!");
return;
}
// Redirect to different server based on condition
if (shouldRedirect(uuid)) {
event.referToServer("lobby.example.com", 25565);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerConnectEvent
Fired when a player connects to the server. Use this to set the spawn world.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| holder | `Holder<EntityStore>` | Entity store holder |
| playerRef | `PlayerRef` | Reference to the connecting player |
| world | `World` | The world player will spawn in |
{{< /tab >}}
{{< tab >}}
- `getHolder()` - Returns the entity store holder
- `getPlayerRef()` - Returns the player reference
- `getPlayer()` - Returns the Player object (deprecated)
- `getWorld()` - Returns current spawn world (may be null)
- `setWorld(World)` - Set the world player will spawn in
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername());
// Set spawn world
World lobbyWorld = Universe.get().getWorld("lobby");
if (lobbyWorld != null) {
event.setWorld(lobbyWorld);
}
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
The `getPlayer()` method is deprecated. Prefer using `getPlayerRef()` or `getHolder()` to access player data.
{{< /callout >}}
---
### PlayerSetupDisconnectEvent
Fired when a player disconnects during the setup phase (before fully connecting).
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| uuid | `UUID` | Player's unique identifier |
| username | `String` | Player's username |
| auth | `PlayerAuthentication` | Authentication information |
| disconnectReason | `DisconnectReason` | Why the player disconnected |
{{< /tab >}}
{{< tab >}}
- `getUuid()` - Returns player UUID
- `getUsername()` - Returns player username
- `getAuth()` - Returns authentication data
- `getDisconnectReason()` - Returns disconnect reason
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerSetupDisconnectEvent.class, event -> {
String username = event.getUsername();
PacketHandler.DisconnectReason reason = event.getDisconnectReason();
getLogger().at(Level.INFO).log("Player " + username + " disconnected during setup: " + reason);
// Cleanup any pre-connection data
cleanupPendingData(event.getUuid());
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerReadyEvent
Fired when a player is fully ready and loaded into the game. This is the safe point to interact with the player.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `Ref<EntityStore>` | Entity store reference |
| player | `Player` | The player object |
| readyId | `int` | Ready event identifier |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns the entity store reference
- `getReadyId()` - Returns the ready event ID
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
// Player is fully loaded, safe to send complex data
player.sendMessage(Message.raw("Welcome to the server!"));
loadPlayerData(player);
sendWelcomeScreen(player);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerDisconnectEvent
Fired when a player disconnects from the server.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `PlayerRef` | Reference to the disconnecting player |
{{< /tab >}}
{{< tab >}}
- `getPlayerRef()` - Returns the player reference
- `getDisconnectReason()` - Returns why the player disconnected
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
savePlayerData(playerRef.getUuid());
getLogger().at(Level.INFO).log(playerRef.getUsername() + " has left the server");
// Check disconnect reason
PacketHandler.DisconnectReason reason = event.getDisconnectReason();
getLogger().at(Level.INFO).log("Reason: " + reason);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## World Events
### AddPlayerToWorldEvent
Fired when a player is added to a world.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| holder | `Holder<EntityStore>` | Entity store holder |
| world | `World` | The world being entered |
| broadcastJoinMessage | `boolean` | Whether to broadcast join message |
{{< /tab >}}
{{< tab >}}
- `getHolder()` - Returns the entity store holder
- `getWorld()` - Returns the world
- `shouldBroadcastJoinMessage()` - Check if join message will be sent
- `setBroadcastJoinMessage(boolean)` - Control join message broadcast
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(AddPlayerToWorldEvent.class, event -> {
World world = event.getWorld();
// Disable default join message for silent joins
if (isSilentJoin(event.getHolder())) {
event.setBroadcastJoinMessage(false);
}
applyWorldEffects(event.getHolder(), world);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### DrainPlayerFromWorldEvent
Fired when a player is removed from a world (before teleporting to another or disconnecting).
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| holder | `Holder<EntityStore>` | Entity store holder |
| world | `World` | The world being left |
| transform | `Transform` | Player's position/rotation |
{{< /tab >}}
{{< tab >}}
- `getHolder()` - Returns the entity store holder
- `getWorld()` - Returns the world being left
- `setWorld(World)` - Change destination world
- `getTransform()` - Returns player's transform
- `setTransform(Transform)` - Set spawn transform in new world
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(DrainPlayerFromWorldEvent.class, event -> {
World world = event.getWorld();
removeWorldEffects(event.getHolder());
saveWorldProgress(event.getHolder(), world);
// Optionally redirect to different world
World newWorld = Universe.get().getWorld("hub");
if (newWorld != null) {
event.setWorld(newWorld);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Chat Events
### PlayerChatEvent
{{< badge "Cancellable" >}} {{< badge "Async" >}}
Fired when a player sends a chat message. This event is asynchronous.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| sender | `PlayerRef` | The player sending the message |
| targets | `List<PlayerRef>` | Players who will receive the message |
| content | `String` | The message content |
| formatter | `Formatter` | Message formatting handler |
{{< /tab >}}
{{< tab >}}
- `getSender()` - Returns the sending player reference
- `setSender(PlayerRef)` - Change the sender
- `getContent()` - Returns the message content
- `setContent(String)` - Modify the message
- `getTargets()` - Returns message recipients
- `setTargets(List<PlayerRef>)` - Change recipients
- `getFormatter()` - Returns the message formatter
- `setFormatter(Formatter)` - Set custom formatter
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel the message
{{< /tab >}}
{{< tab >}}
```java
// PlayerChatEvent has String key and is async - use registerAsyncGlobal()
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
PlayerRef sender = event.getSender();
String message = event.getContent();
// Filter bad words
if (containsBadWord(message)) {
event.setCancelled(true);
// Use PlayerRef.sendMessage() directly
sender.sendMessage(Message.raw("Please don't use that word!"));
return event;
}
// Add prefix based on rank
String prefix = getPlayerPrefix(sender);
event.setContent(prefix + message);
// Custom formatter
event.setFormatter((playerRef, msg) ->
Message.translation("custom.chat.format")
.param("name", playerRef.getUsername())
.param("message", msg)
);
return event;
});
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="info" >}}
**Async Event:** This event runs asynchronously. Use `sender.getReference()` to check if the player is still online (returns null or invalid reference if disconnected).
{{< /callout >}}
---
## Crafting Events
### PlayerCraftEvent
{{< badge "Deprecated" >}}
Fired when a player crafts an item.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< callout type="warning" >}}
**Deprecated:** This event is marked for removal. Use `CraftRecipeEvent` from the ECS events instead.
{{< /callout >}}
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| ref | `Ref<EntityStore>` | Entity store reference |
| player | `Player` | The crafting player |
| craftedRecipe | `CraftingRecipe` | The recipe being crafted |
| quantity | `int` | Number of items crafted |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns entity store reference
- `getCraftedRecipe()` - Returns the crafting recipe
- `getQuantity()` - Returns number crafted
{{< /tab >}}
{{< tab >}}
```java
// Deprecated - prefer CraftRecipeEvent
getEventRegistry().register(PlayerCraftEvent.class, event -> {
Player player = event.getPlayer();
CraftingRecipe recipe = event.getCraftedRecipe();
int quantity = event.getQuantity();
getLogger().at(Level.INFO).log(player.getDisplayName() + " crafted " + quantity + " items");
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Input Events
### PlayerMouseButtonEvent
{{< badge "Cancellable" >}}
Fired when a player presses a mouse button.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `PlayerRef` | Reference to the player |
| clientUseTime | `long` | Client-side timestamp |
| itemInHand | `Item` | The item being held |
| targetBlock | `Vector3i` | Block being targeted |
| targetEntity | `Entity` | Entity being targeted |
| screenPoint | `Vector2f` | Screen coordinates |
| mouseButton | `MouseButtonEvent` | Which button was pressed |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns entity store reference
- `getPlayerRefComponent()` - Returns PlayerRef component
- `getClientUseTime()` - Returns client timestamp
- `getItemInHand()` - Returns held item
- `getTargetBlock()` - Returns targeted block position
- `getTargetEntity()` - Returns targeted entity
- `getScreenPoint()` - Returns screen coordinates
- `getMouseButton()` - Returns mouse button info
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel the input
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerMouseButtonEvent.class, event -> {
Player player = event.getPlayer();
// Prevent input during cutscene
if (isInCutscene(player)) {
event.setCancelled(true);
return;
}
// Custom item interaction
Item item = event.getItemInHand();
if (item != null && item.getId().equals("magic_wand")) {
handleMagicWandUse(player, event.getTargetBlock());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PlayerMouseMotionEvent
{{< badge "Cancellable" >}}
Fired when a player moves their mouse (camera rotation, aiming).
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| playerRef | `Ref<EntityStore>` | Entity store reference |
| player | `Player` | The player |
| clientUseTime | `long` | Client-side timestamp |
| itemInHand | `Item` | The item being held |
| targetBlock | `Vector3i` | Block being targeted |
| targetEntity | `Entity` | Entity being targeted |
| screenPoint | `Vector2f` | Screen coordinates |
| mouseMotion | `MouseMotionEvent` | Mouse motion data |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getPlayerRef()` - Returns entity store reference
- `getClientUseTime()` - Returns client timestamp
- `getItemInHand()` - Returns held item
- `getTargetBlock()` - Returns targeted block position
- `getTargetEntity()` - Returns targeted entity
- `getScreenPoint()` - Returns screen coordinates
- `getMouseMotion()` - Returns mouse motion data
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel the motion event
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PlayerMouseMotionEvent.class, event -> {
Player player = event.getPlayer();
// Track what the player is looking at
Entity target = event.getTargetEntity();
if (target != null) {
updatePlayerTarget(player, target);
}
// Track camera movement for analytics
trackCameraMovement(player, event.getMouseMotion());
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Interaction Events
### PlayerInteractEvent
{{< badge "Deprecated" >}} {{< badge "Cancellable" >}}
Fired when a player interacts with the world.
**Package:** `com.hypixel.hytale.server.core.event.events.player`
{{< callout type="warning" >}}
**Deprecated:** This event is deprecated. Use more specific events like `UseBlockEvent`, `PlayerMouseButtonEvent`, or ECS interaction events instead.
{{< /callout >}}
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| actionType | `InteractionType` | Type of interaction |
| clientUseTime | `long` | Client timestamp |
| itemInHand | `ItemStack` | Item being used |
| targetBlock | `Vector3i` | Block being targeted |
| targetEntity | `Entity` | Entity being targeted |
{{< /tab >}}
{{< tab >}}
- `getPlayer()` - Returns the Player object
- `getActionType()` - Returns interaction type
- `getClientUseTime()` - Returns client timestamp
- `getItemInHand()` - Returns item in hand
- `getTargetBlock()` - Returns targeted block
- `getTargetEntity()` - Returns targeted entity
- `getTargetRef()` - Returns target entity reference
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel interaction
{{< /tab >}}
{{< tab >}}
```java
// Deprecated - prefer UseBlockEvent or PlayerMouseButtonEvent
getEventRegistry().register(PlayerInteractEvent.class, event -> {
Player player = event.getPlayer();
InteractionType action = event.getActionType();
// Handle interaction
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Practical Examples
### Complete Welcome System
```java
public class WelcomePlugin extends JavaPlugin {
private final Set<UUID> firstJoinPlayers = new HashSet<>();
@Override
public void start() {
// Track setup phase
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
if (isFirstJoin(event.getUuid())) {
firstJoinPlayers.add(event.getUuid());
}
});
// Welcome when fully ready
getEventRegistry().register(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
if (firstJoinPlayers.remove(player.getUuid())) {
// First time player
player.sendMessage(Message.raw("Welcome to the server for the first time!"));
giveStarterKit(player);
} else {
// Returning player
player.sendMessage(Message.raw("Welcome back, " + player.getDisplayName() + "!"));
}
});
// Save on disconnect
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
savePlayerData(event.getPlayerRef().getUuid());
});
}
}
```
### Server Referral System
```java
public class ReferralPlugin extends JavaPlugin {
@Override
public void start() {
getEventRegistry().register(PlayerSetupConnectEvent.class, event -> {
// Check if player came from referral
if (event.isReferralConnection()) {
byte[] data = event.getReferralData();
HostAddress source = event.getReferralSource();
getLogger().at(Level.INFO).log("Player referred from " + source.host);
}
// Redirect based on load balancing
String targetServer = getOptimalServer();
if (!isThisServer(targetServer)) {
event.referToServer(targetServer, 25565, createReferralData());
}
});
}
}
```
### Chat Filter System
```java
public class ChatFilterPlugin extends JavaPlugin {
private final Set<String> bannedWords = new HashSet<>();
private final Map<UUID, Integer> warnings = new HashMap<>();
@Override
public void start() {
loadBannedWords();
getEventRegistry().register(PlayerChatEvent.class, event -> {
PlayerRef sender = event.getSender();
String message = event.getContent().toLowerCase();
for (String banned : bannedWords) {
if (message.contains(banned)) {
event.setCancelled(true);
Player player = sender.getPlayer();
if (player != null) {
int count = warnings.merge(sender.getUuid(), 1, Integer::sum);
player.sendMessage(Message.raw("Warning " + count + "/3: Watch your language!"));
if (count >= 3) {
player.kick("Too many chat violations");
}
}
return;
}
}
});
}
}
```

View File

@@ -0,0 +1,468 @@
---
title: Server Events
type: docs
weight: 6
---
Events related to server lifecycle, plugins, and system operations.
## Server Lifecycle Events
### BootEvent
Fired when the server finishes booting up. This is a marker event with no fields.
**Package:** `com.hypixel.hytale.server.core.event.events`
{{< tabs items="Fields,Example" >}}
{{< tab >}}
This event has no fields. It is a simple notification that boot has completed.
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(BootEvent.class, event -> {
getLogger().at(Level.INFO).log("Server boot complete!");
// Initialize post-boot features
initializeMetrics();
startBackgroundTasks();
openConnectionsToExternalServices();
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ShutdownEvent
Fired when the server is shutting down. This is a marker event with priority constants.
**Package:** `com.hypixel.hytale.server.core.event.events`
{{< tabs items="Constants,Example" >}}
{{< tab >}}
| Constant | Value | Description |
|----------|-------|-------------|
| `DISCONNECT_PLAYERS` | -48 | Priority for disconnecting players |
| `UNBIND_LISTENERS` | -40 | Priority for unbinding network listeners |
| `SHUTDOWN_WORLDS` | -32 | Priority for shutting down worlds |
Use these constants with `EventPriority` to order shutdown handlers.
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ShutdownEvent.class, event -> {
getLogger().at(Level.INFO).log("Server shutting down!");
// Perform cleanup
saveAllPlayerData();
closeExternalConnections();
flushMetrics();
});
// Use priority constants for ordering
getEventRegistry().register(
ShutdownEvent.DISCONNECT_PLAYERS,
ShutdownEvent.class,
event -> {
// Notify players before disconnect
Universe.get().getPlayers().forEach(player ->
player.sendMessage(Message.raw("Server shutting down!"))
);
}
);
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
**Important:** Keep shutdown handlers fast and synchronous. The server may force-terminate if handlers take too long.
{{< /callout >}}
---
## Plugin Events
### PluginSetupEvent
Fired when a plugin is being set up (before start).
**Package:** `com.hypixel.hytale.server.core.event.events.plugin`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| plugin | `JavaPlugin` | The plugin being set up |
| pluginInfo | `PluginInfo` | Plugin metadata |
{{< /tab >}}
{{< tab >}}
- `getPlugin()` - Returns the plugin instance
- `getPluginInfo()` - Returns plugin metadata
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PluginSetupEvent.class, event -> {
PluginInfo info = event.getPluginInfo();
getLogger().at(Level.INFO).log("Plugin setting up: " + info.getName() +
" v" + info.getVersion());
// Check for plugin dependencies
if (isDependencyPlugin(info.getName())) {
registerDependencyHooks(event.getPlugin());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PluginEnableEvent
Fired when a plugin is enabled.
**Package:** `com.hypixel.hytale.server.core.event.events.plugin`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| plugin | `JavaPlugin` | The enabled plugin |
{{< /tab >}}
{{< tab >}}
- `getPlugin()` - Returns the plugin instance
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PluginEnableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
getLogger().at(Level.INFO).log("Plugin enabled: " + plugin.getName());
// Hook into other plugins
if (plugin.getName().equals("Economy")) {
hookEconomyPlugin(plugin);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PluginDisableEvent
Fired when a plugin is disabled.
**Package:** `com.hypixel.hytale.server.core.event.events.plugin`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| plugin | `JavaPlugin` | The disabled plugin |
{{< /tab >}}
{{< tab >}}
- `getPlugin()` - Returns the plugin instance
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PluginDisableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
getLogger().at(Level.INFO).log("Plugin disabled: " + plugin.getName());
// Unhook from other plugins
if (plugin.getName().equals("Economy")) {
unhookEconomyPlugin();
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Tick Events
### ServerTickEvent
Fired every server tick (typically 20 times per second).
**Package:** `com.hypixel.hytale.server.core.event.events.server`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| tickNumber | `long` | Current tick number |
| deltaTime | `float` | Time since last tick (seconds) |
{{< /tab >}}
{{< tab >}}
- `getTickNumber()` - Returns current tick count
- `getDeltaTime()` - Returns delta time in seconds
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ServerTickEvent.class, event -> {
long tick = event.getTickNumber();
// Run every second (20 ticks)
if (tick % 20 == 0) {
updateScoreboards();
}
// Run every minute (1200 ticks)
if (tick % 1200 == 0) {
saveAutoSave();
cleanupExpiredData();
}
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
**Performance Critical:** This event fires 20 times per second. Keep handlers extremely lightweight. Use tick counting for periodic tasks instead of running on every tick.
{{< /callout >}}
---
## Command Events
### CommandExecuteEvent
{{< badge "Cancellable" >}}
Fired when a command is executed.
**Package:** `com.hypixel.hytale.server.core.event.events.command`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| sender | `CommandSender` | Who executed the command |
| command | `String` | Command name |
| args | `String[]` | Command arguments |
{{< /tab >}}
{{< tab >}}
- `getSender()` - Returns the command sender
- `getCommand()` - Returns command name
- `getArgs()` - Returns arguments array
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel command
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(CommandExecuteEvent.class, event -> {
CommandSender sender = event.getSender();
String command = event.getCommand();
// Log all commands
getLogger().at(Level.INFO).log(sender.getName() + " executed: /" + command +
" " + String.join(" ", event.getArgs()));
// Block certain commands for non-ops
if (command.equals("stop") && !sender.isOp()) {
event.setCancelled(true);
sender.sendMessage("You don't have permission!");
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Practical Examples
### Server Metrics Plugin
```java
public class MetricsPlugin extends JavaPlugin {
private long startTime;
private long tickCount = 0;
private long lastTickTime;
@Override
public void start() {
// Track server boot
getEventRegistry().register(BootEvent.class, event -> {
startTime = System.currentTimeMillis();
lastTickTime = startTime;
getLogger().at(Level.INFO).log("Server boot complete, tracking metrics...");
});
// Calculate TPS
getEventRegistry().register(ServerTickEvent.class, event -> {
tickCount++;
// Calculate TPS every second
if (tickCount % 20 == 0) {
long now = System.currentTimeMillis();
long elapsed = now - lastTickTime;
double tps = 20000.0 / elapsed;
lastTickTime = now;
if (tps < 18.0) {
getLogger().warning("Low TPS: " + String.format("%.2f", tps));
}
}
});
// Save metrics on shutdown
getEventRegistry().register(ShutdownEvent.class, event -> {
long uptime = System.currentTimeMillis() - startTime;
saveMetrics(uptime, tickCount);
});
}
}
```
### Plugin Dependency Manager
```java
public class DependencyPlugin extends JavaPlugin {
private final Map<String, JavaPlugin> loadedDependencies = new HashMap<>();
private final Set<String> requiredPlugins = Set.of("Economy", "Permissions");
@Override
public void start() {
// Track plugin loading
getEventRegistry().register(PluginEnableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
if (requiredPlugins.contains(plugin.getName())) {
loadedDependencies.put(plugin.getName(), plugin);
getLogger().at(Level.INFO).log("Dependency loaded: " + plugin.getName());
// Check if all dependencies are loaded
if (loadedDependencies.keySet().containsAll(requiredPlugins)) {
initializeDependentFeatures();
}
}
});
// Handle dependency unload
getEventRegistry().register(PluginDisableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
if (loadedDependencies.remove(plugin.getName()) != null) {
getLogger().warning("Dependency unloaded: " + plugin.getName());
disableDependentFeatures();
}
});
}
}
```
### Command Logging & Rate Limiting
```java
public class CommandSecurityPlugin extends JavaPlugin {
private final Map<UUID, List<Long>> commandHistory = new HashMap<>();
private static final int MAX_COMMANDS_PER_SECOND = 5;
@Override
public void start() {
getEventRegistry().register(CommandExecuteEvent.class, event -> {
CommandSender sender = event.getSender();
// Only rate limit players
if (!(sender instanceof Player)) return;
Player player = (Player) sender;
UUID uuid = player.getUuid();
// Get command history
List<Long> history = commandHistory.computeIfAbsent(
uuid, k -> new ArrayList<>()
);
long now = System.currentTimeMillis();
// Remove old entries (older than 1 second)
history.removeIf(time -> now - time > 1000);
// Check rate limit
if (history.size() >= MAX_COMMANDS_PER_SECOND) {
event.setCancelled(true);
player.sendMessage("Too many commands! Please slow down.");
getLogger().warning("Rate limited: " + player.getDisplayName());
return;
}
// Record this command
history.add(now);
// Log to file
logCommand(player, event.getCommand(), event.getArgs());
});
// Cleanup on disconnect
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
commandHistory.remove(event.getPlayer().getUuid());
});
}
}
```
### Scheduled Tasks Manager
```java
public class SchedulerPlugin extends JavaPlugin {
private final List<ScheduledTask> tasks = new ArrayList<>();
@Override
public void start() {
// Schedule various tasks
scheduleTask("autosave", 6000, this::autoSave); // Every 5 minutes
scheduleTask("cleanup", 72000, this::cleanup); // Every hour
scheduleTask("broadcast", 12000, this::broadcast); // Every 10 minutes
// Execute tasks based on tick
getEventRegistry().register(ServerTickEvent.class, event -> {
long tick = event.getTickNumber();
for (ScheduledTask task : tasks) {
if (tick % task.interval() == 0) {
try {
task.runnable().run();
} catch (Exception e) {
getLogger().error("Task failed: " + task.name(), e);
}
}
}
});
}
private void scheduleTask(String name, int intervalTicks, Runnable runnable) {
tasks.add(new ScheduledTask(name, intervalTicks, runnable));
}
record ScheduledTask(String name, int interval, Runnable runnable) {}
}
```
## Best Practices
{{< callout type="info" >}}
**Server Event Guidelines:**
- Keep `ShutdownEvent` handlers fast and reliable
- Use tick counting in `ServerTickEvent` for periodic tasks
- Clean up resources in `PluginDisableEvent`
- Initialize cross-plugin features in `PluginEnableEvent`
- Log important command executions for auditing
{{< /callout >}}
{{< callout type="error" >}}
**Critical:** Never perform blocking operations (I/O, network, database) directly in `ServerTickEvent`. Use async tasks instead.
{{< /callout >}}

View File

@@ -0,0 +1,468 @@
---
title: Événements Serveur
type: docs
weight: 6
---
Événements liés au cycle de vie du serveur, aux plugins et aux opérations système.
## Événements du Cycle de Vie du Serveur
### BootEvent
Déclenché quand le serveur finit de démarrer. C'est un événement marqueur sans champs.
**Package :** `com.hypixel.hytale.server.core.event.events`
{{< tabs items="Champs,Exemple" >}}
{{< tab >}}
Cet événement n'a pas de champs. C'est une simple notification que le démarrage est terminé.
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(BootEvent.class, event -> {
getLogger().at(Level.INFO).log("Démarrage du serveur terminé !");
// Initialiser les fonctionnalités post-démarrage
initializeMetrics();
startBackgroundTasks();
openConnectionsToExternalServices();
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ShutdownEvent
Déclenché quand le serveur s'arrête. C'est un événement marqueur avec des constantes de priorité.
**Package :** `com.hypixel.hytale.server.core.event.events`
{{< tabs items="Constantes,Exemple" >}}
{{< tab >}}
| Constante | Valeur | Description |
|-----------|--------|-------------|
| `DISCONNECT_PLAYERS` | -48 | Priorité pour déconnecter les joueurs |
| `UNBIND_LISTENERS` | -40 | Priorité pour délier les listeners réseau |
| `SHUTDOWN_WORLDS` | -32 | Priorité pour arrêter les mondes |
Utilisez ces constantes avec `EventPriority` pour ordonner les handlers d'arrêt.
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ShutdownEvent.class, event -> {
getLogger().at(Level.INFO).log("Arrêt du serveur !");
// Effectuer le nettoyage
saveAllPlayerData();
closeExternalConnections();
flushMetrics();
});
// Utiliser les constantes de priorité pour l'ordre
getEventRegistry().register(
ShutdownEvent.DISCONNECT_PLAYERS,
ShutdownEvent.class,
event -> {
// Notifier les joueurs avant la déconnexion
Universe.get().getPlayers().forEach(player ->
player.sendMessage(Message.raw("Le serveur s'arrête !"))
);
}
);
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
**Important :** Gardez les handlers d'arrêt rapides et synchrones. Le serveur peut forcer l'arrêt si les handlers prennent trop de temps.
{{< /callout >}}
---
## Événements de Plugin
### PluginSetupEvent
Déclenché quand un plugin est en cours de configuration (avant le démarrage).
**Package :** `com.hypixel.hytale.server.core.event.events.plugin`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| plugin | `JavaPlugin` | Le plugin en configuration |
| pluginInfo | `PluginInfo` | Métadonnées du plugin |
{{< /tab >}}
{{< tab >}}
- `getPlugin()` - Retourne l'instance du plugin
- `getPluginInfo()` - Retourne les métadonnées du plugin
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PluginSetupEvent.class, event -> {
PluginInfo info = event.getPluginInfo();
getLogger().at(Level.INFO).log("Configuration du plugin : " + info.getName() +
" v" + info.getVersion());
// Vérifier les dépendances du plugin
if (isDependencyPlugin(info.getName())) {
registerDependencyHooks(event.getPlugin());
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PluginEnableEvent
Déclenché quand un plugin est activé.
**Package :** `com.hypixel.hytale.server.core.event.events.plugin`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| plugin | `JavaPlugin` | Le plugin activé |
{{< /tab >}}
{{< tab >}}
- `getPlugin()` - Retourne l'instance du plugin
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PluginEnableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
getLogger().at(Level.INFO).log("Plugin activé : " + plugin.getName());
// Se connecter à d'autres plugins
if (plugin.getName().equals("Economy")) {
hookEconomyPlugin(plugin);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### PluginDisableEvent
Déclenché quand un plugin est désactivé.
**Package :** `com.hypixel.hytale.server.core.event.events.plugin`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| plugin | `JavaPlugin` | Le plugin désactivé |
{{< /tab >}}
{{< tab >}}
- `getPlugin()` - Retourne l'instance du plugin
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(PluginDisableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
getLogger().at(Level.INFO).log("Plugin désactivé : " + plugin.getName());
// Se déconnecter des autres plugins
if (plugin.getName().equals("Economy")) {
unhookEconomyPlugin();
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements de Tick
### ServerTickEvent
Déclenché à chaque tick serveur (typiquement 20 fois par seconde).
**Package :** `com.hypixel.hytale.server.core.event.events.server`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| tickNumber | `long` | Numéro du tick actuel |
| deltaTime | `float` | Temps depuis le dernier tick (secondes) |
{{< /tab >}}
{{< tab >}}
- `getTickNumber()` - Retourne le compte de ticks actuel
- `getDeltaTime()` - Retourne le delta time en secondes
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ServerTickEvent.class, event -> {
long tick = event.getTickNumber();
// Exécuter toutes les secondes (20 ticks)
if (tick % 20 == 0) {
updateScoreboards();
}
// Exécuter toutes les minutes (1200 ticks)
if (tick % 1200 == 0) {
saveAutoSave();
cleanupExpiredData();
}
});
```
{{< /tab >}}
{{< /tabs >}}
{{< callout type="warning" >}}
**Performance Critique :** Cet événement se déclenche 20 fois par seconde. Gardez les handlers extrêmement légers. Utilisez le comptage de ticks pour les tâches périodiques au lieu de s'exécuter à chaque tick.
{{< /callout >}}
---
## Événements de Commande
### CommandExecuteEvent
{{< badge "Annulable" >}}
Déclenché quand une commande est exécutée.
**Package :** `com.hypixel.hytale.server.core.event.events.command`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| sender | `CommandSender` | Qui a exécuté la commande |
| command | `String` | Nom de la commande |
| args | `String[]` | Arguments de la commande |
{{< /tab >}}
{{< tab >}}
- `getSender()` - Retourne l'expéditeur de la commande
- `getCommand()` - Retourne le nom de la commande
- `getArgs()` - Retourne le tableau d'arguments
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule la commande
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(CommandExecuteEvent.class, event -> {
CommandSender sender = event.getSender();
String command = event.getCommand();
// Logger toutes les commandes
getLogger().at(Level.INFO).log(sender.getName() + " a exécuté : /" + command +
" " + String.join(" ", event.getArgs()));
// Bloquer certaines commandes pour les non-ops
if (command.equals("stop") && !sender.isOp()) {
event.setCancelled(true);
sender.sendMessage("Vous n'avez pas la permission !");
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Exemples Pratiques
### Plugin de Métriques Serveur
```java
public class MetricsPlugin extends JavaPlugin {
private long startTime;
private long tickCount = 0;
private long lastTickTime;
@Override
public void start() {
// Suivre le démarrage du serveur
getEventRegistry().register(BootEvent.class, event -> {
startTime = System.currentTimeMillis();
lastTickTime = startTime;
getLogger().at(Level.INFO).log("Démarrage terminé, suivi des métriques...");
});
// Calculer les TPS
getEventRegistry().register(ServerTickEvent.class, event -> {
tickCount++;
// Calculer les TPS toutes les secondes
if (tickCount % 20 == 0) {
long now = System.currentTimeMillis();
long elapsed = now - lastTickTime;
double tps = 20000.0 / elapsed;
lastTickTime = now;
if (tps < 18.0) {
getLogger().warning("TPS faibles : " + String.format("%.2f", tps));
}
}
});
// Sauvegarder les métriques à l'arrêt
getEventRegistry().register(ShutdownEvent.class, event -> {
long uptime = System.currentTimeMillis() - startTime;
saveMetrics(uptime, tickCount);
});
}
}
```
### Gestionnaire de Dépendances de Plugins
```java
public class DependencyPlugin extends JavaPlugin {
private final Map<String, JavaPlugin> loadedDependencies = new HashMap<>();
private final Set<String> requiredPlugins = Set.of("Economy", "Permissions");
@Override
public void start() {
// Suivre le chargement des plugins
getEventRegistry().register(PluginEnableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
if (requiredPlugins.contains(plugin.getName())) {
loadedDependencies.put(plugin.getName(), plugin);
getLogger().at(Level.INFO).log("Dépendance chargée : " + plugin.getName());
// Vérifier si toutes les dépendances sont chargées
if (loadedDependencies.keySet().containsAll(requiredPlugins)) {
initializeDependentFeatures();
}
}
});
// Gérer le déchargement de dépendance
getEventRegistry().register(PluginDisableEvent.class, event -> {
JavaPlugin plugin = event.getPlugin();
if (loadedDependencies.remove(plugin.getName()) != null) {
getLogger().warning("Dépendance déchargée : " + plugin.getName());
disableDependentFeatures();
}
});
}
}
```
### Journalisation & Limitation de Débit des Commandes
```java
public class CommandSecurityPlugin extends JavaPlugin {
private final Map<UUID, List<Long>> commandHistory = new HashMap<>();
private static final int MAX_COMMANDS_PER_SECOND = 5;
@Override
public void start() {
getEventRegistry().register(CommandExecuteEvent.class, event -> {
CommandSender sender = event.getSender();
// Ne limiter que les joueurs
if (!(sender instanceof Player)) return;
Player player = (Player) sender;
UUID uuid = player.getUuid();
// Obtenir l'historique des commandes
List<Long> history = commandHistory.computeIfAbsent(
uuid, k -> new ArrayList<>()
);
long now = System.currentTimeMillis();
// Supprimer les anciennes entrées (plus d'1 seconde)
history.removeIf(time -> now - time > 1000);
// Vérifier la limite de débit
if (history.size() >= MAX_COMMANDS_PER_SECOND) {
event.setCancelled(true);
player.sendMessage("Trop de commandes ! Veuillez ralentir.");
getLogger().warning("Limité : " + player.getDisplayName());
return;
}
// Enregistrer cette commande
history.add(now);
// Logger dans un fichier
logCommand(player, event.getCommand(), event.getArgs());
});
// Nettoyer à la déconnexion
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
commandHistory.remove(event.getPlayer().getUuid());
});
}
}
```
### Gestionnaire de Tâches Planifiées
```java
public class SchedulerPlugin extends JavaPlugin {
private final List<ScheduledTask> tasks = new ArrayList<>();
@Override
public void start() {
// Planifier diverses tâches
scheduleTask("autosave", 6000, this::autoSave); // Toutes les 5 minutes
scheduleTask("cleanup", 72000, this::cleanup); // Toutes les heures
scheduleTask("broadcast", 12000, this::broadcast); // Toutes les 10 minutes
// Exécuter les tâches selon le tick
getEventRegistry().register(ServerTickEvent.class, event -> {
long tick = event.getTickNumber();
for (ScheduledTask task : tasks) {
if (tick % task.interval() == 0) {
try {
task.runnable().run();
} catch (Exception e) {
getLogger().error("Tâche échouée : " + task.name(), e);
}
}
}
});
}
private void scheduleTask(String name, int intervalTicks, Runnable runnable) {
tasks.add(new ScheduledTask(name, intervalTicks, runnable));
}
record ScheduledTask(String name, int interval, Runnable runnable) {}
}
```
## Bonnes Pratiques
{{< callout type="info" >}}
**Directives pour les Événements Serveur :**
- Gardez les handlers de `ShutdownEvent` rapides et fiables
- Utilisez le comptage de ticks dans `ServerTickEvent` pour les tâches périodiques
- Nettoyez les ressources dans `PluginDisableEvent`
- Initialisez les fonctionnalités inter-plugins dans `PluginEnableEvent`
- Loggez les exécutions de commandes importantes pour l'audit
{{< /callout >}}
{{< callout type="error" >}}
**Critique :** Ne jamais effectuer d'opérations bloquantes (I/O, réseau, base de données) directement dans `ServerTickEvent`. Utilisez des tâches asynchrones à la place.
{{< /callout >}}

View File

@@ -0,0 +1,496 @@
---
title: World Events
type: docs
weight: 5
---
Events related to world management, chunks, and environmental changes.
## World Lifecycle Events
### AddWorldEvent
{{< badge "Cancellable" >}}
Fired when a world is being added to the server.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world being added |
| universe | `Universe` | The parent universe |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getUniverse()` - Returns the parent universe
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel world addition
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(AddWorldEvent.class, event -> {
World world = event.getWorld();
Universe universe = event.getUniverse();
getLogger().at(Level.INFO).log("World added: " + world.getName() + " to " + universe.getName());
// Prevent certain world types from loading
if (world.getName().contains("test") && !isDevMode()) {
event.setCancelled(true);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### RemoveWorldEvent
{{< badge "Cancellable" >}}
Fired when a world is being removed from the server.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world being removed |
| reason | `RemovalReason` | Why the world is being removed |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getReason()` - Returns removal reason
- `isCancelled()` - Check if cancelled
- `setCancelled(boolean)` - Cancel world removal
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(RemoveWorldEvent.class, event -> {
World world = event.getWorld();
// Prevent removal of main world
if (world.getName().equals("main")) {
event.setCancelled(true);
getLogger().warning("Cannot remove main world!");
return;
}
// Save data before world is removed
saveWorldData(world);
notifyPlayersInWorld(world, "World is being unloaded!");
});
```
{{< /tab >}}
{{< /tabs >}}
---
### StartWorldEvent
Fired when a world starts (finishes initialization).
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world that started |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(StartWorldEvent.class, event -> {
World world = event.getWorld();
getLogger().at(Level.INFO).log("World started: " + world.getName());
// Initialize world-specific features
initializeWorldBorders(world);
spawnWorldBoss(world);
startWeatherCycle(world);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### AllWorldsLoadedEvent
Fired when all worlds have finished loading on server startup.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| worlds | `List<World>` | All loaded worlds |
{{< /tab >}}
{{< tab >}}
- `getWorlds()` - Returns list of all loaded worlds
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(AllWorldsLoadedEvent.class, event -> {
List<World> worlds = event.getWorlds();
getLogger().at(Level.INFO).log("All " + worlds.size() + " worlds loaded!");
// Initialize cross-world features
initializePortalNetwork(worlds);
syncWorldTimes(worlds);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Chunk Events
### ChunkPreLoadProcessEvent
Fired before a chunk begins loading.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world |
| chunkX | `int` | Chunk X coordinate |
| chunkZ | `int` | Chunk Z coordinate |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getChunkX()` - Returns chunk X coordinate
- `getChunkZ()` - Returns chunk Z coordinate
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkPreLoadProcessEvent.class, event -> {
int x = event.getChunkX();
int z = event.getChunkZ();
// Pre-load adjacent chunk data for seamless loading
prepareAdjacentChunkData(event.getWorld(), x, z);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChunkLoadEvent
Fired when a chunk finishes loading.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world |
| chunk | `Chunk` | The loaded chunk |
| isNewChunk | `boolean` | Whether this is a newly generated chunk |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getChunk()` - Returns the chunk
- `isNewChunk()` - Returns true if newly generated
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkLoadEvent.class, event -> {
Chunk chunk = event.getChunk();
World world = event.getWorld();
if (event.isNewChunk()) {
// Add custom structures to new chunks
generateCustomStructures(chunk);
populateCustomOres(chunk);
}
// Restore entities from storage
loadChunkEntities(world, chunk);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChunkUnloadEvent
Fired when a chunk is about to be unloaded.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world |
| chunk | `Chunk` | The chunk being unloaded |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getChunk()` - Returns the chunk
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkUnloadEvent.class, event -> {
Chunk chunk = event.getChunk();
World world = event.getWorld();
// Save custom chunk data
saveChunkEntities(world, chunk);
saveChunkMetadata(chunk);
// Clean up chunk-specific resources
cleanupChunkParticles(chunk);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChunkSaveEvent
Fired when a chunk is being saved.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world |
| chunk | `Chunk` | The chunk being saved |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getChunk()` - Returns the chunk
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkSaveEvent.class, event -> {
Chunk chunk = event.getChunk();
// Save additional custom data alongside chunk
saveCustomChunkData(chunk);
getLogger().debug("Chunk saved: " + chunk.getX() + ", " + chunk.getZ());
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Environmental Events
### MoonPhaseChangeEvent
Fired when the moon phase changes.
**Package:** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Fields,Methods,Example" >}}
{{< tab >}}
| Field | Type | Description |
|-------|------|-------------|
| world | `World` | The world |
| previousPhase | `MoonPhase` | Previous moon phase |
| newPhase | `MoonPhase` | New moon phase |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Returns the world
- `getPreviousPhase()` - Returns previous phase
- `getNewPhase()` - Returns new phase
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(MoonPhaseChangeEvent.class, event -> {
World world = event.getWorld();
MoonPhase phase = event.getNewPhase();
getLogger().at(Level.INFO).log("Moon phase changed to: " + phase);
// Full moon special events
if (phase == MoonPhase.FULL) {
increaseHostileMobSpawns(world);
enableWerewolfTransformations(world);
}
// New moon darkness events
if (phase == MoonPhase.NEW) {
spawnDarkCreatures(world);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Practical Examples
### World Manager Plugin
```java
public class WorldManagerPlugin extends JavaPlugin {
private final Map<String, WorldConfig> worldConfigs = new HashMap<>();
@Override
public void start() {
// Track world additions
getEventRegistry().register(AddWorldEvent.class, event -> {
World world = event.getWorld();
WorldConfig config = loadWorldConfig(world.getName());
worldConfigs.put(world.getName(), config);
// Apply world-specific settings
applyWorldConfig(world, config);
});
// Handle world removal
getEventRegistry().register(RemoveWorldEvent.class, event -> {
World world = event.getWorld();
WorldConfig config = worldConfigs.remove(world.getName());
if (config != null) {
saveWorldConfig(world.getName(), config);
}
});
// Initialize when all worlds ready
getEventRegistry().register(AllWorldsLoadedEvent.class, event -> {
getLogger().at(Level.INFO).log("Initializing world manager with " +
event.getWorlds().size() + " worlds");
initializeCrossWorldFeatures();
});
}
}
```
### Chunk Protection System
```java
public class ChunkProtectionPlugin extends JavaPlugin {
private final Set<ChunkCoord> protectedChunks = new HashSet<>();
@Override
public void start() {
// Load protected chunks when chunk loads
getEventRegistry().register(ChunkLoadEvent.class, event -> {
Chunk chunk = event.getChunk();
ChunkCoord coord = new ChunkCoord(
event.getWorld().getName(),
chunk.getX(),
chunk.getZ()
);
if (isChunkProtected(coord)) {
protectedChunks.add(coord);
// Apply chunk-level protection
markChunkAsProtected(chunk);
}
});
// Save protection status on unload
getEventRegistry().register(ChunkUnloadEvent.class, event -> {
Chunk chunk = event.getChunk();
ChunkCoord coord = new ChunkCoord(
event.getWorld().getName(),
chunk.getX(),
chunk.getZ()
);
if (protectedChunks.contains(coord)) {
saveProtectionStatus(coord);
}
});
}
public void protectChunk(World world, int chunkX, int chunkZ) {
ChunkCoord coord = new ChunkCoord(world.getName(), chunkX, chunkZ);
protectedChunks.add(coord);
saveProtectionStatus(coord);
}
}
```
### Dynamic World Events
```java
public class DynamicWorldPlugin extends JavaPlugin {
@Override
public void start() {
// Moon phase effects
getEventRegistry().register(MoonPhaseChangeEvent.class, event -> {
World world = event.getWorld();
MoonPhase phase = event.getNewPhase();
switch (phase) {
case FULL:
broadcastToWorld(world, "The full moon rises...");
applyMoonEffect(world, "mob_spawn_increase", 2.0);
break;
case NEW:
broadcastToWorld(world, "Darkness falls...");
applyMoonEffect(world, "visibility_decrease", 0.5);
break;
default:
clearMoonEffects(world);
}
});
// Custom chunk generation
getEventRegistry().register(ChunkLoadEvent.class, event -> {
if (event.isNewChunk()) {
Chunk chunk = event.getChunk();
// Add mystery locations
if (random.nextFloat() < 0.01) { // 1% chance
generateMysteryStructure(chunk);
}
// Add resource nodes
populateResourceNodes(chunk, event.getWorld());
}
});
}
}
```
## Best Practices
{{< callout type="info" >}}
**World Event Guidelines:**
- Use `AllWorldsLoadedEvent` for cross-world initialization
- Save important data in `ChunkUnloadEvent` and `RemoveWorldEvent`
- Use `isNewChunk()` to avoid regenerating content in existing chunks
- Be careful with cancelling `AddWorldEvent` and `RemoveWorldEvent`
- Consider memory usage when storing per-chunk data
{{< /callout >}}
{{< callout type="warning" >}}
**Performance Note:** Chunk events can fire frequently during player movement. Keep handlers lightweight and avoid blocking operations.
{{< /callout >}}

View File

@@ -0,0 +1,496 @@
---
title: Événements Monde
type: docs
weight: 5
---
Événements liés à la gestion des mondes, chunks et changements environnementaux.
## Événements du Cycle de Vie des Mondes
### AddWorldEvent
{{< badge "Annulable" >}}
Déclenché quand un monde est en train d'être ajouté au serveur.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde ajouté |
| universe | `Universe` | L'univers parent |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getUniverse()` - Retourne l'univers parent
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule l'ajout du monde
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(AddWorldEvent.class, event -> {
World world = event.getWorld();
Universe universe = event.getUniverse();
getLogger().at(Level.INFO).log("Monde ajouté : " + world.getName() + " à " + universe.getName());
// Empêcher certains types de mondes de se charger
if (world.getName().contains("test") && !isDevMode()) {
event.setCancelled(true);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
### RemoveWorldEvent
{{< badge "Annulable" >}}
Déclenché quand un monde est en train d'être retiré du serveur.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde retiré |
| reason | `RemovalReason` | Raison du retrait |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getReason()` - Retourne la raison du retrait
- `isCancelled()` - Vérifie si annulé
- `setCancelled(boolean)` - Annule le retrait du monde
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(RemoveWorldEvent.class, event -> {
World world = event.getWorld();
// Empêcher le retrait du monde principal
if (world.getName().equals("main")) {
event.setCancelled(true);
getLogger().warning("Impossible de retirer le monde principal !");
return;
}
// Sauvegarder les données avant que le monde soit retiré
saveWorldData(world);
notifyPlayersInWorld(world, "Le monde est en cours de déchargement !");
});
```
{{< /tab >}}
{{< /tabs >}}
---
### StartWorldEvent
Déclenché quand un monde démarre (termine son initialisation).
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde qui a démarré |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(StartWorldEvent.class, event -> {
World world = event.getWorld();
getLogger().at(Level.INFO).log("Monde démarré : " + world.getName());
// Initialiser les fonctionnalités spécifiques au monde
initializeWorldBorders(world);
spawnWorldBoss(world);
startWeatherCycle(world);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### AllWorldsLoadedEvent
Déclenché quand tous les mondes ont fini de charger au démarrage du serveur.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| worlds | `List<World>` | Tous les mondes chargés |
{{< /tab >}}
{{< tab >}}
- `getWorlds()` - Retourne la liste de tous les mondes chargés
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(AllWorldsLoadedEvent.class, event -> {
List<World> worlds = event.getWorlds();
getLogger().at(Level.INFO).log("Tous les " + worlds.size() + " mondes chargés !");
// Initialiser les fonctionnalités inter-mondes
initializePortalNetwork(worlds);
syncWorldTimes(worlds);
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements de Chunk
### ChunkPreLoadProcessEvent
Déclenché avant qu'un chunk commence à charger.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde |
| chunkX | `int` | Coordonnée X du chunk |
| chunkZ | `int` | Coordonnée Z du chunk |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getChunkX()` - Retourne la coordonnée X du chunk
- `getChunkZ()` - Retourne la coordonnée Z du chunk
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkPreLoadProcessEvent.class, event -> {
int x = event.getChunkX();
int z = event.getChunkZ();
// Pré-charger les données des chunks adjacents pour un chargement fluide
prepareAdjacentChunkData(event.getWorld(), x, z);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChunkLoadEvent
Déclenché quand un chunk finit de charger.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde |
| chunk | `Chunk` | Le chunk chargé |
| isNewChunk | `boolean` | Si c'est un chunk nouvellement généré |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getChunk()` - Retourne le chunk
- `isNewChunk()` - Retourne vrai si nouvellement généré
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkLoadEvent.class, event -> {
Chunk chunk = event.getChunk();
World world = event.getWorld();
if (event.isNewChunk()) {
// Ajouter des structures personnalisées aux nouveaux chunks
generateCustomStructures(chunk);
populateCustomOres(chunk);
}
// Restaurer les entités depuis le stockage
loadChunkEntities(world, chunk);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChunkUnloadEvent
Déclenché quand un chunk est sur le point d'être déchargé.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde |
| chunk | `Chunk` | Le chunk déchargé |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getChunk()` - Retourne le chunk
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkUnloadEvent.class, event -> {
Chunk chunk = event.getChunk();
World world = event.getWorld();
// Sauvegarder les données personnalisées du chunk
saveChunkEntities(world, chunk);
saveChunkMetadata(chunk);
// Nettoyer les ressources spécifiques au chunk
cleanupChunkParticles(chunk);
});
```
{{< /tab >}}
{{< /tabs >}}
---
### ChunkSaveEvent
Déclenché quand un chunk est en cours de sauvegarde.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde |
| chunk | `Chunk` | Le chunk sauvegardé |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getChunk()` - Retourne le chunk
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(ChunkSaveEvent.class, event -> {
Chunk chunk = event.getChunk();
// Sauvegarder des données personnalisées supplémentaires avec le chunk
saveCustomChunkData(chunk);
getLogger().debug("Chunk sauvegardé : " + chunk.getX() + ", " + chunk.getZ());
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Événements Environnementaux
### MoonPhaseChangeEvent
Déclenché quand la phase lunaire change.
**Package :** `com.hypixel.hytale.server.core.event.events.world`
{{< tabs items="Champs,Méthodes,Exemple" >}}
{{< tab >}}
| Champ | Type | Description |
|-------|------|-------------|
| world | `World` | Le monde |
| previousPhase | `MoonPhase` | Phase lunaire précédente |
| newPhase | `MoonPhase` | Nouvelle phase lunaire |
{{< /tab >}}
{{< tab >}}
- `getWorld()` - Retourne le monde
- `getPreviousPhase()` - Retourne la phase précédente
- `getNewPhase()` - Retourne la nouvelle phase
{{< /tab >}}
{{< tab >}}
```java
getEventRegistry().register(MoonPhaseChangeEvent.class, event -> {
World world = event.getWorld();
MoonPhase phase = event.getNewPhase();
getLogger().at(Level.INFO).log("Phase lunaire changée en : " + phase);
// Événements spéciaux pleine lune
if (phase == MoonPhase.FULL) {
increaseHostileMobSpawns(world);
enableWerewolfTransformations(world);
}
// Événements de ténèbres nouvelle lune
if (phase == MoonPhase.NEW) {
spawnDarkCreatures(world);
}
});
```
{{< /tab >}}
{{< /tabs >}}
---
## Exemples Pratiques
### Plugin Gestionnaire de Mondes
```java
public class WorldManagerPlugin extends JavaPlugin {
private final Map<String, WorldConfig> worldConfigs = new HashMap<>();
@Override
public void start() {
// Suivre les ajouts de mondes
getEventRegistry().register(AddWorldEvent.class, event -> {
World world = event.getWorld();
WorldConfig config = loadWorldConfig(world.getName());
worldConfigs.put(world.getName(), config);
// Appliquer les paramètres spécifiques au monde
applyWorldConfig(world, config);
});
// Gérer le retrait de monde
getEventRegistry().register(RemoveWorldEvent.class, event -> {
World world = event.getWorld();
WorldConfig config = worldConfigs.remove(world.getName());
if (config != null) {
saveWorldConfig(world.getName(), config);
}
});
// Initialiser quand tous les mondes sont prêts
getEventRegistry().register(AllWorldsLoadedEvent.class, event -> {
getLogger().at(Level.INFO).log("Initialisation du gestionnaire de mondes avec " +
event.getWorlds().size() + " mondes");
initializeCrossWorldFeatures();
});
}
}
```
### Système de Protection de Chunks
```java
public class ChunkProtectionPlugin extends JavaPlugin {
private final Set<ChunkCoord> protectedChunks = new HashSet<>();
@Override
public void start() {
// Charger les chunks protégés quand le chunk charge
getEventRegistry().register(ChunkLoadEvent.class, event -> {
Chunk chunk = event.getChunk();
ChunkCoord coord = new ChunkCoord(
event.getWorld().getName(),
chunk.getX(),
chunk.getZ()
);
if (isChunkProtected(coord)) {
protectedChunks.add(coord);
// Appliquer la protection au niveau du chunk
markChunkAsProtected(chunk);
}
});
// Sauvegarder le statut de protection au déchargement
getEventRegistry().register(ChunkUnloadEvent.class, event -> {
Chunk chunk = event.getChunk();
ChunkCoord coord = new ChunkCoord(
event.getWorld().getName(),
chunk.getX(),
chunk.getZ()
);
if (protectedChunks.contains(coord)) {
saveProtectionStatus(coord);
}
});
}
public void protectChunk(World world, int chunkX, int chunkZ) {
ChunkCoord coord = new ChunkCoord(world.getName(), chunkX, chunkZ);
protectedChunks.add(coord);
saveProtectionStatus(coord);
}
}
```
### Événements de Monde Dynamiques
```java
public class DynamicWorldPlugin extends JavaPlugin {
@Override
public void start() {
// Effets de phase lunaire
getEventRegistry().register(MoonPhaseChangeEvent.class, event -> {
World world = event.getWorld();
MoonPhase phase = event.getNewPhase();
switch (phase) {
case FULL:
broadcastToWorld(world, "La pleine lune se lève...");
applyMoonEffect(world, "mob_spawn_increase", 2.0);
break;
case NEW:
broadcastToWorld(world, "Les ténèbres tombent...");
applyMoonEffect(world, "visibility_decrease", 0.5);
break;
default:
clearMoonEffects(world);
}
});
// Génération de chunk personnalisée
getEventRegistry().register(ChunkLoadEvent.class, event -> {
if (event.isNewChunk()) {
Chunk chunk = event.getChunk();
// Ajouter des emplacements mystères
if (random.nextFloat() < 0.01) { // 1% de chance
generateMysteryStructure(chunk);
}
// Ajouter des nœuds de ressources
populateResourceNodes(chunk, event.getWorld());
}
});
}
}
```
## Bonnes Pratiques
{{< callout type="info" >}}
**Directives pour les Événements de Monde :**
- Utiliser `AllWorldsLoadedEvent` pour l'initialisation inter-mondes
- Sauvegarder les données importantes dans `ChunkUnloadEvent` et `RemoveWorldEvent`
- Utiliser `isNewChunk()` pour éviter de régénérer du contenu dans les chunks existants
- Être prudent en annulant `AddWorldEvent` et `RemoveWorldEvent`
- Considérer l'utilisation mémoire lors du stockage de données par chunk
{{< /callout >}}
{{< callout type="warning" >}}
**Note de Performance :** Les événements de chunk peuvent se déclencher fréquemment pendant le mouvement des joueurs. Gardez les handlers légers et évitez les opérations bloquantes.
{{< /callout >}}

View File

@@ -0,0 +1,217 @@
---
title: Event System
type: docs
weight: 1
---
Hytale uses an event-driven architecture where your plugin can subscribe to and react to game events.
## The EventBus
The `EventBus` is the central hub for all events. It manages event registration and dispatching.
## Registering Event Listeners
Use the `EventRegistry` from your plugin to register listeners:
```java
@Override
public void start() {
// Lambda syntax - PlayerReadyEvent fires when player is fully loaded
// Note: PlayerReadyEvent has a String key, so use registerGlobal()
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
getLogger().at(Level.INFO).log("Player ready: " + player.getDisplayName());
});
// Method reference
getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect);
}
private void onPlayerDisconnect(PlayerDisconnectEvent event) {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log("Player left: " + playerRef.getUsername());
}
```
## Registration Methods
### Basic Registration
```java
// Register with default priority (NORMAL)
getEventRegistry().register(PlayerConnectEvent.class, event -> {
// Handle event
});
```
{{< callout type="warning" >}}
**Important:** The simple `register(Class, Consumer)` method only works for events with a `Void` key type (like `PlayerConnectEvent`). For keyed events (like `PlayerReadyEvent` which has a `String` key), you must use `registerGlobal()` instead. Check the event's type parameter to determine which method to use.
{{< /callout >}}
### With Priority
```java
// Register with specific priority
getEventRegistry().register(EventPriority.EARLY, PlayerConnectEvent.class, event -> {
// This runs before NORMAL priority handlers
});
```
### With Key
Some events support key-based registration for filtering:
```java
// Only listen to events for a specific key
getEventRegistry().register(SomeKeyedEvent.class, myKey, event -> {
// Only called when event key matches myKey
});
```
### Global Registration
Listen to all events of a type, regardless of key:
```java
getEventRegistry().registerGlobal(KeyedEvent.class, event -> {
// Called for all instances of this event
});
```
### Unhandled Registration
Handle events that weren't processed by any key-specific listener:
```java
getEventRegistry().registerUnhandled(KeyedEvent.class, event -> {
// Called when no specific handler matched
});
```
### Async Global and Unhandled
For async events with global or unhandled registration:
```java
// Async global - all keys
getEventRegistry().registerAsyncGlobal(AsyncKeyedEvent.class, future ->
future.thenApply(event -> {
// Process async
return event;
})
);
// Async unhandled - unmatched keys
getEventRegistry().registerAsyncUnhandled(AsyncKeyedEvent.class, future ->
future.thenApply(event -> {
// Handle unmatched
return event;
})
);
```
## EventRegistration
Registration methods return an `EventRegistration` object:
```java
EventRegistration<?, PlayerConnectEvent> registration =
getEventRegistry().register(PlayerConnectEvent.class, this::onConnect);
// Later, you can unregister manually if needed
registration.unregister();
```
{{< callout type="info" >}}
Event registrations are automatically cleaned up when your plugin shuts down. Manual unregistration is rarely needed.
{{< /callout >}}
## Event Interface Hierarchy
Events implement different interfaces based on their capabilities:
| Interface | Description |
|-----------|-------------|
| `IBaseEvent<K>` | Base interface for all events |
| `IEvent<K>` | Synchronous events |
| `IAsyncEvent<K>` | Asynchronous events |
| `ICancellable` | Events that can be cancelled |
| `ICancellableEcsEvent` | ECS events that can be cancelled |
## Getting Registered Events
Query which events are registered:
```java
// Get all registered event classes
Set<Class<? extends IBaseEvent<?>>> eventClasses =
HytaleServer.get().getEventBus().getRegisteredEventClasses();
// Get registered event names
Set<String> eventNames =
HytaleServer.get().getEventBus().getRegisteredEventClassNames();
// Get a specific registry by name
EventBusRegistry<?, ?, ?> registry =
HytaleServer.get().getEventBus().getRegistry("PlayerConnectEvent");
```
## Complete Example
```java
public class WelcomePlugin extends JavaPlugin {
public WelcomePlugin(JavaPluginInit init) {
super(init);
}
@Override
public void start() {
// Handle player connection
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername());
// You can set the world the player spawns in
World lobbyWorld = Universe.get().getWorld("lobby");
if (lobbyWorld != null) {
event.setWorld(lobbyWorld);
}
});
// Welcome message when fully ready
// Note: PlayerReadyEvent has a String key, so use registerGlobal()
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
player.sendMessage(Message.raw("Welcome to the server, " + player.getDisplayName() + "!"));
});
// Goodbye message on disconnect
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log(playerRef.getUsername() + " has left the server");
});
}
}
```
## Player Event Lifecycle
Understanding when each event fires:
```
PlayerSetupConnectEvent → Can cancel/redirect connection
PlayerConnectEvent → Player connecting, set spawn world
AddPlayerToWorldEvent → Player added to world
PlayerReadyEvent → Player fully loaded, safe to interact
(gameplay events)
DrainPlayerFromWorldEvent → Player leaving world
PlayerDisconnectEvent → Player disconnected
```

View File

@@ -0,0 +1,217 @@
---
title: Event System
type: docs
weight: 1
---
Hytale uses an event-driven architecture where your plugin can subscribe to and react to game events.
## The EventBus
The `EventBus` is the central hub for all events. It manages event registration and dispatching.
## Registering Event Listeners
Use the `EventRegistry` from your plugin to register listeners:
```java
@Override
public void start() {
// Lambda syntax - PlayerReadyEvent fires when player is fully loaded
// Note: PlayerReadyEvent has a String key, so use registerGlobal()
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
getLogger().at(Level.INFO).log("Player ready: " + player.getDisplayName());
});
// Method reference
getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect);
}
private void onPlayerDisconnect(PlayerDisconnectEvent event) {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log("Player left: " + playerRef.getUsername());
}
```
## Registration Methods
### Basic Registration
```java
// Register with default priority (NORMAL)
getEventRegistry().register(PlayerConnectEvent.class, event -> {
// Handle event
});
```
{{< callout type="warning" >}}
**Important:** The simple `register(Class, Consumer)` method only works for events with a `Void` key type (like `PlayerConnectEvent`). For keyed events (like `PlayerReadyEvent` which has a `String` key), you must use `registerGlobal()` instead. Check the event's type parameter to determine which method to use.
{{< /callout >}}
### With Priority
```java
// Register with specific priority
getEventRegistry().register(EventPriority.EARLY, PlayerConnectEvent.class, event -> {
// This runs before NORMAL priority handlers
});
```
### With Key
Some events support key-based registration for filtering:
```java
// Only listen to events for a specific key
getEventRegistry().register(SomeKeyedEvent.class, myKey, event -> {
// Only called when event key matches myKey
});
```
### Global Registration
Listen to all events of a type, regardless of key:
```java
getEventRegistry().registerGlobal(KeyedEvent.class, event -> {
// Called for all instances of this event
});
```
### Unhandled Registration
Handle events that weren't processed by any key-specific listener:
```java
getEventRegistry().registerUnhandled(KeyedEvent.class, event -> {
// Called when no specific handler matched
});
```
### Async Global and Unhandled
For async events with global or unhandled registration:
```java
// Async global - all keys
getEventRegistry().registerAsyncGlobal(AsyncKeyedEvent.class, future ->
future.thenApply(event -> {
// Process async
return event;
})
);
// Async unhandled - unmatched keys
getEventRegistry().registerAsyncUnhandled(AsyncKeyedEvent.class, future ->
future.thenApply(event -> {
// Handle unmatched
return event;
})
);
```
## EventRegistration
Registration methods return an `EventRegistration` object:
```java
EventRegistration<?, PlayerConnectEvent> registration =
getEventRegistry().register(PlayerConnectEvent.class, this::onConnect);
// Later, you can unregister manually if needed
registration.unregister();
```
{{< callout type="info" >}}
Event registrations are automatically cleaned up when your plugin shuts down. Manual unregistration is rarely needed.
{{< /callout >}}
## Event Interface Hierarchy
Events implement different interfaces based on their capabilities:
| Interface | Description |
|-----------|-------------|
| `IBaseEvent<K>` | Base interface for all events |
| `IEvent<K>` | Synchronous events |
| `IAsyncEvent<K>` | Asynchronous events |
| `ICancellable` | Events that can be cancelled |
| `ICancellableEcsEvent` | ECS events that can be cancelled |
## Getting Registered Events
Query which events are registered:
```java
// Get all registered event classes
Set<Class<? extends IBaseEvent<?>>> eventClasses =
HytaleServer.get().getEventBus().getRegisteredEventClasses();
// Get registered event names
Set<String> eventNames =
HytaleServer.get().getEventBus().getRegisteredEventClassNames();
// Get a specific registry by name
EventBusRegistry<?, ?, ?> registry =
HytaleServer.get().getEventBus().getRegistry("PlayerConnectEvent");
```
## Complete Example
```java
public class WelcomePlugin extends JavaPlugin {
public WelcomePlugin(JavaPluginInit init) {
super(init);
}
@Override
public void start() {
// Handle player connection
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername());
// You can set the world the player spawns in
World lobbyWorld = Universe.get().getWorld("lobby");
if (lobbyWorld != null) {
event.setWorld(lobbyWorld);
}
});
// Welcome message when fully ready
// Note: PlayerReadyEvent has a String key, so use registerGlobal()
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
player.sendMessage(Message.raw("Welcome to the server, " + player.getDisplayName() + "!"));
});
// Goodbye message on disconnect
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
getLogger().at(Level.INFO).log(playerRef.getUsername() + " has left the server");
});
}
}
```
## Player Event Lifecycle
Understanding when each event fires:
```
PlayerSetupConnectEvent → Can cancel/redirect connection
PlayerConnectEvent → Player connecting, set spawn world
AddPlayerToWorldEvent → Player added to world
PlayerReadyEvent → Player fully loaded, safe to interact
(gameplay events)
DrainPlayerFromWorldEvent → Player leaving world
PlayerDisconnectEvent → Player disconnected
```

View File

@@ -0,0 +1,178 @@
---
title: Registries
type: docs
weight: 1
---
Registries are central to Hytale plugin development. They provide a way to register and manage various game elements like commands, events, entities, and more.
**Packages:**
- `com.hypixel.hytale.server.core.command.system` (CommandRegistry)
- `com.hypixel.hytale.event` (EventRegistry)
- `com.hypixel.hytale.server.core.plugin.registry` (AssetRegistry, CodecRegistry)
## Available Registries
Your plugin has access to these registries through the `PluginBase` class:
| Registry | Method | Purpose |
|----------|--------|---------|
| CommandRegistry | `getCommandRegistry()` | Register server commands |
| EventRegistry | `getEventRegistry()` | Register event listeners |
| EntityRegistry | `getEntityRegistry()` | Register custom entities |
| BlockStateRegistry | `getBlockStateRegistry()` | Register block states |
| TaskRegistry | `getTaskRegistry()` | Schedule and manage tasks |
| AssetRegistry | `getAssetRegistry()` | Register custom assets |
| ClientFeatureRegistry | `getClientFeatureRegistry()` | Register client features |
| EntityStoreRegistry | `getEntityStoreRegistry()` | Entity storage components |
| ChunkStoreRegistry | `getChunkStoreRegistry()` | Chunk storage components |
## CommandRegistry
Register commands that players can execute:
```java
@Override
public void start() {
getCommandRegistry().registerCommand(new MyCommand());
}
```
See [Commands](../../commands) for detailed documentation.
## EventRegistry
Subscribe to game events:
```java
import com.hypixel.hytale.event.EventPriority;
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
import java.util.logging.Level;
@Override
public void start() {
// Simple registration
getEventRegistry().register(PlayerConnectEvent.class, event -> {
getLogger().at(Level.INFO).log("Player connecting: " + event.getPlayerRef().getUsername());
});
// With priority
getEventRegistry().register(
EventPriority.EARLY,
PlayerConnectEvent.class,
this::onPlayerConnect
);
}
```
See [Events](../../events) for detailed documentation.
## EntityRegistry
Register custom entity types:
```java
@Override
public void start() {
getEntityRegistry().register(MyCustomEntity.class, MyCustomEntity::new);
}
```
See [Entities](../../entities) for detailed documentation.
## BlockStateRegistry
Register custom block states:
```java
@Override
public void start() {
getBlockStateRegistry().register(myBlockState);
}
```
## TaskRegistry
Track async tasks for cleanup during plugin shutdown:
{{< callout type="warning" >}}
TaskRegistry does NOT have `runAsync()`, `runSync()`, `runLater()`, or `runRepeating()` methods. Use Java's standard concurrency APIs.
{{< /callout >}}
```java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@Override
public void start() {
// Async operation
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Background work
});
getTaskRegistry().registerTask(task);
// Delayed operation (3 seconds)
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
getLogger().at(Level.INFO).log("Delayed task executed!");
});
// Repeating operation (every 5 minutes)
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> repeating = scheduler.scheduleAtFixedRate(() -> {
getLogger().at(Level.INFO).log("Repeating task!");
}, 0, 5, TimeUnit.MINUTES);
getTaskRegistry().registerTask((ScheduledFuture<Void>) repeating);
}
```
See [Tasks](../../tasks) for detailed documentation.
## AssetRegistry
Register custom game assets:
```java
@Override
public void start() {
getAssetRegistry().register(myAsset);
}
```
## CodecRegistry
For registering custom serialization codecs:
```java
getCodecRegistry(MyType.CODEC_MAP).register("my_type", MyType.CODEC);
```
## Automatic Cleanup
{{< callout type="info" >}}
All registrations are automatically cleaned up when your plugin is disabled. You don't need to manually unregister anything in your `shutdown()` method.
{{< /callout >}}
## Registration Timing
Register your components in the `start()` method, not in `setup()`:
```java
@Override
public void setup() {
// Configuration only - don't register here
withConfig(MyConfig.CODEC);
}
@Override
public void start() {
// Register everything here
getCommandRegistry().registerCommand(new MyCommand());
getEventRegistry().register(PlayerConnectEvent.class, this::onJoin);
}
```
This ensures all plugins have completed their setup phase before any registrations occur.

View File

@@ -0,0 +1,178 @@
---
title: Registres
type: docs
weight: 1
---
Les registres sont au cœur du développement de plugins Hytale. Ils fournissent un moyen d'enregistrer et gérer divers éléments du jeu comme les commandes, événements, entités, et plus.
**Packages:**
- `com.hypixel.hytale.server.core.command.system` (CommandRegistry)
- `com.hypixel.hytale.event` (EventRegistry)
- `com.hypixel.hytale.server.core.plugin.registry` (AssetRegistry, CodecRegistry)
## Registres Disponibles
Votre plugin a accès à ces registres via la classe `PluginBase` :
| Registre | Méthode | Utilité |
|----------|---------|---------|
| CommandRegistry | `getCommandRegistry()` | Enregistrer des commandes serveur |
| EventRegistry | `getEventRegistry()` | Enregistrer des écouteurs d'événements |
| EntityRegistry | `getEntityRegistry()` | Enregistrer des entités personnalisées |
| BlockStateRegistry | `getBlockStateRegistry()` | Enregistrer des états de blocs |
| TaskRegistry | `getTaskRegistry()` | Planifier et gérer des tâches |
| AssetRegistry | `getAssetRegistry()` | Enregistrer des assets personnalisés |
| ClientFeatureRegistry | `getClientFeatureRegistry()` | Enregistrer des fonctionnalités client |
| EntityStoreRegistry | `getEntityStoreRegistry()` | Composants de stockage d'entités |
| ChunkStoreRegistry | `getChunkStoreRegistry()` | Composants de stockage de chunks |
## CommandRegistry
Enregistrez des commandes que les joueurs peuvent exécuter :
```java
@Override
public void start() {
getCommandRegistry().registerCommand(new MyCommand());
}
```
Voir [Commandes](../../commands) pour la documentation détaillée.
## EventRegistry
Abonnez-vous aux événements du jeu :
```java
import com.hypixel.hytale.event.EventPriority;
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
import java.util.logging.Level;
@Override
public void start() {
// Enregistrement simple
getEventRegistry().register(PlayerConnectEvent.class, event -> {
getLogger().at(Level.INFO).log("Joueur connecté : " + event.getPlayerRef().getUsername());
});
// Avec priorité
getEventRegistry().register(
EventPriority.EARLY,
PlayerConnectEvent.class,
this::onPlayerJoin
);
}
```
Voir [Événements](../../events) pour la documentation détaillée.
## EntityRegistry
Enregistrez des types d'entités personnalisées :
```java
@Override
public void start() {
getEntityRegistry().register(MyCustomEntity.class, MyCustomEntity::new);
}
```
Voir [Entités](../../entities) pour la documentation détaillée.
## BlockStateRegistry
Enregistrez des états de blocs personnalisés :
```java
@Override
public void start() {
getBlockStateRegistry().register(myBlockState);
}
```
## TaskRegistry
Suit les tâches async pour le nettoyage lors de l'arrêt du plugin :
{{< callout type="warning" >}}
TaskRegistry n'a PAS de méthodes `runAsync()`, `runSync()`, `runLater()`, ou `runRepeating()`. Utilisez les APIs de concurrence standard de Java.
{{< /callout >}}
```java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@Override
public void start() {
// Opération async
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Travail en arrière-plan
});
getTaskRegistry().registerTask(task);
// Opération différée (3 secondes)
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
getLogger().at(Level.INFO).log("Tâche différée exécutée !");
});
// Opération répétitive (toutes les 5 minutes)
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> repeating = scheduler.scheduleAtFixedRate(() -> {
getLogger().at(Level.INFO).log("Tâche répétitive !");
}, 0, 5, TimeUnit.MINUTES);
getTaskRegistry().registerTask((ScheduledFuture<Void>) repeating);
}
```
Voir [Tâches](../../tasks) pour la documentation détaillée.
## AssetRegistry
Enregistrez des assets de jeu personnalisés :
```java
@Override
public void start() {
getAssetRegistry().register(myAsset);
}
```
## CodecRegistry
Pour enregistrer des codecs de sérialisation personnalisés :
```java
getCodecRegistry(MyType.CODEC_MAP).register("my_type", MyType.CODEC);
```
## Nettoyage Automatique
{{< callout type="info" >}}
Tous les enregistrements sont automatiquement nettoyés quand votre plugin est désactivé. Vous n'avez pas besoin de désinscrire manuellement quoi que ce soit dans votre méthode `shutdown()`.
{{< /callout >}}
## Timing des Enregistrements
Enregistrez vos composants dans la méthode `start()`, pas dans `setup()` :
```java
@Override
public void setup() {
// Configuration uniquement - n'enregistrez pas ici
withConfig(MyConfig.CODEC);
}
@Override
public void start() {
// Enregistrez tout ici
getCommandRegistry().registerCommand(new MyCommand());
getEventRegistry().register(PlayerConnectEvent.class, this::onJoin);
}
```
Cela garantit que tous les plugins ont terminé leur phase de configuration avant que tout enregistrement ne se produise.

View File

@@ -0,0 +1,69 @@
---
title: Tasks
type: docs
weight: 3
---
The task system in Hytale manages asynchronous operations using Java's standard concurrency APIs.
{{< cards >}}
{{< card link="task-registry" title="TaskRegistry" subtitle="Registering and tracking tasks" >}}
{{< card link="async-operations" title="Async Operations" subtitle="Thread-safe async patterns" >}}
{{< /cards >}}
## Overview
{{< callout type="warning" >}}
**Important:** TaskRegistry does NOT have `runAsync()`, `runSync()`, `runLater()`, or `runRepeating()` methods. Hytale uses Java's standard `CompletableFuture` and `ScheduledExecutorService` APIs.
{{< /callout >}}
The TaskRegistry allows plugins to:
- **Register tasks** - Track `CompletableFuture` and `ScheduledFuture` for cleanup
- **Async operations** - Use `CompletableFuture.runAsync()`
- **Delayed operations** - Use `CompletableFuture.delayedExecutor()`
- **Repeating operations** - Use `ScheduledExecutorService`
## Quick Start
```java
// Run async operation
CompletableFuture.runAsync(() -> {
// Background thread code
Data result = computeExpensiveData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
// Delayed operation (3 seconds)
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
// Delayed code on world thread
});
});
```
## Key Concepts
{{< callout type="warning" >}}
**Thread Safety:** Each World runs on its own dedicated thread. Always use `world.execute()` when modifying game state (players, entities, blocks) from async code. Direct manipulation from async threads can cause crashes or data corruption.
{{< /callout >}}
```java
// Correct pattern
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Do heavy computation here (background thread)
Data result = computeExpensiveData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
```

View File

@@ -0,0 +1,69 @@
---
title: Tâches
type: docs
weight: 3
---
Le système de tâches dans Hytale gère les opérations asynchrones en utilisant les APIs de concurrence standard de Java.
{{< cards >}}
{{< card link="task-registry" title="TaskRegistry" subtitle="Enregistrement et suivi des tâches" >}}
{{< card link="async-operations" title="Opérations Async" subtitle="Patterns async thread-safe" >}}
{{< /cards >}}
## Aperçu
{{< callout type="warning" >}}
**Important :** TaskRegistry n'a PAS de méthodes `runAsync()`, `runSync()`, `runLater()`, ou `runRepeating()`. Hytale utilise les APIs standard Java `CompletableFuture` et `ScheduledExecutorService`.
{{< /callout >}}
Le TaskRegistry permet aux plugins de :
- **Enregistrer des tâches** - Suivre les `CompletableFuture` et `ScheduledFuture` pour le nettoyage
- **Opérations async** - Utiliser `CompletableFuture.runAsync()`
- **Opérations différées** - Utiliser `CompletableFuture.delayedExecutor()`
- **Opérations répétitives** - Utiliser `ScheduledExecutorService`
## Démarrage Rapide
```java
// Exécuter une opération async
CompletableFuture.runAsync(() -> {
// Code thread d'arrière-plan
Data result = computeExpensiveData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
// Opération différée (3 secondes)
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
// Code différé sur le thread du monde
});
});
```
## Concepts Clés
{{< callout type="warning" >}}
**Sécurité des Threads :** Chaque World s'exécute sur son propre thread dédié. Utilisez toujours `world.execute()` lors de la modification de l'état du jeu (joueurs, entités, blocs) depuis du code async. La manipulation directe depuis des threads async peut causer des crashs ou corruption de données.
{{< /callout >}}
```java
// Pattern correct
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Faire le calcul lourd ici (thread d'arrière-plan)
Data result = computeExpensiveData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
```

View File

@@ -0,0 +1,295 @@
---
title: Async Operations
type: docs
weight: 2
---
Asynchronous operations allow plugins to perform heavy tasks without blocking the world's ticking thread.
## Why Async?
{{< callout type="info" >}}
Each World has a dedicated ticking thread that handles game logic, player interactions, and tick updates. Blocking it causes lag and poor player experience. Use async for:
- Database queries
- File I/O
- Network requests
- Complex calculations
{{< /callout >}}
## Basic Async Pattern
Use `CompletableFuture.runAsync()` and `world.execute()`:
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// This runs on a background thread
// Do heavy work here
Data result = computeData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
```
## Thread Safety
### Using PlayerRef
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.runAsync(() -> {
// Load data asynchronously
PlayerData data = loadFromDatabase(playerRef.getUuid());
// Return to world thread
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
}
});
});
});
```
### Thread-Safe Collections
```java
// Use concurrent collections for shared data
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
private final Set<UUID> processing = ConcurrentHashMap.newKeySet();
public void processPlayer(PlayerRef playerRef, World world) {
UUID uuid = playerRef.getUuid();
// Prevent duplicate processing
if (!processing.add(uuid)) {
return; // Already processing
}
CompletableFuture.runAsync(() -> {
try {
PlayerData data = compute(uuid);
playerData.put(uuid, data);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Processing complete!"));
});
} finally {
processing.remove(uuid);
}
});
}
```
## Async Events
Handle async events with CompletableFuture:
```java
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Async processing
return processEvent(event);
});
});
```
## Common Patterns
### Database Operations
```java
public void savePlayerData(PlayerRef playerRef, World world) {
// Capture data on world thread before going async
PlayerData data = captureData(playerRef);
CompletableFuture.runAsync(() -> {
try {
database.save(playerRef.getUuid(), data);
getLogger().at(Level.INFO).log("Saved data for " + playerRef.getUsername());
} catch (Exception e) {
getLogger().error("Failed to save data", e);
}
});
}
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
PlayerData data = database.load(playerRef.getUuid());
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && data != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
});
});
}
```
### HTTP Requests
```java
public void fetchPlayerStats(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
String response = httpClient.get("https://api.example.com/stats/" + playerRef.getUuid());
Stats stats = parseStats(response);
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
displayStats(ref, stats);
}
});
} catch (Exception e) {
world.execute(() -> {
playerRef.sendMessage(Message.raw("Failed to fetch stats."));
});
}
});
}
```
### Batch Processing
```java
public void processAllPlayers(World world) {
// Capture player refs on world thread
List<PlayerRef> players = new ArrayList<>();
world.getEntityStore().forEach((ref, store) -> {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
players.add(playerRef);
}
});
CompletableFuture.runAsync(() -> {
Map<UUID, Result> results = new HashMap<>();
for (PlayerRef playerRef : players) {
results.put(playerRef.getUuid(), computeResult(playerRef));
}
world.execute(() -> {
for (PlayerRef playerRef : players) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Result result = results.get(playerRef.getUuid());
applyResult(ref, result);
}
}
});
});
}
```
## Error Handling
```java
public void safeAsyncOperation(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
riskyOperation(playerRef.getUuid());
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation successful!"));
});
} catch (Exception e) {
getLogger().error("Async operation failed", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation failed. Please try again."));
});
}
});
}
```
## Chaining Async Operations
Use `CompletableFuture` chaining for sequential async operations:
```java
public void chainedOperations(PlayerRef playerRef, World world) {
CompletableFuture
.supplyAsync(() -> {
// First async operation
return fetchFromDatabase(playerRef.getUuid());
})
.thenApplyAsync(data -> {
// Second async operation
return processData(data);
})
.thenAccept(result -> {
// Return to world thread with final result
world.execute(() -> {
playerRef.sendMessage(Message.raw("Final result: " + result));
});
})
.exceptionally(e -> {
getLogger().error("Chain failed", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation failed."));
});
return null;
});
}
```
## Best Practices
{{< callout type="warning" >}}
**Async Rules:**
1. Never access game state directly from async threads
2. Always use `PlayerRef`, never store `Player` or `Ref<EntityStore>`
3. Capture needed data before going async
4. Use `world.execute()` to return to world thread
5. Handle exceptions properly
6. Use concurrent collections for shared data
{{< /callout >}}
```java
// CORRECT: Capture data, process async, apply on world thread
public void correctPattern(PlayerRef playerRef, World world) {
String username = playerRef.getUsername(); // Capture on world thread
CompletableFuture.runAsync(() -> {
String result = process(username);
world.execute(() -> {
playerRef.sendMessage(Message.raw(result));
});
});
}
// WRONG: Using world objects directly in async
// public void wrongPattern(Player player, Ref<EntityStore> ref) {
// CompletableFuture.runAsync(() -> {
// // DON'T DO THIS - these may be invalid from async context
// String name = player.getDisplayName();
// player.sendMessage("Hello");
// });
// }
```
## Comparison with Other Platforms
{{< callout type="info" >}}
**Note for developers from other platforms:**
- Unlike Bukkit/Spigot, Hytale does NOT have `runSync()` or `runLater()` methods
- Use `CompletableFuture.runAsync()` instead of platform-specific async methods
- Use `world.execute()` instead of `runSync()` to return to the world thread
- Use `CompletableFuture.delayedExecutor()` for delayed tasks
- Use `ScheduledExecutorService` for repeating tasks
{{< /callout >}}

View File

@@ -0,0 +1,295 @@
---
title: Opérations Async
type: docs
weight: 2
---
Les opérations asynchrones permettent aux plugins d'effectuer des tâches lourdes sans bloquer le thread de tick du monde.
## Pourquoi Async ?
{{< callout type="info" >}}
Chaque World a un thread de tick dédié qui gère la logique du jeu, les interactions des joueurs et les mises à jour de tick. Le bloquer cause du lag et une mauvaise expérience joueur. Utilisez async pour :
- Les requêtes de base de données
- Les I/O de fichiers
- Les requêtes réseau
- Les calculs complexes
{{< /callout >}}
## Pattern Async de Base
Utilisez `CompletableFuture.runAsync()` et `world.execute()` :
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Ceci s'exécute sur un thread d'arrière-plan
// Faire le travail lourd ici
Data result = computeData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
```
## Sécurité des Threads
### Utiliser PlayerRef
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.runAsync(() -> {
// Charger les données de manière asynchrone
PlayerData data = loadFromDatabase(playerRef.getUuid());
// Retourner au thread du monde
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
}
});
});
});
```
### Collections Thread-Safe
```java
// Utiliser des collections concurrentes pour les données partagées
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
private final Set<UUID> processing = ConcurrentHashMap.newKeySet();
public void processPlayer(PlayerRef playerRef, World world) {
UUID uuid = playerRef.getUuid();
// Empêcher le traitement en double
if (!processing.add(uuid)) {
return; // Déjà en traitement
}
CompletableFuture.runAsync(() -> {
try {
PlayerData data = compute(uuid);
playerData.put(uuid, data);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Traitement terminé !"));
});
} finally {
processing.remove(uuid);
}
});
}
```
## Événements Async
Gérer les événements async avec CompletableFuture :
```java
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Traitement async
return processEvent(event);
});
});
```
## Patterns Courants
### Opérations de Base de Données
```java
public void savePlayerData(PlayerRef playerRef, World world) {
// Capturer les données sur le thread du monde avant de passer en async
PlayerData data = captureData(playerRef);
CompletableFuture.runAsync(() -> {
try {
database.save(playerRef.getUuid(), data);
getLogger().at(Level.INFO).log("Données sauvegardées pour " + playerRef.getUsername());
} catch (Exception e) {
getLogger().error("Échec de la sauvegarde des données", e);
}
});
}
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
PlayerData data = database.load(playerRef.getUuid());
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && data != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Données chargées !"));
}
});
});
}
```
### Requêtes HTTP
```java
public void fetchPlayerStats(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
String response = httpClient.get("https://api.example.com/stats/" + playerRef.getUuid());
Stats stats = parseStats(response);
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
displayStats(ref, stats);
}
});
} catch (Exception e) {
world.execute(() -> {
playerRef.sendMessage(Message.raw("Échec de la récupération des stats."));
});
}
});
}
```
### Traitement par Lots
```java
public void processAllPlayers(World world) {
// Capturer les refs de joueurs sur le thread du monde
List<PlayerRef> players = new ArrayList<>();
world.getEntityStore().forEach((ref, store) -> {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
players.add(playerRef);
}
});
CompletableFuture.runAsync(() -> {
Map<UUID, Result> results = new HashMap<>();
for (PlayerRef playerRef : players) {
results.put(playerRef.getUuid(), computeResult(playerRef));
}
world.execute(() -> {
for (PlayerRef playerRef : players) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Result result = results.get(playerRef.getUuid());
applyResult(ref, result);
}
}
});
});
}
```
## Gestion des Erreurs
```java
public void safeAsyncOperation(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
riskyOperation(playerRef.getUuid());
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération réussie !"));
});
} catch (Exception e) {
getLogger().error("L'opération async a échoué", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération échouée. Veuillez réessayer."));
});
}
});
}
```
## Chaînage des Opérations Async
Utilisez le chaînage `CompletableFuture` pour les opérations async séquentielles :
```java
public void chainedOperations(PlayerRef playerRef, World world) {
CompletableFuture
.supplyAsync(() -> {
// Première opération async
return fetchFromDatabase(playerRef.getUuid());
})
.thenApplyAsync(data -> {
// Deuxième opération async
return processData(data);
})
.thenAccept(result -> {
// Retourner au thread du monde avec le résultat final
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat final : " + result));
});
})
.exceptionally(e -> {
getLogger().error("La chaîne a échoué", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération échouée."));
});
return null;
});
}
```
## Bonnes Pratiques
{{< callout type="warning" >}}
**Règles Async :**
1. Ne jamais accéder à l'état du jeu directement depuis des threads async
2. Toujours utiliser `PlayerRef`, ne jamais stocker `Player` ou `Ref<EntityStore>`
3. Capturer les données nécessaires avant de passer en async
4. Utiliser `world.execute()` pour retourner au thread du monde
5. Gérer les exceptions correctement
6. Utiliser des collections concurrentes pour les données partagées
{{< /callout >}}
```java
// CORRECT : Capturer les données, traiter en async, appliquer sur le thread du monde
public void correctPattern(PlayerRef playerRef, World world) {
String username = playerRef.getUsername(); // Capturer sur le thread du monde
CompletableFuture.runAsync(() -> {
String result = process(username);
world.execute(() -> {
playerRef.sendMessage(Message.raw(result));
});
});
}
// MAUVAIS : Utiliser les objets monde directement en async
// public void wrongPattern(Player player, Ref<EntityStore> ref) {
// CompletableFuture.runAsync(() -> {
// // NE FAITES PAS ÇA - ceux-ci peuvent être invalides depuis le contexte async
// String name = player.getDisplayName();
// player.sendMessage("Bonjour");
// });
// }
```
## Comparaison avec d'Autres Plateformes
{{< callout type="info" >}}
**Note pour les développeurs d'autres plateformes :**
- Contrairement à Bukkit/Spigot, Hytale n'a PAS de méthodes `runSync()` ou `runLater()`
- Utilisez `CompletableFuture.runAsync()` au lieu des méthodes async spécifiques à la plateforme
- Utilisez `world.execute()` au lieu de `runSync()` pour retourner au thread du monde
- Utilisez `CompletableFuture.delayedExecutor()` pour les tâches différées
- Utilisez `ScheduledExecutorService` pour les tâches répétitives
{{< /callout >}}

View File

@@ -0,0 +1,260 @@
---
title: TaskRegistry
type: docs
weight: 1
---
The TaskRegistry allows plugins to register and track asynchronous tasks. Hytale uses Java's standard concurrency APIs for task scheduling.
## Understanding TaskRegistry
{{< callout type="warning" >}}
**Important:** TaskRegistry does NOT have `runAsync()`, `runLater()`, or `runRepeating()` methods. These are common misconceptions from other platforms. Hytale uses Java's standard `CompletableFuture` and `ScheduledExecutorService` APIs.
{{< /callout >}}
## TaskRegistry API
```java
public class TaskRegistry extends Registry<TaskRegistration> {
// Register a CompletableFuture task
public TaskRegistration registerTask(CompletableFuture<Void> task);
// Register a ScheduledFuture task
public TaskRegistration registerTask(ScheduledFuture<Void> task);
}
```
The TaskRegistry tracks tasks for proper cleanup during plugin shutdown.
## Asynchronous Tasks
Use `CompletableFuture` for async operations:
```java
// Run async operation
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Heavy computation, I/O, network requests
String data = fetchFromDatabase(playerId);
});
// Register with TaskRegistry for tracking
getTaskRegistry().registerTask(task);
// Handle completion
task.thenAccept(result -> {
// Process result - NOTE: This runs on a thread pool, not the world thread
// Use world.execute() to run code on the world thread
});
```
### Returning to World Thread
After async operations, use `world.execute()` to return to the world's thread:
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Load from database (blocking I/O is OK here)
PlayerData data = database.load(playerRef.getUuid());
// Return to world thread for game state changes
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Player p = ref.getStore().getComponent(ref, Player.getComponentType());
if (p != null) {
applyData(p, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
}
});
});
getTaskRegistry().registerTask(task);
```
## Delayed Tasks
Use `CompletableFuture.delayedExecutor()` for delays:
```java
import java.util.concurrent.TimeUnit;
// Run after 3 seconds
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
playerRef.sendMessage(Message.raw("3 seconds have passed!"));
});
});
```
## Repeating Tasks
Use `ScheduledExecutorService` for repeating tasks:
```java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MyPlugin extends JavaPlugin {
private ScheduledExecutorService scheduler;
@Override
public void start() {
scheduler = Executors.newSingleThreadScheduledExecutor();
// Run every 5 minutes
ScheduledFuture<?> saveTask = scheduler.scheduleAtFixedRate(
() -> {
saveAllData();
getLogger().at(Level.INFO).log("Auto-save complete");
},
5, // Initial delay
5, // Period
TimeUnit.MINUTES
);
// Register for tracking
getTaskRegistry().registerTask((ScheduledFuture<Void>) saveTask);
}
@Override
public void shutdown() {
if (scheduler != null) {
scheduler.shutdown();
}
}
}
```
## Tick-Based Timing
For tick-based timing, track ticks manually in a world tick handler:
```java
public class TickTimerPlugin extends JavaPlugin {
private final Map<String, TickTimer> timers = new ConcurrentHashMap<>();
public void scheduleAfterTicks(String id, int ticks, Runnable action) {
timers.put(id, new TickTimer(ticks, action));
}
// Called from your tick handler
public void onWorldTick() {
timers.entrySet().removeIf(entry -> {
TickTimer timer = entry.getValue();
timer.ticksRemaining--;
if (timer.ticksRemaining <= 0) {
timer.action.run();
return true; // Remove
}
return false;
});
}
private static class TickTimer {
int ticksRemaining;
Runnable action;
TickTimer(int ticks, Runnable action) {
this.ticksRemaining = ticks;
this.action = action;
}
}
}
```
## Common Patterns
### Countdown Timer
```java
public void startCountdown(PlayerRef playerRef, int seconds) {
AtomicInteger remaining = new AtomicInteger(seconds);
World world = Universe.get().getWorld(playerRef.getWorldUuid());
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> countdown = scheduler.scheduleAtFixedRate(() -> {
int count = remaining.decrementAndGet();
world.execute(() -> {
if (count > 0) {
playerRef.sendMessage(Message.raw("Starting in: " + count));
} else {
playerRef.sendMessage(Message.raw("Go!"));
scheduler.shutdown();
}
});
}, 0, 1, TimeUnit.SECONDS);
}
```
### Async Data Loading
```java
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
// Load from database (blocking I/O is OK here)
PlayerData data = database.load(playerRef.getUuid());
// Return to world thread
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
});
});
}
```
## Time Conversion
Hytale runs at **30 TPS** (ticks per second):
| Time | Ticks (at 30 TPS) | Milliseconds |
|------|-------------------|--------------|
| 1 tick | 1 | ~33ms |
| 1 second | 30 | 1,000ms |
| 5 seconds | 150 | 5,000ms |
| 1 minute | 1,800 | 60,000ms |
| 5 minutes | 9,000 | 300,000ms |
## Best Practices
{{< callout type="info" >}}
**Task Guidelines:**
- Use `CompletableFuture.runAsync()` for I/O, database, and network operations
- Use `world.execute()` to return to the world thread for game state changes
- Use `PlayerRef` in async tasks, not `Player` or `Ref<EntityStore>`
- Register long-running tasks with `getTaskRegistry()` for cleanup
- Shut down custom `ScheduledExecutorService` instances in `shutdown()`
{{< /callout >}}
{{< callout type="warning" >}}
**Thread Safety:** Each World runs on its own thread. Always use `world.execute()` or check `world.isInThread()` before modifying game state from async code.
{{< /callout >}}
```java
// Good: Proper async pattern with PlayerRef
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
String result = heavyComputation();
world.execute(() -> {
ref.sendMessage(Message.raw(result));
});
});
// Bad: Using Player directly in async
// CompletableFuture.runAsync(() -> {
// player.sendMessage("Not safe!"); // DON'T DO THIS
// });
```

View File

@@ -0,0 +1,260 @@
---
title: TaskRegistry
type: docs
weight: 1
---
Le TaskRegistry permet aux plugins d'enregistrer et de suivre les tâches asynchrones. Hytale utilise les APIs de concurrence standard de Java pour la planification des tâches.
## Comprendre TaskRegistry
{{< callout type="warning" >}}
**Important :** TaskRegistry n'a PAS de méthodes `runAsync()`, `runLater()`, ou `runRepeating()`. Ce sont des idées fausses courantes venant d'autres plateformes. Hytale utilise les APIs standard Java `CompletableFuture` et `ScheduledExecutorService`.
{{< /callout >}}
## API TaskRegistry
```java
public class TaskRegistry extends Registry<TaskRegistration> {
// Enregistrer une tâche CompletableFuture
public TaskRegistration registerTask(CompletableFuture<Void> task);
// Enregistrer une tâche ScheduledFuture
public TaskRegistration registerTask(ScheduledFuture<Void> task);
}
```
Le TaskRegistry suit les tâches pour un nettoyage correct lors de l'arrêt du plugin.
## Tâches Asynchrones
Utilisez `CompletableFuture` pour les opérations async :
```java
// Exécuter une opération async
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Calcul lourd, I/O, requêtes réseau
String data = fetchFromDatabase(playerId);
});
// Enregistrer avec TaskRegistry pour le suivi
getTaskRegistry().registerTask(task);
// Gérer l'achèvement
task.thenAccept(result -> {
// Traiter le résultat - NOTE : Ceci s'exécute sur un pool de threads, pas le thread du monde
// Utilisez world.execute() pour exécuter du code sur le thread du monde
});
```
### Retourner au Thread du Monde
Après les opérations async, utilisez `world.execute()` pour retourner au thread du monde :
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Charger depuis la base de données (I/O bloquant est OK ici)
PlayerData data = database.load(playerRef.getUuid());
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Player p = ref.getStore().getComponent(ref, Player.getComponentType());
if (p != null) {
applyData(p, data);
playerRef.sendMessage(Message.raw("Données chargées !"));
}
}
});
});
getTaskRegistry().registerTask(task);
```
## Tâches Différées
Utilisez `CompletableFuture.delayedExecutor()` pour les délais :
```java
import java.util.concurrent.TimeUnit;
// Exécuter après 3 secondes
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
playerRef.sendMessage(Message.raw("3 secondes se sont écoulées !"));
});
});
```
## Tâches Répétitives
Utilisez `ScheduledExecutorService` pour les tâches répétitives :
```java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MyPlugin extends JavaPlugin {
private ScheduledExecutorService scheduler;
@Override
public void start() {
scheduler = Executors.newSingleThreadScheduledExecutor();
// Exécuter toutes les 5 minutes
ScheduledFuture<?> saveTask = scheduler.scheduleAtFixedRate(
() -> {
saveAllData();
getLogger().at(Level.INFO).log("Auto-sauvegarde terminée");
},
5, // Délai initial
5, // Période
TimeUnit.MINUTES
);
// Enregistrer pour le suivi
getTaskRegistry().registerTask((ScheduledFuture<Void>) saveTask);
}
@Override
public void shutdown() {
if (scheduler != null) {
scheduler.shutdown();
}
}
}
```
## Timing Basé sur les Ticks
Pour un timing basé sur les ticks, suivez les ticks manuellement dans un gestionnaire de tick du monde :
```java
public class TickTimerPlugin extends JavaPlugin {
private final Map<String, TickTimer> timers = new ConcurrentHashMap<>();
public void scheduleAfterTicks(String id, int ticks, Runnable action) {
timers.put(id, new TickTimer(ticks, action));
}
// Appeler depuis votre gestionnaire de tick
public void onWorldTick() {
timers.entrySet().removeIf(entry -> {
TickTimer timer = entry.getValue();
timer.ticksRemaining--;
if (timer.ticksRemaining <= 0) {
timer.action.run();
return true; // Supprimer
}
return false;
});
}
private static class TickTimer {
int ticksRemaining;
Runnable action;
TickTimer(int ticks, Runnable action) {
this.ticksRemaining = ticks;
this.action = action;
}
}
}
```
## Patterns Courants
### Compte à Rebours
```java
public void startCountdown(PlayerRef playerRef, int seconds) {
AtomicInteger remaining = new AtomicInteger(seconds);
World world = Universe.get().getWorld(playerRef.getWorldUuid());
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> countdown = scheduler.scheduleAtFixedRate(() -> {
int count = remaining.decrementAndGet();
world.execute(() -> {
if (count > 0) {
playerRef.sendMessage(Message.raw("Démarrage dans : " + count));
} else {
playerRef.sendMessage(Message.raw("Go !"));
scheduler.shutdown();
}
});
}, 0, 1, TimeUnit.SECONDS);
}
```
### Chargement de Données Async
```java
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
// Charger depuis la base de données (I/O bloquant est OK ici)
PlayerData data = database.load(playerRef.getUuid());
// Retourner au thread du monde
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Données chargées !"));
}
});
});
}
```
## Conversion de Temps
Hytale tourne à **30 TPS** (ticks par seconde) :
| Temps | Ticks (à 30 TPS) | Millisecondes |
|-------|------------------|---------------|
| 1 tick | 1 | ~33ms |
| 1 seconde | 30 | 1 000ms |
| 5 secondes | 150 | 5 000ms |
| 1 minute | 1 800 | 60 000ms |
| 5 minutes | 9 000 | 300 000ms |
## Bonnes Pratiques
{{< callout type="info" >}}
**Directives pour les Tâches :**
- Utilisez `CompletableFuture.runAsync()` pour les opérations I/O, base de données et réseau
- Utilisez `world.execute()` pour retourner au thread du monde pour les changements d'état du jeu
- Utilisez `PlayerRef` dans les tâches async, pas `Player` ou `Ref<EntityStore>`
- Enregistrez les tâches longue durée avec `getTaskRegistry()` pour le nettoyage
- Arrêtez les instances `ScheduledExecutorService` personnalisées dans `shutdown()`
{{< /callout >}}
{{< callout type="warning" >}}
**Thread Safety :** Chaque World s'exécute sur son propre thread. Utilisez toujours `world.execute()` ou vérifiez `world.isInThread()` avant de modifier l'état du jeu depuis du code async.
{{< /callout >}}
```java
// Bien : Pattern async correct avec PlayerRef
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
String result = heavyComputation();
world.execute(() -> {
ref.sendMessage(Message.raw(result));
});
});
// Mauvais : Utiliser Player directement en async
// CompletableFuture.runAsync(() -> {
// player.sendMessage("Pas sûr !"); // NE FAITES PAS ÇA
// });
```

View File

@@ -0,0 +1,190 @@
---
title: Threading
type: docs
weight: 4
---
Understanding threading in Hytale is crucial for writing safe and performant plugins. Unlike many game servers, Hytale uses a **per-world threading model** where each world runs on its own dedicated thread.
## Per-World Threading
Each `World` in Hytale extends `TickingThread` and runs on its own dedicated thread at 30 TPS (ticks per second). This means:
- Each world ticks independently
- World operations must be called from that world's thread
- Players in different worlds run on different threads
```java
World world = player.getWorld();
// Check if we're on this world's thread
if (world.isInThread()) {
// Safe to modify world state
world.setBlock(position, blockType);
}
```
{{< callout type="warning" >}}
Never perform blocking operations (I/O, network, database) on a world thread. This will cause that world to lag.
{{< /callout >}}
## Async Operations
For long-running operations, use `CompletableFuture`:
```java
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.supplyAsync(() -> {
// This runs on a worker thread
return loadDataFromDatabase(ref.getUuid());
}).thenAccept(data -> {
// PlayerRef.sendMessage() is thread-safe
ref.sendMessage(Message.raw("Data loaded: " + data));
});
```
## Thread-Safe Classes
Some Hytale classes are designed for thread safety:
| Class | Thread Safety |
|-------|---------------|
| `PlayerRef` | Safe to use across threads |
| `World` | Must be accessed on its own thread (`world.isInThread()`) |
| `Entity` | Must be accessed on its world's thread |
| `ItemStack` | Immutable, thread-safe |
### PlayerRef
`PlayerRef` is a persistent, thread-safe reference to a player:
```java
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.Message;
// Store reference (thread-safe)
PlayerRef playerRef = player.getPlayerRef();
// Safe to access on any thread:
UUID uuid = playerRef.getUuid();
String username = playerRef.getUsername();
String language = playerRef.getLanguage();
// Get current world UUID (may change if player switches worlds)
UUID worldUuid = playerRef.getWorldUuid();
// Send message directly (thread-safe)
playerRef.sendMessage(Message.raw("Hello!"));
// For ECS operations, get the entity reference
Ref<EntityStore> entityRef = playerRef.getReference(); // null if not in world
```
### Checking Thread Context
```java
World world = player.getWorld();
// Check if on world's thread
if (!world.isInThread()) {
throw new IllegalStateException("Must be called from world thread!");
}
// Debug: print current thread
System.out.println("Current thread: " + Thread.currentThread().getName());
```
## Common Patterns
### Database Operations
```java
public void savePlayer(Player player) {
PlayerRef ref = player.getPlayerRef();
PlayerData data = collectPlayerData(player);
CompletableFuture.runAsync(() -> {
// Runs on worker thread
database.save(ref.getUuid(), data);
}).thenRun(() -> {
// Notify player (PlayerRef.sendMessage is thread-safe)
ref.sendMessage(Message.raw("Saved!"));
});
}
```
### Loading Data on Join
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef ref = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.supplyAsync(() -> {
// Load data on worker thread
return database.load(ref.getUuid());
}).thenAccept(data -> {
if (world != null && data != null) {
world.execute(() -> {
// Back on world thread - safe to access ECS
Ref<EntityStore> entityRef = ref.getReference();
if (entityRef != null) {
applyData(entityRef, data);
}
});
}
});
});
```
## Async Events
Some events support asynchronous handling. Events with a keyed type (like `PlayerChatEvent` which has a `String` key) must use `registerAsyncGlobal`:
```java
getEventRegistry().registerAsyncGlobal(
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// This runs asynchronously
// Can perform slow operations here
String filtered = filterMessage(event.getContent());
event.setContent(filtered);
return event;
})
);
```
## Cross-World Operations
When working with entities across worlds or transferring players:
```java
// Player transfer returns CompletableFuture
PlayerRef ref = player.getPlayerRef();
World targetWorld = universe.getWorld("target_world");
// Note: player operations that change worlds use async patterns
// Always use PlayerRef to track player across world changes
```
## Best Practices
{{< callout type="tip" >}}
- Always use `PlayerRef` when passing player references across async boundaries
- Check `world.isInThread()` before modifying world state
- Use `CompletableFuture` for async database/network operations
- Keep world thread operations fast (< 33ms per tick)
- Use connection pools for database access
- Remember: each world has its own thread - no single "main thread"
{{< /callout >}}
## Debugging Thread Issues
If you encounter threading issues:
1. Check `world.isInThread()` before world modifications
2. Use `PlayerRef` instead of direct `Player` references for async work
3. Log the current thread: `Thread.currentThread().getName()`
4. World thread names follow pattern: `World-{worldName}`

View File

@@ -0,0 +1,190 @@
---
title: Threading
type: docs
weight: 4
---
Comprendre le threading dans Hytale est crucial pour écrire des plugins sûrs et performants. Contrairement à de nombreux serveurs de jeu, Hytale utilise un **modèle de threading par monde** où chaque monde s'exécute sur son propre thread dédié.
## Threading Par Monde
Chaque `World` dans Hytale étend `TickingThread` et s'exécute sur son propre thread dédié à 30 TPS (ticks par seconde). Cela signifie :
- Chaque monde tick indépendamment
- Les opérations de monde doivent être appelées depuis le thread de ce monde
- Les joueurs dans différents mondes s'exécutent sur différents threads
```java
World world = player.getWorld();
// Vérifier si on est sur le thread de ce monde
if (world.isInThread()) {
// Sûr de modifier l'état du monde
world.setBlock(position, blockType);
}
```
{{< callout type="warning" >}}
N'effectuez jamais d'opérations bloquantes (I/O, réseau, base de données) sur un thread de monde. Cela causera du lag pour ce monde.
{{< /callout >}}
## Opérations Asynchrones
Pour les opérations longues, utilisez `CompletableFuture` :
```java
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.supplyAsync(() -> {
// Ceci s'exécute sur un worker thread
return loadDataFromDatabase(ref.getUuid());
}).thenAccept(data -> {
// PlayerRef.sendMessage() est thread-safe
ref.sendMessage(Message.raw("Données chargées : " + data));
});
```
## Classes Thread-Safe
Certaines classes Hytale sont conçues pour la sécurité des threads :
| Classe | Sécurité Thread |
|--------|-----------------|
| `PlayerRef` | Sûr à utiliser entre threads |
| `World` | Doit être accédé sur son propre thread (`world.isInThread()`) |
| `Entity` | Doit être accédé sur le thread de son monde |
| `ItemStack` | Immuable, thread-safe |
### PlayerRef
`PlayerRef` est une référence persistante et thread-safe vers un joueur :
```java
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.Message;
// Stocker la référence (thread-safe)
PlayerRef playerRef = player.getPlayerRef();
// Sûr d'accéder sur n'importe quel thread :
UUID uuid = playerRef.getUuid();
String username = playerRef.getUsername();
String language = playerRef.getLanguage();
// Obtenir l'UUID du monde actuel (peut changer si le joueur change de monde)
UUID worldUuid = playerRef.getWorldUuid();
// Envoyer un message directement (thread-safe)
playerRef.sendMessage(Message.raw("Bonjour !"));
// Pour les opérations ECS, obtenir la référence d'entité
Ref<EntityStore> entityRef = playerRef.getReference(); // null si pas dans un monde
```
### Vérifier le Contexte Thread
```java
World world = player.getWorld();
// Vérifier si on est sur le thread du monde
if (!world.isInThread()) {
throw new IllegalStateException("Doit être appelé depuis le thread du monde !");
}
// Debug : afficher le thread courant
System.out.println("Thread courant : " + Thread.currentThread().getName());
```
## Patterns Courants
### Opérations Base de Données
```java
public void savePlayer(Player player) {
PlayerRef ref = player.getPlayerRef();
PlayerData data = collectPlayerData(player);
CompletableFuture.runAsync(() -> {
// S'exécute sur un worker thread
database.save(ref.getUuid(), data);
}).thenRun(() -> {
// Notifier le joueur (PlayerRef.sendMessage est thread-safe)
ref.sendMessage(Message.raw("Sauvegardé !"));
});
}
```
### Chargement des Données à la Connexion
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef ref = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.supplyAsync(() -> {
// Charger les données sur un worker thread
return database.load(ref.getUuid());
}).thenAccept(data -> {
if (world != null && data != null) {
world.execute(() -> {
// Retour sur le thread du monde - accès ECS sécurisé
Ref<EntityStore> entityRef = ref.getReference();
if (entityRef != null) {
applyData(entityRef, data);
}
});
}
});
});
```
## Événements Async
Certains événements supportent la gestion asynchrone. Les événements avec une clé typée (comme `PlayerChatEvent` qui a une clé `String`) doivent utiliser `registerAsyncGlobal` :
```java
getEventRegistry().registerAsyncGlobal(
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Ceci s'exécute de façon asynchrone
// Peut effectuer des opérations lentes ici
String filtered = filterMessage(event.getContent());
event.setContent(filtered);
return event;
})
);
```
## Opérations Inter-Mondes
Lors du travail avec des entités entre mondes ou du transfert de joueurs :
```java
// Le transfert de joueur retourne CompletableFuture
PlayerRef ref = player.getPlayerRef();
World targetWorld = universe.getWorld("target_world");
// Note : les opérations joueur qui changent de monde utilisent des patterns async
// Toujours utiliser PlayerRef pour suivre le joueur entre changements de monde
```
## Bonnes Pratiques
{{< callout type="tip" >}}
- Utilisez toujours `PlayerRef` pour passer des références de joueurs à travers les limites async
- Vérifiez `world.isInThread()` avant de modifier l'état du monde
- Utilisez `CompletableFuture` pour les opérations async base de données/réseau
- Gardez les opérations du thread de monde rapides (< 33ms par tick)
- Utilisez des pools de connexions pour l'accès base de données
- Rappelez-vous : chaque monde a son propre thread - pas de "thread principal" unique
{{< /callout >}}
## Déboguer les Problèmes de Threading
Si vous rencontrez des problèmes de threading :
1. Vérifiez `world.isInThread()` avant les modifications de monde
2. Utilisez `PlayerRef` au lieu de références directes `Player` pour le travail async
3. Loguez le thread courant : `Thread.currentThread().getName()`
4. Les noms de threads de monde suivent le pattern : `World-{worldName}`