Init
This commit is contained in:
54
.github/workflows/hugo.yml
vendored
Normal file
54
.github/workflows/hugo.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Deploy Hugo site to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
hugo-version: 'latest'
|
||||
extended: true
|
||||
|
||||
- name: Build
|
||||
run: hugo --minify
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./public
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
public/
|
||||
.hugo_build.lock
|
||||
resources/_gen/
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "themes/hextra"]
|
||||
path = themes/hextra
|
||||
url = git@github.com:imfing/hextra.git
|
||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM hugomods/hugo:exts-0.154.5
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
EXPOSE 1313
|
||||
|
||||
ENTRYPOINT ["hugo"]
|
||||
CMD ["server", "--bind=0.0.0.0", "--port=1313", "--disableFastRender"]
|
||||
53
README.fr.md
Normal file
53
README.fr.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Hytale Plugin Documentation
|
||||
|
||||
Documentation complète pour le développement de plugins serveur Hytale.
|
||||
|
||||
## Accéder à la documentation
|
||||
|
||||
**https://hytale-france.github.io**
|
||||
|
||||
## Versioning
|
||||
|
||||
Les versions de la documentation suivent ce format :
|
||||
```
|
||||
v{DOC_VERSION}-HY#{HYTALE_DATE}-{HYTALE_BUILD}
|
||||
```
|
||||
|
||||
Exemple : `v0.0.1-HY#2026.01.13-dcad8778f`
|
||||
|
||||
| Partie | Description |
|
||||
|--------|-------------|
|
||||
| `v0.0.1` | Version de la documentation (semantic versioning) |
|
||||
| `HY#2026.01.13` | Date de la version du serveur Hytale |
|
||||
| `dcad8778f` | Hash du build Hytale |
|
||||
|
||||
Cela garantit que la documentation est toujours liée à une version spécifique du serveur Hytale.
|
||||
|
||||
## Contenu
|
||||
|
||||
La documentation couvre tous les aspects du développement de plugins :
|
||||
|
||||
- **Getting Started** - Configuration de l'environnement et création de votre premier plugin
|
||||
- **Core Concepts** - Registres, assets, codecs et threading
|
||||
- **Commands** - Création de commandes personnalisées avec arguments
|
||||
- **Events** - Système d'événements et listeners
|
||||
- **Entities** - Entités, joueurs et système ECS
|
||||
- **World** - Univers, mondes, chunks et blocs
|
||||
- **Inventory** - Items, inventaires et conteneurs
|
||||
- **Tasks** - Tâches planifiées et asynchrones
|
||||
- **Effects** - Particules, lumières dynamiques et effets d'entité
|
||||
- **Reference** - Référence API et schéma du manifest
|
||||
|
||||
## Langues
|
||||
|
||||
La documentation est disponible en :
|
||||
- English
|
||||
- Français
|
||||
|
||||
## Contribuer
|
||||
|
||||
Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
|
||||
|
||||
## Licence
|
||||
|
||||
Ce projet est maintenu par la communauté Hytale France.
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Hytale Plugin Documentation
|
||||
|
||||
Comprehensive documentation for Hytale server plugin development.
|
||||
|
||||
## Access the documentation
|
||||
|
||||
**https://hytale-france.github.io**
|
||||
|
||||
## Versioning
|
||||
|
||||
Documentation versions follow this format:
|
||||
```
|
||||
v{DOC_VERSION}-HY#{HYTALE_DATE}-{HYTALE_BUILD}
|
||||
```
|
||||
|
||||
Example: `v0.0.1-HY#2026.01.13-dcad8778f`
|
||||
|
||||
| Part | Description |
|
||||
|------|-------------|
|
||||
| `v0.0.1` | Documentation version (semantic versioning) |
|
||||
| `HY#2026.01.13` | Hytale server version date |
|
||||
| `dcad8778f` | Hytale build hash |
|
||||
|
||||
This ensures documentation is always tied to a specific Hytale server version.
|
||||
|
||||
## Content
|
||||
|
||||
The documentation covers all aspects of plugin development:
|
||||
|
||||
- **Getting Started** - Set up your environment and create your first plugin
|
||||
- **Core Concepts** - Registries, assets, codecs and threading
|
||||
- **Commands** - Create custom commands with arguments
|
||||
- **Events** - Event system and listeners
|
||||
- **Entities** - Entities, players and ECS system
|
||||
- **World** - Universes, worlds, chunks and blocks
|
||||
- **Inventory** - Items, inventories and containers
|
||||
- **Tasks** - Scheduled and asynchronous tasks
|
||||
- **Effects** - Particles, dynamic lights and entity effects
|
||||
- **Reference** - API reference and manifest schema
|
||||
|
||||
## Languages
|
||||
|
||||
Documentation is available in:
|
||||
- English
|
||||
- Français
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Feel free to open an issue or pull request.
|
||||
|
||||
## License
|
||||
|
||||
This project is maintained by the Hytale France community.
|
||||
5
archetypes/default.md
Normal file
5
archetypes/default.md
Normal file
@@ -0,0 +1,5 @@
|
||||
+++
|
||||
date = '{{ .Date }}'
|
||||
draft = true
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
+++
|
||||
100
content/_index.en.md
Normal file
100
content/_index.en.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Hytale Plugin Documentation
|
||||
layout: hextra-home
|
||||
---
|
||||
|
||||
{{< hextra/hero-badge >}}
|
||||
<span>Hytale Server API</span>
|
||||
{{< /hextra/hero-badge >}}
|
||||
|
||||
<div class="hx-mt-6 hx-mb-6">
|
||||
{{< hextra/hero-headline >}}
|
||||
Build Plugins for Hytale
|
||||
{{< /hextra/hero-headline >}}
|
||||
</div>
|
||||
|
||||
<div class="hx-mb-12">
|
||||
{{< hextra/hero-subtitle >}}
|
||||
Comprehensive documentation for creating Hytale server plugins. <br class="sm:hx-block hx-hidden" />From your first plugin to advanced features.
|
||||
{{< /hextra/hero-subtitle >}}
|
||||
</div>
|
||||
|
||||
<div class="hx-mt-6 hx-mb-12"> </div>
|
||||
|
||||
<div class="hx-mt-12">
|
||||
{{< hextra/hero-button text="Join Discord" link="https://discord.gg/4UPCz84Nst" icon="discord" >}}
|
||||
</div>
|
||||
|
||||
<div class="hx-mt-6 hx-mb-12"> </div>
|
||||
|
||||
## Explore the Documentation
|
||||
|
||||
<div class="hx-mt-6 hx-mb-12"> </div>
|
||||
|
||||
{{< hextra/feature-grid >}}
|
||||
{{< hextra/feature-card
|
||||
title="Getting Started"
|
||||
subtitle="Set up your environment and create your first plugin"
|
||||
link="getting-started"
|
||||
icon="play"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Core Concepts"
|
||||
subtitle="Registries, assets, codecs, commands, events, and tasks"
|
||||
link="core-concepts"
|
||||
icon="cube"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Gameplay Systems"
|
||||
subtitle="Farming, shops, reputation, memories, and objectives"
|
||||
link="gameplay-systems"
|
||||
icon="puzzle"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="World"
|
||||
subtitle="Universes, chunks, blocks, entities, worldgen, and portals"
|
||||
link="world"
|
||||
icon="globe"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="UI Systems"
|
||||
subtitle="HUD, custom pages, windows, inventory, and permissions"
|
||||
link="ui-systems"
|
||||
icon="desktop-computer"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Advanced"
|
||||
subtitle="Networking, effects, particles, and dynamic lights"
|
||||
link="advanced"
|
||||
icon="lightning-bolt"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Reference"
|
||||
subtitle="API reference, manifest schema, and registries"
|
||||
link="reference"
|
||||
icon="book-open"
|
||||
>}}
|
||||
{{< /hextra/feature-grid >}}
|
||||
|
||||
<div class="hx-mt-6"> </div>
|
||||
|
||||
## Quick Example
|
||||
|
||||
Here's a simple plugin that logs a message when a player joins:
|
||||
|
||||
```java
|
||||
public class MyPlugin extends JavaPlugin {
|
||||
@Override
|
||||
public void start() {
|
||||
getEventRegistry().register(PlayerConnectEvent.class, event -> {
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
getLogger().at(Level.INFO).log("Plugin is shutting down!");
|
||||
}
|
||||
}
|
||||
```
|
||||
100
content/_index.fr.md
Normal file
100
content/_index.fr.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Documentation des Plugins Hytale
|
||||
layout: hextra-home
|
||||
---
|
||||
|
||||
{{< hextra/hero-badge >}}
|
||||
<span>API Serveur Hytale</span>
|
||||
{{< /hextra/hero-badge >}}
|
||||
|
||||
<div class="hx-mt-6 hx-mb-6">
|
||||
{{< hextra/hero-headline >}}
|
||||
Créez des Plugins pour Hytale
|
||||
{{< /hextra/hero-headline >}}
|
||||
</div>
|
||||
|
||||
<div class="hx-mb-12">
|
||||
{{< hextra/hero-subtitle >}}
|
||||
Documentation complète pour la création de plugins serveur Hytale. <br class="sm:hx-block hx-hidden" />De votre premier plugin aux fonctionnalités avancées.
|
||||
{{< /hextra/hero-subtitle >}}
|
||||
</div>
|
||||
|
||||
<div class="hx-mt-6 hx-mb-12"> </div>
|
||||
|
||||
<div class="hx-mt-12">
|
||||
{{< hextra/hero-button text="Rejoindre Discord" link="https://discord.gg/4UPCz84Nst" icon="discord" >}}
|
||||
</div>
|
||||
|
||||
<div class="hx-mt-6 hx-mb-12"> </div>
|
||||
|
||||
## Explorer la Documentation
|
||||
|
||||
<div class="hx-mt-6 hx-mb-12"> </div>
|
||||
|
||||
{{< hextra/feature-grid >}}
|
||||
{{< hextra/feature-card
|
||||
title="Premiers Pas"
|
||||
subtitle="Configurez votre environnement et créez votre premier plugin"
|
||||
link="getting-started"
|
||||
icon="play"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Concepts de Base"
|
||||
subtitle="Registres, assets, codecs, commandes, événements et tâches"
|
||||
link="core-concepts"
|
||||
icon="cube"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Systèmes de Gameplay"
|
||||
subtitle="Farming, boutiques, réputation, mémoires et objectifs"
|
||||
link="gameplay-systems"
|
||||
icon="puzzle"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Monde"
|
||||
subtitle="Univers, chunks, blocs, entités, worldgen et portails"
|
||||
link="world"
|
||||
icon="globe"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Systèmes UI"
|
||||
subtitle="HUD, pages personnalisées, fenêtres, inventaire et permissions"
|
||||
link="ui-systems"
|
||||
icon="desktop-computer"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Avancé"
|
||||
subtitle="Réseau, effets, particules et lumières dynamiques"
|
||||
link="advanced"
|
||||
icon="lightning-bolt"
|
||||
>}}
|
||||
{{< hextra/feature-card
|
||||
title="Référence"
|
||||
subtitle="Référence API, schéma du manifest et registres"
|
||||
link="reference"
|
||||
icon="book-open"
|
||||
>}}
|
||||
{{< /hextra/feature-grid >}}
|
||||
|
||||
<div class="hx-mt-6"> </div>
|
||||
|
||||
## Exemple Rapide
|
||||
|
||||
Voici un plugin simple qui affiche un message quand un joueur rejoint :
|
||||
|
||||
```java
|
||||
public class MyPlugin extends JavaPlugin {
|
||||
@Override
|
||||
public void start() {
|
||||
getEventRegistry().register(PlayerConnectEvent.class, event -> {
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
getLogger().at(Level.INFO).log("Joueur en connexion : " + playerRef.getUsername());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
getLogger().at(Level.INFO).log("Le plugin s'arrête !");
|
||||
}
|
||||
}
|
||||
```
|
||||
21
content/_index.md
Normal file
21
content/_index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "Redirecting..."
|
||||
layout: "redirect"
|
||||
---
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var supportedLangs = ['en', 'fr'];
|
||||
var defaultLang = 'en';
|
||||
var userLang = navigator.language || navigator.userLanguage;
|
||||
var langCode = userLang.split('-')[0].toLowerCase();
|
||||
var targetLang = supportedLangs.includes(langCode) ? langCode : defaultLang;
|
||||
window.location.replace('/' + targetLang + '/');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="0; url=/en/">
|
||||
</noscript>
|
||||
|
||||
<p>Redirecting... <a href="/en/">English</a> | <a href="/fr/">Français</a></p>
|
||||
34
content/advanced/_index.en.md
Normal file
34
content/advanced/_index.en.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Advanced
|
||||
type: docs
|
||||
weight: 6
|
||||
---
|
||||
|
||||
Advanced systems for networking, effects, and other specialized functionality.
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="networking" title="Networking" subtitle="Network communication and protocols" icon="wifi" >}}
|
||||
{{< card link="effects" title="Effects" subtitle="Particles, dynamic lights, and entity effects" icon="sparkles" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
## Networking
|
||||
|
||||
Network communication allows plugins to send custom data between server and clients:
|
||||
|
||||
```java
|
||||
// Send custom packet
|
||||
CustomPacket packet = new CustomPacket(data);
|
||||
player.sendPacket(packet);
|
||||
```
|
||||
|
||||
## Effects
|
||||
|
||||
Create visual effects using particles, lights, and entity animations:
|
||||
|
||||
```java
|
||||
// Spawn particles
|
||||
world.spawnParticles(ParticleType.SMOKE, position, count);
|
||||
|
||||
// Add dynamic light
|
||||
entity.addComponent(new DynamicLight(Color.YELLOW, 10.0f));
|
||||
```
|
||||
34
content/advanced/_index.fr.md
Normal file
34
content/advanced/_index.fr.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Avancé
|
||||
type: docs
|
||||
weight: 6
|
||||
---
|
||||
|
||||
Systèmes avancés pour le réseau, les effets et autres fonctionnalités spécialisées.
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="networking" title="Réseau" subtitle="Communication réseau et protocoles" icon="wifi" >}}
|
||||
{{< card link="effects" title="Effets" subtitle="Particules, lumières dynamiques et effets d'entité" icon="sparkles" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
## Réseau
|
||||
|
||||
La communication réseau permet aux plugins d'envoyer des données personnalisées entre le serveur et les clients :
|
||||
|
||||
```java
|
||||
// Envoyer un paquet personnalisé
|
||||
CustomPacket packet = new CustomPacket(data);
|
||||
player.sendPacket(packet);
|
||||
```
|
||||
|
||||
## Effets
|
||||
|
||||
Créez des effets visuels avec des particules, lumières et animations d'entités :
|
||||
|
||||
```java
|
||||
// Faire apparaître des particules
|
||||
world.spawnParticles(ParticleType.SMOKE, position, count);
|
||||
|
||||
// Ajouter une lumière dynamique
|
||||
entity.addComponent(new DynamicLight(Color.YELLOW, 10.0f));
|
||||
```
|
||||
92
content/advanced/effects/_index.en.md
Normal file
92
content/advanced/effects/_index.en.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Effects
|
||||
type: docs
|
||||
weight: 10
|
||||
---
|
||||
|
||||
Visual effects in Hytale include particles, dynamic lights, and entity status effects.
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="particles" title="Particles" subtitle="Spawn and configure particle systems" >}}
|
||||
{{< card link="dynamic-lights" title="Dynamic Lights" subtitle="Glow effects on entities" >}}
|
||||
{{< card link="entity-effects" title="Entity Effects" subtitle="Status effects with visuals" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
## Overview
|
||||
|
||||
Hytale provides three complementary visual effect systems:
|
||||
|
||||
| System | Purpose | Use Cases |
|
||||
|--------|---------|-----------|
|
||||
| **Particles** | Visual atmosphere and feedback | Explosions, trails, ambient effects |
|
||||
| **Dynamic Lights** | Glow effects on entities | Glowing items, magic auras |
|
||||
| **Entity Effects** | Status effects with visuals | Buffs, debuffs, status indicators |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Spawning Particles
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.universe.world.ParticleUtil;
|
||||
|
||||
// Spawn particles at a position
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"explosion_small", // Particle system ID
|
||||
position, // Vector3d position
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
### Adding Dynamic Light
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.protocol.ColorLight;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight;
|
||||
|
||||
// Create a colored light (radius, R, G, B)
|
||||
ColorLight light = new ColorLight(
|
||||
(byte) 15, // Radius
|
||||
(byte) 255, // Red
|
||||
(byte) 100, // Green
|
||||
(byte) 50 // Blue
|
||||
);
|
||||
|
||||
// Add to entity
|
||||
DynamicLight dynamicLight = new DynamicLight(light);
|
||||
componentAccessor.putComponent(entityRef, DynamicLight.getComponentType(), dynamicLight);
|
||||
```
|
||||
|
||||
### Applying Entity Effects
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent;
|
||||
|
||||
// Get effect controller
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
// Apply effect
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset("fire_resistance");
|
||||
controller.addEffect(entityRef, effect, componentAccessor);
|
||||
```
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Package | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `ParticleUtil` | server.core.universe.world | Spawn particles |
|
||||
| `ParticleSystem` | asset.type.particle.config | System configuration |
|
||||
| `DynamicLight` | modules.entity.component | Entity glow effects |
|
||||
| `ColorLight` | protocol | Light color/radius data |
|
||||
| `EntityEffect` | asset.type.entityeffect.config | Status effect definition |
|
||||
|
||||
## Network Considerations
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Network Details:**
|
||||
- Particles are sent within 75 blocks by default (`DEFAULT_PARTICLE_DISTANCE`)
|
||||
- DynamicLight changes sync automatically when marked as outdated
|
||||
- Entity effects sync via `EffectControllerComponent`
|
||||
{{< /callout >}}
|
||||
92
content/advanced/effects/_index.fr.md
Normal file
92
content/advanced/effects/_index.fr.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Effets
|
||||
type: docs
|
||||
weight: 10
|
||||
---
|
||||
|
||||
Les effets visuels dans Hytale incluent les particules, les lumières dynamiques et les effets de statut d'entité.
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="particles" title="Particules" subtitle="Générer et configurer les systèmes de particules" >}}
|
||||
{{< card link="dynamic-lights" title="Lumières Dynamiques" subtitle="Effets de lueur sur les entités" >}}
|
||||
{{< card link="entity-effects" title="Effets d'Entité" subtitle="Effets de statut avec visuels" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
## Aperçu
|
||||
|
||||
Hytale fournit trois systèmes d'effets visuels complémentaires :
|
||||
|
||||
| Système | Objectif | Cas d'Usage |
|
||||
|---------|----------|-------------|
|
||||
| **Particules** | Atmosphère visuelle et feedback | Explosions, traînées, effets ambiants |
|
||||
| **Lumières Dynamiques** | Effets de lueur sur les entités | Objets lumineux, auras magiques |
|
||||
| **Effets d'Entité** | Effets de statut avec visuels | Buffs, debuffs, indicateurs de statut |
|
||||
|
||||
## Démarrage Rapide
|
||||
|
||||
### Générer des Particules
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.universe.world.ParticleUtil;
|
||||
|
||||
// Générer des particules à une position
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"explosion_small", // ID du système de particules
|
||||
position, // Position Vector3d
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
### Ajouter une Lumière Dynamique
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.protocol.ColorLight;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight;
|
||||
|
||||
// Créer une lumière colorée (rayon, R, V, B)
|
||||
ColorLight light = new ColorLight(
|
||||
(byte) 15, // Rayon
|
||||
(byte) 255, // Rouge
|
||||
(byte) 100, // Vert
|
||||
(byte) 50 // Bleu
|
||||
);
|
||||
|
||||
// Ajouter à l'entité
|
||||
DynamicLight dynamicLight = new DynamicLight(light);
|
||||
componentAccessor.putComponent(entityRef, DynamicLight.getComponentType(), dynamicLight);
|
||||
```
|
||||
|
||||
### Appliquer des Effets d'Entité
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent;
|
||||
|
||||
// Obtenir le contrôleur d'effets
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
// Appliquer l'effet
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset("fire_resistance");
|
||||
controller.addEffect(entityRef, effect, componentAccessor);
|
||||
```
|
||||
|
||||
## Classes Principales
|
||||
|
||||
| Classe | Package | Objectif |
|
||||
|--------|---------|----------|
|
||||
| `ParticleUtil` | server.core.universe.world | Générer des particules |
|
||||
| `ParticleSystem` | asset.type.particle.config | Configuration du système |
|
||||
| `DynamicLight` | modules.entity.component | Effets de lueur d'entité |
|
||||
| `ColorLight` | protocol | Données couleur/rayon de lumière |
|
||||
| `EntityEffect` | asset.type.entityeffect.config | Définition d'effet de statut |
|
||||
|
||||
## Considérations Réseau
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Détails Réseau :**
|
||||
- Les particules sont envoyées dans un rayon de 75 blocs par défaut (`DEFAULT_PARTICLE_DISTANCE`)
|
||||
- Les changements de DynamicLight se synchronisent automatiquement quand marqués obsolètes
|
||||
- Les effets d'entité se synchronisent via `EffectControllerComponent`
|
||||
{{< /callout >}}
|
||||
268
content/advanced/effects/dynamic-lights.en.md
Normal file
268
content/advanced/effects/dynamic-lights.en.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
title: Dynamic Lights
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Dynamic lights add glow effects to entities using the `DynamicLight` component.
|
||||
|
||||
## ColorLight Structure
|
||||
|
||||
The `ColorLight` class defines light properties:
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.protocol.ColorLight;
|
||||
|
||||
public class ColorLight {
|
||||
public byte radius; // 0-255 (light radius/intensity)
|
||||
public byte red; // 0-255 (red channel)
|
||||
public byte green; // 0-255 (green channel)
|
||||
public byte blue; // 0-255 (blue channel)
|
||||
|
||||
public ColorLight(byte radius, byte red, byte green, byte blue) {
|
||||
this.radius = radius;
|
||||
this.red = red;
|
||||
this.green = green;
|
||||
this.blue = blue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DynamicLight Component
|
||||
|
||||
Add temporary glow effects to entities:
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight;
|
||||
import com.hypixel.hytale.protocol.ColorLight;
|
||||
|
||||
// Create colored light (radius, R, G, B)
|
||||
ColorLight light = new ColorLight(
|
||||
(byte) 15, // Radius (0-255)
|
||||
(byte) 255, // Red (0-255)
|
||||
(byte) 100, // Green (0-255)
|
||||
(byte) 50 // Blue (0-255)
|
||||
);
|
||||
|
||||
// Add DynamicLight component
|
||||
DynamicLight dynamicLight = new DynamicLight(light);
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType(),
|
||||
dynamicLight
|
||||
);
|
||||
```
|
||||
|
||||
## PersistentDynamicLight Component
|
||||
|
||||
Use this component when light should be saved with entity data:
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.PersistentDynamicLight;
|
||||
|
||||
// Create persistent light
|
||||
PersistentDynamicLight persistentLight = new PersistentDynamicLight(light);
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
PersistentDynamicLight.getComponentType(),
|
||||
persistentLight
|
||||
);
|
||||
```
|
||||
|
||||
## Updating Light Properties
|
||||
|
||||
```java
|
||||
// Get existing light component
|
||||
DynamicLight dynamicLight = store.getComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
|
||||
if (dynamicLight != null) {
|
||||
// Create new light properties
|
||||
ColorLight newLight = new ColorLight(
|
||||
(byte) 20, // New radius
|
||||
(byte) 0, // No red
|
||||
(byte) 255, // Full green
|
||||
(byte) 100 // Some blue
|
||||
);
|
||||
|
||||
// Update and mark for network sync
|
||||
dynamicLight.setColorLight(newLight);
|
||||
// isNetworkOutdated is set automatically
|
||||
}
|
||||
```
|
||||
|
||||
## Removing Light
|
||||
|
||||
```java
|
||||
// Remove the component to disable light
|
||||
componentAccessor.removeComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
```
|
||||
|
||||
## Common Light Configurations
|
||||
|
||||
### Fire/Warm Glow
|
||||
|
||||
```java
|
||||
ColorLight fireLight = new ColorLight(
|
||||
(byte) 12,
|
||||
(byte) 255, // High red
|
||||
(byte) 150, // Medium orange
|
||||
(byte) 50 // Low blue
|
||||
);
|
||||
```
|
||||
|
||||
### Ice/Cold Glow
|
||||
|
||||
```java
|
||||
ColorLight iceLight = new ColorLight(
|
||||
(byte) 10,
|
||||
(byte) 100, // Low red
|
||||
(byte) 200, // Medium green
|
||||
(byte) 255 // High blue
|
||||
);
|
||||
```
|
||||
|
||||
### Magic/Purple Glow
|
||||
|
||||
```java
|
||||
ColorLight magicLight = new ColorLight(
|
||||
(byte) 15,
|
||||
(byte) 200, // High red
|
||||
(byte) 50, // Low green
|
||||
(byte) 255 // High blue
|
||||
);
|
||||
```
|
||||
|
||||
### Healing/Green Glow
|
||||
|
||||
```java
|
||||
ColorLight healLight = new ColorLight(
|
||||
(byte) 10,
|
||||
(byte) 50, // Low red
|
||||
(byte) 255, // High green
|
||||
(byte) 100 // Medium blue
|
||||
);
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Adding a Glow Effect to a Player
|
||||
|
||||
```java
|
||||
// Add temporary glow effect to a player
|
||||
public void addGlowEffect(Player player, ComponentAccessor<EntityStore> componentAccessor,
|
||||
ScheduledExecutorService scheduler) {
|
||||
World world = player.getWorld();
|
||||
|
||||
// Add temporary glow
|
||||
ColorLight glow = new ColorLight(
|
||||
(byte) 8,
|
||||
(byte) 255,
|
||||
(byte) 215,
|
||||
(byte) 0 // Gold color
|
||||
);
|
||||
|
||||
DynamicLight light = new DynamicLight(glow);
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType(),
|
||||
light
|
||||
);
|
||||
|
||||
// Remove after 3 seconds using standard Java scheduling
|
||||
scheduler.schedule(() -> {
|
||||
world.execute(() -> {
|
||||
if (entityRef.isValid()) {
|
||||
componentAccessor.removeComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
}
|
||||
});
|
||||
}, 3, TimeUnit.SECONDS);
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Note:** Use `player.getReference()` to get the entity reference, not `player.getEntityRef()`. Use standard Java `ScheduledExecutorService` for delayed tasks.
|
||||
{{< /callout >}}
|
||||
|
||||
### Pulsing Light Effect
|
||||
|
||||
Implement pulsing effects by updating the light in a tick handler:
|
||||
|
||||
```java
|
||||
// Track pulsing entities
|
||||
private final Map<Ref<EntityStore>, Integer> pulsingEntities = new ConcurrentHashMap<>();
|
||||
|
||||
// Call this method each tick
|
||||
public void onTick(float deltaTime) {
|
||||
for (Map.Entry<Ref<EntityStore>, Integer> entry : pulsingEntities.entrySet()) {
|
||||
Ref<EntityStore> entityRef = entry.getKey();
|
||||
int tick = entry.getValue() + 1;
|
||||
entry.setValue(tick);
|
||||
|
||||
// Calculate pulsing intensity
|
||||
double pulse = Math.sin(tick * 0.1) * 0.5 + 0.5;
|
||||
byte radius = (byte) (5 + pulse * 10);
|
||||
|
||||
ColorLight light = new ColorLight(
|
||||
radius,
|
||||
(byte) 255,
|
||||
(byte) (int)(100 + pulse * 100),
|
||||
(byte) 50
|
||||
);
|
||||
|
||||
DynamicLight dynamicLight = store.getComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
|
||||
if (dynamicLight != null) {
|
||||
dynamicLight.setColorLight(light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startPulsingLight(Ref<EntityStore> entityRef) {
|
||||
pulsingEntities.put(entityRef, 0);
|
||||
}
|
||||
|
||||
public void stopPulsingLight(Ref<EntityStore> entityRef) {
|
||||
pulsingEntities.remove(entityRef);
|
||||
}
|
||||
```
|
||||
|
||||
## Network Synchronization
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Sync Details:**
|
||||
- `DynamicLight` changes are automatically marked for sync when using `setColorLight()`
|
||||
- The `isNetworkOutdated` flag triggers client updates
|
||||
- Use `consumeNetworkOutdated()` before manual sync operations
|
||||
{{< /callout >}}
|
||||
|
||||
## DynamicLight vs PersistentDynamicLight
|
||||
|
||||
| Feature | DynamicLight | PersistentDynamicLight |
|
||||
|---------|--------------|------------------------|
|
||||
| Saved to disk | No | Yes |
|
||||
| Survives restart | No | Yes |
|
||||
| Use case | Temporary effects | Permanent glows |
|
||||
| Performance | Lighter | Slightly heavier |
|
||||
|
||||
## Best Practices
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Light Guidelines:**
|
||||
- Keep radius values reasonable (5-20 for most effects)
|
||||
- Don't add lights to too many entities simultaneously
|
||||
- Remove temporary lights when no longer needed
|
||||
- Use `PersistentDynamicLight` only when persistence is required
|
||||
{{< /callout >}}
|
||||
268
content/advanced/effects/dynamic-lights.fr.md
Normal file
268
content/advanced/effects/dynamic-lights.fr.md
Normal file
@@ -0,0 +1,268 @@
|
||||
---
|
||||
title: Lumières Dynamiques
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Les lumières dynamiques ajoutent des effets de lueur aux entités en utilisant le composant `DynamicLight`.
|
||||
|
||||
## Structure ColorLight
|
||||
|
||||
La classe `ColorLight` définit les propriétés de la lumière :
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.protocol.ColorLight;
|
||||
|
||||
public class ColorLight {
|
||||
public byte radius; // 0-255 (rayon/intensité de la lumière)
|
||||
public byte red; // 0-255 (canal rouge)
|
||||
public byte green; // 0-255 (canal vert)
|
||||
public byte blue; // 0-255 (canal bleu)
|
||||
|
||||
public ColorLight(byte radius, byte red, byte green, byte blue) {
|
||||
this.radius = radius;
|
||||
this.red = red;
|
||||
this.green = green;
|
||||
this.blue = blue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Composant DynamicLight
|
||||
|
||||
Ajoutez des effets de lueur temporaires aux entités :
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.DynamicLight;
|
||||
import com.hypixel.hytale.protocol.ColorLight;
|
||||
|
||||
// Créer une lumière colorée (rayon, R, V, B)
|
||||
ColorLight light = new ColorLight(
|
||||
(byte) 15, // Rayon (0-255)
|
||||
(byte) 255, // Rouge (0-255)
|
||||
(byte) 100, // Vert (0-255)
|
||||
(byte) 50 // Bleu (0-255)
|
||||
);
|
||||
|
||||
// Ajouter le composant DynamicLight
|
||||
DynamicLight dynamicLight = new DynamicLight(light);
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType(),
|
||||
dynamicLight
|
||||
);
|
||||
```
|
||||
|
||||
## Composant PersistentDynamicLight
|
||||
|
||||
Utilisez ce composant quand la lumière doit être sauvegardée avec les données de l'entité :
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.PersistentDynamicLight;
|
||||
|
||||
// Créer une lumière persistante
|
||||
PersistentDynamicLight persistentLight = new PersistentDynamicLight(light);
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
PersistentDynamicLight.getComponentType(),
|
||||
persistentLight
|
||||
);
|
||||
```
|
||||
|
||||
## Mettre à Jour les Propriétés de Lumière
|
||||
|
||||
```java
|
||||
// Obtenir le composant de lumière existant
|
||||
DynamicLight dynamicLight = store.getComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
|
||||
if (dynamicLight != null) {
|
||||
// Créer de nouvelles propriétés de lumière
|
||||
ColorLight newLight = new ColorLight(
|
||||
(byte) 20, // Nouveau rayon
|
||||
(byte) 0, // Pas de rouge
|
||||
(byte) 255, // Vert maximum
|
||||
(byte) 100 // Un peu de bleu
|
||||
);
|
||||
|
||||
// Mettre à jour et marquer pour sync réseau
|
||||
dynamicLight.setColorLight(newLight);
|
||||
// isNetworkOutdated est défini automatiquement
|
||||
}
|
||||
```
|
||||
|
||||
## Supprimer la Lumière
|
||||
|
||||
```java
|
||||
// Supprimer le composant pour désactiver la lumière
|
||||
componentAccessor.removeComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
```
|
||||
|
||||
## Configurations de Lumière Courantes
|
||||
|
||||
### Lueur Feu/Chaleur
|
||||
|
||||
```java
|
||||
ColorLight fireLight = new ColorLight(
|
||||
(byte) 12,
|
||||
(byte) 255, // Rouge élevé
|
||||
(byte) 150, // Orange moyen
|
||||
(byte) 50 // Bleu faible
|
||||
);
|
||||
```
|
||||
|
||||
### Lueur Glace/Froid
|
||||
|
||||
```java
|
||||
ColorLight iceLight = new ColorLight(
|
||||
(byte) 10,
|
||||
(byte) 100, // Rouge faible
|
||||
(byte) 200, // Vert moyen
|
||||
(byte) 255 // Bleu élevé
|
||||
);
|
||||
```
|
||||
|
||||
### Lueur Magie/Violet
|
||||
|
||||
```java
|
||||
ColorLight magicLight = new ColorLight(
|
||||
(byte) 15,
|
||||
(byte) 200, // Rouge élevé
|
||||
(byte) 50, // Vert faible
|
||||
(byte) 255 // Bleu élevé
|
||||
);
|
||||
```
|
||||
|
||||
### Lueur Soin/Vert
|
||||
|
||||
```java
|
||||
ColorLight healLight = new ColorLight(
|
||||
(byte) 10,
|
||||
(byte) 50, // Rouge faible
|
||||
(byte) 255, // Vert élevé
|
||||
(byte) 100 // Bleu moyen
|
||||
);
|
||||
```
|
||||
|
||||
## Exemples Pratiques
|
||||
|
||||
### Ajouter un Effet de Lueur à un Joueur
|
||||
|
||||
```java
|
||||
// Ajouter un effet de lueur temporaire à un joueur
|
||||
public void addGlowEffect(Player player, ComponentAccessor<EntityStore> componentAccessor,
|
||||
ScheduledExecutorService scheduler) {
|
||||
World world = player.getWorld();
|
||||
|
||||
// Ajouter une lueur temporaire
|
||||
ColorLight glow = new ColorLight(
|
||||
(byte) 8,
|
||||
(byte) 255,
|
||||
(byte) 215,
|
||||
(byte) 0 // Couleur dorée
|
||||
);
|
||||
|
||||
DynamicLight light = new DynamicLight(glow);
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType(),
|
||||
light
|
||||
);
|
||||
|
||||
// Supprimer après 3 secondes avec le scheduling Java standard
|
||||
scheduler.schedule(() -> {
|
||||
world.execute(() -> {
|
||||
if (entityRef.isValid()) {
|
||||
componentAccessor.removeComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
}
|
||||
});
|
||||
}, 3, TimeUnit.SECONDS);
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Note :** Utilisez `player.getReference()` pour obtenir la référence d'entité, pas `player.getEntityRef()`. Utilisez le `ScheduledExecutorService` Java standard pour les tâches différées.
|
||||
{{< /callout >}}
|
||||
|
||||
### Effet de Lumière Pulsante
|
||||
|
||||
Implémentez les effets pulsants en mettant à jour la lumière dans un gestionnaire de tick :
|
||||
|
||||
```java
|
||||
// Suivre les entités pulsantes
|
||||
private final Map<Ref<EntityStore>, Integer> pulsingEntities = new ConcurrentHashMap<>();
|
||||
|
||||
// Appeler cette méthode à chaque tick
|
||||
public void onTick(float deltaTime) {
|
||||
for (Map.Entry<Ref<EntityStore>, Integer> entry : pulsingEntities.entrySet()) {
|
||||
Ref<EntityStore> entityRef = entry.getKey();
|
||||
int tick = entry.getValue() + 1;
|
||||
entry.setValue(tick);
|
||||
|
||||
// Calculer l'intensité pulsante
|
||||
double pulse = Math.sin(tick * 0.1) * 0.5 + 0.5;
|
||||
byte radius = (byte) (5 + pulse * 10);
|
||||
|
||||
ColorLight light = new ColorLight(
|
||||
radius,
|
||||
(byte) 255,
|
||||
(byte) (int)(100 + pulse * 100),
|
||||
(byte) 50
|
||||
);
|
||||
|
||||
DynamicLight dynamicLight = store.getComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType()
|
||||
);
|
||||
|
||||
if (dynamicLight != null) {
|
||||
dynamicLight.setColorLight(light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startPulsingLight(Ref<EntityStore> entityRef) {
|
||||
pulsingEntities.put(entityRef, 0);
|
||||
}
|
||||
|
||||
public void stopPulsingLight(Ref<EntityStore> entityRef) {
|
||||
pulsingEntities.remove(entityRef);
|
||||
}
|
||||
```
|
||||
|
||||
## Synchronisation Réseau
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Détails de Sync :**
|
||||
- Les changements de `DynamicLight` sont automatiquement marqués pour sync lors de l'utilisation de `setColorLight()`
|
||||
- Le flag `isNetworkOutdated` déclenche les mises à jour client
|
||||
- Utilisez `consumeNetworkOutdated()` avant les opérations de sync manuelles
|
||||
{{< /callout >}}
|
||||
|
||||
## DynamicLight vs PersistentDynamicLight
|
||||
|
||||
| Fonctionnalité | DynamicLight | PersistentDynamicLight |
|
||||
|----------------|--------------|------------------------|
|
||||
| Sauvegardé sur disque | Non | Oui |
|
||||
| Survit au redémarrage | Non | Oui |
|
||||
| Cas d'usage | Effets temporaires | Lueurs permanentes |
|
||||
| Performance | Plus léger | Légèrement plus lourd |
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Directives pour les Lumières :**
|
||||
- Gardez des valeurs de rayon raisonnables (5-20 pour la plupart des effets)
|
||||
- N'ajoutez pas de lumières à trop d'entités simultanément
|
||||
- Supprimez les lumières temporaires quand plus nécessaires
|
||||
- Utilisez `PersistentDynamicLight` seulement quand la persistance est requise
|
||||
{{< /callout >}}
|
||||
369
content/advanced/effects/entity-effects.en.md
Normal file
369
content/advanced/effects/entity-effects.en.md
Normal file
@@ -0,0 +1,369 @@
|
||||
---
|
||||
title: Entity Effects
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
Entity effects are status effects that can include visual components, stat modifiers, and duration-based behaviors.
|
||||
|
||||
## EntityEffect Configuration
|
||||
|
||||
```java
|
||||
public class EntityEffect {
|
||||
protected String id;
|
||||
protected String name; // Localization key
|
||||
|
||||
protected ApplicationEffects applicationEffects; // Visual/audio effects
|
||||
|
||||
protected String modelChange; // Change entity model
|
||||
protected float duration; // Default duration (seconds)
|
||||
|
||||
protected boolean infinite; // Never expires?
|
||||
protected boolean debuff; // Is a negative effect?
|
||||
protected String statusEffectIcon; // UI icon
|
||||
|
||||
// Damage and stats
|
||||
protected Int2FloatMap entityStats; // Stat modifiers
|
||||
protected ValueType valueType; // Absolute or Percent
|
||||
|
||||
// Behavior
|
||||
protected OverlapBehavior overlapBehavior; // EXTEND, OVERWRITE, IGNORE
|
||||
protected RemovalBehavior removalBehavior; // COMPLETE, DURATION, INFINITE
|
||||
|
||||
protected boolean invulnerable; // Grant invulnerability
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Effect Assets
|
||||
|
||||
```java
|
||||
// Get effect by ID
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset("fire_resistance");
|
||||
|
||||
// Get effect index for operations
|
||||
int effectIndex = EntityEffect.getAssetMap().getIndex(effect.getId());
|
||||
```
|
||||
|
||||
## EffectControllerComponent
|
||||
|
||||
The `EffectControllerComponent` manages active effects on an entity:
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent;
|
||||
|
||||
// Get effect controller from entity
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
```
|
||||
|
||||
## Adding Effects
|
||||
|
||||
### Basic Addition
|
||||
|
||||
```java
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset("speed_boost");
|
||||
|
||||
// Add with default parameters
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
### With Custom Duration
|
||||
|
||||
```java
|
||||
// Add with custom duration and overlap behavior
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
100.0f, // Duration in seconds
|
||||
OverlapBehavior.EXTEND, // How to handle overlap
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
### Infinite Effects
|
||||
|
||||
```java
|
||||
// Add an effect that never expires
|
||||
controller.addInfiniteEffect(
|
||||
entityRef,
|
||||
EntityEffect.getAssetMap().getIndex(effect.getId()),
|
||||
effect,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
## Removing Effects
|
||||
|
||||
```java
|
||||
// Remove specific effect
|
||||
controller.removeEffect(
|
||||
entityRef,
|
||||
EntityEffect.getAssetMap().getIndex(effect.getId()),
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
## Overlap Behaviors
|
||||
|
||||
```java
|
||||
public enum OverlapBehavior {
|
||||
EXTEND, // Add duration to existing effect
|
||||
OVERWRITE, // Replace existing effect
|
||||
IGNORE // Don't apply if already active
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```java
|
||||
// Extend duration if already applied
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
30.0f,
|
||||
OverlapBehavior.EXTEND,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Replace with fresh duration
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
30.0f,
|
||||
OverlapBehavior.OVERWRITE,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Only apply if not already active
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
30.0f,
|
||||
OverlapBehavior.IGNORE,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
## Removal Behaviors
|
||||
|
||||
```java
|
||||
public enum RemovalBehavior {
|
||||
COMPLETE, // Remove when complete
|
||||
DURATION, // Remove after duration
|
||||
INFINITE // Never remove automatically
|
||||
}
|
||||
```
|
||||
|
||||
## Value Types for Stats
|
||||
|
||||
```java
|
||||
public enum ValueType {
|
||||
Absolute, // Add/subtract flat value
|
||||
Percent // Multiply by percentage
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Buff on Command
|
||||
|
||||
```java
|
||||
public class BuffCommand extends AbstractCommand {
|
||||
private final RequiredArg<PlayerRef> playerArg;
|
||||
private final RequiredArg<String> effectArg;
|
||||
private final RequiredArg<Integer> durationArg;
|
||||
|
||||
// Would be set during plugin initialization
|
||||
private Store<EntityStore> store;
|
||||
private ComponentAccessor<EntityStore> componentAccessor;
|
||||
|
||||
public BuffCommand() {
|
||||
super("buff", "effects.command.buff.description");
|
||||
this.playerArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF);
|
||||
this.effectArg = withRequiredArg("effect", "Effect ID", ArgTypes.STRING);
|
||||
this.durationArg = withRequiredArg("duration", "Duration in seconds", ArgTypes.INTEGER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Void> execute(CommandContext ctx) {
|
||||
PlayerRef target = playerArg.get(ctx);
|
||||
String effectId = effectArg.get(ctx);
|
||||
int duration = durationArg.get(ctx);
|
||||
|
||||
Ref<EntityStore> entityRef = target.getReference();
|
||||
if (entityRef == null || !entityRef.isValid()) {
|
||||
ctx.sender().sendMessage(Message.raw("Player not found"));
|
||||
return null;
|
||||
}
|
||||
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset(effectId);
|
||||
if (effect == null) {
|
||||
ctx.sender().sendMessage(Message.raw("Unknown effect: " + effectId));
|
||||
return null;
|
||||
}
|
||||
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null) {
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
(float) duration,
|
||||
OverlapBehavior.EXTEND,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
ctx.sender().sendMessage(Message.raw("Applied " + effectId + " to " + target.getUsername()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**API Notes:**
|
||||
- Use `withRequiredArg()` to define arguments, not `addArgument()`
|
||||
- Use `arg.get(ctx)` to retrieve values, not `ctx.getArg()`
|
||||
- Use `ctx.sender()` not `ctx.getSender()`
|
||||
- Use `target.getUsername()` not `target.getName()`
|
||||
- Use `target.getReference()` to get entity reference (PlayerRef doesn't have `getPlayer()`)
|
||||
{{< /callout >}}
|
||||
|
||||
### Damage Over Time Effect
|
||||
|
||||
```java
|
||||
public void applyPoison(Player player, float duration, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
EntityEffect poison = EntityEffect.getAssetMap().getAsset("poison");
|
||||
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null && poison != null) {
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
poison,
|
||||
duration,
|
||||
OverlapBehavior.EXTEND,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
player.sendMessage(Message.raw("You have been poisoned!"));
|
||||
}
|
||||
```
|
||||
|
||||
### Temporary Invulnerability
|
||||
|
||||
```java
|
||||
public void grantInvulnerability(Player player, float seconds, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
EntityEffect invuln = EntityEffect.getAssetMap().getAsset("invulnerability");
|
||||
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null && invuln != null) {
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
invuln,
|
||||
seconds,
|
||||
OverlapBehavior.OVERWRITE, // Fresh duration
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Clear All Effects
|
||||
|
||||
```java
|
||||
public void clearAllEffects(Player player, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null) {
|
||||
// Use the built-in clearEffects method
|
||||
controller.clearEffects(
|
||||
entityRef,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
player.sendMessage(Message.raw("All effects cleared!"));
|
||||
}
|
||||
```
|
||||
|
||||
## Combining with Other Effects
|
||||
|
||||
Entity effects work well with particles and dynamic lights:
|
||||
|
||||
```java
|
||||
public void applyMagicBuff(Player player, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
|
||||
// Apply status effect
|
||||
EntityEffect buff = EntityEffect.getAssetMap().getAsset("magic_power");
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
if (controller != null && buff != null) {
|
||||
controller.addEffect(entityRef, buff, componentAccessor);
|
||||
}
|
||||
|
||||
// Add visual glow
|
||||
ColorLight glow = new ColorLight((byte) 10, (byte) 200, (byte) 50, (byte) 255);
|
||||
DynamicLight light = new DynamicLight(glow);
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType(),
|
||||
light
|
||||
);
|
||||
|
||||
// Get position from TransformComponent for particle spawn
|
||||
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
if (transform != null) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_aura",
|
||||
transform.getPosition(),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Note:** Use `player.getReference()` to get the entity reference. Position must be obtained from `TransformComponent`, not directly from Player.
|
||||
{{< /callout >}}
|
||||
|
||||
## Best Practices
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Effect Guidelines:**
|
||||
- Use `EXTEND` for stackable buffs to reward repeated application
|
||||
- Use `OVERWRITE` for effects that should reset duration
|
||||
- Use `IGNORE` to prevent effect stacking when undesired
|
||||
- Always check if effect exists before applying
|
||||
- Consider performance with many simultaneous effects
|
||||
{{< /callout >}}
|
||||
369
content/advanced/effects/entity-effects.fr.md
Normal file
369
content/advanced/effects/entity-effects.fr.md
Normal file
@@ -0,0 +1,369 @@
|
||||
---
|
||||
title: Effets d'Entité
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
Les effets d'entité sont des effets de statut qui peuvent inclure des composants visuels, des modificateurs de stats et des comportements basés sur la durée.
|
||||
|
||||
## Configuration EntityEffect
|
||||
|
||||
```java
|
||||
public class EntityEffect {
|
||||
protected String id;
|
||||
protected String name; // Clé de localisation
|
||||
|
||||
protected ApplicationEffects applicationEffects; // Effets visuels/audio
|
||||
|
||||
protected String modelChange; // Changer le modèle de l'entité
|
||||
protected float duration; // Durée par défaut (secondes)
|
||||
|
||||
protected boolean infinite; // N'expire jamais ?
|
||||
protected boolean debuff; // Est un effet négatif ?
|
||||
protected String statusEffectIcon; // Icône UI
|
||||
|
||||
// Dégâts et stats
|
||||
protected Int2FloatMap entityStats; // Modificateurs de stats
|
||||
protected ValueType valueType; // Absolute ou Percent
|
||||
|
||||
// Comportement
|
||||
protected OverlapBehavior overlapBehavior; // EXTEND, OVERWRITE, IGNORE
|
||||
protected RemovalBehavior removalBehavior; // COMPLETE, DURATION, INFINITE
|
||||
|
||||
protected boolean invulnerable; // Accorder l'invulnérabilité
|
||||
}
|
||||
```
|
||||
|
||||
## Accéder aux Assets d'Effets
|
||||
|
||||
```java
|
||||
// Obtenir un effet par ID
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset("fire_resistance");
|
||||
|
||||
// Obtenir l'index de l'effet pour les opérations
|
||||
int effectIndex = EntityEffect.getAssetMap().getIndex(effect.getId());
|
||||
```
|
||||
|
||||
## EffectControllerComponent
|
||||
|
||||
Le `EffectControllerComponent` gère les effets actifs sur une entité :
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent;
|
||||
|
||||
// Obtenir le contrôleur d'effets de l'entité
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
```
|
||||
|
||||
## Ajouter des Effets
|
||||
|
||||
### Ajout Basique
|
||||
|
||||
```java
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset("speed_boost");
|
||||
|
||||
// Ajouter avec les paramètres par défaut
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
### Avec Durée Personnalisée
|
||||
|
||||
```java
|
||||
// Ajouter avec durée personnalisée et comportement de superposition
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
100.0f, // Durée en secondes
|
||||
OverlapBehavior.EXTEND, // Comment gérer la superposition
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
### Effets Infinis
|
||||
|
||||
```java
|
||||
// Ajouter un effet qui n'expire jamais
|
||||
controller.addInfiniteEffect(
|
||||
entityRef,
|
||||
EntityEffect.getAssetMap().getIndex(effect.getId()),
|
||||
effect,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
## Supprimer des Effets
|
||||
|
||||
```java
|
||||
// Supprimer un effet spécifique
|
||||
controller.removeEffect(
|
||||
entityRef,
|
||||
EntityEffect.getAssetMap().getIndex(effect.getId()),
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
## Comportements de Superposition
|
||||
|
||||
```java
|
||||
public enum OverlapBehavior {
|
||||
EXTEND, // Ajouter la durée à l'effet existant
|
||||
OVERWRITE, // Remplacer l'effet existant
|
||||
IGNORE // Ne pas appliquer si déjà actif
|
||||
}
|
||||
```
|
||||
|
||||
### Exemples d'Utilisation
|
||||
|
||||
```java
|
||||
// Étendre la durée si déjà appliqué
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
30.0f,
|
||||
OverlapBehavior.EXTEND,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Remplacer avec une durée fraîche
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
30.0f,
|
||||
OverlapBehavior.OVERWRITE,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Appliquer seulement si pas déjà actif
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
30.0f,
|
||||
OverlapBehavior.IGNORE,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
## Comportements de Suppression
|
||||
|
||||
```java
|
||||
public enum RemovalBehavior {
|
||||
COMPLETE, // Supprimer quand terminé
|
||||
DURATION, // Supprimer après la durée
|
||||
INFINITE // Ne jamais supprimer automatiquement
|
||||
}
|
||||
```
|
||||
|
||||
## Types de Valeurs pour les Stats
|
||||
|
||||
```java
|
||||
public enum ValueType {
|
||||
Absolute, // Ajouter/soustraire une valeur fixe
|
||||
Percent // Multiplier par un pourcentage
|
||||
}
|
||||
```
|
||||
|
||||
## Exemples Pratiques
|
||||
|
||||
### Buff par Commande
|
||||
|
||||
```java
|
||||
public class BuffCommand extends AbstractCommand {
|
||||
private final RequiredArg<PlayerRef> playerArg;
|
||||
private final RequiredArg<String> effectArg;
|
||||
private final RequiredArg<Integer> durationArg;
|
||||
|
||||
// Serait défini lors de l'initialisation du plugin
|
||||
private Store<EntityStore> store;
|
||||
private ComponentAccessor<EntityStore> componentAccessor;
|
||||
|
||||
public BuffCommand() {
|
||||
super("buff", "effects.command.buff.description");
|
||||
this.playerArg = withRequiredArg("player", "Joueur cible", ArgTypes.PLAYER_REF);
|
||||
this.effectArg = withRequiredArg("effect", "ID de l'effet", ArgTypes.STRING);
|
||||
this.durationArg = withRequiredArg("duration", "Durée en secondes", ArgTypes.INTEGER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Void> execute(CommandContext ctx) {
|
||||
PlayerRef target = playerArg.get(ctx);
|
||||
String effectId = effectArg.get(ctx);
|
||||
int duration = durationArg.get(ctx);
|
||||
|
||||
Ref<EntityStore> entityRef = target.getReference();
|
||||
if (entityRef == null || !entityRef.isValid()) {
|
||||
ctx.sender().sendMessage(Message.raw("Joueur non trouvé"));
|
||||
return null;
|
||||
}
|
||||
|
||||
EntityEffect effect = EntityEffect.getAssetMap().getAsset(effectId);
|
||||
if (effect == null) {
|
||||
ctx.sender().sendMessage(Message.raw("Effet inconnu : " + effectId));
|
||||
return null;
|
||||
}
|
||||
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null) {
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
effect,
|
||||
(float) duration,
|
||||
OverlapBehavior.EXTEND,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
ctx.sender().sendMessage(Message.raw("Appliqué " + effectId + " à " + target.getUsername()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Notes API :**
|
||||
- Utilisez `withRequiredArg()` pour définir les arguments, pas `addArgument()`
|
||||
- Utilisez `arg.get(ctx)` pour récupérer les valeurs, pas `ctx.getArg()`
|
||||
- Utilisez `ctx.sender()` pas `ctx.getSender()`
|
||||
- Utilisez `target.getUsername()` pas `target.getName()`
|
||||
- Utilisez `target.getReference()` pour obtenir la référence d'entité (PlayerRef n'a pas `getPlayer()`)
|
||||
{{< /callout >}}
|
||||
|
||||
### Effet de Dégâts dans le Temps
|
||||
|
||||
```java
|
||||
public void applyPoison(Player player, float duration, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
EntityEffect poison = EntityEffect.getAssetMap().getAsset("poison");
|
||||
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null && poison != null) {
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
poison,
|
||||
duration,
|
||||
OverlapBehavior.EXTEND,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
player.sendMessage(Message.raw("Vous avez été empoisonné !"));
|
||||
}
|
||||
```
|
||||
|
||||
### Invulnérabilité Temporaire
|
||||
|
||||
```java
|
||||
public void grantInvulnerability(Player player, float seconds, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
EntityEffect invuln = EntityEffect.getAssetMap().getAsset("invulnerability");
|
||||
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null && invuln != null) {
|
||||
controller.addEffect(
|
||||
entityRef,
|
||||
invuln,
|
||||
seconds,
|
||||
OverlapBehavior.OVERWRITE, // Durée fraîche
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Effacer Tous les Effets
|
||||
|
||||
```java
|
||||
public void clearAllEffects(Player player, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
|
||||
if (controller != null) {
|
||||
// Utiliser la méthode clearEffects intégrée
|
||||
controller.clearEffects(
|
||||
entityRef,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
player.sendMessage(Message.raw("Tous les effets effacés !"));
|
||||
}
|
||||
```
|
||||
|
||||
## Combiner avec d'Autres Effets
|
||||
|
||||
Les effets d'entité fonctionnent bien avec les particules et les lumières dynamiques :
|
||||
|
||||
```java
|
||||
public void applyMagicBuff(Player player, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
|
||||
// Appliquer l'effet de statut
|
||||
EntityEffect buff = EntityEffect.getAssetMap().getAsset("magic_power");
|
||||
EffectControllerComponent controller = store.getComponent(
|
||||
entityRef,
|
||||
EffectControllerComponent.getComponentType()
|
||||
);
|
||||
if (controller != null && buff != null) {
|
||||
controller.addEffect(entityRef, buff, componentAccessor);
|
||||
}
|
||||
|
||||
// Ajouter une lueur visuelle
|
||||
ColorLight glow = new ColorLight((byte) 10, (byte) 200, (byte) 50, (byte) 255);
|
||||
DynamicLight light = new DynamicLight(glow);
|
||||
componentAccessor.putComponent(
|
||||
entityRef,
|
||||
DynamicLight.getComponentType(),
|
||||
light
|
||||
);
|
||||
|
||||
// Obtenir la position via TransformComponent pour les particules
|
||||
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
if (transform != null) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_aura",
|
||||
transform.getPosition(),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Note :** Utilisez `player.getReference()` pour obtenir la référence d'entité. La position doit être obtenue via `TransformComponent`, pas directement depuis Player.
|
||||
{{< /callout >}}
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Directives pour les Effets :**
|
||||
- Utilisez `EXTEND` pour les buffs cumulables afin de récompenser l'application répétée
|
||||
- Utilisez `OVERWRITE` pour les effets qui doivent réinitialiser leur durée
|
||||
- Utilisez `IGNORE` pour empêcher l'empilement d'effets quand indésirable
|
||||
- Vérifiez toujours si l'effet existe avant de l'appliquer
|
||||
- Considérez les performances avec de nombreux effets simultanés
|
||||
{{< /callout >}}
|
||||
788
content/advanced/effects/particles.en.md
Normal file
788
content/advanced/effects/particles.en.md
Normal file
@@ -0,0 +1,788 @@
|
||||
---
|
||||
title: Particles
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
The particle system in Hytale provides rich visual effects through configurable emitters and renderers. Create explosions, trails, auras, and complex visual effects with precise control over behavior and appearance.
|
||||
|
||||
**Package:** `com.hypixel.hytale.server.core.asset.type.particle`
|
||||
|
||||
{{< cards cols="3" >}}
|
||||
{{< card link="#spawning-particles" title="Spawning" subtitle="Create particle effects" icon="sparkles" >}}
|
||||
{{< card link="#particle-system-configuration" title="Configuration" subtitle="System and spawner setup" icon="adjustments" >}}
|
||||
{{< card link="#particle-attractors" title="Attractors" subtitle="Forces and movement" icon="arrow-circle-right" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
---
|
||||
|
||||
## Spawning Particles
|
||||
|
||||
### ParticleUtil Methods
|
||||
|
||||
{{< tabs items="Basic,Advanced,WorldParticle" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.universe.world.ParticleUtil;
|
||||
|
||||
// Basic spawn at position (auto-finds nearby players)
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"explosion_small", // Particle system ID
|
||||
position, // Vector3d position
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Spawn for specific players
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_trail",
|
||||
position,
|
||||
playerRefs, // List<Ref<EntityStore>>
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
**Broadcast Distance:** `DEFAULT_PARTICLE_DISTANCE = 75` blocks
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// With source entity reference
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"attack_swing",
|
||||
position,
|
||||
sourceEntityRef, // Entity that spawned this
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// With rotation control
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"directional_beam",
|
||||
position,
|
||||
yaw, pitch, roll, // Rotation angles
|
||||
sourceEntityRef,
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// With scale and color
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"colored_burst",
|
||||
position,
|
||||
yaw, pitch, roll,
|
||||
2.0f, // Scale multiplier
|
||||
new Color(255, 100, 50, 255), // RGBA color
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.asset.type.particle.config.WorldParticle;
|
||||
|
||||
// Full control with WorldParticle wrapper
|
||||
WorldParticle worldParticle = new WorldParticle(
|
||||
"my_particle_system",
|
||||
new Color(255, 100, 50, 255), // RGBA color override
|
||||
1.5f, // Scale multiplier
|
||||
new Vector3f(0, 1, 0), // Position offset
|
||||
new Direction(0, 0, 0) // Rotation offset
|
||||
);
|
||||
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
worldParticle,
|
||||
position,
|
||||
sourceRef,
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Particle System Configuration
|
||||
|
||||
### ParticleSystem Class
|
||||
|
||||
The root configuration for a particle effect:
|
||||
|
||||
```java
|
||||
public class ParticleSystem {
|
||||
protected String id; // Unique identifier
|
||||
protected ParticleSpawnerGroup[] spawners; // Spawner configurations
|
||||
protected float lifeSpan; // System lifetime (seconds)
|
||||
protected float cullDistance; // Distance before culling
|
||||
protected float boundingRadius; // Collision bounding box
|
||||
protected boolean isImportant; // Network priority flag
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | String | Unique identifier for the particle system |
|
||||
| `spawners` | ParticleSpawnerGroup[] | Array of spawner configurations |
|
||||
| `lifeSpan` | float | How long the system lives (seconds) |
|
||||
| `cullDistance` | float | Distance at which particles are culled |
|
||||
| `boundingRadius` | float | Bounding sphere for culling calculations |
|
||||
| `isImportant` | boolean | If true, prioritized in network sync |
|
||||
|
||||
### ParticleSpawner Configuration
|
||||
|
||||
Controls how individual particles are emitted:
|
||||
|
||||
{{< tabs items="Properties,Emission,Movement,Rendering" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
public class ParticleSpawner {
|
||||
protected String id;
|
||||
protected EmitShape shape;
|
||||
protected RangeVector3f emitOffset;
|
||||
protected boolean useEmitDirection;
|
||||
protected Range totalParticles;
|
||||
protected float lifeSpan;
|
||||
protected int maxConcurrentParticles;
|
||||
protected Rangef particleLifeSpan;
|
||||
protected Rangef spawnRate;
|
||||
protected boolean spawnBurst;
|
||||
protected InitialVelocity initialVelocity;
|
||||
protected ParticleAttractor[] attractors;
|
||||
protected FXRenderMode renderMode;
|
||||
protected float lightInfluence;
|
||||
protected Particle particle;
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Emission Properties:**
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `shape` | EmitShape | Sphere or Cube emission shape |
|
||||
| `emitOffset` | RangeVector3f | Random offset range from origin |
|
||||
| `useEmitDirection` | boolean | Use spawn direction for velocity |
|
||||
| `totalParticles` | Range | Min/max total particles to emit |
|
||||
| `spawnRate` | Rangef | Particles per second |
|
||||
| `spawnBurst` | boolean | Emit all at once vs over time |
|
||||
|
||||
**Example emission setup:**
|
||||
```java
|
||||
// Burst explosion - all at once
|
||||
spawner.setSpawnBurst(true);
|
||||
spawner.setTotalParticles(new Range(50, 100));
|
||||
|
||||
// Continuous stream
|
||||
spawner.setSpawnBurst(false);
|
||||
spawner.setSpawnRate(new Rangef(10, 20)); // 10-20 per second
|
||||
spawner.setLifeSpan(5.0f); // Emit for 5 seconds
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Movement Properties:**
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `initialVelocity` | InitialVelocity | Starting velocity configuration |
|
||||
| `attractors` | ParticleAttractor[] | Forces applied to particles |
|
||||
|
||||
**Initial Velocity:**
|
||||
```java
|
||||
public class InitialVelocity {
|
||||
protected float speed; // Base speed
|
||||
protected float speedVariance; // Random variance
|
||||
protected Vector3f direction; // Base direction
|
||||
protected float coneAngle; // Spread angle (degrees)
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Rendering Properties:**
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `renderMode` | FXRenderMode | How particles are blended |
|
||||
| `lightInfluence` | float | How much lighting affects particles |
|
||||
| `particle` | Particle | Texture and animation config |
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Emit Shapes
|
||||
|
||||
Control the volume from which particles spawn:
|
||||
|
||||
```java
|
||||
public enum EmitShape {
|
||||
Sphere, // Emit from spherical volume
|
||||
Cube // Emit from cubic volume
|
||||
}
|
||||
```
|
||||
|
||||
{{< tabs items="Sphere,Cube" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Spherical Emission:**
|
||||
|
||||
Particles spawn within a spherical volume, with direction pointing outward from center:
|
||||
|
||||
```java
|
||||
// Configure spherical emission
|
||||
spawner.setShape(EmitShape.Sphere);
|
||||
spawner.setEmitOffset(new RangeVector3f(
|
||||
new Rangef(-1, 1), // X range (radius)
|
||||
new Rangef(-1, 1), // Y range (radius)
|
||||
new Rangef(-1, 1) // Z range (radius)
|
||||
));
|
||||
|
||||
// Use emit direction for outward velocity
|
||||
spawner.setUseEmitDirection(true);
|
||||
```
|
||||
|
||||
Best for: Explosions, bursts, radial effects
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Cubic Emission:**
|
||||
|
||||
Particles spawn within an axis-aligned box volume:
|
||||
|
||||
```java
|
||||
// Configure cubic emission
|
||||
spawner.setShape(EmitShape.Cube);
|
||||
spawner.setEmitOffset(new RangeVector3f(
|
||||
new Rangef(-2, 2), // X range
|
||||
new Rangef(0, 3), // Y range (above ground)
|
||||
new Rangef(-2, 2) // Z range
|
||||
));
|
||||
```
|
||||
|
||||
Best for: Area effects, rain, ground effects
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Render Modes
|
||||
|
||||
Determine how particles blend with the scene:
|
||||
|
||||
```java
|
||||
public enum FXRenderMode {
|
||||
BlendLinear, // Standard transparency blending
|
||||
BlendAdd, // Additive blending (bright/glowing)
|
||||
Erosion, // Erosion/dissolve effect
|
||||
Distortion // Distortion/refraction effect
|
||||
}
|
||||
```
|
||||
|
||||
{{< tabs items="BlendLinear,BlendAdd,Erosion,Distortion" >}}
|
||||
{{< tab >}}
|
||||
|
||||
### BlendLinear
|
||||
|
||||
Standard alpha blending. Best for:
|
||||
- Smoke
|
||||
- Dust
|
||||
- Clouds
|
||||
- Solid-looking particles
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.BlendLinear);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### BlendAdd
|
||||
|
||||
Additive blending - particles add light to the scene. Best for:
|
||||
- Fire
|
||||
- Sparks
|
||||
- Magic effects
|
||||
- Glowing particles
|
||||
- Light beams
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.BlendAdd);
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Use `BlendAdd` for any glowing or luminous effects. Multiple overlapping particles will create brighter spots.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Erosion
|
||||
|
||||
Creates dissolving/eroding visual effect. Best for:
|
||||
- Disintegration
|
||||
- Dissolve transitions
|
||||
- Energy dissipation
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.Erosion);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Distortion
|
||||
|
||||
Refracts the background, creating heat-shimmer effects. Best for:
|
||||
- Heat waves
|
||||
- Portals
|
||||
- Energy fields
|
||||
- Underwater effects
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.Distortion);
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
Distortion effects are more GPU-intensive. Use sparingly.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Particle Attractors
|
||||
|
||||
Apply forces to particles for dynamic movement:
|
||||
|
||||
```java
|
||||
public class ParticleAttractor {
|
||||
protected Vector3f position; // Local attractor position
|
||||
protected Vector3f radialAxis; // Radial force direction
|
||||
protected float radius; // Influence radius
|
||||
|
||||
// Accelerations (continuous forces)
|
||||
protected float radialAcceleration; // Outward/inward acceleration
|
||||
protected float radialTangentAcceleration; // Tangential (orbit) acceleration
|
||||
protected Vector3f linearAcceleration; // Direct linear acceleration
|
||||
|
||||
// Impulses (one-time forces)
|
||||
protected float radialImpulse; // Outward/inward impulse
|
||||
protected float radialTangentImpulse; // Tangential impulse
|
||||
protected Vector3f linearImpulse; // Direct linear impulse
|
||||
|
||||
// Damping
|
||||
protected Vector3f dampingMultiplier; // Velocity reduction per frame
|
||||
}
|
||||
```
|
||||
|
||||
### Force Types
|
||||
|
||||
{{< tabs items="Radial,Tangential,Linear,Damping" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Radial Forces:**
|
||||
|
||||
Push particles toward or away from the attractor position:
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
attractor.setPosition(new Vector3f(0, 0, 0)); // Center of system
|
||||
|
||||
// Outward explosion
|
||||
attractor.setRadialAcceleration(10.0f); // Positive = outward
|
||||
|
||||
// Inward pull (black hole effect)
|
||||
attractor.setRadialAcceleration(-5.0f); // Negative = inward
|
||||
|
||||
// Instant outward burst
|
||||
attractor.setRadialImpulse(20.0f);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Tangential Forces:**
|
||||
|
||||
Create swirling/orbiting motion around the attractor:
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
attractor.setPosition(new Vector3f(0, 0, 0));
|
||||
attractor.setRadialAxis(new Vector3f(0, 1, 0)); // Orbit around Y axis
|
||||
|
||||
// Clockwise swirl
|
||||
attractor.setRadialTangentAcceleration(5.0f);
|
||||
|
||||
// Counter-clockwise
|
||||
attractor.setRadialTangentAcceleration(-5.0f);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Linear Forces:**
|
||||
|
||||
Apply constant directional force (like gravity or wind):
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
|
||||
// Gravity (downward)
|
||||
attractor.setLinearAcceleration(new Vector3f(0, -9.8f, 0));
|
||||
|
||||
// Wind (horizontal)
|
||||
attractor.setLinearAcceleration(new Vector3f(2.0f, 0, 0));
|
||||
|
||||
// Upward buoyancy
|
||||
attractor.setLinearAcceleration(new Vector3f(0, 3.0f, 0));
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Damping:**
|
||||
|
||||
Slow particles down over time:
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
|
||||
// Uniform damping (air resistance)
|
||||
attractor.setDampingMultiplier(new Vector3f(0.98f, 0.98f, 0.98f));
|
||||
|
||||
// Strong horizontal damping, weak vertical
|
||||
attractor.setDampingMultiplier(new Vector3f(0.9f, 0.99f, 0.9f));
|
||||
|
||||
// No damping (particles maintain velocity)
|
||||
attractor.setDampingMultiplier(new Vector3f(1.0f, 1.0f, 1.0f));
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Damping values < 1.0 slow particles. Values of 0.98-0.99 give realistic air resistance.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Particle Visual Configuration
|
||||
|
||||
Configure particle appearance and animation:
|
||||
|
||||
```java
|
||||
public class Particle {
|
||||
protected String texture; // Texture atlas path
|
||||
protected Size frameSize; // Frame dimensions
|
||||
|
||||
protected ParticleUVOption uvOption; // None, Animated, Random
|
||||
protected SoftParticle softParticle; // Soft particle blending
|
||||
protected float softParticlesFadeFactor; // 0.1 to 2.0
|
||||
|
||||
// Animation
|
||||
protected ParticleAnimationFrame initialAnimationFrame;
|
||||
protected Map<Integer, ParticleAnimationFrame> animation;
|
||||
}
|
||||
```
|
||||
|
||||
### Animation Options
|
||||
|
||||
```java
|
||||
public enum ParticleUVOption {
|
||||
None, // Single static frame
|
||||
Animated, // Play through frames in sequence
|
||||
Random // Random frame per particle
|
||||
}
|
||||
```
|
||||
|
||||
### Animation Frames
|
||||
|
||||
```java
|
||||
public class ParticleAnimationFrame {
|
||||
protected int frame; // Frame number in atlas
|
||||
protected Rangef scale; // Size range
|
||||
protected Rangef alpha; // Transparency range (0-1)
|
||||
protected Color color; // Color tint
|
||||
protected Rangef rotation; // Rotation range (degrees)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessing Particle Assets
|
||||
|
||||
```java
|
||||
// Get particle system by ID
|
||||
ParticleSystem system = ParticleSystem.getAssetMap().getAsset("explosion_large");
|
||||
|
||||
// Get spawner configuration
|
||||
ParticleSpawner spawner = ParticleSpawner.getAssetMap().getAsset("fire_spawner");
|
||||
|
||||
// Use in command arguments
|
||||
// ArgTypes.PARTICLE_SYSTEM for command parameters
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Event-Based Particles
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Note:** The examples below are simplified. Entity doesn't have a direct `getPosition()` method. In real code, obtain position via `TransformComponent` from the entity's store. Example:
|
||||
```java
|
||||
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
Vector3d position = transform.getPosition();
|
||||
```
|
||||
{{< /callout >}}
|
||||
|
||||
{{< tabs items="Block Break,Combat,Death" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onBlockBreak(BreakBlockEvent event) {
|
||||
// Use getTargetBlock() - not getPosition()
|
||||
Vector3i blockPos = event.getTargetBlock();
|
||||
Vector3d pos = new Vector3d(
|
||||
blockPos.x + 0.5,
|
||||
blockPos.y + 0.5,
|
||||
blockPos.z + 0.5
|
||||
);
|
||||
|
||||
// Spawn break particles at block center
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"block_break_particles",
|
||||
pos,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onEntityDamage(EntityDamageEvent event) {
|
||||
Entity target = event.getTarget();
|
||||
Vector3d hitPos = target.getPosition().add(0, 1, 0);
|
||||
|
||||
// Blood/damage effect
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"damage_hit",
|
||||
hitPos,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Critical hit special effect
|
||||
if (event.isCritical()) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"critical_hit_sparks",
|
||||
hitPos,
|
||||
0, 0, 0, // rotation
|
||||
1.5f, // larger scale
|
||||
new Color(255, 215, 0, 255), // gold color
|
||||
getNearbyPlayers(hitPos, 50),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onEntityDeath(EntityDeathEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
Vector3d deathPos = entity.getPosition();
|
||||
|
||||
// Death particles
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"entity_death_poof",
|
||||
deathPos,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Soul rising effect for players
|
||||
if (entity instanceof Player) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"soul_ascend",
|
||||
deathPos.add(0, 0.5, 0),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### Continuous Effects
|
||||
|
||||
{{< tabs items="Player Trail,Aura,Area Effect" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Spawn particle trail at player position
|
||||
public void spawnTrailEffect(Player player, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (hasTrailEffect(player)) {
|
||||
// Get position from TransformComponent
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
if (transform != null) {
|
||||
Vector3d position = transform.getPosition();
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_trail",
|
||||
position,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Note:** Player doesn't have a `getPosition()` method. Get position via `TransformComponent` from the entity store.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Persistent aura around player
|
||||
public class AuraManager {
|
||||
private final Map<UUID, Boolean> activeAuras = new HashMap<>();
|
||||
private Store<EntityStore> store;
|
||||
private ComponentAccessor<EntityStore> componentAccessor;
|
||||
|
||||
public void enableAura(Player player) {
|
||||
// Use player.getPlayerRef().getUuid() - not player.getUuid()
|
||||
activeAuras.put(player.getPlayerRef().getUuid(), true);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onTick(ServerTickEvent event) {
|
||||
for (Map.Entry<UUID, Boolean> entry : activeAuras.entrySet()) {
|
||||
if (entry.getValue()) {
|
||||
Player player = getPlayer(entry.getKey());
|
||||
if (player != null) {
|
||||
// Get position from TransformComponent
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
TransformComponent transform = store.getComponent(
|
||||
entityRef, TransformComponent.getComponentType());
|
||||
if (transform != null) {
|
||||
Vector3d pos = transform.getPosition();
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_aura",
|
||||
new Vector3d(pos.getX(), pos.getY() + 1, pos.getZ()),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Area healing effect
|
||||
public void createHealingZone(Vector3d center, double radius, int durationTicks) {
|
||||
// Spawn ring particles
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double angle = (2 * Math.PI * i) / 16;
|
||||
Vector3d pos = center.add(
|
||||
Math.cos(angle) * radius,
|
||||
0.1,
|
||||
Math.sin(angle) * radius
|
||||
);
|
||||
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"healing_sparkle",
|
||||
pos,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
// Central pillar effect
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"healing_pillar",
|
||||
center,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Performance Notes:**
|
||||
- Default broadcast distance: **75 blocks** (`DEFAULT_PARTICLE_DISTANCE`)
|
||||
- Particles use `SpawnParticleSystem` packet (ID: 152)
|
||||
- Set `isImportant = true` for critical visual feedback
|
||||
- Limit particle count for client performance
|
||||
- Use `maxConcurrentParticles` to cap active particles
|
||||
{{< /callout >}}
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
| Tip | Description |
|
||||
|-----|-------------|
|
||||
| Limit total particles | Keep under 100-200 for burst effects |
|
||||
| Use appropriate `cullDistance` | Don't render particles too far away |
|
||||
| Batch spawns | Spawn multiple particles in same tick |
|
||||
| Use `spawnBurst` wisely | Bursts are cheaper than continuous |
|
||||
| Consider player count | More players = more network traffic |
|
||||
|
||||
```java
|
||||
// Good: Efficient particle spawning
|
||||
public void spawnEfficiently(Vector3d position) {
|
||||
// Only spawn for nearby players
|
||||
List<PlayerRef> nearbyPlayers = getNearbyPlayers(position, 50);
|
||||
|
||||
if (!nearbyPlayers.isEmpty()) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"my_effect",
|
||||
position,
|
||||
nearbyPlayers, // Limited audience
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Entity Effects]({{< relref "entity-effects" >}}) - Status effects with visual components
|
||||
- [Networking]({{< relref "/advanced/networking" >}}) - How particles are broadcast
|
||||
- [Events]({{< relref "/core-concepts/events" >}}) - Triggering particles from events
|
||||
788
content/advanced/effects/particles.fr.md
Normal file
788
content/advanced/effects/particles.fr.md
Normal file
@@ -0,0 +1,788 @@
|
||||
---
|
||||
title: Particules
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Le système de particules de Hytale fournit des effets visuels riches à travers des émetteurs et renderers configurables. Créez des explosions, traînées, auras et effets visuels complexes avec un contrôle précis sur le comportement et l'apparence.
|
||||
|
||||
**Package :** `com.hypixel.hytale.server.core.asset.type.particle`
|
||||
|
||||
{{< cards cols="3" >}}
|
||||
{{< card link="#générer-des-particules" title="Génération" subtitle="Créer des effets de particules" icon="sparkles" >}}
|
||||
{{< card link="#configuration-du-système-de-particules" title="Configuration" subtitle="Config système et spawner" icon="adjustments" >}}
|
||||
{{< card link="#attracteurs-de-particules" title="Attracteurs" subtitle="Forces et mouvement" icon="arrow-circle-right" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
---
|
||||
|
||||
## Générer des Particules
|
||||
|
||||
### Méthodes ParticleUtil
|
||||
|
||||
{{< tabs items="Basique,Avancé,WorldParticle" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.universe.world.ParticleUtil;
|
||||
|
||||
// Génération basique à une position (trouve auto les joueurs proches)
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"explosion_small", // ID du système de particules
|
||||
position, // Position Vector3d
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Générer pour des joueurs spécifiques
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_trail",
|
||||
position,
|
||||
playerRefs, // List<Ref<EntityStore>>
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
**Distance de Diffusion :** `DEFAULT_PARTICLE_DISTANCE = 75` blocs
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Avec référence à l'entité source
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"attack_swing",
|
||||
position,
|
||||
sourceEntityRef, // Entité qui a généré ceci
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Avec contrôle de rotation
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"directional_beam",
|
||||
position,
|
||||
yaw, pitch, roll, // Angles de rotation
|
||||
sourceEntityRef,
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Avec échelle et couleur
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"colored_burst",
|
||||
position,
|
||||
yaw, pitch, roll,
|
||||
2.0f, // Multiplicateur d'échelle
|
||||
new Color(255, 100, 50, 255), // Couleur RGBA
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.asset.type.particle.config.WorldParticle;
|
||||
|
||||
// Contrôle complet avec le wrapper WorldParticle
|
||||
WorldParticle worldParticle = new WorldParticle(
|
||||
"my_particle_system",
|
||||
new Color(255, 100, 50, 255), // Override couleur RGBA
|
||||
1.5f, // Multiplicateur d'échelle
|
||||
new Vector3f(0, 1, 0), // Offset de position
|
||||
new Direction(0, 0, 0) // Offset de rotation
|
||||
);
|
||||
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
worldParticle,
|
||||
position,
|
||||
sourceRef,
|
||||
playerRefs,
|
||||
componentAccessor
|
||||
);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Configuration du Système de Particules
|
||||
|
||||
### Classe ParticleSystem
|
||||
|
||||
La configuration racine pour un effet de particules :
|
||||
|
||||
```java
|
||||
public class ParticleSystem {
|
||||
protected String id; // Identifiant unique
|
||||
protected ParticleSpawnerGroup[] spawners; // Configurations de spawners
|
||||
protected float lifeSpan; // Durée de vie du système (secondes)
|
||||
protected float cullDistance; // Distance avant culling
|
||||
protected float boundingRadius; // Boîte englobante de collision
|
||||
protected boolean isImportant; // Flag de priorité réseau
|
||||
}
|
||||
```
|
||||
|
||||
| Champ | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | String | Identifiant unique du système de particules |
|
||||
| `spawners` | ParticleSpawnerGroup[] | Tableau de configurations de spawners |
|
||||
| `lifeSpan` | float | Durée de vie du système (secondes) |
|
||||
| `cullDistance` | float | Distance à laquelle les particules sont culled |
|
||||
| `boundingRadius` | float | Sphère englobante pour les calculs de culling |
|
||||
| `isImportant` | boolean | Si vrai, prioritisé dans la sync réseau |
|
||||
|
||||
### Configuration ParticleSpawner
|
||||
|
||||
Contrôle comment les particules individuelles sont émises :
|
||||
|
||||
{{< tabs items="Propriétés,Émission,Mouvement,Rendu" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
public class ParticleSpawner {
|
||||
protected String id;
|
||||
protected EmitShape shape;
|
||||
protected RangeVector3f emitOffset;
|
||||
protected boolean useEmitDirection;
|
||||
protected Range totalParticles;
|
||||
protected float lifeSpan;
|
||||
protected int maxConcurrentParticles;
|
||||
protected Rangef particleLifeSpan;
|
||||
protected Rangef spawnRate;
|
||||
protected boolean spawnBurst;
|
||||
protected InitialVelocity initialVelocity;
|
||||
protected ParticleAttractor[] attractors;
|
||||
protected FXRenderMode renderMode;
|
||||
protected float lightInfluence;
|
||||
protected Particle particle;
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Propriétés d'Émission :**
|
||||
|
||||
| Propriété | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `shape` | EmitShape | Forme d'émission Sphere ou Cube |
|
||||
| `emitOffset` | RangeVector3f | Plage d'offset aléatoire depuis l'origine |
|
||||
| `useEmitDirection` | boolean | Utiliser la direction de spawn pour la vélocité |
|
||||
| `totalParticles` | Range | Nombre min/max total de particules à émettre |
|
||||
| `spawnRate` | Rangef | Particules par seconde |
|
||||
| `spawnBurst` | boolean | Émettre tout d'un coup vs sur la durée |
|
||||
|
||||
**Exemple de config d'émission :**
|
||||
```java
|
||||
// Explosion burst - tout d'un coup
|
||||
spawner.setSpawnBurst(true);
|
||||
spawner.setTotalParticles(new Range(50, 100));
|
||||
|
||||
// Flux continu
|
||||
spawner.setSpawnBurst(false);
|
||||
spawner.setSpawnRate(new Rangef(10, 20)); // 10-20 par seconde
|
||||
spawner.setLifeSpan(5.0f); // Émettre pendant 5 secondes
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Propriétés de Mouvement :**
|
||||
|
||||
| Propriété | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `initialVelocity` | InitialVelocity | Configuration de vélocité initiale |
|
||||
| `attractors` | ParticleAttractor[] | Forces appliquées aux particules |
|
||||
|
||||
**Vélocité Initiale :**
|
||||
```java
|
||||
public class InitialVelocity {
|
||||
protected float speed; // Vitesse de base
|
||||
protected float speedVariance; // Variance aléatoire
|
||||
protected Vector3f direction; // Direction de base
|
||||
protected float coneAngle; // Angle de dispersion (degrés)
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Propriétés de Rendu :**
|
||||
|
||||
| Propriété | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `renderMode` | FXRenderMode | Comment les particules sont blendées |
|
||||
| `lightInfluence` | float | Influence de l'éclairage sur les particules |
|
||||
| `particle` | Particle | Config texture et animation |
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Formes d'Émission
|
||||
|
||||
Contrôle le volume depuis lequel les particules apparaissent :
|
||||
|
||||
```java
|
||||
public enum EmitShape {
|
||||
Sphere, // Émettre depuis un volume sphérique
|
||||
Cube // Émettre depuis un volume cubique
|
||||
}
|
||||
```
|
||||
|
||||
{{< tabs items="Sphere,Cube" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Émission Sphérique :**
|
||||
|
||||
Les particules apparaissent dans un volume sphérique, avec la direction pointant vers l'extérieur du centre :
|
||||
|
||||
```java
|
||||
// Configurer l'émission sphérique
|
||||
spawner.setShape(EmitShape.Sphere);
|
||||
spawner.setEmitOffset(new RangeVector3f(
|
||||
new Rangef(-1, 1), // Plage X (rayon)
|
||||
new Rangef(-1, 1), // Plage Y (rayon)
|
||||
new Rangef(-1, 1) // Plage Z (rayon)
|
||||
));
|
||||
|
||||
// Utiliser la direction d'émission pour la vélocité vers l'extérieur
|
||||
spawner.setUseEmitDirection(true);
|
||||
```
|
||||
|
||||
Idéal pour : Explosions, bursts, effets radiaux
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Émission Cubique :**
|
||||
|
||||
Les particules apparaissent dans un volume de boîte aligné sur les axes :
|
||||
|
||||
```java
|
||||
// Configurer l'émission cubique
|
||||
spawner.setShape(EmitShape.Cube);
|
||||
spawner.setEmitOffset(new RangeVector3f(
|
||||
new Rangef(-2, 2), // Plage X
|
||||
new Rangef(0, 3), // Plage Y (au-dessus du sol)
|
||||
new Rangef(-2, 2) // Plage Z
|
||||
));
|
||||
```
|
||||
|
||||
Idéal pour : Effets de zone, pluie, effets au sol
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Modes de Rendu
|
||||
|
||||
Détermine comment les particules se mélangent avec la scène :
|
||||
|
||||
```java
|
||||
public enum FXRenderMode {
|
||||
BlendLinear, // Blending de transparence standard
|
||||
BlendAdd, // Blending additif (brillant/lumineux)
|
||||
Erosion, // Effet d'érosion/dissolution
|
||||
Distortion // Effet de distorsion/réfraction
|
||||
}
|
||||
```
|
||||
|
||||
{{< tabs items="BlendLinear,BlendAdd,Erosion,Distortion" >}}
|
||||
{{< tab >}}
|
||||
|
||||
### BlendLinear
|
||||
|
||||
Blending alpha standard. Idéal pour :
|
||||
- Fumée
|
||||
- Poussière
|
||||
- Nuages
|
||||
- Particules d'apparence solide
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.BlendLinear);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### BlendAdd
|
||||
|
||||
Blending additif - les particules ajoutent de la lumière à la scène. Idéal pour :
|
||||
- Feu
|
||||
- Étincelles
|
||||
- Effets magiques
|
||||
- Particules lumineuses
|
||||
- Rayons de lumière
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.BlendAdd);
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Utilisez `BlendAdd` pour tout effet lumineux ou brillant. Plusieurs particules superposées créeront des zones plus lumineuses.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Erosion
|
||||
|
||||
Crée un effet visuel de dissolution/érosion. Idéal pour :
|
||||
- Désintégration
|
||||
- Transitions de dissolution
|
||||
- Dissipation d'énergie
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.Erosion);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Distortion
|
||||
|
||||
Réfracte l'arrière-plan, créant des effets de miroitement de chaleur. Idéal pour :
|
||||
- Vagues de chaleur
|
||||
- Portails
|
||||
- Champs d'énergie
|
||||
- Effets sous-marins
|
||||
|
||||
```java
|
||||
spawner.setRenderMode(FXRenderMode.Distortion);
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
Les effets de distorsion sont plus intensifs en GPU. Utilisez avec parcimonie.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Attracteurs de Particules
|
||||
|
||||
Applique des forces aux particules pour un mouvement dynamique :
|
||||
|
||||
```java
|
||||
public class ParticleAttractor {
|
||||
protected Vector3f position; // Position locale de l'attracteur
|
||||
protected Vector3f radialAxis; // Direction de la force radiale
|
||||
protected float radius; // Rayon d'influence
|
||||
|
||||
// Accélérations (forces continues)
|
||||
protected float radialAcceleration; // Accélération vers l'extérieur/intérieur
|
||||
protected float radialTangentAcceleration; // Accélération tangentielle (orbite)
|
||||
protected Vector3f linearAcceleration; // Accélération linéaire directe
|
||||
|
||||
// Impulsions (forces ponctuelles)
|
||||
protected float radialImpulse; // Impulsion vers l'extérieur/intérieur
|
||||
protected float radialTangentImpulse; // Impulsion tangentielle
|
||||
protected Vector3f linearImpulse; // Impulsion linéaire directe
|
||||
|
||||
// Amortissement
|
||||
protected Vector3f dampingMultiplier; // Réduction de vélocité par frame
|
||||
}
|
||||
```
|
||||
|
||||
### Types de Forces
|
||||
|
||||
{{< tabs items="Radiale,Tangentielle,Linéaire,Amortissement" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Forces Radiales :**
|
||||
|
||||
Pousse les particules vers ou loin de la position de l'attracteur :
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
attractor.setPosition(new Vector3f(0, 0, 0)); // Centre du système
|
||||
|
||||
// Explosion vers l'extérieur
|
||||
attractor.setRadialAcceleration(10.0f); // Positif = vers l'extérieur
|
||||
|
||||
// Attraction vers l'intérieur (effet trou noir)
|
||||
attractor.setRadialAcceleration(-5.0f); // Négatif = vers l'intérieur
|
||||
|
||||
// Burst instantané vers l'extérieur
|
||||
attractor.setRadialImpulse(20.0f);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Forces Tangentielles :**
|
||||
|
||||
Crée un mouvement tourbillonnant/orbital autour de l'attracteur :
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
attractor.setPosition(new Vector3f(0, 0, 0));
|
||||
attractor.setRadialAxis(new Vector3f(0, 1, 0)); // Orbite autour de l'axe Y
|
||||
|
||||
// Tourbillon horaire
|
||||
attractor.setRadialTangentAcceleration(5.0f);
|
||||
|
||||
// Anti-horaire
|
||||
attractor.setRadialTangentAcceleration(-5.0f);
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Forces Linéaires :**
|
||||
|
||||
Applique une force directionnelle constante (comme la gravité ou le vent) :
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
|
||||
// Gravité (vers le bas)
|
||||
attractor.setLinearAcceleration(new Vector3f(0, -9.8f, 0));
|
||||
|
||||
// Vent (horizontal)
|
||||
attractor.setLinearAcceleration(new Vector3f(2.0f, 0, 0));
|
||||
|
||||
// Flottabilité vers le haut
|
||||
attractor.setLinearAcceleration(new Vector3f(0, 3.0f, 0));
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Amortissement :**
|
||||
|
||||
Ralentit les particules au fil du temps :
|
||||
|
||||
```java
|
||||
ParticleAttractor attractor = new ParticleAttractor();
|
||||
|
||||
// Amortissement uniforme (résistance de l'air)
|
||||
attractor.setDampingMultiplier(new Vector3f(0.98f, 0.98f, 0.98f));
|
||||
|
||||
// Fort amortissement horizontal, faible vertical
|
||||
attractor.setDampingMultiplier(new Vector3f(0.9f, 0.99f, 0.9f));
|
||||
|
||||
// Pas d'amortissement (particules maintiennent leur vélocité)
|
||||
attractor.setDampingMultiplier(new Vector3f(1.0f, 1.0f, 1.0f));
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Les valeurs d'amortissement < 1.0 ralentissent les particules. Des valeurs de 0.98-0.99 donnent une résistance de l'air réaliste.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Configuration Visuelle des Particules
|
||||
|
||||
Configure l'apparence et l'animation des particules :
|
||||
|
||||
```java
|
||||
public class Particle {
|
||||
protected String texture; // Chemin de l'atlas de texture
|
||||
protected Size frameSize; // Dimensions du frame
|
||||
|
||||
protected ParticleUVOption uvOption; // None, Animated, Random
|
||||
protected SoftParticle softParticle; // Blending soft particle
|
||||
protected float softParticlesFadeFactor; // 0.1 à 2.0
|
||||
|
||||
// Animation
|
||||
protected ParticleAnimationFrame initialAnimationFrame;
|
||||
protected Map<Integer, ParticleAnimationFrame> animation;
|
||||
}
|
||||
```
|
||||
|
||||
### Options d'Animation
|
||||
|
||||
```java
|
||||
public enum ParticleUVOption {
|
||||
None, // Frame unique statique
|
||||
Animated, // Jouer les frames en séquence
|
||||
Random // Frame aléatoire par particule
|
||||
}
|
||||
```
|
||||
|
||||
### Frames d'Animation
|
||||
|
||||
```java
|
||||
public class ParticleAnimationFrame {
|
||||
protected int frame; // Numéro de frame dans l'atlas
|
||||
protected Rangef scale; // Plage de taille
|
||||
protected Rangef alpha; // Plage de transparence (0-1)
|
||||
protected Color color; // Teinte de couleur
|
||||
protected Rangef rotation; // Plage de rotation (degrés)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accéder aux Assets de Particules
|
||||
|
||||
```java
|
||||
// Obtenir un système de particules par ID
|
||||
ParticleSystem system = ParticleSystem.getAssetMap().getAsset("explosion_large");
|
||||
|
||||
// Obtenir la configuration d'un spawner
|
||||
ParticleSpawner spawner = ParticleSpawner.getAssetMap().getAsset("fire_spawner");
|
||||
|
||||
// Utiliser dans les arguments de commande
|
||||
// ArgTypes.PARTICLE_SYSTEM pour les paramètres de commande
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patterns Courants
|
||||
|
||||
### Particules Basées sur les Événements
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Note :** Les exemples ci-dessous sont simplifiés. Entity n'a pas de méthode `getPosition()` directe. Dans le code réel, obtenez la position via `TransformComponent` depuis le store de l'entité. Exemple :
|
||||
```java
|
||||
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
Vector3d position = transform.getPosition();
|
||||
```
|
||||
{{< /callout >}}
|
||||
|
||||
{{< tabs items="Destruction Bloc,Combat,Mort" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onBlockBreak(BreakBlockEvent event) {
|
||||
// Utiliser getTargetBlock() - pas getPosition()
|
||||
Vector3i blockPos = event.getTargetBlock();
|
||||
Vector3d pos = new Vector3d(
|
||||
blockPos.x + 0.5,
|
||||
blockPos.y + 0.5,
|
||||
blockPos.z + 0.5
|
||||
);
|
||||
|
||||
// Générer des particules de destruction au centre du bloc
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"block_break_particles",
|
||||
pos,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onEntityDamage(EntityDamageEvent event) {
|
||||
Entity target = event.getTarget();
|
||||
Vector3d hitPos = target.getPosition().add(0, 1, 0);
|
||||
|
||||
// Effet de sang/dégâts
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"damage_hit",
|
||||
hitPos,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Effet spécial coup critique
|
||||
if (event.isCritical()) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"critical_hit_sparks",
|
||||
hitPos,
|
||||
0, 0, 0, // rotation
|
||||
1.5f, // échelle plus grande
|
||||
new Color(255, 215, 0, 255), // couleur or
|
||||
getNearbyPlayers(hitPos, 50),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onEntityDeath(EntityDeathEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
Vector3d deathPos = entity.getPosition();
|
||||
|
||||
// Particules de mort
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"entity_death_poof",
|
||||
deathPos,
|
||||
componentAccessor
|
||||
);
|
||||
|
||||
// Effet d'âme qui monte pour les joueurs
|
||||
if (entity instanceof Player) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"soul_ascend",
|
||||
deathPos.add(0, 0.5, 0),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### Effets Continus
|
||||
|
||||
{{< tabs items="Traînée Joueur,Aura,Effet de Zone" >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Générer une traînée de particules à la position du joueur
|
||||
public void spawnTrailEffect(Player player, Store<EntityStore> store,
|
||||
ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (hasTrailEffect(player)) {
|
||||
// Obtenir la position via TransformComponent
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
TransformComponent transform = store.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
if (transform != null) {
|
||||
Vector3d position = transform.getPosition();
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_trail",
|
||||
position,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Note :** Player n'a pas de méthode `getPosition()`. Obtenez la position via `TransformComponent` depuis le store d'entités.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Aura persistante autour du joueur
|
||||
public class AuraManager {
|
||||
private final Map<UUID, Boolean> activeAuras = new HashMap<>();
|
||||
private Store<EntityStore> store;
|
||||
private ComponentAccessor<EntityStore> componentAccessor;
|
||||
|
||||
public void enableAura(Player player) {
|
||||
// Utiliser player.getPlayerRef().getUuid() - pas player.getUuid()
|
||||
activeAuras.put(player.getPlayerRef().getUuid(), true);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onTick(ServerTickEvent event) {
|
||||
for (Map.Entry<UUID, Boolean> entry : activeAuras.entrySet()) {
|
||||
if (entry.getValue()) {
|
||||
Player player = getPlayer(entry.getKey());
|
||||
if (player != null) {
|
||||
// Obtenir la position via TransformComponent
|
||||
Ref<EntityStore> entityRef = player.getReference();
|
||||
TransformComponent transform = store.getComponent(
|
||||
entityRef, TransformComponent.getComponentType());
|
||||
if (transform != null) {
|
||||
Vector3d pos = transform.getPosition();
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"magic_aura",
|
||||
new Vector3d(pos.getX(), pos.getY() + 1, pos.getZ()),
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// Effet de zone de soin
|
||||
public void createHealingZone(Vector3d center, double radius, int durationTicks) {
|
||||
// Générer des particules en anneau
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double angle = (2 * Math.PI * i) / 16;
|
||||
Vector3d pos = center.add(
|
||||
Math.cos(angle) * radius,
|
||||
0.1,
|
||||
Math.sin(angle) * radius
|
||||
);
|
||||
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"healing_sparkle",
|
||||
pos,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
|
||||
// Effet pilier central
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"healing_pillar",
|
||||
center,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Directives de Performance
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Notes de Performance :**
|
||||
- Distance de diffusion par défaut : **75 blocs** (`DEFAULT_PARTICLE_DISTANCE`)
|
||||
- Les particules utilisent le paquet `SpawnParticleSystem` (ID : 152)
|
||||
- Définissez `isImportant = true` pour les feedbacks visuels critiques
|
||||
- Limitez le nombre de particules pour les performances client
|
||||
- Utilisez `maxConcurrentParticles` pour limiter les particules actives
|
||||
{{< /callout >}}
|
||||
|
||||
### Conseils d'Optimisation
|
||||
|
||||
| Conseil | Description |
|
||||
|---------|-------------|
|
||||
| Limiter le total de particules | Gardez sous 100-200 pour les effets burst |
|
||||
| Utiliser une `cullDistance` appropriée | Ne pas rendre les particules trop éloignées |
|
||||
| Grouper les spawns | Générer plusieurs particules dans le même tick |
|
||||
| Utiliser `spawnBurst` judicieusement | Les bursts sont moins coûteux que le continu |
|
||||
| Considérer le nombre de joueurs | Plus de joueurs = plus de trafic réseau |
|
||||
|
||||
```java
|
||||
// Bon : Génération de particules efficace
|
||||
public void spawnEfficiently(Vector3d position) {
|
||||
// Générer seulement pour les joueurs proches
|
||||
List<PlayerRef> nearbyPlayers = getNearbyPlayers(position, 50);
|
||||
|
||||
if (!nearbyPlayers.isEmpty()) {
|
||||
ParticleUtil.spawnParticleEffect(
|
||||
"my_effect",
|
||||
position,
|
||||
nearbyPlayers, // Audience limitée
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sujets Connexes
|
||||
|
||||
- [Effets d'Entité]({{< relref "entity-effects" >}}) - Effets de statut avec composants visuels
|
||||
- [Réseau]({{< relref "/advanced/networking" >}}) - Comment les particules sont diffusées
|
||||
- [Événements]({{< relref "/core-concepts/events" >}}) - Déclencher des particules depuis les événements
|
||||
272
content/advanced/networking/_index.en.md
Normal file
272
content/advanced/networking/_index.en.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
title: Networking
|
||||
type: docs
|
||||
weight: 6
|
||||
---
|
||||
|
||||
The Hytale server uses a custom networking system for client-server communication. Understanding how data is synchronized and transmitted is essential for creating responsive and efficient plugins.
|
||||
|
||||
{{< cards cols="2" >}}
|
||||
{{< card link="packets" title="Packets" subtitle="Network packet reference and usage" icon="server" >}}
|
||||
{{< card link="sync" title="Synchronization" subtitle="Entity and world state synchronization" icon="refresh" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The networking system in Hytale handles:
|
||||
|
||||
- **Client-Server Communication**: Bidirectional packet transmission
|
||||
- **Entity Synchronization**: Keeping entity states consistent across clients
|
||||
- **World Updates**: Broadcasting block changes and world events
|
||||
- **Player Actions**: Processing and validating player inputs
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Most networking operations are handled automatically by the server. Plugins typically interact with the high-level APIs rather than raw packets.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Server │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ World │ │ Entity │ │ Plugin │ │
|
||||
│ │ State │ │ Manager │ │ Events │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────────────┼─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────┴─────┐ │
|
||||
│ │ Network │ │
|
||||
│ │ Layer │ │
|
||||
│ └─────┬─────┘ │
|
||||
└──────────────────────────┼──────────────────────────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │ │
|
||||
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
|
||||
│ Client │ │ Client │ │ Client │
|
||||
│ 1 │ │ 2 │ │ N │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Broadcast Distance
|
||||
|
||||
Different types of data have different broadcast distances:
|
||||
|
||||
| Data Type | Default Distance | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| Particles | 75 blocks | `DEFAULT_PARTICLE_DISTANCE` |
|
||||
| Entities | View distance | Based on server configuration |
|
||||
| Sounds | 16 blocks | Default sound range |
|
||||
| Block changes | Chunk-based | Sent to players with loaded chunks |
|
||||
|
||||
### Network Outdated Pattern
|
||||
|
||||
The server uses a "network outdated" pattern to efficiently synchronize state:
|
||||
|
||||
```java
|
||||
// Components implement this pattern for network synchronization
|
||||
public interface NetworkSyncable {
|
||||
boolean isNetworkOutdated();
|
||||
void markNetworkOutdated();
|
||||
void clearNetworkOutdated();
|
||||
}
|
||||
```
|
||||
|
||||
When a component's state changes:
|
||||
1. The component marks itself as `networkOutdated`
|
||||
2. The sync system detects outdated components
|
||||
3. Updates are batched and sent to relevant clients
|
||||
4. The outdated flag is cleared after sending
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Sending Messages to Players
|
||||
|
||||
```java
|
||||
// Send to a specific player
|
||||
player.sendMessage(Message.raw("Hello!"));
|
||||
|
||||
// Send to all players in a world
|
||||
for (Player p : world.getPlayers()) {
|
||||
p.sendMessage(Message.raw("World announcement"));
|
||||
}
|
||||
|
||||
// Send to all online players
|
||||
for (Player p : server.getOnlinePlayers()) {
|
||||
p.sendMessage(Message.raw("Server broadcast"));
|
||||
}
|
||||
```
|
||||
|
||||
### Spawning Particles (Network Broadcast)
|
||||
|
||||
```java
|
||||
// Spawn particles visible to nearby players
|
||||
world.spawnParticle(particleSystem, position);
|
||||
|
||||
// The server handles:
|
||||
// 1. Finding players within DEFAULT_PARTICLE_DISTANCE (75 blocks)
|
||||
// 2. Sending SpawnParticleSystem packet (ID: 152) to those players
|
||||
// 3. Client-side rendering
|
||||
```
|
||||
|
||||
### Entity Position Updates
|
||||
|
||||
```java
|
||||
// When you modify an entity's position
|
||||
entity.setPosition(newPosition);
|
||||
|
||||
// The server automatically:
|
||||
// 1. Marks the TransformComponent as network outdated
|
||||
// 2. Batches position updates
|
||||
// 3. Sends updates to players tracking this entity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
{{< tabs items="Performance,Security,Reliability" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Minimize Network Traffic:**
|
||||
|
||||
```java
|
||||
// BAD: Sending updates every tick
|
||||
@Subscribe
|
||||
public void onTick(TickEvent event) {
|
||||
for (Player p : server.getOnlinePlayers()) {
|
||||
p.sendMessage(Message.raw("Tick!")); // Don't do this!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Batch updates and use intervals
|
||||
private int tickCounter = 0;
|
||||
|
||||
@Subscribe
|
||||
public void onTick(TickEvent event) {
|
||||
tickCounter++;
|
||||
if (tickCounter >= 20) { // Every second
|
||||
tickCounter = 0;
|
||||
broadcastScoreboard();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Appropriate Broadcast Distances:**
|
||||
|
||||
```java
|
||||
// Only notify nearby players for local events
|
||||
Vector3d eventPos = event.getPosition();
|
||||
double range = 50.0;
|
||||
|
||||
for (Player p : world.getPlayers()) {
|
||||
if (p.getPosition().distanceTo(eventPos) <= range) {
|
||||
p.sendMessage(Message.raw("Something happened nearby!"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Validate Client Input:**
|
||||
|
||||
```java
|
||||
// Note: PlayerInteractEvent is deprecated. For block interactions, use UseBlockEvent.
|
||||
getEventRegistry().register(PlayerInteractEvent.class, event -> {
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
Vector3i targetBlock = event.getTargetBlock();
|
||||
|
||||
if (targetBlock != null && playerRef != null) {
|
||||
// Get player position via PlayerRef's transform
|
||||
Transform transform = playerRef.getTransform();
|
||||
if (transform != null) {
|
||||
Vector3d playerPos = transform.getPosition();
|
||||
// Note: Vector3i uses getX(), getY(), getZ() - NOT x(), y(), z()
|
||||
Vector3d targetPos = new Vector3d(targetBlock.getX(), targetBlock.getY(), targetBlock.getZ());
|
||||
double distance = playerPos.distance(targetPos);
|
||||
if (distance > MAX_INTERACTION_DISTANCE) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Rate Limit Actions:**
|
||||
|
||||
```java
|
||||
private final Map<UUID, Long> lastAction = new HashMap<>();
|
||||
private static final long COOLDOWN_MS = 1000;
|
||||
|
||||
public boolean canPerformAction(Player player) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastAction.get(player.getUuid());
|
||||
|
||||
if (last != null && now - last < COOLDOWN_MS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lastAction.put(player.getUuid(), now);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Handle Disconnections:**
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
UUID playerId = event.getPlayerRef().getUuid();
|
||||
|
||||
// Clean up player-specific data
|
||||
playerData.remove(playerId);
|
||||
pendingOperations.removeIf(op -> op.getPlayerId().equals(playerId));
|
||||
}
|
||||
```
|
||||
|
||||
**Use PlayerRef for Async Operations:**
|
||||
|
||||
```java
|
||||
// PlayerRef is safe to use across async boundaries
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
World world = event.getWorld();
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// Some async operation
|
||||
processData();
|
||||
}).thenAccept(result -> {
|
||||
// Return to world thread for game state changes
|
||||
world.execute(() -> {
|
||||
// PlayerRef.getPlayer() does NOT exist - send message directly on PlayerRef
|
||||
playerRef.sendMessage(Message.raw("Operation complete!"));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Player API]({{< relref "/world/entities/player-api" >}}) - Player networking methods
|
||||
- [Events]({{< relref "/core-concepts/events" >}}) - Network-related events
|
||||
- [Entity Components]({{< relref "/world/entities/entity-components" >}}) - Synchronized component data
|
||||
266
content/advanced/networking/_index.fr.md
Normal file
266
content/advanced/networking/_index.fr.md
Normal file
@@ -0,0 +1,266 @@
|
||||
---
|
||||
title: Networking
|
||||
type: docs
|
||||
weight: 6
|
||||
---
|
||||
|
||||
The Hytale server uses a custom networking system for client-server communication. Understanding how data is synchronized and transmitted is essential for creating responsive and efficient plugins.
|
||||
|
||||
{{< cards cols="2" >}}
|
||||
{{< card link="packets" title="Packets" subtitle="Network packet reference and usage" icon="server" >}}
|
||||
{{< card link="sync" title="Synchronization" subtitle="Entity and world state synchronization" icon="refresh" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The networking system in Hytale handles:
|
||||
|
||||
- **Client-Server Communication**: Bidirectional packet transmission
|
||||
- **Entity Synchronization**: Keeping entity states consistent across clients
|
||||
- **World Updates**: Broadcasting block changes and world events
|
||||
- **Player Actions**: Processing and validating player inputs
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Most networking operations are handled automatically by the server. Plugins typically interact with the high-level APIs rather than raw packets.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Server │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ World │ │ Entity │ │ Plugin │ │
|
||||
│ │ State │ │ Manager │ │ Events │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────────────┼─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────┴─────┐ │
|
||||
│ │ Network │ │
|
||||
│ │ Layer │ │
|
||||
│ └─────┬─────┘ │
|
||||
└──────────────────────────┼──────────────────────────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │ │
|
||||
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
|
||||
│ Client │ │ Client │ │ Client │
|
||||
│ 1 │ │ 2 │ │ N │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Broadcast Distance
|
||||
|
||||
Different types of data have different broadcast distances:
|
||||
|
||||
| Data Type | Default Distance | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| Particles | 75 blocks | `DEFAULT_PARTICLE_DISTANCE` |
|
||||
| Entities | View distance | Based on server configuration |
|
||||
| Sounds | 16 blocks | Default sound range |
|
||||
| Block changes | Chunk-based | Sent to players with loaded chunks |
|
||||
|
||||
### Network Outdated Pattern
|
||||
|
||||
The server uses a "network outdated" pattern to efficiently synchronize state:
|
||||
|
||||
```java
|
||||
// Components implement this pattern for network synchronization
|
||||
public interface NetworkSyncable {
|
||||
boolean isNetworkOutdated();
|
||||
void markNetworkOutdated();
|
||||
void clearNetworkOutdated();
|
||||
}
|
||||
```
|
||||
|
||||
When a component's state changes:
|
||||
1. The component marks itself as `networkOutdated`
|
||||
2. The sync system detects outdated components
|
||||
3. Updates are batched and sent to relevant clients
|
||||
4. The outdated flag is cleared after sending
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Sending Messages to Players
|
||||
|
||||
```java
|
||||
// Send to a specific player
|
||||
player.sendMessage(Message.raw("Hello!"));
|
||||
|
||||
// Send to all players in a world
|
||||
for (Player p : world.getPlayers()) {
|
||||
p.sendMessage(Message.raw("World announcement"));
|
||||
}
|
||||
|
||||
// Send to all online players
|
||||
for (Player p : server.getOnlinePlayers()) {
|
||||
p.sendMessage(Message.raw("Server broadcast"));
|
||||
}
|
||||
```
|
||||
|
||||
### Spawning Particles (Network Broadcast)
|
||||
|
||||
```java
|
||||
// Spawn particles visible to nearby players
|
||||
world.spawnParticle(particleSystem, position);
|
||||
|
||||
// The server handles:
|
||||
// 1. Finding players within DEFAULT_PARTICLE_DISTANCE (75 blocks)
|
||||
// 2. Sending SpawnParticleSystem packet (ID: 152) to those players
|
||||
// 3. Client-side rendering
|
||||
```
|
||||
|
||||
### Entity Position Updates
|
||||
|
||||
```java
|
||||
// When you modify an entity's position
|
||||
entity.setPosition(newPosition);
|
||||
|
||||
// The server automatically:
|
||||
// 1. Marks the TransformComponent as network outdated
|
||||
// 2. Batches position updates
|
||||
// 3. Sends updates to players tracking this entity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
{{< tabs items="Performance,Security,Reliability" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Minimize Network Traffic:**
|
||||
|
||||
```java
|
||||
// BAD: Sending updates every tick
|
||||
@Subscribe
|
||||
public void onTick(TickEvent event) {
|
||||
for (Player p : server.getOnlinePlayers()) {
|
||||
p.sendMessage(Message.raw("Tick!")); // Don't do this!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Batch updates and use intervals
|
||||
private int tickCounter = 0;
|
||||
|
||||
@Subscribe
|
||||
public void onTick(TickEvent event) {
|
||||
tickCounter++;
|
||||
if (tickCounter >= 20) { // Every second
|
||||
tickCounter = 0;
|
||||
broadcastScoreboard();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Appropriate Broadcast Distances:**
|
||||
|
||||
```java
|
||||
// Only notify nearby players for local events
|
||||
Vector3d eventPos = event.getPosition();
|
||||
double range = 50.0;
|
||||
|
||||
for (Player p : world.getPlayers()) {
|
||||
if (p.getPosition().distanceTo(eventPos) <= range) {
|
||||
p.sendMessage(Message.raw("Something happened nearby!"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Validate Client Input:**
|
||||
|
||||
```java
|
||||
// Note: PlayerInteractEvent is deprecated. For block interactions, use UseBlockEvent.
|
||||
getEventRegistry().register(PlayerInteractEvent.class, event -> {
|
||||
Player player = event.getPlayer();
|
||||
Vector3i targetBlock = event.getTargetBlock();
|
||||
|
||||
if (targetBlock != null) {
|
||||
// Validate distance
|
||||
Vector3d targetPos = new Vector3d(targetBlock.x(), targetBlock.y(), targetBlock.z());
|
||||
double distance = player.getPosition().distance(targetPos);
|
||||
if (distance > MAX_INTERACTION_DISTANCE) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Rate Limit Actions:**
|
||||
|
||||
```java
|
||||
private final Map<UUID, Long> lastAction = new HashMap<>();
|
||||
private static final long COOLDOWN_MS = 1000;
|
||||
|
||||
public boolean canPerformAction(Player player) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastAction.get(player.getUuid());
|
||||
|
||||
if (last != null && now - last < COOLDOWN_MS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lastAction.put(player.getUuid(), now);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Handle Disconnections:**
|
||||
|
||||
```java
|
||||
@Subscribe
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
UUID playerId = event.getPlayerRef().getUuid();
|
||||
|
||||
// Clean up player-specific data
|
||||
playerData.remove(playerId);
|
||||
pendingOperations.removeIf(op -> op.getPlayerId().equals(playerId));
|
||||
}
|
||||
```
|
||||
|
||||
**Use PlayerRef for Async Operations:**
|
||||
|
||||
```java
|
||||
// PlayerRef is safe to use across async boundaries
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// Some async operation
|
||||
processData();
|
||||
}).thenAccept(result -> {
|
||||
// Player might have disconnected
|
||||
Player player = playerRef.getPlayer();
|
||||
if (player != null) {
|
||||
player.sendMessage(Message.raw("Operation complete!"));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Player API]({{< relref "/world/entities/player-api" >}}) - Player networking methods
|
||||
- [Events]({{< relref "/core-concepts/events" >}}) - Network-related events
|
||||
- [Entity Components]({{< relref "/world/entities/entity-components" >}}) - Synchronized component data
|
||||
414
content/advanced/networking/packets.en.md
Normal file
414
content/advanced/networking/packets.en.md
Normal file
@@ -0,0 +1,414 @@
|
||||
---
|
||||
title: Packets
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Hytale uses a binary packet protocol for efficient client-server communication. While most networking is handled internally, understanding the packet system helps when debugging or implementing advanced features.
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
Direct packet manipulation is generally not recommended. Use the high-level APIs provided by the plugin system whenever possible.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## Packet Structure
|
||||
|
||||
All packets follow a common structure:
|
||||
|
||||
```
|
||||
┌──────────────┬───────────────┬─────────────────────┐
|
||||
│ Packet ID │ Payload Size │ Payload │
|
||||
│ (varint) │ (varint) │ (variable bytes) │
|
||||
└──────────────┴───────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| Packet ID | VarInt | Unique identifier for packet type |
|
||||
| Payload Size | VarInt | Size of payload in bytes |
|
||||
| Payload | bytes | Packet-specific data |
|
||||
|
||||
---
|
||||
|
||||
## Known Packet IDs
|
||||
|
||||
{{< tabs items="Client-bound,Server-bound" >}}
|
||||
{{< tab >}}
|
||||
|
||||
Packets sent from server to client:
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| 152 | SpawnParticleSystem | Spawns particles at a location |
|
||||
| -- | EntitySpawn | Spawns an entity on the client |
|
||||
| -- | EntityDespawn | Removes an entity from the client |
|
||||
| -- | EntityMove | Updates entity position |
|
||||
| -- | BlockChange | Updates a single block |
|
||||
| -- | ChunkData | Sends chunk block data |
|
||||
| -- | ChatMessage | Sends chat/system message |
|
||||
| -- | PlaySound | Plays a sound effect |
|
||||
| -- | SetWeather | Changes weather state |
|
||||
| -- | TimeUpdate | Synchronizes world time |
|
||||
| -- | PlayerInfo | Updates player list data |
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Packet IDs marked with `--` are internal and subject to change between versions.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
Packets sent from client to server:
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| -- | PlayerPosition | Player movement update |
|
||||
| -- | PlayerAction | Player action (attack, use, etc.) |
|
||||
| -- | ChatMessage | Chat message from player |
|
||||
| -- | BlockBreak | Block breaking progress |
|
||||
| -- | BlockPlace | Block placement request |
|
||||
| -- | UseItem | Item use request |
|
||||
| -- | Interaction | Entity/block interaction |
|
||||
| -- | MouseMotion | Mouse movement data |
|
||||
| -- | MouseButton | Mouse button press/release |
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Particle System Packet
|
||||
|
||||
The particle system packet (ID: 152) is used to spawn visual effects:
|
||||
|
||||
```java
|
||||
// Internal packet structure (for reference only)
|
||||
public class SpawnParticleSystemPacket {
|
||||
private ParticleSystem particleSystem;
|
||||
private Vector3d position;
|
||||
private Vector3f rotation; // Optional
|
||||
private Vector3f scale; // Optional
|
||||
private Entity attachedEntity; // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Broadcast Distance:** `DEFAULT_PARTICLE_DISTANCE = 75` blocks
|
||||
|
||||
### Spawning Particles via API
|
||||
|
||||
```java
|
||||
// Simple particle spawn
|
||||
world.spawnParticle(particleSystem, position);
|
||||
|
||||
// Particle with rotation
|
||||
world.spawnParticle(particleSystem, position, rotation);
|
||||
|
||||
// Particle attached to entity
|
||||
world.spawnParticle(particleSystem, entity);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Packets
|
||||
|
||||
### Entity Spawn
|
||||
|
||||
When an entity enters a player's view distance, the server sends entity spawn data:
|
||||
|
||||
```java
|
||||
// Automatic - when entity is created in world
|
||||
Entity entity = world.spawnEntity(entityType, position);
|
||||
// Server automatically notifies nearby clients
|
||||
|
||||
// Manual control via entity visibility
|
||||
entity.setVisibleTo(player, true); // Force show to specific player
|
||||
entity.setVisibleTo(player, false); // Hide from specific player
|
||||
```
|
||||
|
||||
### Entity Updates
|
||||
|
||||
Entity state changes are batched and sent periodically:
|
||||
|
||||
| Component | Update Frequency | Priority |
|
||||
|-----------|------------------|----------|
|
||||
| TransformComponent | Every tick if changed | High |
|
||||
| Velocity | Every tick if changed | High |
|
||||
| Health | On change | Medium |
|
||||
| DisplayName | On change | Low |
|
||||
| Equipment | On change | Medium |
|
||||
| Effects | On change | Low |
|
||||
|
||||
```java
|
||||
// Position updates are automatic
|
||||
entity.setPosition(newPos);
|
||||
|
||||
// Velocity updates are automatic
|
||||
entity.setVelocity(newVelocity);
|
||||
|
||||
// Display name updates
|
||||
entity.setComponent(DisplayNameComponent.class, new DisplayNameComponent("New Name"));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Block Packets
|
||||
|
||||
### Single Block Change
|
||||
|
||||
```java
|
||||
// Server-side API
|
||||
world.setBlock(position, blockType);
|
||||
// Automatically sends BlockChange to players with loaded chunk
|
||||
|
||||
// With block state
|
||||
world.setBlock(position, blockType, blockState);
|
||||
```
|
||||
|
||||
### Multi-Block Change
|
||||
|
||||
For bulk updates, changes are batched by chunk:
|
||||
|
||||
```java
|
||||
// Multiple blocks in same chunk are batched
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
world.setBlock(new Vector3i(chunkX * 16 + x, 64, chunkZ * 16 + z), blockType);
|
||||
}
|
||||
}
|
||||
// Server batches these into a single multi-block update packet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sound Packets
|
||||
|
||||
### Playing Sounds
|
||||
|
||||
```java
|
||||
// Play sound at position
|
||||
world.playSound(soundEvent, position, volume, pitch);
|
||||
|
||||
// Play sound to specific player
|
||||
player.playSound(soundEvent, position, volume, pitch);
|
||||
|
||||
// Play sound from entity
|
||||
entity.playSound(soundEvent, volume, pitch);
|
||||
```
|
||||
|
||||
### Sound Categories
|
||||
|
||||
| Category | Description | Default Volume |
|
||||
|----------|-------------|----------------|
|
||||
| MASTER | Overall volume | 1.0 |
|
||||
| MUSIC | Background music | 0.5 |
|
||||
| AMBIENT | Environmental sounds | 0.8 |
|
||||
| BLOCKS | Block interaction sounds | 1.0 |
|
||||
| ENTITIES | Entity sounds | 1.0 |
|
||||
| PLAYERS | Player-made sounds | 1.0 |
|
||||
| UI | User interface sounds | 1.0 |
|
||||
|
||||
---
|
||||
|
||||
## Chat/Message Packets
|
||||
|
||||
### Message Types
|
||||
|
||||
```java
|
||||
// Chat message (appears in chat)
|
||||
player.sendMessage(Message.raw("Hello!"));
|
||||
|
||||
// Translation-based message
|
||||
player.sendMessage(Message.translation("my.translation.key")
|
||||
.param("player", player.getDisplayName()));
|
||||
```
|
||||
|
||||
### Rich Text Formatting
|
||||
|
||||
Hytale's Message class supports styling through method chaining:
|
||||
|
||||
```java
|
||||
// Raw text message
|
||||
Message message = Message.raw("Hello, World!");
|
||||
|
||||
// Colored message (using java.awt.Color)
|
||||
Message colored = Message.raw("Important!")
|
||||
.color(Color.RED);
|
||||
|
||||
// Message with parameters (for translations)
|
||||
Message parameterized = Message.translation("quest.completed")
|
||||
.param("player", playerName)
|
||||
.param("reward", "100 XP");
|
||||
|
||||
// Bold/italic formatting
|
||||
Message styled = Message.raw("Notice!")
|
||||
.bold(true)
|
||||
.italic(false);
|
||||
|
||||
// Inserting multiple messages
|
||||
Message combined = Message.empty()
|
||||
.insert(Message.raw("Part 1"))
|
||||
.insert(Message.raw(" - "))
|
||||
.insert(Message.raw("Part 2"));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Events
|
||||
|
||||
### Intercepting Network Activity
|
||||
|
||||
```java
|
||||
public class NetworkMonitorPlugin extends ServerPlugin {
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerConnect(PlayerConnectEvent event) {
|
||||
// New connection established
|
||||
log.info("Player connected: " + event.getUsername());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerSetupConnect(PlayerSetupConnectEvent event) {
|
||||
// Before connection is fully established
|
||||
// Can be cancelled
|
||||
if (isBlacklisted(event.getUuid())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
// Connection closed
|
||||
log.info("Player disconnected: " + event.getPlayerRef().getUuid());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
{{< tabs items="Optimization,Debugging" >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Reducing Network Load
|
||||
|
||||
**1. Batch Updates:**
|
||||
```java
|
||||
// Instead of sending many small updates
|
||||
world.setBlock(pos1, block);
|
||||
world.setBlock(pos2, block);
|
||||
world.setBlock(pos3, block);
|
||||
|
||||
// The server automatically batches these per tick
|
||||
// No manual batching needed
|
||||
```
|
||||
|
||||
**2. Use Appropriate Update Rates:**
|
||||
```java
|
||||
// For frequently changing data, consider throttling
|
||||
private long lastUpdate = 0;
|
||||
private static final long UPDATE_INTERVAL_MS = 50; // 20 updates/sec
|
||||
|
||||
public void updateDisplay() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUpdate >= UPDATE_INTERVAL_MS) {
|
||||
lastUpdate = now;
|
||||
sendDisplayUpdate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Limit Broadcast Scope:**
|
||||
```java
|
||||
// Only send to players who need the update
|
||||
double maxDistance = 100.0;
|
||||
Vector3d sourcePos = event.getPosition();
|
||||
|
||||
// Note: Player doesn't have getPosition() directly
|
||||
// Use PlayerRef to get position via getTransform()
|
||||
for (PlayerRef playerRef : world.getPlayerRefs()) {
|
||||
Transform transform = playerRef.getTransform();
|
||||
if (transform != null) {
|
||||
double distance = transform.getPosition().distance(sourcePos);
|
||||
if (distance <= maxDistance) {
|
||||
playerRef.sendMessage(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Debugging Network Issues
|
||||
|
||||
**1. Log Network Activity:**
|
||||
```java
|
||||
@Subscribe
|
||||
public void onPlayerConnect(PlayerConnectEvent event) {
|
||||
log.debug("Connect from {} ({})",
|
||||
event.getUsername(),
|
||||
event.getUuid());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
log.debug("Disconnect: {}",
|
||||
event.getPlayerRef().getUuid());
|
||||
}
|
||||
```
|
||||
|
||||
**2. Monitor Packet Timing:**
|
||||
```java
|
||||
// Track message round-trip time
|
||||
private final Map<UUID, Long> pingTimes = new ConcurrentHashMap<>();
|
||||
|
||||
public void sendPing(Player player) {
|
||||
pingTimes.put(player.getUuid(), System.currentTimeMillis());
|
||||
player.sendMessage(Message.raw("ping"));
|
||||
}
|
||||
|
||||
// In start() method:
|
||||
getEventRegistry().register(PlayerChatEvent.class, event -> {
|
||||
if (event.getContent().equals("pong")) {
|
||||
Long sent = pingTimes.remove(event.getSender().getUuid());
|
||||
if (sent != null) {
|
||||
long rtt = System.currentTimeMillis() - sent;
|
||||
getLogger().at(Level.INFO).log("RTT for " + event.getSender().getUsername() + ": " + rtt + "ms");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Never trust client data!** Always validate:
|
||||
- Positions (within valid range, not too far from player)
|
||||
- Actions (player has permission, cooldown respected)
|
||||
- Resources (item counts, block placements)
|
||||
{{< /callout >}}
|
||||
|
||||
```java
|
||||
// Example: Validating a block placement
|
||||
getEventRegistry().register(PlaceBlockEvent.class, event -> {
|
||||
Vector3i targetPos = event.getTargetBlock();
|
||||
|
||||
// Validate world bounds
|
||||
// Note: Vector3i uses getY(), NOT y()
|
||||
if (targetPos.getY() < 0 || targetPos.getY() > 255) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: PlaceBlockEvent is an ECS event - player access is through
|
||||
// the component system, not direct getPlayer() calls.
|
||||
// For player-specific validation, consider using entity components.
|
||||
});
|
||||
```
|
||||
407
content/advanced/networking/packets.fr.md
Normal file
407
content/advanced/networking/packets.fr.md
Normal file
@@ -0,0 +1,407 @@
|
||||
---
|
||||
title: Packets
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Hytale uses a binary packet protocol for efficient client-server communication. While most networking is handled internally, understanding the packet system helps when debugging or implementing advanced features.
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
Direct packet manipulation is generally not recommended. Use the high-level APIs provided by the plugin system whenever possible.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## Packet Structure
|
||||
|
||||
All packets follow a common structure:
|
||||
|
||||
```
|
||||
┌──────────────┬───────────────┬─────────────────────┐
|
||||
│ Packet ID │ Payload Size │ Payload │
|
||||
│ (varint) │ (varint) │ (variable bytes) │
|
||||
└──────────────┴───────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| Packet ID | VarInt | Unique identifier for packet type |
|
||||
| Payload Size | VarInt | Size of payload in bytes |
|
||||
| Payload | bytes | Packet-specific data |
|
||||
|
||||
---
|
||||
|
||||
## Known Packet IDs
|
||||
|
||||
{{< tabs items="Client-bound,Server-bound" >}}
|
||||
{{< tab >}}
|
||||
|
||||
Packets sent from server to client:
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| 152 | SpawnParticleSystem | Spawns particles at a location |
|
||||
| -- | EntitySpawn | Spawns an entity on the client |
|
||||
| -- | EntityDespawn | Removes an entity from the client |
|
||||
| -- | EntityMove | Updates entity position |
|
||||
| -- | BlockChange | Updates a single block |
|
||||
| -- | ChunkData | Sends chunk block data |
|
||||
| -- | ChatMessage | Sends chat/system message |
|
||||
| -- | PlaySound | Plays a sound effect |
|
||||
| -- | SetWeather | Changes weather state |
|
||||
| -- | TimeUpdate | Synchronizes world time |
|
||||
| -- | PlayerInfo | Updates player list data |
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Packet IDs marked with `--` are internal and subject to change between versions.
|
||||
{{< /callout >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
Packets sent from client to server:
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| -- | PlayerPosition | Player movement update |
|
||||
| -- | PlayerAction | Player action (attack, use, etc.) |
|
||||
| -- | ChatMessage | Chat message from player |
|
||||
| -- | BlockBreak | Block breaking progress |
|
||||
| -- | BlockPlace | Block placement request |
|
||||
| -- | UseItem | Item use request |
|
||||
| -- | Interaction | Entity/block interaction |
|
||||
| -- | MouseMotion | Mouse movement data |
|
||||
| -- | MouseButton | Mouse button press/release |
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Particle System Packet
|
||||
|
||||
The particle system packet (ID: 152) is used to spawn visual effects:
|
||||
|
||||
```java
|
||||
// Internal packet structure (for reference only)
|
||||
public class SpawnParticleSystemPacket {
|
||||
private ParticleSystem particleSystem;
|
||||
private Vector3d position;
|
||||
private Vector3f rotation; // Optional
|
||||
private Vector3f scale; // Optional
|
||||
private Entity attachedEntity; // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Broadcast Distance:** `DEFAULT_PARTICLE_DISTANCE = 75` blocks
|
||||
|
||||
### Spawning Particles via API
|
||||
|
||||
```java
|
||||
// Simple particle spawn
|
||||
world.spawnParticle(particleSystem, position);
|
||||
|
||||
// Particle with rotation
|
||||
world.spawnParticle(particleSystem, position, rotation);
|
||||
|
||||
// Particle attached to entity
|
||||
world.spawnParticle(particleSystem, entity);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Packets
|
||||
|
||||
### Entity Spawn
|
||||
|
||||
When an entity enters a player's view distance, the server sends entity spawn data:
|
||||
|
||||
```java
|
||||
// Automatic - when entity is created in world
|
||||
Entity entity = world.spawnEntity(entityType, position);
|
||||
// Server automatically notifies nearby clients
|
||||
|
||||
// Manual control via entity visibility
|
||||
entity.setVisibleTo(player, true); // Force show to specific player
|
||||
entity.setVisibleTo(player, false); // Hide from specific player
|
||||
```
|
||||
|
||||
### Entity Updates
|
||||
|
||||
Entity state changes are batched and sent periodically:
|
||||
|
||||
| Component | Update Frequency | Priority |
|
||||
|-----------|------------------|----------|
|
||||
| TransformComponent | Every tick if changed | High |
|
||||
| Velocity | Every tick if changed | High |
|
||||
| Health | On change | Medium |
|
||||
| DisplayName | On change | Low |
|
||||
| Equipment | On change | Medium |
|
||||
| Effects | On change | Low |
|
||||
|
||||
```java
|
||||
// Position updates are automatic
|
||||
entity.setPosition(newPos);
|
||||
|
||||
// Velocity updates are automatic
|
||||
entity.setVelocity(newVelocity);
|
||||
|
||||
// Display name updates
|
||||
entity.setComponent(DisplayNameComponent.class, new DisplayNameComponent("New Name"));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Block Packets
|
||||
|
||||
### Single Block Change
|
||||
|
||||
```java
|
||||
// Server-side API
|
||||
world.setBlock(position, blockType);
|
||||
// Automatically sends BlockChange to players with loaded chunk
|
||||
|
||||
// With block state
|
||||
world.setBlock(position, blockType, blockState);
|
||||
```
|
||||
|
||||
### Multi-Block Change
|
||||
|
||||
For bulk updates, changes are batched by chunk:
|
||||
|
||||
```java
|
||||
// Multiple blocks in same chunk are batched
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
world.setBlock(new Vector3i(chunkX * 16 + x, 64, chunkZ * 16 + z), blockType);
|
||||
}
|
||||
}
|
||||
// Server batches these into a single multi-block update packet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sound Packets
|
||||
|
||||
### Playing Sounds
|
||||
|
||||
```java
|
||||
// Play sound at position
|
||||
world.playSound(soundEvent, position, volume, pitch);
|
||||
|
||||
// Play sound to specific player
|
||||
player.playSound(soundEvent, position, volume, pitch);
|
||||
|
||||
// Play sound from entity
|
||||
entity.playSound(soundEvent, volume, pitch);
|
||||
```
|
||||
|
||||
### Sound Categories
|
||||
|
||||
| Category | Description | Default Volume |
|
||||
|----------|-------------|----------------|
|
||||
| MASTER | Overall volume | 1.0 |
|
||||
| MUSIC | Background music | 0.5 |
|
||||
| AMBIENT | Environmental sounds | 0.8 |
|
||||
| BLOCKS | Block interaction sounds | 1.0 |
|
||||
| ENTITIES | Entity sounds | 1.0 |
|
||||
| PLAYERS | Player-made sounds | 1.0 |
|
||||
| UI | User interface sounds | 1.0 |
|
||||
|
||||
---
|
||||
|
||||
## Chat/Message Packets
|
||||
|
||||
### Message Types
|
||||
|
||||
```java
|
||||
// Chat message (appears in chat)
|
||||
player.sendMessage(Message.raw("Hello!"));
|
||||
|
||||
// Translation-based message
|
||||
player.sendMessage(Message.translation("my.translation.key")
|
||||
.param("player", player.getDisplayName()));
|
||||
```
|
||||
|
||||
### Rich Text Formatting
|
||||
|
||||
Hytale's Message class supports styling through method chaining:
|
||||
|
||||
```java
|
||||
// Raw text message
|
||||
Message message = Message.raw("Hello, World!");
|
||||
|
||||
// Colored message (using java.awt.Color)
|
||||
Message colored = Message.raw("Important!")
|
||||
.color(Color.RED);
|
||||
|
||||
// Message with parameters (for translations)
|
||||
Message parameterized = Message.translation("quest.completed")
|
||||
.param("player", playerName)
|
||||
.param("reward", "100 XP");
|
||||
|
||||
// Bold/italic formatting
|
||||
Message styled = Message.raw("Notice!")
|
||||
.bold(true)
|
||||
.italic(false);
|
||||
|
||||
// Inserting multiple messages
|
||||
Message combined = Message.empty()
|
||||
.insert(Message.raw("Part 1"))
|
||||
.insert(Message.raw(" - "))
|
||||
.insert(Message.raw("Part 2"));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Events
|
||||
|
||||
### Intercepting Network Activity
|
||||
|
||||
```java
|
||||
public class NetworkMonitorPlugin extends ServerPlugin {
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerConnect(PlayerConnectEvent event) {
|
||||
// New connection established
|
||||
log.info("Player connected: " + event.getUsername());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerSetupConnect(PlayerSetupConnectEvent event) {
|
||||
// Before connection is fully established
|
||||
// Can be cancelled
|
||||
if (isBlacklisted(event.getUuid())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
// Connection closed
|
||||
log.info("Player disconnected: " + event.getPlayerRef().getUuid());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
{{< tabs items="Optimization,Debugging" >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Reducing Network Load
|
||||
|
||||
**1. Batch Updates:**
|
||||
```java
|
||||
// Instead of sending many small updates
|
||||
world.setBlock(pos1, block);
|
||||
world.setBlock(pos2, block);
|
||||
world.setBlock(pos3, block);
|
||||
|
||||
// The server automatically batches these per tick
|
||||
// No manual batching needed
|
||||
```
|
||||
|
||||
**2. Use Appropriate Update Rates:**
|
||||
```java
|
||||
// For frequently changing data, consider throttling
|
||||
private long lastUpdate = 0;
|
||||
private static final long UPDATE_INTERVAL_MS = 50; // 20 updates/sec
|
||||
|
||||
public void updateDisplay() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUpdate >= UPDATE_INTERVAL_MS) {
|
||||
lastUpdate = now;
|
||||
sendDisplayUpdate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Limit Broadcast Scope:**
|
||||
```java
|
||||
// Only send to players who need the update
|
||||
double maxDistance = 100.0;
|
||||
Vector3d sourcePos = event.getPosition();
|
||||
|
||||
for (Player player : world.getPlayers()) {
|
||||
if (player.getPosition().distanceTo(sourcePos) <= maxDistance) {
|
||||
player.sendMessage(notification);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
### Debugging Network Issues
|
||||
|
||||
**1. Log Network Activity:**
|
||||
```java
|
||||
@Subscribe
|
||||
public void onPlayerConnect(PlayerConnectEvent event) {
|
||||
log.debug("Connect from {} ({})",
|
||||
event.getUsername(),
|
||||
event.getUuid());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
log.debug("Disconnect: {}",
|
||||
event.getPlayerRef().getUuid());
|
||||
}
|
||||
```
|
||||
|
||||
**2. Monitor Packet Timing:**
|
||||
```java
|
||||
// Track message round-trip time
|
||||
private final Map<UUID, Long> pingTimes = new ConcurrentHashMap<>();
|
||||
|
||||
public void sendPing(Player player) {
|
||||
pingTimes.put(player.getUuid(), System.currentTimeMillis());
|
||||
player.sendMessage(Message.raw("ping"));
|
||||
}
|
||||
|
||||
// In start() method:
|
||||
getEventRegistry().register(PlayerChatEvent.class, event -> {
|
||||
if (event.getContent().equals("pong")) {
|
||||
Long sent = pingTimes.remove(event.getSender().getUuid());
|
||||
if (sent != null) {
|
||||
long rtt = System.currentTimeMillis() - sent;
|
||||
getLogger().at(Level.INFO).log("RTT for " + event.getSender().getUsername() + ": " + rtt + "ms");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Never trust client data!** Always validate:
|
||||
- Positions (within valid range, not too far from player)
|
||||
- Actions (player has permission, cooldown respected)
|
||||
- Resources (item counts, block placements)
|
||||
{{< /callout >}}
|
||||
|
||||
```java
|
||||
// Example: Validating a block placement
|
||||
getEventRegistry().register(PlaceBlockEvent.class, event -> {
|
||||
Vector3i targetPos = event.getTargetBlock();
|
||||
|
||||
// Validate world bounds
|
||||
if (targetPos.y() < 0 || targetPos.y() > 255) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: PlaceBlockEvent is an ECS event - player access is through
|
||||
// the component system, not direct getPlayer() calls.
|
||||
// For player-specific validation, consider using entity components.
|
||||
});
|
||||
```
|
||||
467
content/advanced/networking/sync.en.md
Normal file
467
content/advanced/networking/sync.en.md
Normal file
@@ -0,0 +1,467 @@
|
||||
---
|
||||
title: Synchronization
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Hytale uses an efficient synchronization system to keep game state consistent between the server and all connected clients. This page explains how entity and world data is synchronized and how to work effectively with the sync system.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The synchronization system handles:
|
||||
|
||||
- **Entity State**: Position, rotation, velocity, health, equipment
|
||||
- **World State**: Block changes, chunk loading/unloading
|
||||
- **Player Data**: Inventory, effects, permissions
|
||||
- **Game Events**: Particles, sounds, animations
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Synchronization is automatic for most operations. The server tracks what needs to be sent to each client and batches updates efficiently.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## The Network Outdated Pattern
|
||||
|
||||
Hytale uses a "dirty flag" pattern called `isNetworkOutdated` to track which components need synchronization:
|
||||
|
||||
```java
|
||||
// Simplified component sync interface
|
||||
public interface NetworkSyncable {
|
||||
/**
|
||||
* Check if this component has changes that need to be sent to clients
|
||||
*/
|
||||
boolean isNetworkOutdated();
|
||||
|
||||
/**
|
||||
* Mark this component as having pending changes
|
||||
*/
|
||||
void markNetworkOutdated();
|
||||
|
||||
/**
|
||||
* Clear the outdated flag after changes have been sent
|
||||
*/
|
||||
void clearNetworkOutdated();
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Update Cycle │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Game Logic 2. Mark Outdated 3. Sync Phase │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ entity.set │ → │ component. │ → │ Send to │ │
|
||||
│ │ Position() │ │ markNetwork │ │ clients │ │
|
||||
│ │ │ │ Outdated() │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ 4. Clear Flag │ │
|
||||
│ ┌─────────────┐ │ │
|
||||
│ │ clearNetwork│ ←────────────────────────────────┘ │
|
||||
│ │ Outdated() │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Synchronization
|
||||
|
||||
### Components That Sync
|
||||
|
||||
| Component | Sync Trigger | Data Sent |
|
||||
|-----------|--------------|-----------|
|
||||
| TransformComponent | Position/rotation change | x, y, z, pitch, yaw, roll |
|
||||
| Velocity | Velocity change | vx, vy, vz |
|
||||
| Health | Health change | current, max |
|
||||
| DisplayNameComponent | Name change | display name |
|
||||
| EntityScaleComponent | Scale change | scale factor |
|
||||
| ActiveAnimationComponent | Animation change | animation state |
|
||||
|
||||
### Automatic Sync
|
||||
|
||||
Most entity modifications automatically trigger synchronization:
|
||||
|
||||
```java
|
||||
// All of these automatically sync to clients
|
||||
entity.setPosition(new Vector3d(100, 64, 200));
|
||||
entity.setVelocity(new Vector3d(0, 1, 0));
|
||||
|
||||
Player player = (Player) entity;
|
||||
player.setHealth(10.0f);
|
||||
```
|
||||
|
||||
### Manual Component Updates
|
||||
|
||||
When modifying components directly, the sync flag is managed automatically:
|
||||
|
||||
```java
|
||||
// Getting a component and modifying it
|
||||
TransformComponent transform = entity.getComponent(TransformComponent.class);
|
||||
if (transform != null) {
|
||||
// Internal methods handle markNetworkOutdated()
|
||||
transform.setPosition(newPosition);
|
||||
}
|
||||
|
||||
// Setting a new component
|
||||
entity.setComponent(DisplayNameComponent.class,
|
||||
new DisplayNameComponent(Message.raw("Custom Name")));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## World Synchronization
|
||||
|
||||
### Block Updates
|
||||
|
||||
Block changes are synchronized to players who have the relevant chunks loaded:
|
||||
|
||||
```java
|
||||
// Single block change
|
||||
world.setBlock(position, blockType);
|
||||
|
||||
// The server:
|
||||
// 1. Updates the block in world data
|
||||
// 2. Marks the chunk as having pending updates
|
||||
// 3. On next sync tick, sends BlockChange to relevant players
|
||||
```
|
||||
|
||||
### Chunk Loading
|
||||
|
||||
Chunks are sent to players based on view distance:
|
||||
|
||||
```java
|
||||
// When a player moves or joins
|
||||
// 1. Server calculates which chunks should be visible
|
||||
// 2. New chunks are queued for sending
|
||||
// 3. Chunks out of range are marked for unloading
|
||||
|
||||
// Events you can listen to:
|
||||
@Subscribe
|
||||
public void onChunkLoad(ChunkPreLoadProcessEvent event) {
|
||||
// Chunk is about to be loaded for processing
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onChunkUnload(ChunkUnloadEvent event) {
|
||||
// Chunk is being unloaded
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onChunkSave(ChunkSaveEvent event) {
|
||||
// Chunk is being saved
|
||||
}
|
||||
```
|
||||
|
||||
### View Distance
|
||||
|
||||
```java
|
||||
// Players receive updates based on their view distance
|
||||
// This is configured server-side
|
||||
|
||||
// Particles have a fixed distance
|
||||
public static final int DEFAULT_PARTICLE_DISTANCE = 75;
|
||||
|
||||
// Entity sync uses view distance (configurable)
|
||||
// Block sync uses chunk loading range
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Player Data Synchronization
|
||||
|
||||
### Inventory Sync
|
||||
|
||||
Inventory changes sync automatically:
|
||||
|
||||
```java
|
||||
Player player = ...;
|
||||
Inventory inventory = player.getInventory();
|
||||
|
||||
// These trigger sync automatically
|
||||
inventory.addItem(itemStack);
|
||||
inventory.removeItem(slot);
|
||||
inventory.setItem(slot, itemStack);
|
||||
```
|
||||
|
||||
### Effects Sync
|
||||
|
||||
Status effects are synchronized when added or removed:
|
||||
|
||||
```java
|
||||
// Adding an effect syncs to the player
|
||||
player.addEffect(effect, duration, amplifier);
|
||||
|
||||
// Removing syncs the removal
|
||||
player.removeEffect(effectType);
|
||||
|
||||
// Clearing all effects
|
||||
player.clearEffects();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batching
|
||||
|
||||
The server automatically batches updates:
|
||||
|
||||
{{< tabs items="How Batching Works,Example" >}}
|
||||
{{< tab >}}
|
||||
|
||||
Updates are collected throughout each tick and sent together:
|
||||
|
||||
```
|
||||
Tick Start
|
||||
│
|
||||
├── Game Logic: entity1.setPosition(...)
|
||||
├── Game Logic: entity2.setPosition(...)
|
||||
├── Game Logic: entity3.setHealth(...)
|
||||
├── Game Logic: world.setBlock(...)
|
||||
├── Game Logic: world.setBlock(...)
|
||||
│
|
||||
Tick End
|
||||
│
|
||||
└── Sync Phase: Send all updates in batched packets
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Fewer packets sent
|
||||
- Lower network overhead
|
||||
- More efficient compression
|
||||
- Reduced client processing
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// This code results in ONE batched update, not 100 individual packets
|
||||
public void updateManyBlocks(World world) {
|
||||
for (int x = 0; x < 10; x++) {
|
||||
for (int z = 0; z < 10; z++) {
|
||||
world.setBlock(
|
||||
new Vector3i(baseX + x, 64, baseZ + z),
|
||||
blockType
|
||||
);
|
||||
}
|
||||
}
|
||||
// All 100 block changes are batched and sent together
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### Delta Compression
|
||||
|
||||
Only changed data is sent:
|
||||
|
||||
```java
|
||||
// If an entity only moves (position changes)
|
||||
// Only position data is sent, not health, inventory, etc.
|
||||
|
||||
entity.setPosition(newPosition);
|
||||
// Sends: position update only
|
||||
|
||||
entity.setHealth(newHealth);
|
||||
// Sends: health update only
|
||||
```
|
||||
|
||||
### Relevancy Filtering
|
||||
|
||||
Updates are only sent to relevant clients:
|
||||
|
||||
```java
|
||||
// Entity updates go to players tracking that entity
|
||||
// (players within view distance)
|
||||
|
||||
// Block updates go to players with that chunk loaded
|
||||
|
||||
// Sound effects respect distance
|
||||
world.playSound(soundEvent, position, volume, pitch);
|
||||
// Only players within hearing range receive this
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Synchronization
|
||||
|
||||
### Creating Synced Data
|
||||
|
||||
If you need to sync custom data, use player metadata or entity components:
|
||||
|
||||
```java
|
||||
// Using player metadata (persisted per-player)
|
||||
public class CustomSyncPlugin extends ServerPlugin {
|
||||
|
||||
private static final String CUSTOM_DATA_KEY = "myplugin:custom_data";
|
||||
|
||||
public void setCustomData(Player player, int value) {
|
||||
player.setMetadata(CUSTOM_DATA_KEY, value);
|
||||
// Send update to player
|
||||
sendCustomDataUpdate(player, value);
|
||||
}
|
||||
|
||||
public int getCustomData(Player player) {
|
||||
Object data = player.getMetadata(CUSTOM_DATA_KEY);
|
||||
return data instanceof Integer ? (Integer) data : 0;
|
||||
}
|
||||
|
||||
private void sendCustomDataUpdate(Player player, int value) {
|
||||
// Use messages to inform client of data changes
|
||||
// (custom client mod required to interpret this)
|
||||
player.sendMessage(Message.raw("")); // or use a custom channel
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Visibility Control
|
||||
|
||||
Control which entities players can see:
|
||||
|
||||
```java
|
||||
// Hide an entity from a specific player
|
||||
entity.setVisibleTo(player, false);
|
||||
|
||||
// Show a previously hidden entity
|
||||
entity.setVisibleTo(player, true);
|
||||
|
||||
// Check visibility
|
||||
boolean canSee = entity.isVisibleTo(player);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Sync Issues
|
||||
|
||||
{{< tabs items="Common Issues,Diagnostic Tools" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Entity Not Appearing:**
|
||||
```java
|
||||
// Check if entity is in a loaded chunk
|
||||
Chunk chunk = world.getChunk(entity.getChunkPosition());
|
||||
if (chunk == null || !chunk.isLoaded()) {
|
||||
log.warn("Entity in unloaded chunk!");
|
||||
}
|
||||
|
||||
// Check visibility
|
||||
if (!entity.isVisibleTo(player)) {
|
||||
log.warn("Entity hidden from player!");
|
||||
}
|
||||
|
||||
// Check distance
|
||||
double distance = entity.getPosition().distanceTo(player.getPosition());
|
||||
if (distance > viewDistance) {
|
||||
log.warn("Entity out of view distance: " + distance);
|
||||
}
|
||||
```
|
||||
|
||||
**Block Changes Not Syncing:**
|
||||
```java
|
||||
// Verify chunk is loaded for player
|
||||
Vector3i chunkPos = new Vector3i(
|
||||
blockPos.x() >> 4,
|
||||
0,
|
||||
blockPos.z() >> 4
|
||||
);
|
||||
|
||||
// Check if player has chunk loaded
|
||||
// (implementation depends on server API)
|
||||
```
|
||||
|
||||
**Delayed Updates:**
|
||||
```java
|
||||
// Updates are sent at end of tick
|
||||
// If you need immediate sync, there's usually no way to force it
|
||||
// Design your code to work with batched updates
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Logging Sync Activity:**
|
||||
```java
|
||||
@Subscribe
|
||||
public void onEntityMove(EntityMoveEvent event) {
|
||||
log.debug("Entity {} moved to {}",
|
||||
event.getEntity().getUuid(),
|
||||
event.getNewPosition());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onBlockChange(BlockChangeEvent event) {
|
||||
log.debug("Block at {} changed to {}",
|
||||
event.getPosition(),
|
||||
event.getNewBlockType());
|
||||
}
|
||||
```
|
||||
|
||||
**Monitoring Sync Timing:**
|
||||
```java
|
||||
// Track update frequency for an entity
|
||||
private final Map<UUID, Long> lastUpdateTime = new HashMap<>();
|
||||
|
||||
public void onEntityUpdate(Entity entity) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastUpdateTime.put(entity.getUuid(), now);
|
||||
|
||||
if (last != null) {
|
||||
long delta = now - last;
|
||||
if (delta < 50) { // Less than 1 tick
|
||||
log.warn("Rapid updates on entity {}: {}ms apart",
|
||||
entity.getUuid(), delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Summary of best practices:**
|
||||
|
||||
1. **Trust automatic sync** - Don't try to manually trigger synchronization
|
||||
2. **Batch your updates** - Make multiple changes within a single tick when possible
|
||||
3. **Use events** - Listen for sync-related events to react to state changes
|
||||
4. **Consider view distance** - Don't waste resources updating entities far from players
|
||||
5. **Handle disconnections** - Clean up player-specific sync data when they leave
|
||||
{{< /callout >}}
|
||||
|
||||
```java
|
||||
// Example: Efficient entity spawning with proper sync
|
||||
public class EfficientSpawnPlugin extends ServerPlugin {
|
||||
|
||||
public void spawnManyEntities(World world, Vector3d center, int count) {
|
||||
// Spawn all entities in one tick - they'll be batched
|
||||
for (int i = 0; i < count; i++) {
|
||||
Vector3d offset = new Vector3d(
|
||||
Math.random() * 10 - 5,
|
||||
0,
|
||||
Math.random() * 10 - 5
|
||||
);
|
||||
|
||||
Entity entity = world.spawnEntity(
|
||||
entityType,
|
||||
center.add(offset)
|
||||
);
|
||||
|
||||
// Configure entity (still same tick, still batched)
|
||||
entity.setComponent(DisplayNameComponent.class,
|
||||
new DisplayNameComponent(Message.raw("Entity #" + i)));
|
||||
}
|
||||
// All spawns sent in one batched update at end of tick
|
||||
}
|
||||
}
|
||||
```
|
||||
467
content/advanced/networking/sync.fr.md
Normal file
467
content/advanced/networking/sync.fr.md
Normal file
@@ -0,0 +1,467 @@
|
||||
---
|
||||
title: Synchronization
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Hytale uses an efficient synchronization system to keep game state consistent between the server and all connected clients. This page explains how entity and world data is synchronized and how to work effectively with the sync system.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The synchronization system handles:
|
||||
|
||||
- **Entity State**: Position, rotation, velocity, health, equipment
|
||||
- **World State**: Block changes, chunk loading/unloading
|
||||
- **Player Data**: Inventory, effects, permissions
|
||||
- **Game Events**: Particles, sounds, animations
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Synchronization is automatic for most operations. The server tracks what needs to be sent to each client and batches updates efficiently.
|
||||
{{< /callout >}}
|
||||
|
||||
---
|
||||
|
||||
## The Network Outdated Pattern
|
||||
|
||||
Hytale uses a "dirty flag" pattern called `isNetworkOutdated` to track which components need synchronization:
|
||||
|
||||
```java
|
||||
// Simplified component sync interface
|
||||
public interface NetworkSyncable {
|
||||
/**
|
||||
* Check if this component has changes that need to be sent to clients
|
||||
*/
|
||||
boolean isNetworkOutdated();
|
||||
|
||||
/**
|
||||
* Mark this component as having pending changes
|
||||
*/
|
||||
void markNetworkOutdated();
|
||||
|
||||
/**
|
||||
* Clear the outdated flag after changes have been sent
|
||||
*/
|
||||
void clearNetworkOutdated();
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Update Cycle │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Game Logic 2. Mark Outdated 3. Sync Phase │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ entity.set │ → │ component. │ → │ Send to │ │
|
||||
│ │ Position() │ │ markNetwork │ │ clients │ │
|
||||
│ │ │ │ Outdated() │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ 4. Clear Flag │ │
|
||||
│ ┌─────────────┐ │ │
|
||||
│ │ clearNetwork│ ←────────────────────────────────┘ │
|
||||
│ │ Outdated() │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Synchronization
|
||||
|
||||
### Components That Sync
|
||||
|
||||
| Component | Sync Trigger | Data Sent |
|
||||
|-----------|--------------|-----------|
|
||||
| TransformComponent | Position/rotation change | x, y, z, pitch, yaw, roll |
|
||||
| Velocity | Velocity change | vx, vy, vz |
|
||||
| Health | Health change | current, max |
|
||||
| DisplayNameComponent | Name change | display name |
|
||||
| EntityScaleComponent | Scale change | scale factor |
|
||||
| ActiveAnimationComponent | Animation change | animation state |
|
||||
|
||||
### Automatic Sync
|
||||
|
||||
Most entity modifications automatically trigger synchronization:
|
||||
|
||||
```java
|
||||
// All of these automatically sync to clients
|
||||
entity.setPosition(new Vector3d(100, 64, 200));
|
||||
entity.setVelocity(new Vector3d(0, 1, 0));
|
||||
|
||||
Player player = (Player) entity;
|
||||
player.setHealth(10.0f);
|
||||
```
|
||||
|
||||
### Manual Component Updates
|
||||
|
||||
When modifying components directly, the sync flag is managed automatically:
|
||||
|
||||
```java
|
||||
// Getting a component and modifying it
|
||||
TransformComponent transform = entity.getComponent(TransformComponent.class);
|
||||
if (transform != null) {
|
||||
// Internal methods handle markNetworkOutdated()
|
||||
transform.setPosition(newPosition);
|
||||
}
|
||||
|
||||
// Setting a new component
|
||||
entity.setComponent(DisplayNameComponent.class,
|
||||
new DisplayNameComponent(Message.raw("Custom Name")));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## World Synchronization
|
||||
|
||||
### Block Updates
|
||||
|
||||
Block changes are synchronized to players who have the relevant chunks loaded:
|
||||
|
||||
```java
|
||||
// Single block change
|
||||
world.setBlock(position, blockType);
|
||||
|
||||
// The server:
|
||||
// 1. Updates the block in world data
|
||||
// 2. Marks the chunk as having pending updates
|
||||
// 3. On next sync tick, sends BlockChange to relevant players
|
||||
```
|
||||
|
||||
### Chunk Loading
|
||||
|
||||
Chunks are sent to players based on view distance:
|
||||
|
||||
```java
|
||||
// When a player moves or joins
|
||||
// 1. Server calculates which chunks should be visible
|
||||
// 2. New chunks are queued for sending
|
||||
// 3. Chunks out of range are marked for unloading
|
||||
|
||||
// Events you can listen to:
|
||||
@Subscribe
|
||||
public void onChunkLoad(ChunkPreLoadProcessEvent event) {
|
||||
// Chunk is about to be loaded for processing
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onChunkUnload(ChunkUnloadEvent event) {
|
||||
// Chunk is being unloaded
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onChunkSave(ChunkSaveEvent event) {
|
||||
// Chunk is being saved
|
||||
}
|
||||
```
|
||||
|
||||
### View Distance
|
||||
|
||||
```java
|
||||
// Players receive updates based on their view distance
|
||||
// This is configured server-side
|
||||
|
||||
// Particles have a fixed distance
|
||||
public static final int DEFAULT_PARTICLE_DISTANCE = 75;
|
||||
|
||||
// Entity sync uses view distance (configurable)
|
||||
// Block sync uses chunk loading range
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Player Data Synchronization
|
||||
|
||||
### Inventory Sync
|
||||
|
||||
Inventory changes sync automatically:
|
||||
|
||||
```java
|
||||
Player player = ...;
|
||||
Inventory inventory = player.getInventory();
|
||||
|
||||
// These trigger sync automatically
|
||||
inventory.addItem(itemStack);
|
||||
inventory.removeItem(slot);
|
||||
inventory.setItem(slot, itemStack);
|
||||
```
|
||||
|
||||
### Effects Sync
|
||||
|
||||
Status effects are synchronized when added or removed:
|
||||
|
||||
```java
|
||||
// Adding an effect syncs to the player
|
||||
player.addEffect(effect, duration, amplifier);
|
||||
|
||||
// Removing syncs the removal
|
||||
player.removeEffect(effectType);
|
||||
|
||||
// Clearing all effects
|
||||
player.clearEffects();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batching
|
||||
|
||||
The server automatically batches updates:
|
||||
|
||||
{{< tabs items="How Batching Works,Example" >}}
|
||||
{{< tab >}}
|
||||
|
||||
Updates are collected throughout each tick and sent together:
|
||||
|
||||
```
|
||||
Tick Start
|
||||
│
|
||||
├── Game Logic: entity1.setPosition(...)
|
||||
├── Game Logic: entity2.setPosition(...)
|
||||
├── Game Logic: entity3.setHealth(...)
|
||||
├── Game Logic: world.setBlock(...)
|
||||
├── Game Logic: world.setBlock(...)
|
||||
│
|
||||
Tick End
|
||||
│
|
||||
└── Sync Phase: Send all updates in batched packets
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Fewer packets sent
|
||||
- Lower network overhead
|
||||
- More efficient compression
|
||||
- Reduced client processing
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
```java
|
||||
// This code results in ONE batched update, not 100 individual packets
|
||||
public void updateManyBlocks(World world) {
|
||||
for (int x = 0; x < 10; x++) {
|
||||
for (int z = 0; z < 10; z++) {
|
||||
world.setBlock(
|
||||
new Vector3i(baseX + x, 64, baseZ + z),
|
||||
blockType
|
||||
);
|
||||
}
|
||||
}
|
||||
// All 100 block changes are batched and sent together
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### Delta Compression
|
||||
|
||||
Only changed data is sent:
|
||||
|
||||
```java
|
||||
// If an entity only moves (position changes)
|
||||
// Only position data is sent, not health, inventory, etc.
|
||||
|
||||
entity.setPosition(newPosition);
|
||||
// Sends: position update only
|
||||
|
||||
entity.setHealth(newHealth);
|
||||
// Sends: health update only
|
||||
```
|
||||
|
||||
### Relevancy Filtering
|
||||
|
||||
Updates are only sent to relevant clients:
|
||||
|
||||
```java
|
||||
// Entity updates go to players tracking that entity
|
||||
// (players within view distance)
|
||||
|
||||
// Block updates go to players with that chunk loaded
|
||||
|
||||
// Sound effects respect distance
|
||||
world.playSound(soundEvent, position, volume, pitch);
|
||||
// Only players within hearing range receive this
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Synchronization
|
||||
|
||||
### Creating Synced Data
|
||||
|
||||
If you need to sync custom data, use player metadata or entity components:
|
||||
|
||||
```java
|
||||
// Using player metadata (persisted per-player)
|
||||
public class CustomSyncPlugin extends ServerPlugin {
|
||||
|
||||
private static final String CUSTOM_DATA_KEY = "myplugin:custom_data";
|
||||
|
||||
public void setCustomData(Player player, int value) {
|
||||
player.setMetadata(CUSTOM_DATA_KEY, value);
|
||||
// Send update to player
|
||||
sendCustomDataUpdate(player, value);
|
||||
}
|
||||
|
||||
public int getCustomData(Player player) {
|
||||
Object data = player.getMetadata(CUSTOM_DATA_KEY);
|
||||
return data instanceof Integer ? (Integer) data : 0;
|
||||
}
|
||||
|
||||
private void sendCustomDataUpdate(Player player, int value) {
|
||||
// Use messages to inform client of data changes
|
||||
// (custom client mod required to interpret this)
|
||||
player.sendMessage(Message.raw("")); // or use a custom channel
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Visibility Control
|
||||
|
||||
Control which entities players can see:
|
||||
|
||||
```java
|
||||
// Hide an entity from a specific player
|
||||
entity.setVisibleTo(player, false);
|
||||
|
||||
// Show a previously hidden entity
|
||||
entity.setVisibleTo(player, true);
|
||||
|
||||
// Check visibility
|
||||
boolean canSee = entity.isVisibleTo(player);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Sync Issues
|
||||
|
||||
{{< tabs items="Common Issues,Diagnostic Tools" >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Entity Not Appearing:**
|
||||
```java
|
||||
// Check if entity is in a loaded chunk
|
||||
Chunk chunk = world.getChunk(entity.getChunkPosition());
|
||||
if (chunk == null || !chunk.isLoaded()) {
|
||||
log.warn("Entity in unloaded chunk!");
|
||||
}
|
||||
|
||||
// Check visibility
|
||||
if (!entity.isVisibleTo(player)) {
|
||||
log.warn("Entity hidden from player!");
|
||||
}
|
||||
|
||||
// Check distance
|
||||
double distance = entity.getPosition().distanceTo(player.getPosition());
|
||||
if (distance > viewDistance) {
|
||||
log.warn("Entity out of view distance: " + distance);
|
||||
}
|
||||
```
|
||||
|
||||
**Block Changes Not Syncing:**
|
||||
```java
|
||||
// Verify chunk is loaded for player
|
||||
Vector3i chunkPos = new Vector3i(
|
||||
blockPos.x() >> 4,
|
||||
0,
|
||||
blockPos.z() >> 4
|
||||
);
|
||||
|
||||
// Check if player has chunk loaded
|
||||
// (implementation depends on server API)
|
||||
```
|
||||
|
||||
**Delayed Updates:**
|
||||
```java
|
||||
// Updates are sent at end of tick
|
||||
// If you need immediate sync, there's usually no way to force it
|
||||
// Design your code to work with batched updates
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
|
||||
**Logging Sync Activity:**
|
||||
```java
|
||||
@Subscribe
|
||||
public void onEntityMove(EntityMoveEvent event) {
|
||||
log.debug("Entity {} moved to {}",
|
||||
event.getEntity().getUuid(),
|
||||
event.getNewPosition());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onBlockChange(BlockChangeEvent event) {
|
||||
log.debug("Block at {} changed to {}",
|
||||
event.getPosition(),
|
||||
event.getNewBlockType());
|
||||
}
|
||||
```
|
||||
|
||||
**Monitoring Sync Timing:**
|
||||
```java
|
||||
// Track update frequency for an entity
|
||||
private final Map<UUID, Long> lastUpdateTime = new HashMap<>();
|
||||
|
||||
public void onEntityUpdate(Entity entity) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastUpdateTime.put(entity.getUuid(), now);
|
||||
|
||||
if (last != null) {
|
||||
long delta = now - last;
|
||||
if (delta < 50) { // Less than 1 tick
|
||||
log.warn("Rapid updates on entity {}: {}ms apart",
|
||||
entity.getUuid(), delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
{{< callout type="info" >}}
|
||||
**Summary of best practices:**
|
||||
|
||||
1. **Trust automatic sync** - Don't try to manually trigger synchronization
|
||||
2. **Batch your updates** - Make multiple changes within a single tick when possible
|
||||
3. **Use events** - Listen for sync-related events to react to state changes
|
||||
4. **Consider view distance** - Don't waste resources updating entities far from players
|
||||
5. **Handle disconnections** - Clean up player-specific sync data when they leave
|
||||
{{< /callout >}}
|
||||
|
||||
```java
|
||||
// Example: Efficient entity spawning with proper sync
|
||||
public class EfficientSpawnPlugin extends ServerPlugin {
|
||||
|
||||
public void spawnManyEntities(World world, Vector3d center, int count) {
|
||||
// Spawn all entities in one tick - they'll be batched
|
||||
for (int i = 0; i < count; i++) {
|
||||
Vector3d offset = new Vector3d(
|
||||
Math.random() * 10 - 5,
|
||||
0,
|
||||
Math.random() * 10 - 5
|
||||
);
|
||||
|
||||
Entity entity = world.spawnEntity(
|
||||
entityType,
|
||||
center.add(offset)
|
||||
);
|
||||
|
||||
// Configure entity (still same tick, still batched)
|
||||
entity.setComponent(DisplayNameComponent.class,
|
||||
new DisplayNameComponent(Message.raw("Entity #" + i)));
|
||||
}
|
||||
// All spawns sent in one batched update at end of tick
|
||||
}
|
||||
}
|
||||
```
|
||||
17
content/core-concepts/_index.en.md
Normal file
17
content/core-concepts/_index.en.md
Normal 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 >}}
|
||||
17
content/core-concepts/_index.fr.md
Normal file
17
content/core-concepts/_index.fr.md
Normal 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 >}}
|
||||
135
content/core-concepts/assets.en.md
Normal file
135
content/core-concepts/assets.en.md
Normal 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 >}}
|
||||
135
content/core-concepts/assets.fr.md
Normal file
135
content/core-concepts/assets.fr.md
Normal 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 >}}
|
||||
227
content/core-concepts/codecs.en.md
Normal file
227
content/core-concepts/codecs.en.md
Normal 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 >}}
|
||||
227
content/core-concepts/codecs.fr.md
Normal file
227
content/core-concepts/codecs.fr.md
Normal 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 >}}
|
||||
14
content/core-concepts/commands/_index.en.md
Normal file
14
content/core-concepts/commands/_index.en.md
Normal 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 >}}
|
||||
14
content/core-concepts/commands/_index.fr.md
Normal file
14
content/core-concepts/commands/_index.fr.md
Normal 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 >}}
|
||||
1065
content/core-concepts/commands/argument-types.en.md
Normal file
1065
content/core-concepts/commands/argument-types.en.md
Normal file
File diff suppressed because it is too large
Load Diff
1065
content/core-concepts/commands/argument-types.fr.md
Normal file
1065
content/core-concepts/commands/argument-types.fr.md
Normal file
File diff suppressed because it is too large
Load Diff
272
content/core-concepts/commands/command-context.en.md
Normal file
272
content/core-concepts/commands/command-context.en.md
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
272
content/core-concepts/commands/command-context.fr.md
Normal file
272
content/core-concepts/commands/command-context.fr.md
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
264
content/core-concepts/commands/creating-commands.en.md
Normal file
264
content/core-concepts/commands/creating-commands.en.md
Normal 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");
|
||||
}
|
||||
```
|
||||
264
content/core-concepts/commands/creating-commands.fr.md
Normal file
264
content/core-concepts/commands/creating-commands.fr.md
Normal 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");
|
||||
}
|
||||
```
|
||||
224
content/core-concepts/commands/subcommands.en.md
Normal file
224
content/core-concepts/commands/subcommands.en.md
Normal 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");
|
||||
}
|
||||
```
|
||||
224
content/core-concepts/commands/subcommands.fr.md
Normal file
224
content/core-concepts/commands/subcommands.fr.md
Normal 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");
|
||||
}
|
||||
```
|
||||
15
content/core-concepts/events/_index.en.md
Normal file
15
content/core-concepts/events/_index.en.md
Normal 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 >}}
|
||||
15
content/core-concepts/events/_index.fr.md
Normal file
15
content/core-concepts/events/_index.fr.md
Normal 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 >}}
|
||||
361
content/core-concepts/events/async-events.en.md
Normal file
361
content/core-concepts/events/async-events.en.md
Normal 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;
|
||||
});
|
||||
});
|
||||
```
|
||||
361
content/core-concepts/events/async-events.fr.md
Normal file
361
content/core-concepts/events/async-events.fr.md
Normal 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;
|
||||
});
|
||||
});
|
||||
```
|
||||
250
content/core-concepts/events/cancellable-events.en.md
Normal file
250
content/core-concepts/events/cancellable-events.en.md
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
250
content/core-concepts/events/cancellable-events.fr.md
Normal file
250
content/core-concepts/events/cancellable-events.fr.md
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
246
content/core-concepts/events/event-priorities.en.md
Normal file
246
content/core-concepts/events/event-priorities.en.md
Normal 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 >}}
|
||||
201
content/core-concepts/events/event-priorities.fr.md
Normal file
201
content/core-concepts/events/event-priorities.fr.md
Normal 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());
|
||||
}
|
||||
});
|
||||
```
|
||||
50
content/core-concepts/events/event-reference/_index.en.md
Normal file
50
content/core-concepts/events/event-reference/_index.en.md
Normal 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
|
||||
50
content/core-concepts/events/event-reference/_index.fr.md
Normal file
50
content/core-concepts/events/event-reference/_index.fr.md
Normal 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
|
||||
721
content/core-concepts/events/event-reference/block-events.en.md
Normal file
721
content/core-concepts/events/event-reference/block-events.en.md
Normal 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 >}}
|
||||
721
content/core-concepts/events/event-reference/block-events.fr.md
Normal file
721
content/core-concepts/events/event-reference/block-events.fr.md
Normal 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 >}}
|
||||
193
content/core-concepts/events/event-reference/entity-events.en.md
Normal file
193
content/core-concepts/events/event-reference/entity-events.en.md
Normal 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 >}}
|
||||
193
content/core-concepts/events/event-reference/entity-events.fr.md
Normal file
193
content/core-concepts/events/event-reference/entity-events.fr.md
Normal 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 >}}
|
||||
@@ -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 >}}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
675
content/core-concepts/events/event-reference/player-events.en.md
Normal file
675
content/core-concepts/events/event-reference/player-events.en.md
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
675
content/core-concepts/events/event-reference/player-events.fr.md
Normal file
675
content/core-concepts/events/event-reference/player-events.fr.md
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
468
content/core-concepts/events/event-reference/server-events.en.md
Normal file
468
content/core-concepts/events/event-reference/server-events.en.md
Normal 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 >}}
|
||||
468
content/core-concepts/events/event-reference/server-events.fr.md
Normal file
468
content/core-concepts/events/event-reference/server-events.fr.md
Normal 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 >}}
|
||||
496
content/core-concepts/events/event-reference/world-events.en.md
Normal file
496
content/core-concepts/events/event-reference/world-events.en.md
Normal 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 >}}
|
||||
496
content/core-concepts/events/event-reference/world-events.fr.md
Normal file
496
content/core-concepts/events/event-reference/world-events.fr.md
Normal 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 >}}
|
||||
217
content/core-concepts/events/event-system.en.md
Normal file
217
content/core-concepts/events/event-system.en.md
Normal 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
|
||||
```
|
||||
217
content/core-concepts/events/event-system.fr.md
Normal file
217
content/core-concepts/events/event-system.fr.md
Normal 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
|
||||
```
|
||||
178
content/core-concepts/registries.en.md
Normal file
178
content/core-concepts/registries.en.md
Normal 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.
|
||||
178
content/core-concepts/registries.fr.md
Normal file
178
content/core-concepts/registries.fr.md
Normal 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.
|
||||
69
content/core-concepts/tasks/_index.en.md
Normal file
69
content/core-concepts/tasks/_index.en.md
Normal 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));
|
||||
});
|
||||
});
|
||||
```
|
||||
69
content/core-concepts/tasks/_index.fr.md
Normal file
69
content/core-concepts/tasks/_index.fr.md
Normal 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));
|
||||
});
|
||||
});
|
||||
```
|
||||
295
content/core-concepts/tasks/async-operations.en.md
Normal file
295
content/core-concepts/tasks/async-operations.en.md
Normal 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 >}}
|
||||
295
content/core-concepts/tasks/async-operations.fr.md
Normal file
295
content/core-concepts/tasks/async-operations.fr.md
Normal 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 >}}
|
||||
260
content/core-concepts/tasks/task-registry.en.md
Normal file
260
content/core-concepts/tasks/task-registry.en.md
Normal 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
|
||||
// });
|
||||
```
|
||||
260
content/core-concepts/tasks/task-registry.fr.md
Normal file
260
content/core-concepts/tasks/task-registry.fr.md
Normal 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
|
||||
// });
|
||||
```
|
||||
190
content/core-concepts/threading.en.md
Normal file
190
content/core-concepts/threading.en.md
Normal 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}`
|
||||
190
content/core-concepts/threading.fr.md
Normal file
190
content/core-concepts/threading.fr.md
Normal 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}`
|
||||
100
content/gameplay-systems/_index.en.md
Normal file
100
content/gameplay-systems/_index.en.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Gameplay Systems
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
The Gameplay Systems provide quest mechanics, progression systems, and gameplay features for the adventure mode.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure`
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="objectives" title="Objectives" subtitle="Quest and goal system" >}}
|
||||
{{< card link="farming" title="Farming" subtitle="Crop growth and agriculture" >}}
|
||||
{{< card link="npc-objectives" title="NPC Objectives" subtitle="NPC-related quests" >}}
|
||||
{{< card link="memories" title="Memories" subtitle="Persistent state tracking" >}}
|
||||
{{< card link="shop" title="Shop" subtitle="Trading and commerce" >}}
|
||||
{{< card link="camera-effects" title="Camera Effects" subtitle="Cinematic camera control" >}}
|
||||
{{< card link="reputation" title="Reputation" subtitle="Faction standing system" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Adventure system is composed of multiple plugins:
|
||||
|
||||
```
|
||||
Adventure System
|
||||
├── ObjectivePlugin (objectives/)
|
||||
│ ├── Objective - Quest instance with TaskSets
|
||||
│ ├── ObjectiveTask - Individual task types
|
||||
│ ├── ObjectiveCompletion - Reward handlers
|
||||
│ └── ObjectiveHistoryComponent - Progress tracking
|
||||
├── FarmingPlugin (farming/)
|
||||
│ ├── FarmingBlock/FarmingBlockState - Crop state
|
||||
│ ├── TilledSoilBlock - Farmland mechanics
|
||||
│ ├── GrowthModifier - Fertilizer, water, light
|
||||
│ └── CoopBlock/CoopResidentComponent - Animal coops
|
||||
├── NPCObjectivesPlugin (npcobjectives/)
|
||||
│ ├── KillTask/BountyTask - Combat objectives
|
||||
│ ├── KillTrackerSystem - Kill counting
|
||||
│ └── NPC Actions (StartObjective, CompleteTask)
|
||||
├── MemoriesPlugin (memories/)
|
||||
│ ├── PlayerMemories - Player memory component
|
||||
│ ├── Memory/MemoryProvider - Memory types
|
||||
│ └── NPCMemory - NPC-related memories
|
||||
├── ShopPlugin (shop/)
|
||||
│ ├── ShopAsset - Shop definitions
|
||||
│ ├── BarterShopAsset - Trade-based shops
|
||||
│ └── ShopElement/GiveItemInteraction
|
||||
├── CameraPlugin (camera/)
|
||||
│ ├── CameraShake - Screen shake effects
|
||||
│ ├── ViewBobbing - Movement camera bob
|
||||
│ └── CameraEffectSystem
|
||||
└── ReputationPlugin (reputation/)
|
||||
├── ReputationGroup - Faction definitions
|
||||
├── ReputationRank - Standing levels
|
||||
└── ReputationGroupComponent - NPC faction
|
||||
```
|
||||
|
||||
## Subpackages
|
||||
|
||||
| Package | Files | Description |
|
||||
|---------|-------|-------------|
|
||||
| `objectives/` | 82 | Quest and objective system |
|
||||
| `farming/` | 24 | Agriculture mechanics |
|
||||
| `npcobjectives/` | 22 | NPC-related objectives (kill tasks) |
|
||||
| `memories/` | 19 | State persistence and collectibles |
|
||||
| `shop/` | 16 | Trading and barter system |
|
||||
| `camera/` | 13 | Camera effects (shake, bobbing) |
|
||||
| `reputation/` | 12 | Faction reputation system |
|
||||
| `objectivereputation/` | 4 | Reputation rewards for objectives |
|
||||
| `npcreputation/` | 2 | NPC attitude based on reputation |
|
||||
| `shopreputation/` | 1 | Reputation-gated shop items |
|
||||
|
||||
## Quick Example
|
||||
|
||||
```java
|
||||
// Start an objective for a player
|
||||
ObjectivePlugin objectives = ObjectivePlugin.get();
|
||||
objectives.startObjective(player, "collect_wood");
|
||||
|
||||
// Change reputation with a faction
|
||||
ReputationPlugin rep = ReputationPlugin.get();
|
||||
rep.changeReputation(player, "village_faction", 10, componentAccessor);
|
||||
|
||||
// Get reputation rank
|
||||
ReputationRank rank = rep.getReputationRank(store, playerRef, "village_faction");
|
||||
|
||||
// Check if player has a memory
|
||||
MemoriesPlugin memories = MemoriesPlugin.get();
|
||||
boolean hasMemory = memories.hasRecordedMemory(someMemory);
|
||||
```
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
Each adventure subsystem is a separate plugin that registers:
|
||||
- **Components** on EntityStore or ChunkStore
|
||||
- **Interactions** for block/entity interactions
|
||||
- **Assets** loaded from JSON/YAML files
|
||||
- **Systems** for ECS processing
|
||||
- **Commands** for administration
|
||||
100
content/gameplay-systems/_index.fr.md
Normal file
100
content/gameplay-systems/_index.fr.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Systèmes de Gameplay
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
Les Systèmes de Gameplay fournissent des mécaniques de quêtes, des systèmes de progression et des fonctionnalités de gameplay pour le mode aventure.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure`
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="objectives" title="Objectifs" subtitle="Système de quêtes et buts" >}}
|
||||
{{< card link="farming" title="Agriculture" subtitle="Croissance des cultures et agriculture" >}}
|
||||
{{< card link="npc-objectives" title="Objectifs NPC" subtitle="Quêtes liées aux NPCs" >}}
|
||||
{{< card link="memories" title="Mémoires" subtitle="Suivi d'état persistant" >}}
|
||||
{{< card link="shop" title="Boutique" subtitle="Commerce et échanges" >}}
|
||||
{{< card link="camera-effects" title="Effets Caméra" subtitle="Contrôle caméra cinématique" >}}
|
||||
{{< card link="reputation" title="Réputation" subtitle="Système de standing de faction" >}}
|
||||
{{< /cards >}}
|
||||
|
||||
## Vue d'Ensemble de l'Architecture
|
||||
|
||||
Le système Adventure est composé de plusieurs plugins :
|
||||
|
||||
```
|
||||
Système Adventure
|
||||
├── ObjectivePlugin (objectives/)
|
||||
│ ├── Objective - Instance de quête avec TaskSets
|
||||
│ ├── ObjectiveTask - Types de tâches individuelles
|
||||
│ ├── ObjectiveCompletion - Gestionnaires de récompenses
|
||||
│ └── ObjectiveHistoryComponent - Suivi de progression
|
||||
├── FarmingPlugin (farming/)
|
||||
│ ├── FarmingBlock/FarmingBlockState - État des cultures
|
||||
│ ├── TilledSoilBlock - Mécaniques de terre labourée
|
||||
│ ├── GrowthModifier - Engrais, eau, lumière
|
||||
│ └── CoopBlock/CoopResidentComponent - Poulaillers
|
||||
├── NPCObjectivesPlugin (npcobjectives/)
|
||||
│ ├── KillTask/BountyTask - Objectifs de combat
|
||||
│ ├── KillTrackerSystem - Compteur d'éliminations
|
||||
│ └── Actions NPC (StartObjective, CompleteTask)
|
||||
├── MemoriesPlugin (memories/)
|
||||
│ ├── PlayerMemories - Composant mémoire joueur
|
||||
│ ├── Memory/MemoryProvider - Types de mémoires
|
||||
│ └── NPCMemory - Mémoires liées aux NPCs
|
||||
├── ShopPlugin (shop/)
|
||||
│ ├── ShopAsset - Définitions de boutiques
|
||||
│ ├── BarterShopAsset - Boutiques de troc
|
||||
│ └── ShopElement/GiveItemInteraction
|
||||
├── CameraPlugin (camera/)
|
||||
│ ├── CameraShake - Effets de tremblement
|
||||
│ ├── ViewBobbing - Balancement de caméra
|
||||
│ └── CameraEffectSystem
|
||||
└── ReputationPlugin (reputation/)
|
||||
├── ReputationGroup - Définitions de factions
|
||||
├── ReputationRank - Niveaux de standing
|
||||
└── ReputationGroupComponent - Faction du NPC
|
||||
```
|
||||
|
||||
## Sous-packages
|
||||
|
||||
| Package | Fichiers | Description |
|
||||
|---------|----------|-------------|
|
||||
| `objectives/` | 82 | Système de quêtes et objectifs |
|
||||
| `farming/` | 24 | Mécaniques d'agriculture |
|
||||
| `npcobjectives/` | 22 | Objectifs liés aux NPCs (tâches d'élimination) |
|
||||
| `memories/` | 19 | Persistance d'état et collectibles |
|
||||
| `shop/` | 16 | Système de commerce et troc |
|
||||
| `camera/` | 13 | Effets caméra (tremblement, balancement) |
|
||||
| `reputation/` | 12 | Système de réputation de faction |
|
||||
| `objectivereputation/` | 4 | Récompenses de réputation pour objectifs |
|
||||
| `npcreputation/` | 2 | Attitude NPC basée sur réputation |
|
||||
| `shopreputation/` | 1 | Items de boutique conditionnés par réputation |
|
||||
|
||||
## Exemple Rapide
|
||||
|
||||
```java
|
||||
// Démarrer un objectif pour un joueur
|
||||
ObjectivePlugin objectives = ObjectivePlugin.get();
|
||||
objectives.startObjective(player, "collect_wood");
|
||||
|
||||
// Changer la réputation avec une faction
|
||||
ReputationPlugin rep = ReputationPlugin.get();
|
||||
rep.changeReputation(player, "village_faction", 10, componentAccessor);
|
||||
|
||||
// Obtenir le rang de réputation
|
||||
ReputationRank rank = rep.getReputationRank(store, playerRef, "village_faction");
|
||||
|
||||
// Vérifier si le joueur a une mémoire
|
||||
MemoriesPlugin memories = MemoriesPlugin.get();
|
||||
boolean hasMemory = memories.hasRecordedMemory(someMemory);
|
||||
```
|
||||
|
||||
## Enregistrement des Plugins
|
||||
|
||||
Chaque sous-système adventure est un plugin séparé qui enregistre :
|
||||
- **Composants** sur EntityStore ou ChunkStore
|
||||
- **Interactions** pour les interactions bloc/entité
|
||||
- **Assets** chargés depuis fichiers JSON/YAML
|
||||
- **Systèmes** pour le traitement ECS
|
||||
- **Commandes** pour l'administration
|
||||
191
content/gameplay-systems/camera-effects.en.md
Normal file
191
content/gameplay-systems/camera-effects.en.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Camera Effects
|
||||
type: docs
|
||||
weight: 6
|
||||
---
|
||||
|
||||
The camera system provides screen effects like camera shake and view bobbing.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.camera`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CameraPlugin
|
||||
├── Effects
|
||||
│ ├── CameraShakeEffect - Screen shake effect
|
||||
│ └── CameraShake asset
|
||||
├── View Bobbing
|
||||
│ ├── ViewBobbing asset
|
||||
│ └── ViewBobbingPacketGenerator
|
||||
├── Interactions
|
||||
│ └── CameraShakeInteraction
|
||||
├── Systems
|
||||
│ └── CameraEffectSystem
|
||||
└── Commands
|
||||
└── CameraEffectCommand
|
||||
```
|
||||
|
||||
## Camera Shake
|
||||
|
||||
### CameraShakeEffect
|
||||
|
||||
Registered camera effect type:
|
||||
|
||||
```java
|
||||
getCodecRegistry(CameraEffect.CODEC).register(
|
||||
"CameraShake",
|
||||
CameraShakeEffect.class,
|
||||
CameraShakeEffect.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### CameraShake Asset
|
||||
|
||||
Camera shake definitions loaded from `Camera/CameraShake/`:
|
||||
|
||||
```java
|
||||
HytaleAssetStore.builder(String.class, CameraShake.class, new IndexedAssetMap())
|
||||
.loadsBefore(CameraEffect.class)
|
||||
.setPath("Camera/CameraShake")
|
||||
.setKeyFunction(CameraShake::getId)
|
||||
.setReplaceOnRemove(CameraShake::new)
|
||||
.setPacketGenerator(new CameraShakePacketGenerator())
|
||||
.build();
|
||||
```
|
||||
|
||||
### CameraShake Configuration
|
||||
|
||||
```yaml
|
||||
# Camera/CameraShake/explosion.json
|
||||
{
|
||||
"Id": "explosion",
|
||||
"Intensity": 1.0,
|
||||
"Duration": 0.5,
|
||||
"Frequency": 10.0,
|
||||
"FalloffDistance": 20.0
|
||||
}
|
||||
```
|
||||
|
||||
## View Bobbing
|
||||
|
||||
Camera movement during player locomotion:
|
||||
|
||||
### ViewBobbing Asset
|
||||
|
||||
```java
|
||||
HytaleAssetStore.builder(MovementType.class, ViewBobbing.class, new DefaultAssetMap())
|
||||
.setPath("Camera/ViewBobbing")
|
||||
.setKeyFunction(ViewBobbing::getId)
|
||||
.setPacketGenerator(new ViewBobbingPacketGenerator())
|
||||
.build();
|
||||
```
|
||||
|
||||
Key indexed by `MovementType` enum for different movement states.
|
||||
|
||||
### ViewBobbing Configuration
|
||||
|
||||
```yaml
|
||||
# Camera/ViewBobbing/walking.json
|
||||
{
|
||||
"Id": "WALKING",
|
||||
"HorizontalAmplitude": 0.02,
|
||||
"VerticalAmplitude": 0.01,
|
||||
"Frequency": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
### CameraShakeInteraction
|
||||
|
||||
Trigger camera shake from interactions:
|
||||
|
||||
```java
|
||||
getCodecRegistry(Interaction.CODEC).register(
|
||||
"CameraShake",
|
||||
CameraShakeInteraction.class,
|
||||
CameraShakeInteraction.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```yaml
|
||||
# Item or block interaction
|
||||
{
|
||||
"Type": "CameraShake",
|
||||
"ShakeId": "explosion",
|
||||
"Intensity": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
## Systems
|
||||
|
||||
### CameraEffectSystem
|
||||
|
||||
ECS system for processing camera effects:
|
||||
|
||||
```java
|
||||
getEntityStoreRegistry().registerSystem(new CameraEffectSystem());
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### CameraEffectCommand
|
||||
|
||||
Admin command for testing camera effects:
|
||||
|
||||
```java
|
||||
getCommandRegistry().registerCommand(new CameraEffectCommand());
|
||||
```
|
||||
|
||||
Usage:
|
||||
```
|
||||
/cameraeffect <effect_id> [intensity] [duration]
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Applying Camera Shake
|
||||
|
||||
```java
|
||||
// Through interaction system
|
||||
// Define in asset JSON and trigger via interaction
|
||||
|
||||
// Or programmatically through CameraEffectSystem
|
||||
```
|
||||
|
||||
### Getting Camera Assets
|
||||
|
||||
```java
|
||||
// Get camera shake asset
|
||||
CameraShake shake = CameraShake.getAssetMap().getAsset("explosion");
|
||||
|
||||
// Get view bobbing for movement type
|
||||
ViewBobbing bobbing = ViewBobbing.getAssetMap().getAsset(MovementType.WALKING);
|
||||
```
|
||||
|
||||
## Packet Generation
|
||||
|
||||
Both camera shake and view bobbing use packet generators for client sync:
|
||||
|
||||
```java
|
||||
public class CameraShakePacketGenerator implements PacketGenerator<CameraShake> {
|
||||
// Generates network packets for camera shake
|
||||
}
|
||||
|
||||
public class ViewBobbingPacketGenerator implements PacketGenerator<ViewBobbing> {
|
||||
// Generates network packets for view bobbing
|
||||
}
|
||||
```
|
||||
|
||||
## Asset Dependencies
|
||||
|
||||
Camera shake assets load before camera effects:
|
||||
|
||||
```java
|
||||
.loadsBefore(CameraEffect.class)
|
||||
```
|
||||
|
||||
This ensures shake definitions are available when camera effects reference them.
|
||||
191
content/gameplay-systems/camera-effects.fr.md
Normal file
191
content/gameplay-systems/camera-effects.fr.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Effets Camera
|
||||
type: docs
|
||||
weight: 6
|
||||
---
|
||||
|
||||
Le systeme de camera fournit des effets d'ecran comme les tremblements et le balancement de vue.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.camera`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CameraPlugin
|
||||
├── Effets
|
||||
│ ├── CameraShakeEffect - Effet de tremblement
|
||||
│ └── CameraShake asset
|
||||
├── Balancement de Vue
|
||||
│ ├── ViewBobbing asset
|
||||
│ └── ViewBobbingPacketGenerator
|
||||
├── Interactions
|
||||
│ └── CameraShakeInteraction
|
||||
├── Systemes
|
||||
│ └── CameraEffectSystem
|
||||
└── Commandes
|
||||
└── CameraEffectCommand
|
||||
```
|
||||
|
||||
## Tremblement de Camera
|
||||
|
||||
### CameraShakeEffect
|
||||
|
||||
Type d'effet camera enregistre:
|
||||
|
||||
```java
|
||||
getCodecRegistry(CameraEffect.CODEC).register(
|
||||
"CameraShake",
|
||||
CameraShakeEffect.class,
|
||||
CameraShakeEffect.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Asset CameraShake
|
||||
|
||||
Definitions de tremblements chargees depuis `Camera/CameraShake/`:
|
||||
|
||||
```java
|
||||
HytaleAssetStore.builder(String.class, CameraShake.class, new IndexedAssetMap())
|
||||
.loadsBefore(CameraEffect.class)
|
||||
.setPath("Camera/CameraShake")
|
||||
.setKeyFunction(CameraShake::getId)
|
||||
.setReplaceOnRemove(CameraShake::new)
|
||||
.setPacketGenerator(new CameraShakePacketGenerator())
|
||||
.build();
|
||||
```
|
||||
|
||||
### Configuration CameraShake
|
||||
|
||||
```yaml
|
||||
# Camera/CameraShake/explosion.json
|
||||
{
|
||||
"Id": "explosion",
|
||||
"Intensity": 1.0,
|
||||
"Duration": 0.5,
|
||||
"Frequency": 10.0,
|
||||
"FalloffDistance": 20.0
|
||||
}
|
||||
```
|
||||
|
||||
## Balancement de Vue
|
||||
|
||||
Mouvement de camera pendant la locomotion du joueur:
|
||||
|
||||
### Asset ViewBobbing
|
||||
|
||||
```java
|
||||
HytaleAssetStore.builder(MovementType.class, ViewBobbing.class, new DefaultAssetMap())
|
||||
.setPath("Camera/ViewBobbing")
|
||||
.setKeyFunction(ViewBobbing::getId)
|
||||
.setPacketGenerator(new ViewBobbingPacketGenerator())
|
||||
.build();
|
||||
```
|
||||
|
||||
Indexe par enum `MovementType` pour differents etats de mouvement.
|
||||
|
||||
### Configuration ViewBobbing
|
||||
|
||||
```yaml
|
||||
# Camera/ViewBobbing/walking.json
|
||||
{
|
||||
"Id": "WALKING",
|
||||
"HorizontalAmplitude": 0.02,
|
||||
"VerticalAmplitude": 0.01,
|
||||
"Frequency": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
### CameraShakeInteraction
|
||||
|
||||
Declencher un tremblement depuis les interactions:
|
||||
|
||||
```java
|
||||
getCodecRegistry(Interaction.CODEC).register(
|
||||
"CameraShake",
|
||||
CameraShakeInteraction.class,
|
||||
CameraShakeInteraction.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Exemple d'Utilisation
|
||||
|
||||
```yaml
|
||||
# Interaction d'item ou bloc
|
||||
{
|
||||
"Type": "CameraShake",
|
||||
"ShakeId": "explosion",
|
||||
"Intensity": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
## Systemes
|
||||
|
||||
### CameraEffectSystem
|
||||
|
||||
Systeme ECS pour traiter les effets camera:
|
||||
|
||||
```java
|
||||
getEntityStoreRegistry().registerSystem(new CameraEffectSystem());
|
||||
```
|
||||
|
||||
## Commandes
|
||||
|
||||
### CameraEffectCommand
|
||||
|
||||
Commande admin pour tester les effets camera:
|
||||
|
||||
```java
|
||||
getCommandRegistry().registerCommand(new CameraEffectCommand());
|
||||
```
|
||||
|
||||
Utilisation:
|
||||
```
|
||||
/cameraeffect <effect_id> [intensity] [duration]
|
||||
```
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
### Appliquer un Tremblement
|
||||
|
||||
```java
|
||||
// Via le systeme d'interaction
|
||||
// Definir dans JSON asset et declencher via interaction
|
||||
|
||||
// Ou programmatiquement via CameraEffectSystem
|
||||
```
|
||||
|
||||
### Obtenir les Assets Camera
|
||||
|
||||
```java
|
||||
// Obtenir asset de tremblement
|
||||
CameraShake shake = CameraShake.getAssetMap().getAsset("explosion");
|
||||
|
||||
// Obtenir balancement pour type de mouvement
|
||||
ViewBobbing bobbing = ViewBobbing.getAssetMap().getAsset(MovementType.WALKING);
|
||||
```
|
||||
|
||||
## Generation de Paquets
|
||||
|
||||
Les tremblements et balancements utilisent des generateurs de paquets pour sync client:
|
||||
|
||||
```java
|
||||
public class CameraShakePacketGenerator implements PacketGenerator<CameraShake> {
|
||||
// Genere paquets reseau pour tremblement
|
||||
}
|
||||
|
||||
public class ViewBobbingPacketGenerator implements PacketGenerator<ViewBobbing> {
|
||||
// Genere paquets reseau pour balancement
|
||||
}
|
||||
```
|
||||
|
||||
## Dependances d'Assets
|
||||
|
||||
Les assets de tremblement chargent avant les effets camera:
|
||||
|
||||
```java
|
||||
.loadsBefore(CameraEffect.class)
|
||||
```
|
||||
|
||||
Cela assure que les definitions de tremblement sont disponibles quand les effets camera les referencent.
|
||||
200
content/gameplay-systems/farming.en.md
Normal file
200
content/gameplay-systems/farming.en.md
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: Farming
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
The farming system provides crop growth, harvesting, and animal coop mechanics.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.farming`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
FarmingPlugin
|
||||
├── Block Components (ChunkStore)
|
||||
│ ├── TilledSoilBlock - Farmland state
|
||||
│ ├── FarmingBlock - Crop growth tracking
|
||||
│ ├── FarmingBlockState - Block growth state
|
||||
│ └── CoopBlock - Animal coop state
|
||||
├── Entity Components (EntityStore)
|
||||
│ └── CoopResidentComponent - Animals in coops
|
||||
├── Growth Modifiers
|
||||
│ ├── FertilizerGrowthModifier
|
||||
│ ├── WaterGrowthModifier
|
||||
│ └── LightLevelGrowthModifier
|
||||
└── Farming Stages
|
||||
├── BlockTypeFarmingStageData
|
||||
├── BlockStateFarmingStageData
|
||||
├── PrefabFarmingStageData
|
||||
└── SpreadFarmingStageData
|
||||
```
|
||||
|
||||
## Block Components
|
||||
|
||||
### TilledSoilBlock
|
||||
|
||||
Tracks tilled soil state on ChunkStore:
|
||||
|
||||
```java
|
||||
ComponentType<ChunkStore, TilledSoilBlock> type =
|
||||
FarmingPlugin.get().getTiledSoilBlockComponentType();
|
||||
```
|
||||
|
||||
### FarmingBlock
|
||||
|
||||
Tracks crop growth progress:
|
||||
|
||||
```java
|
||||
public class FarmingBlock {
|
||||
// Growth rate modifier (0 = no spread on newly generated)
|
||||
public void setSpreadRate(float rate);
|
||||
}
|
||||
```
|
||||
|
||||
### FarmingBlockState
|
||||
|
||||
Current farming state for a block position.
|
||||
|
||||
### CoopBlock
|
||||
|
||||
Animal coop block state for housing creatures.
|
||||
|
||||
## Growth Modifiers
|
||||
|
||||
Growth modifiers affect crop growth rate:
|
||||
|
||||
| Modifier | Type ID | Description |
|
||||
|----------|---------|-------------|
|
||||
| `FertilizerGrowthModifierAsset` | `Fertilizer` | Boosts growth with fertilizer |
|
||||
| `WaterGrowthModifierAsset` | `Water` | Water proximity bonus |
|
||||
| `LightLevelGrowthModifierAsset` | `LightLevel` | Light-based growth |
|
||||
|
||||
### Asset Configuration
|
||||
|
||||
```yaml
|
||||
# Farming/Modifiers/bone_meal.json
|
||||
{
|
||||
"Id": "bone_meal",
|
||||
"Type": "Fertilizer",
|
||||
"GrowthMultiplier": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
## Farming Stages
|
||||
|
||||
Crops progress through stages defined as assets:
|
||||
|
||||
| Stage Type | Type ID | Description |
|
||||
|------------|---------|-------------|
|
||||
| `BlockTypeFarmingStageData` | `BlockType` | Changes block type |
|
||||
| `BlockStateFarmingStageData` | `BlockState` | Changes block state |
|
||||
| `PrefabFarmingStageData` | `Prefab` | Places a prefab structure |
|
||||
| `SpreadFarmingStageData` | `Spread` | Spreads to adjacent blocks |
|
||||
|
||||
### Spread Growth Behavior
|
||||
|
||||
```java
|
||||
public interface SpreadGrowthBehaviour {
|
||||
// Directional growth (vines, etc.)
|
||||
}
|
||||
|
||||
public class DirectionalGrowthBehaviour implements SpreadGrowthBehaviour {
|
||||
// Grows in specified direction
|
||||
}
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
Registered farming interactions:
|
||||
|
||||
| Interaction | Type ID | Description |
|
||||
|-------------|---------|-------------|
|
||||
| `HarvestCropInteraction` | `HarvestCrop` | Harvest mature crops |
|
||||
| `FertilizeSoilInteraction` | `FertilizeSoil` | Apply fertilizer |
|
||||
| `ChangeFarmingStageInteraction` | `ChangeFarmingStage` | Force stage change |
|
||||
| `UseWateringCanInteraction` | `UseWateringCan` | Water crops |
|
||||
| `UseCoopInteraction` | `UseCoop` | Interact with animal coop |
|
||||
| `UseCaptureCrateInteraction` | `UseCaptureCrate` | Capture animals |
|
||||
|
||||
### Interaction Example
|
||||
|
||||
```yaml
|
||||
# Item interaction definition
|
||||
{
|
||||
"Type": "HarvestCrop",
|
||||
"DropTable": "wheat_drops"
|
||||
}
|
||||
```
|
||||
|
||||
## Animal Coops
|
||||
|
||||
### FarmingCoopAsset
|
||||
|
||||
Define coop configurations:
|
||||
|
||||
```yaml
|
||||
# Farming/Coops/chicken_coop.json
|
||||
{
|
||||
"Id": "chicken_coop",
|
||||
"Capacity": 4,
|
||||
"NPCGroups": ["chickens"],
|
||||
"Drops": "egg_drops"
|
||||
}
|
||||
```
|
||||
|
||||
### CoopResidentComponent
|
||||
|
||||
Entity component for animals living in coops:
|
||||
|
||||
```java
|
||||
ComponentType<EntityStore, CoopResidentComponent> type =
|
||||
FarmingPlugin.get().getCoopResidentComponentType();
|
||||
```
|
||||
|
||||
## Systems
|
||||
|
||||
The farming plugin registers these ECS systems:
|
||||
|
||||
| System | Description |
|
||||
|--------|-------------|
|
||||
| `OnSoilAdded` | Initialize soil when tilled |
|
||||
| `OnFarmBlockAdded` | Initialize crop when planted |
|
||||
| `Ticking` | Process growth each tick |
|
||||
| `MigrateFarming` | Migration for old data |
|
||||
| `OnCoopAdded` | Initialize coop blocks |
|
||||
| `CoopResidentEntitySystem` | Manage coop residents |
|
||||
| `CoopResidentTicking` | Coop production ticking |
|
||||
|
||||
## Events
|
||||
|
||||
```java
|
||||
// Prevent spread on newly generated chunks
|
||||
getEventRegistry().registerGlobal(
|
||||
EventPriority.LAST,
|
||||
ChunkPreLoadProcessEvent.class,
|
||||
FarmingPlugin::preventSpreadOnNew
|
||||
);
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
```java
|
||||
FarmingPlugin farming = FarmingPlugin.get();
|
||||
|
||||
// Get component types for queries
|
||||
ComponentType<ChunkStore, FarmingBlock> farmingType =
|
||||
farming.getFarmingBlockComponentType();
|
||||
|
||||
// Check farming state
|
||||
FarmingBlock block = holder.getComponent(farmingType);
|
||||
if (block != null) {
|
||||
block.setSpreadRate(1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Assets loaded from `Farming/` directory:
|
||||
- `Farming/Modifiers/` - Growth modifier definitions
|
||||
- `Farming/Coops/` - Animal coop definitions
|
||||
200
content/gameplay-systems/farming.fr.md
Normal file
200
content/gameplay-systems/farming.fr.md
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: Agriculture
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Le systeme d'agriculture fournit la croissance des cultures, la recolte et les mecaniques de poulaillers.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.farming`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
FarmingPlugin
|
||||
├── Composants de Bloc (ChunkStore)
|
||||
│ ├── TilledSoilBlock - Etat de la terre labouree
|
||||
│ ├── FarmingBlock - Suivi de croissance
|
||||
│ ├── FarmingBlockState - Etat de croissance du bloc
|
||||
│ └── CoopBlock - Etat du poulailler
|
||||
├── Composants d'Entite (EntityStore)
|
||||
│ └── CoopResidentComponent - Animaux dans les poulaillers
|
||||
├── Modificateurs de Croissance
|
||||
│ ├── FertilizerGrowthModifier
|
||||
│ ├── WaterGrowthModifier
|
||||
│ └── LightLevelGrowthModifier
|
||||
└── Etapes d'Agriculture
|
||||
├── BlockTypeFarmingStageData
|
||||
├── BlockStateFarmingStageData
|
||||
├── PrefabFarmingStageData
|
||||
└── SpreadFarmingStageData
|
||||
```
|
||||
|
||||
## Composants de Bloc
|
||||
|
||||
### TilledSoilBlock
|
||||
|
||||
Suit l'etat du sol laboure sur ChunkStore:
|
||||
|
||||
```java
|
||||
ComponentType<ChunkStore, TilledSoilBlock> type =
|
||||
FarmingPlugin.get().getTiledSoilBlockComponentType();
|
||||
```
|
||||
|
||||
### FarmingBlock
|
||||
|
||||
Suit la progression de croissance des cultures:
|
||||
|
||||
```java
|
||||
public class FarmingBlock {
|
||||
// Modificateur de taux de croissance (0 = pas de propagation sur nouveau)
|
||||
public void setSpreadRate(float rate);
|
||||
}
|
||||
```
|
||||
|
||||
### FarmingBlockState
|
||||
|
||||
Etat actuel d'agriculture pour une position de bloc.
|
||||
|
||||
### CoopBlock
|
||||
|
||||
Etat du bloc poulailler pour heberger des creatures.
|
||||
|
||||
## Modificateurs de Croissance
|
||||
|
||||
Les modificateurs de croissance affectent le taux de croissance:
|
||||
|
||||
| Modificateur | ID Type | Description |
|
||||
|--------------|---------|-------------|
|
||||
| `FertilizerGrowthModifierAsset` | `Fertilizer` | Boost avec engrais |
|
||||
| `WaterGrowthModifierAsset` | `Water` | Bonus proximite eau |
|
||||
| `LightLevelGrowthModifierAsset` | `LightLevel` | Croissance basee sur lumiere |
|
||||
|
||||
### Configuration d'Asset
|
||||
|
||||
```yaml
|
||||
# Farming/Modifiers/bone_meal.json
|
||||
{
|
||||
"Id": "bone_meal",
|
||||
"Type": "Fertilizer",
|
||||
"GrowthMultiplier": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
## Etapes d'Agriculture
|
||||
|
||||
Les cultures progressent a travers des etapes definies comme assets:
|
||||
|
||||
| Type d'Etape | ID Type | Description |
|
||||
|--------------|---------|-------------|
|
||||
| `BlockTypeFarmingStageData` | `BlockType` | Change le type de bloc |
|
||||
| `BlockStateFarmingStageData` | `BlockState` | Change l'etat du bloc |
|
||||
| `PrefabFarmingStageData` | `Prefab` | Place une structure prefab |
|
||||
| `SpreadFarmingStageData` | `Spread` | Se propage aux blocs adjacents |
|
||||
|
||||
### Comportement de Propagation
|
||||
|
||||
```java
|
||||
public interface SpreadGrowthBehaviour {
|
||||
// Croissance directionnelle (vignes, etc.)
|
||||
}
|
||||
|
||||
public class DirectionalGrowthBehaviour implements SpreadGrowthBehaviour {
|
||||
// Pousse dans la direction specifiee
|
||||
}
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
Interactions d'agriculture enregistrees:
|
||||
|
||||
| Interaction | ID Type | Description |
|
||||
|-------------|---------|-------------|
|
||||
| `HarvestCropInteraction` | `HarvestCrop` | Recolter les cultures matures |
|
||||
| `FertilizeSoilInteraction` | `FertilizeSoil` | Appliquer de l'engrais |
|
||||
| `ChangeFarmingStageInteraction` | `ChangeFarmingStage` | Forcer changement d'etape |
|
||||
| `UseWateringCanInteraction` | `UseWateringCan` | Arroser les cultures |
|
||||
| `UseCoopInteraction` | `UseCoop` | Interagir avec le poulailler |
|
||||
| `UseCaptureCrateInteraction` | `UseCaptureCrate` | Capturer des animaux |
|
||||
|
||||
### Exemple d'Interaction
|
||||
|
||||
```yaml
|
||||
# Definition d'interaction d'item
|
||||
{
|
||||
"Type": "HarvestCrop",
|
||||
"DropTable": "wheat_drops"
|
||||
}
|
||||
```
|
||||
|
||||
## Poulaillers
|
||||
|
||||
### FarmingCoopAsset
|
||||
|
||||
Definir les configurations de poulailler:
|
||||
|
||||
```yaml
|
||||
# Farming/Coops/chicken_coop.json
|
||||
{
|
||||
"Id": "chicken_coop",
|
||||
"Capacity": 4,
|
||||
"NPCGroups": ["chickens"],
|
||||
"Drops": "egg_drops"
|
||||
}
|
||||
```
|
||||
|
||||
### CoopResidentComponent
|
||||
|
||||
Composant d'entite pour les animaux vivant dans les poulaillers:
|
||||
|
||||
```java
|
||||
ComponentType<EntityStore, CoopResidentComponent> type =
|
||||
FarmingPlugin.get().getCoopResidentComponentType();
|
||||
```
|
||||
|
||||
## Systemes
|
||||
|
||||
Le plugin d'agriculture enregistre ces systemes ECS:
|
||||
|
||||
| Systeme | Description |
|
||||
|---------|-------------|
|
||||
| `OnSoilAdded` | Initialiser le sol quand laboure |
|
||||
| `OnFarmBlockAdded` | Initialiser la culture quand plantee |
|
||||
| `Ticking` | Traiter la croissance chaque tick |
|
||||
| `MigrateFarming` | Migration pour anciennes donnees |
|
||||
| `OnCoopAdded` | Initialiser les blocs poulailler |
|
||||
| `CoopResidentEntitySystem` | Gerer les residents du poulailler |
|
||||
| `CoopResidentTicking` | Ticking de production du poulailler |
|
||||
|
||||
## Evenements
|
||||
|
||||
```java
|
||||
// Empecher la propagation sur les chunks nouvellement generes
|
||||
getEventRegistry().registerGlobal(
|
||||
EventPriority.LAST,
|
||||
ChunkPreLoadProcessEvent.class,
|
||||
FarmingPlugin::preventSpreadOnNew
|
||||
);
|
||||
```
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
```java
|
||||
FarmingPlugin farming = FarmingPlugin.get();
|
||||
|
||||
// Obtenir les types de composants pour les requetes
|
||||
ComponentType<ChunkStore, FarmingBlock> farmingType =
|
||||
farming.getFarmingBlockComponentType();
|
||||
|
||||
// Verifier l'etat d'agriculture
|
||||
FarmingBlock block = holder.getComponent(farmingType);
|
||||
if (block != null) {
|
||||
block.setSpreadRate(1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Assets charges depuis le repertoire `Farming/`:
|
||||
- `Farming/Modifiers/` - Definitions des modificateurs de croissance
|
||||
- `Farming/Coops/` - Definitions des poulaillers
|
||||
225
content/gameplay-systems/memories.en.md
Normal file
225
content/gameplay-systems/memories.en.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: Memories
|
||||
type: docs
|
||||
weight: 4
|
||||
---
|
||||
|
||||
The memories system provides persistent collectible discovery tracking for players.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.memories`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
MemoriesPlugin
|
||||
├── Components
|
||||
│ └── PlayerMemories - Player's collected memories
|
||||
├── Memory Types
|
||||
│ ├── Memory - Base memory interface
|
||||
│ ├── MemoryProvider - Supplies memory instances
|
||||
│ └── NPCMemory/NPCMemoryProvider - NPC-based memories
|
||||
├── Storage
|
||||
│ └── RecordedMemories - Persistent storage (memories.json)
|
||||
├── UI
|
||||
│ ├── MemoriesWindow - Client window
|
||||
│ ├── MemoriesPage - UI page
|
||||
│ └── MemoriesPageSupplier
|
||||
├── Systems
|
||||
│ ├── PlayerAddedSystem - Initialize on player join
|
||||
│ ├── NPCMemory.GatherMemoriesSystem - Collect NPC memories
|
||||
│ └── TempleRespawnPlayersSystem - Temple respawn logic
|
||||
└── Commands
|
||||
└── MemoriesCommand (capacity, clear, level, unlock)
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Memory Interface
|
||||
|
||||
Base interface for all memory types:
|
||||
|
||||
```java
|
||||
public interface Memory {
|
||||
// Codec for serialization
|
||||
static final Codec<Memory> CODEC;
|
||||
}
|
||||
```
|
||||
|
||||
### MemoryProvider
|
||||
|
||||
Provides memory instances and configuration:
|
||||
|
||||
```java
|
||||
public interface MemoryProvider<T extends Memory> {
|
||||
String getId();
|
||||
BuilderCodec<T> getCodec();
|
||||
Map<String, Set<Memory>> getAllMemories();
|
||||
}
|
||||
```
|
||||
|
||||
### NPCMemory
|
||||
|
||||
Memories discovered from NPCs:
|
||||
|
||||
```java
|
||||
public class NPCMemory implements Memory {
|
||||
// Collected when player approaches NPC within radius
|
||||
}
|
||||
|
||||
public class NPCMemoryProvider implements MemoryProvider<NPCMemory> {
|
||||
public double getCollectionRadius(); // From config
|
||||
}
|
||||
```
|
||||
|
||||
## Player Memories Component
|
||||
|
||||
```java
|
||||
ComponentType<EntityStore, PlayerMemories> type =
|
||||
MemoriesPlugin.get().getPlayerMemoriesComponentType();
|
||||
```
|
||||
|
||||
The `PlayerMemories` component tracks:
|
||||
- Memories discovered by player
|
||||
- Memory capacity
|
||||
- Transfer to recorded memories
|
||||
|
||||
## Recorded Memories
|
||||
|
||||
World-persistent memory storage saved to `memories.json`:
|
||||
|
||||
```java
|
||||
// Check if memory is recorded
|
||||
boolean hasMemory = MemoriesPlugin.get().hasRecordedMemory(memory);
|
||||
|
||||
// Get all recorded memories
|
||||
Set<Memory> recorded = MemoriesPlugin.get().getRecordedMemories();
|
||||
|
||||
// Record player's memories
|
||||
boolean recorded = MemoriesPlugin.get().recordPlayerMemories(playerMemories);
|
||||
|
||||
// Clear all recorded memories
|
||||
MemoriesPlugin.get().clearRecordedMemories();
|
||||
|
||||
// Record all possible memories
|
||||
MemoriesPlugin.get().recordAllMemories();
|
||||
```
|
||||
|
||||
## Memory Levels
|
||||
|
||||
Memory count determines player level:
|
||||
|
||||
```java
|
||||
// Get current memories level
|
||||
int level = MemoriesPlugin.get().getMemoriesLevel(gameplayConfig);
|
||||
|
||||
// Get memories needed for next level
|
||||
int needed = MemoriesPlugin.get().getMemoriesForNextLevel(gameplayConfig);
|
||||
```
|
||||
|
||||
### Level Configuration
|
||||
|
||||
```java
|
||||
public class MemoriesGameplayConfig {
|
||||
int[] memoriesAmountPerLevel; // Thresholds for each level
|
||||
}
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
| Interaction | Type ID | Description |
|
||||
|-------------|---------|-------------|
|
||||
| `SetMemoriesCapacityInteraction` | `SetMemoriesCapacity` | Set player memory capacity |
|
||||
| `MemoriesConditionInteraction` | `MemoriesCondition` | Condition based on memories |
|
||||
|
||||
## UI Integration
|
||||
|
||||
### Memory Window
|
||||
|
||||
Client-requestable window for viewing memories:
|
||||
|
||||
```java
|
||||
Window.CLIENT_REQUESTABLE_WINDOW_TYPES.put(WindowType.Memories, MemoriesWindow::new);
|
||||
```
|
||||
|
||||
### Custom UI Page
|
||||
|
||||
```java
|
||||
OpenCustomUIInteraction.registerCustomPageSupplier(
|
||||
this,
|
||||
MemoriesPage.class,
|
||||
"Memories",
|
||||
new MemoriesPageSupplier()
|
||||
);
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/memories` | Base memories command |
|
||||
| `/memories capacity <amount>` | Set memory capacity |
|
||||
| `/memories clear` | Clear recorded memories |
|
||||
| `/memories level` | Show current level |
|
||||
| `/memories unlock` | Unlock all memories |
|
||||
|
||||
## Configuration
|
||||
|
||||
### Plugin Config
|
||||
|
||||
```java
|
||||
public class MemoriesPluginConfig {
|
||||
// Collection radius per memory type
|
||||
Object2DoubleMap<String> collectionRadius;
|
||||
|
||||
public Object2DoubleMap<String> getCollectionRadius();
|
||||
}
|
||||
```
|
||||
|
||||
### Gameplay Config
|
||||
|
||||
```yaml
|
||||
# gameplay_config.json
|
||||
{
|
||||
"Memories": {
|
||||
"MemoriesAmountPerLevel": [5, 15, 30, 50, 75, 100]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Temple System
|
||||
|
||||
The Forgotten Temple system for memory-related respawns:
|
||||
|
||||
```java
|
||||
public class ForgottenTempleConfig {
|
||||
// Temple configuration for respawn
|
||||
}
|
||||
|
||||
public class TempleRespawnPlayersSystem {
|
||||
// Handles respawn at temples
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
Player memories feature status is sent on join:
|
||||
|
||||
```java
|
||||
// Sent to client when player joins
|
||||
playerConnection.writeNoCache(new UpdateMemoriesFeatureStatus(isFeatureUnlocked));
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
```java
|
||||
MemoriesPlugin memories = MemoriesPlugin.get();
|
||||
|
||||
// Get all available memories
|
||||
Map<String, Set<Memory>> allMemories = memories.getAllMemories();
|
||||
|
||||
// Register custom memory provider
|
||||
memories.registerMemoryProvider(new CustomMemoryProvider());
|
||||
|
||||
// Check player memories component
|
||||
PlayerMemories playerMemories = store.getComponent(ref, PlayerMemories.getComponentType());
|
||||
```
|
||||
225
content/gameplay-systems/memories.fr.md
Normal file
225
content/gameplay-systems/memories.fr.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: Memoires
|
||||
type: docs
|
||||
weight: 4
|
||||
---
|
||||
|
||||
Le systeme de memoires fournit un suivi persistant des decouvertes collectibles pour les joueurs.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.memories`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
MemoriesPlugin
|
||||
├── Composants
|
||||
│ └── PlayerMemories - Memoires collectees du joueur
|
||||
├── Types de Memoire
|
||||
│ ├── Memory - Interface de base
|
||||
│ ├── MemoryProvider - Fournit les instances de memoire
|
||||
│ └── NPCMemory/NPCMemoryProvider - Memoires basees sur NPCs
|
||||
├── Stockage
|
||||
│ └── RecordedMemories - Stockage persistant (memories.json)
|
||||
├── UI
|
||||
│ ├── MemoriesWindow - Fenetre client
|
||||
│ ├── MemoriesPage - Page UI
|
||||
│ └── MemoriesPageSupplier
|
||||
├── Systemes
|
||||
│ ├── PlayerAddedSystem - Initialiser a la connexion
|
||||
│ ├── NPCMemory.GatherMemoriesSystem - Collecter memoires NPC
|
||||
│ └── TempleRespawnPlayersSystem - Logique respawn temple
|
||||
└── Commandes
|
||||
└── MemoriesCommand (capacity, clear, level, unlock)
|
||||
```
|
||||
|
||||
## Concepts Cles
|
||||
|
||||
### Interface Memory
|
||||
|
||||
Interface de base pour tous les types de memoire:
|
||||
|
||||
```java
|
||||
public interface Memory {
|
||||
// Codec pour serialisation
|
||||
static final Codec<Memory> CODEC;
|
||||
}
|
||||
```
|
||||
|
||||
### MemoryProvider
|
||||
|
||||
Fournit les instances et configuration de memoire:
|
||||
|
||||
```java
|
||||
public interface MemoryProvider<T extends Memory> {
|
||||
String getId();
|
||||
BuilderCodec<T> getCodec();
|
||||
Map<String, Set<Memory>> getAllMemories();
|
||||
}
|
||||
```
|
||||
|
||||
### NPCMemory
|
||||
|
||||
Memoires decouvertes depuis les NPCs:
|
||||
|
||||
```java
|
||||
public class NPCMemory implements Memory {
|
||||
// Collectee quand joueur approche NPC dans le rayon
|
||||
}
|
||||
|
||||
public class NPCMemoryProvider implements MemoryProvider<NPCMemory> {
|
||||
public double getCollectionRadius(); // Depuis config
|
||||
}
|
||||
```
|
||||
|
||||
## Composant Player Memories
|
||||
|
||||
```java
|
||||
ComponentType<EntityStore, PlayerMemories> type =
|
||||
MemoriesPlugin.get().getPlayerMemoriesComponentType();
|
||||
```
|
||||
|
||||
Le composant `PlayerMemories` suit:
|
||||
- Memoires decouvertes par le joueur
|
||||
- Capacite de memoire
|
||||
- Transfert vers memoires enregistrees
|
||||
|
||||
## Memoires Enregistrees
|
||||
|
||||
Stockage persistant de memoires sauvegarde dans `memories.json`:
|
||||
|
||||
```java
|
||||
// Verifier si memoire est enregistree
|
||||
boolean hasMemory = MemoriesPlugin.get().hasRecordedMemory(memory);
|
||||
|
||||
// Obtenir toutes les memoires enregistrees
|
||||
Set<Memory> recorded = MemoriesPlugin.get().getRecordedMemories();
|
||||
|
||||
// Enregistrer les memoires du joueur
|
||||
boolean recorded = MemoriesPlugin.get().recordPlayerMemories(playerMemories);
|
||||
|
||||
// Effacer toutes les memoires enregistrees
|
||||
MemoriesPlugin.get().clearRecordedMemories();
|
||||
|
||||
// Enregistrer toutes les memoires possibles
|
||||
MemoriesPlugin.get().recordAllMemories();
|
||||
```
|
||||
|
||||
## Niveaux de Memoire
|
||||
|
||||
Le nombre de memoires determine le niveau du joueur:
|
||||
|
||||
```java
|
||||
// Obtenir niveau actuel de memoires
|
||||
int level = MemoriesPlugin.get().getMemoriesLevel(gameplayConfig);
|
||||
|
||||
// Obtenir memoires necessaires pour prochain niveau
|
||||
int needed = MemoriesPlugin.get().getMemoriesForNextLevel(gameplayConfig);
|
||||
```
|
||||
|
||||
### Configuration des Niveaux
|
||||
|
||||
```java
|
||||
public class MemoriesGameplayConfig {
|
||||
int[] memoriesAmountPerLevel; // Seuils pour chaque niveau
|
||||
}
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
| Interaction | ID Type | Description |
|
||||
|-------------|---------|-------------|
|
||||
| `SetMemoriesCapacityInteraction` | `SetMemoriesCapacity` | Definir capacite memoire joueur |
|
||||
| `MemoriesConditionInteraction` | `MemoriesCondition` | Condition basee sur memoires |
|
||||
|
||||
## Integration UI
|
||||
|
||||
### Fenetre Memoires
|
||||
|
||||
Fenetre demandable par le client pour voir les memoires:
|
||||
|
||||
```java
|
||||
Window.CLIENT_REQUESTABLE_WINDOW_TYPES.put(WindowType.Memories, MemoriesWindow::new);
|
||||
```
|
||||
|
||||
### Page UI Personnalisee
|
||||
|
||||
```java
|
||||
OpenCustomUIInteraction.registerCustomPageSupplier(
|
||||
this,
|
||||
MemoriesPage.class,
|
||||
"Memories",
|
||||
new MemoriesPageSupplier()
|
||||
);
|
||||
```
|
||||
|
||||
## Commandes
|
||||
|
||||
| Commande | Description |
|
||||
|----------|-------------|
|
||||
| `/memories` | Commande memoires de base |
|
||||
| `/memories capacity <amount>` | Definir capacite memoire |
|
||||
| `/memories clear` | Effacer memoires enregistrees |
|
||||
| `/memories level` | Afficher niveau actuel |
|
||||
| `/memories unlock` | Debloquer toutes les memoires |
|
||||
|
||||
## Configuration
|
||||
|
||||
### Config Plugin
|
||||
|
||||
```java
|
||||
public class MemoriesPluginConfig {
|
||||
// Rayon de collection par type de memoire
|
||||
Object2DoubleMap<String> collectionRadius;
|
||||
|
||||
public Object2DoubleMap<String> getCollectionRadius();
|
||||
}
|
||||
```
|
||||
|
||||
### Config Gameplay
|
||||
|
||||
```yaml
|
||||
# gameplay_config.json
|
||||
{
|
||||
"Memories": {
|
||||
"MemoriesAmountPerLevel": [5, 15, 30, 50, 75, 100]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Systeme de Temple
|
||||
|
||||
Le systeme du Temple Oublie pour les respawns lies aux memoires:
|
||||
|
||||
```java
|
||||
public class ForgottenTempleConfig {
|
||||
// Configuration temple pour respawn
|
||||
}
|
||||
|
||||
public class TempleRespawnPlayersSystem {
|
||||
// Gere le respawn aux temples
|
||||
}
|
||||
```
|
||||
|
||||
## Evenements
|
||||
|
||||
Le statut de la fonctionnalite memoires est envoye a la connexion:
|
||||
|
||||
```java
|
||||
// Envoye au client quand joueur rejoint
|
||||
playerConnection.writeNoCache(new UpdateMemoriesFeatureStatus(isFeatureUnlocked));
|
||||
```
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
```java
|
||||
MemoriesPlugin memories = MemoriesPlugin.get();
|
||||
|
||||
// Obtenir toutes les memoires disponibles
|
||||
Map<String, Set<Memory>> allMemories = memories.getAllMemories();
|
||||
|
||||
// Enregistrer fournisseur de memoire personnalise
|
||||
memories.registerMemoryProvider(new CustomMemoryProvider());
|
||||
|
||||
// Verifier composant memoires joueur
|
||||
PlayerMemories playerMemories = store.getComponent(ref, PlayerMemories.getComponentType());
|
||||
```
|
||||
202
content/gameplay-systems/npc-objectives.en.md
Normal file
202
content/gameplay-systems/npc-objectives.en.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: NPC Objectives
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
The NPC objectives system extends the base objective system with combat-related tasks.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.npcobjectives`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
NPCObjectivesPlugin
|
||||
├── Task Types
|
||||
│ ├── KillSpawnBeaconObjectiveTask - Kill NPCs from spawn beacons
|
||||
│ ├── KillSpawnMarkerObjectiveTask - Kill NPCs from spawn markers
|
||||
│ ├── BountyObjectiveTask - Hunt specific NPC targets
|
||||
│ └── KillNPCObjectiveTask - Kill NPCs by type/group
|
||||
├── Resources
|
||||
│ └── KillTrackerResource - Tracks kill counts
|
||||
├── Systems
|
||||
│ ├── KillTrackerSystem - Monitors NPC deaths
|
||||
│ └── SpawnBeaconCheckRemovalSystem - Cleanup spawn beacons
|
||||
└── NPC Actions
|
||||
├── BuilderActionCompleteTask - Complete task on NPC
|
||||
├── BuilderActionStartObjective - Start objective from NPC
|
||||
└── BuilderSensorHasTask - Check if player has task
|
||||
```
|
||||
|
||||
## Task Types
|
||||
|
||||
### KillTask Interface
|
||||
|
||||
All kill-based tasks implement this interface:
|
||||
|
||||
```java
|
||||
public interface KillTask {
|
||||
void checkKilledEntity(
|
||||
Store<EntityStore> store,
|
||||
Ref<EntityStore> npcRef,
|
||||
Objective objective,
|
||||
NPCEntity npc,
|
||||
Damage damageInfo
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Registered Task Types
|
||||
|
||||
| Type | Task Asset Class | Description |
|
||||
|------|------------------|-------------|
|
||||
| `KillSpawnBeacon` | `KillSpawnBeaconObjectiveTaskAsset` | Kill NPCs from beacons |
|
||||
| `KillSpawnMarker` | `KillSpawnMarkerObjectiveTaskAsset` | Kill NPCs from markers |
|
||||
| `Bounty` | `BountyObjectiveTaskAsset` | Hunt specific target |
|
||||
| `KillNPC` | `KillObjectiveTaskAsset` | Kill NPCs by group |
|
||||
|
||||
### Bounty Task
|
||||
|
||||
Spawns a specific NPC and marks it as a bounty target:
|
||||
|
||||
```java
|
||||
public class BountyObjectiveTask extends ObjectiveTask implements KillTask {
|
||||
boolean completed;
|
||||
UUID entityUuid; // Target NPC UUID
|
||||
|
||||
// Spawns NPC at position and adds map marker
|
||||
protected TransactionRecord[] setup0(Objective objective, World world, Store<EntityStore> store) {
|
||||
// Get spawn position from asset configuration
|
||||
Vector3i spawnPosition = getAsset().getWorldLocationProvider()
|
||||
.runCondition(world, objectivePosition);
|
||||
|
||||
// Spawn the bounty NPC
|
||||
Pair<Ref<EntityStore>, INonPlayerCharacter> npcPair =
|
||||
NPCPlugin.get().spawnNPC(store, getAsset().getNpcId(), null, spawnPosition, Vector3f.ZERO);
|
||||
|
||||
// Add map marker for bounty
|
||||
addMarker(new MapMarker(
|
||||
getBountyMarkerIDFromUUID(npcUuid),
|
||||
"Bounty Target",
|
||||
"Home.png",
|
||||
transform
|
||||
));
|
||||
|
||||
// Register kill tracker
|
||||
store.getResource(KillTrackerResource.getResourceType()).watch(transaction);
|
||||
|
||||
return transactionRecords;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkKilledEntity(Store<EntityStore> store, Ref<EntityStore> npcRef,
|
||||
Objective objective, NPCEntity npc, Damage damageInfo) {
|
||||
if (!this.entityUuid.equals(uuid)) return;
|
||||
this.completed = true;
|
||||
this.complete(objective, store);
|
||||
objective.checkTaskSetCompletion(store);
|
||||
this.removeMarker(getBountyMarkerIDFromUUID(uuid));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Asset Configuration
|
||||
|
||||
```yaml
|
||||
# Objectives/bounty_quest.json
|
||||
{
|
||||
"Id": "bounty_quest",
|
||||
"TaskSets": [
|
||||
{
|
||||
"Tasks": [
|
||||
{
|
||||
"Type": "Bounty",
|
||||
"NpcId": "bandit_leader",
|
||||
"WorldLocationProvider": {
|
||||
"Type": "LocationRadius",
|
||||
"Radius": 50
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Kill Tracker
|
||||
|
||||
### KillTrackerResource
|
||||
|
||||
World resource that tracks NPC kills:
|
||||
|
||||
```java
|
||||
ResourceType<EntityStore, KillTrackerResource> type =
|
||||
NPCObjectivesPlugin.get().getKillTrackerResourceType();
|
||||
```
|
||||
|
||||
### KillTrackerSystem
|
||||
|
||||
ECS system that monitors NPC deaths and notifies watching tasks:
|
||||
|
||||
```java
|
||||
// Register transaction to watch for kills
|
||||
KillTaskTransaction transaction = new KillTaskTransaction(task, objective, store);
|
||||
store.getResource(KillTrackerResource.getResourceType()).watch(transaction);
|
||||
```
|
||||
|
||||
## NPC Actions
|
||||
|
||||
Registered as NPC core component types for AI integration:
|
||||
|
||||
### CompleteTask Action
|
||||
|
||||
```java
|
||||
// Completes a task when NPC interaction occurs
|
||||
NPCPlugin.get().registerCoreComponentType("CompleteTask", BuilderActionCompleteTask::new);
|
||||
```
|
||||
|
||||
### StartObjective Action
|
||||
|
||||
```java
|
||||
// Starts an objective from NPC dialogue/interaction
|
||||
NPCPlugin.get().registerCoreComponentType("StartObjective", BuilderActionStartObjective::new);
|
||||
```
|
||||
|
||||
### HasTask Sensor
|
||||
|
||||
```java
|
||||
// Sensor that checks if player has a specific task
|
||||
NPCPlugin.get().registerCoreComponentType("HasTask", BuilderSensorHasTask::new);
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Check if Player Has Task
|
||||
|
||||
```java
|
||||
boolean hasTask = NPCObjectivesPlugin.hasTask(playerUUID, npcId, "taskId");
|
||||
```
|
||||
|
||||
### Update Task Completion
|
||||
|
||||
```java
|
||||
String animationId = NPCObjectivesPlugin.updateTaskCompletion(
|
||||
store, ref, playerRef, npcId, "taskId"
|
||||
);
|
||||
```
|
||||
|
||||
### Start Objective from NPC
|
||||
|
||||
```java
|
||||
NPCObjectivesPlugin.startObjective(playerReference, "quest_id", store);
|
||||
```
|
||||
|
||||
## Asset Loading Order
|
||||
|
||||
The plugin ensures proper asset loading order:
|
||||
|
||||
```java
|
||||
AssetRegistry.getAssetStore(ObjectiveAsset.class).injectLoadsAfter(SpawnMarker.class);
|
||||
AssetRegistry.getAssetStore(ObjectiveAsset.class).injectLoadsAfter(BeaconNPCSpawn.class);
|
||||
AssetRegistry.getAssetStore(ObjectiveAsset.class).injectLoadsAfter(NPCGroup.class);
|
||||
```
|
||||
202
content/gameplay-systems/npc-objectives.fr.md
Normal file
202
content/gameplay-systems/npc-objectives.fr.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: Objectifs NPC
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
Le systeme d'objectifs NPC etend le systeme d'objectifs de base avec des taches liees au combat.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.npcobjectives`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
NPCObjectivesPlugin
|
||||
├── Types de Taches
|
||||
│ ├── KillSpawnBeaconObjectiveTask - Tuer NPCs de spawn beacons
|
||||
│ ├── KillSpawnMarkerObjectiveTask - Tuer NPCs de spawn markers
|
||||
│ ├── BountyObjectiveTask - Chasser des cibles NPC specifiques
|
||||
│ └── KillNPCObjectiveTask - Tuer NPCs par type/groupe
|
||||
├── Ressources
|
||||
│ └── KillTrackerResource - Suit les compteurs de kills
|
||||
├── Systemes
|
||||
│ ├── KillTrackerSystem - Surveille les morts de NPCs
|
||||
│ └── SpawnBeaconCheckRemovalSystem - Nettoyage spawn beacons
|
||||
└── Actions NPC
|
||||
├── BuilderActionCompleteTask - Completer tache sur NPC
|
||||
├── BuilderActionStartObjective - Demarrer objectif depuis NPC
|
||||
└── BuilderSensorHasTask - Verifier si joueur a une tache
|
||||
```
|
||||
|
||||
## Types de Taches
|
||||
|
||||
### Interface KillTask
|
||||
|
||||
Toutes les taches basees sur les kills implementent cette interface:
|
||||
|
||||
```java
|
||||
public interface KillTask {
|
||||
void checkKilledEntity(
|
||||
Store<EntityStore> store,
|
||||
Ref<EntityStore> npcRef,
|
||||
Objective objective,
|
||||
NPCEntity npc,
|
||||
Damage damageInfo
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Types de Taches Enregistres
|
||||
|
||||
| Type | Classe Asset | Description |
|
||||
|------|--------------|-------------|
|
||||
| `KillSpawnBeacon` | `KillSpawnBeaconObjectiveTaskAsset` | Tuer NPCs de beacons |
|
||||
| `KillSpawnMarker` | `KillSpawnMarkerObjectiveTaskAsset` | Tuer NPCs de markers |
|
||||
| `Bounty` | `BountyObjectiveTaskAsset` | Chasser une cible specifique |
|
||||
| `KillNPC` | `KillObjectiveTaskAsset` | Tuer NPCs par groupe |
|
||||
|
||||
### Tache Bounty
|
||||
|
||||
Fait apparaitre un NPC specifique et le marque comme cible de prime:
|
||||
|
||||
```java
|
||||
public class BountyObjectiveTask extends ObjectiveTask implements KillTask {
|
||||
boolean completed;
|
||||
UUID entityUuid; // UUID du NPC cible
|
||||
|
||||
// Fait apparaitre le NPC a la position et ajoute un marqueur carte
|
||||
protected TransactionRecord[] setup0(Objective objective, World world, Store<EntityStore> store) {
|
||||
// Obtenir la position de spawn depuis la configuration
|
||||
Vector3i spawnPosition = getAsset().getWorldLocationProvider()
|
||||
.runCondition(world, objectivePosition);
|
||||
|
||||
// Faire apparaitre le NPC bounty
|
||||
Pair<Ref<EntityStore>, INonPlayerCharacter> npcPair =
|
||||
NPCPlugin.get().spawnNPC(store, getAsset().getNpcId(), null, spawnPosition, Vector3f.ZERO);
|
||||
|
||||
// Ajouter marqueur sur la carte
|
||||
addMarker(new MapMarker(
|
||||
getBountyMarkerIDFromUUID(npcUuid),
|
||||
"Bounty Target",
|
||||
"Home.png",
|
||||
transform
|
||||
));
|
||||
|
||||
// Enregistrer le tracker de kills
|
||||
store.getResource(KillTrackerResource.getResourceType()).watch(transaction);
|
||||
|
||||
return transactionRecords;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkKilledEntity(Store<EntityStore> store, Ref<EntityStore> npcRef,
|
||||
Objective objective, NPCEntity npc, Damage damageInfo) {
|
||||
if (!this.entityUuid.equals(uuid)) return;
|
||||
this.completed = true;
|
||||
this.complete(objective, store);
|
||||
objective.checkTaskSetCompletion(store);
|
||||
this.removeMarker(getBountyMarkerIDFromUUID(uuid));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration d'Asset
|
||||
|
||||
```yaml
|
||||
# Objectives/bounty_quest.json
|
||||
{
|
||||
"Id": "bounty_quest",
|
||||
"TaskSets": [
|
||||
{
|
||||
"Tasks": [
|
||||
{
|
||||
"Type": "Bounty",
|
||||
"NpcId": "bandit_leader",
|
||||
"WorldLocationProvider": {
|
||||
"Type": "LocationRadius",
|
||||
"Radius": 50
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Tracker de Kills
|
||||
|
||||
### KillTrackerResource
|
||||
|
||||
Ressource monde qui suit les kills de NPCs:
|
||||
|
||||
```java
|
||||
ResourceType<EntityStore, KillTrackerResource> type =
|
||||
NPCObjectivesPlugin.get().getKillTrackerResourceType();
|
||||
```
|
||||
|
||||
### KillTrackerSystem
|
||||
|
||||
Systeme ECS qui surveille les morts de NPCs et notifie les taches:
|
||||
|
||||
```java
|
||||
// Enregistrer une transaction pour surveiller les kills
|
||||
KillTaskTransaction transaction = new KillTaskTransaction(task, objective, store);
|
||||
store.getResource(KillTrackerResource.getResourceType()).watch(transaction);
|
||||
```
|
||||
|
||||
## Actions NPC
|
||||
|
||||
Enregistrees comme types de composants core NPC pour l'integration IA:
|
||||
|
||||
### Action CompleteTask
|
||||
|
||||
```java
|
||||
// Complete une tache quand interaction NPC se produit
|
||||
NPCPlugin.get().registerCoreComponentType("CompleteTask", BuilderActionCompleteTask::new);
|
||||
```
|
||||
|
||||
### Action StartObjective
|
||||
|
||||
```java
|
||||
// Demarre un objectif depuis dialogue/interaction NPC
|
||||
NPCPlugin.get().registerCoreComponentType("StartObjective", BuilderActionStartObjective::new);
|
||||
```
|
||||
|
||||
### Sensor HasTask
|
||||
|
||||
```java
|
||||
// Sensor qui verifie si le joueur a une tache specifique
|
||||
NPCPlugin.get().registerCoreComponentType("HasTask", BuilderSensorHasTask::new);
|
||||
```
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
### Verifier si Joueur a une Tache
|
||||
|
||||
```java
|
||||
boolean hasTask = NPCObjectivesPlugin.hasTask(playerUUID, npcId, "taskId");
|
||||
```
|
||||
|
||||
### Mettre a Jour Completion de Tache
|
||||
|
||||
```java
|
||||
String animationId = NPCObjectivesPlugin.updateTaskCompletion(
|
||||
store, ref, playerRef, npcId, "taskId"
|
||||
);
|
||||
```
|
||||
|
||||
### Demarrer Objectif depuis NPC
|
||||
|
||||
```java
|
||||
NPCObjectivesPlugin.startObjective(playerReference, "quest_id", store);
|
||||
```
|
||||
|
||||
## Ordre de Chargement des Assets
|
||||
|
||||
Le plugin assure un ordre de chargement correct:
|
||||
|
||||
```java
|
||||
AssetRegistry.getAssetStore(ObjectiveAsset.class).injectLoadsAfter(SpawnMarker.class);
|
||||
AssetRegistry.getAssetStore(ObjectiveAsset.class).injectLoadsAfter(BeaconNPCSpawn.class);
|
||||
AssetRegistry.getAssetStore(ObjectiveAsset.class).injectLoadsAfter(NPCGroup.class);
|
||||
```
|
||||
204
content/gameplay-systems/objectives.en.md
Normal file
204
content/gameplay-systems/objectives.en.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
title: Objectives
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
The objectives system provides quest mechanics with tasks, completions, and progress tracking.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.objectives`
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### ObjectiveAsset
|
||||
|
||||
Objectives are defined as JSON assets loaded from `Objectives/`:
|
||||
|
||||
```java
|
||||
public class ObjectiveAsset {
|
||||
protected String id; // Unique identifier
|
||||
protected String category; // Quest category
|
||||
protected TaskSet[] taskSets; // Sequential task groups
|
||||
protected ObjectiveCompletionAsset[] completionHandlers; // Rewards
|
||||
protected String objectiveTitleKey; // Localization key for title
|
||||
protected String objectiveDescriptionKey; // Localization key for description
|
||||
protected boolean removeOnItemDrop; // Remove objective items on drop
|
||||
}
|
||||
```
|
||||
|
||||
### TaskSet
|
||||
|
||||
Each objective contains one or more TaskSets that must be completed sequentially:
|
||||
|
||||
```java
|
||||
public class TaskSet {
|
||||
protected String descriptionId; // Optional description key
|
||||
protected ObjectiveTaskAsset[] tasks; // Tasks to complete in parallel
|
||||
}
|
||||
```
|
||||
|
||||
### Objective Instance
|
||||
|
||||
When started, an `Objective` instance tracks state:
|
||||
|
||||
```java
|
||||
public class Objective {
|
||||
private final UUID uuid; // Unique instance ID
|
||||
private final String objectiveId; // Asset ID reference
|
||||
private final Set<UUID> playerUUIDs; // Participating players
|
||||
private ObjectiveTask[] currentTasks; // Active tasks
|
||||
private int taskSetIndex; // Current TaskSet index
|
||||
}
|
||||
```
|
||||
|
||||
## Task Types
|
||||
|
||||
The system provides built-in task types registered by `ObjectivePlugin`:
|
||||
|
||||
| Type | Class | Description |
|
||||
|------|-------|-------------|
|
||||
| `Craft` | `CraftObjectiveTask` | Craft specific items |
|
||||
| `Gather` | `GatherObjectiveTask` | Collect items in inventory |
|
||||
| `UseBlock` | `UseBlockObjectiveTask` | Interact with blocks |
|
||||
| `UseEntity` | `UseEntityObjectiveTask` | Interact with entities |
|
||||
| `TreasureMap` | `TreasureMapObjectiveTask` | Find treasure locations |
|
||||
| `ReachLocation` | `ReachLocationTask` | Reach specific coordinates |
|
||||
|
||||
### Task Asset Example
|
||||
|
||||
```yaml
|
||||
# Objectives/collect_wood.json
|
||||
{
|
||||
"Id": "collect_wood",
|
||||
"Category": "tutorial",
|
||||
"TaskSets": [
|
||||
{
|
||||
"Tasks": [
|
||||
{
|
||||
"Type": "Gather",
|
||||
"Item": "oak_log",
|
||||
"Amount": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Completions": [
|
||||
{
|
||||
"Type": "GiveItems",
|
||||
"Items": [
|
||||
{ "Item": "gold_coin", "Amount": 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Completion Handlers
|
||||
|
||||
When all TaskSets complete, completion handlers execute:
|
||||
|
||||
| Type | Class | Description |
|
||||
|------|-------|-------------|
|
||||
| `GiveItems` | `GiveItemsCompletion` | Give items to player |
|
||||
| `ClearObjectiveItems` | `ClearObjectiveItemsCompletion` | Remove objective-related items |
|
||||
|
||||
## API Usage
|
||||
|
||||
### Starting Objectives
|
||||
|
||||
```java
|
||||
ObjectivePlugin plugin = ObjectivePlugin.get();
|
||||
|
||||
// Start objective for player
|
||||
plugin.startObjective(player, "collect_wood");
|
||||
|
||||
// With custom data
|
||||
Objective objective = plugin.startObjective(player, "collect_wood", customData);
|
||||
```
|
||||
|
||||
### Tracking Progress
|
||||
|
||||
```java
|
||||
// Get player's active objectives
|
||||
ObjectiveHistoryComponent history = store.getComponent(
|
||||
playerRef,
|
||||
ObjectiveHistoryComponent.getComponentType()
|
||||
);
|
||||
|
||||
// Check objective status
|
||||
ObjectiveHistoryData data = history.getObjectiveData("collect_wood");
|
||||
if (data != null && data.isCompleted()) {
|
||||
// Already completed
|
||||
}
|
||||
```
|
||||
|
||||
### Completing Objectives
|
||||
|
||||
```java
|
||||
// Mark objective as complete (triggers completions)
|
||||
objective.complete(objectivePlugin);
|
||||
|
||||
// Cancel objective without rewards
|
||||
objective.cancel(objectivePlugin);
|
||||
```
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
Objectives can have trigger conditions for activation:
|
||||
|
||||
```java
|
||||
// Weather-based trigger
|
||||
public class WeatherTriggerCondition { /* ... */ }
|
||||
|
||||
// Location-based trigger
|
||||
public class ObjectiveLocationTriggerCondition { /* ... */ }
|
||||
|
||||
// Time-based trigger
|
||||
public class HourRangeTriggerCondition { /* ... */ }
|
||||
```
|
||||
|
||||
## Location Markers
|
||||
|
||||
Use markers to create spatial objectives:
|
||||
|
||||
```java
|
||||
// Define marker area
|
||||
public interface ObjectiveLocationMarkerArea {
|
||||
boolean isInArea(Vector3d position);
|
||||
}
|
||||
|
||||
// Built-in area types
|
||||
public class ObjectiveLocationAreaRadius { /* Spherical area */ }
|
||||
public class ObjectiveLocationAreaBox { /* Box area */ }
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/objective start <id> [player]` | Start an objective |
|
||||
| `/objective complete <id> [player]` | Force complete |
|
||||
| `/objective history [player]` | View history |
|
||||
| `/objective panel` | Open admin panel |
|
||||
|
||||
## Events
|
||||
|
||||
```java
|
||||
// Treasure chest opened during objective
|
||||
public class TreasureChestOpeningEvent implements IEvent<UUID> {
|
||||
public TreasureChestState getChestState();
|
||||
public Player getPlayer();
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Assets are loaded from `Objectives/` directory with inheritance support:
|
||||
|
||||
```yaml
|
||||
# Objectives/config.yaml
|
||||
ObjectiveGameplayConfig:
|
||||
DefaultObjectives:
|
||||
- tutorial_quest
|
||||
- welcome_message
|
||||
```
|
||||
204
content/gameplay-systems/objectives.fr.md
Normal file
204
content/gameplay-systems/objectives.fr.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
title: Objectifs
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Le systeme d'objectifs fournit des mecaniques de quetes avec des taches, des completions et un suivi de progression.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.objectives`
|
||||
|
||||
## Concepts Cles
|
||||
|
||||
### ObjectiveAsset
|
||||
|
||||
Les objectifs sont definis comme des assets JSON charges depuis `Objectives/`:
|
||||
|
||||
```java
|
||||
public class ObjectiveAsset {
|
||||
protected String id; // Identifiant unique
|
||||
protected String category; // Categorie de quete
|
||||
protected TaskSet[] taskSets; // Groupes de taches sequentiels
|
||||
protected ObjectiveCompletionAsset[] completionHandlers; // Recompenses
|
||||
protected String objectiveTitleKey; // Cle de localisation du titre
|
||||
protected String objectiveDescriptionKey; // Cle de localisation de description
|
||||
protected boolean removeOnItemDrop; // Retirer items objectif au drop
|
||||
}
|
||||
```
|
||||
|
||||
### TaskSet
|
||||
|
||||
Chaque objectif contient un ou plusieurs TaskSets a completer sequentiellement:
|
||||
|
||||
```java
|
||||
public class TaskSet {
|
||||
protected String descriptionId; // Cle de description optionnelle
|
||||
protected ObjectiveTaskAsset[] tasks; // Taches a completer en parallele
|
||||
}
|
||||
```
|
||||
|
||||
### Instance d'Objectif
|
||||
|
||||
Quand demarre, une instance `Objective` suit l'etat:
|
||||
|
||||
```java
|
||||
public class Objective {
|
||||
private final UUID uuid; // ID unique de l'instance
|
||||
private final String objectiveId; // Reference ID de l'asset
|
||||
private final Set<UUID> playerUUIDs; // Joueurs participants
|
||||
private ObjectiveTask[] currentTasks; // Taches actives
|
||||
private int taskSetIndex; // Index du TaskSet courant
|
||||
}
|
||||
```
|
||||
|
||||
## Types de Taches
|
||||
|
||||
Le systeme fournit des types de taches integres enregistres par `ObjectivePlugin`:
|
||||
|
||||
| Type | Classe | Description |
|
||||
|------|--------|-------------|
|
||||
| `Craft` | `CraftObjectiveTask` | Fabriquer des items specifiques |
|
||||
| `Gather` | `GatherObjectiveTask` | Collecter des items dans l'inventaire |
|
||||
| `UseBlock` | `UseBlockObjectiveTask` | Interagir avec des blocs |
|
||||
| `UseEntity` | `UseEntityObjectiveTask` | Interagir avec des entites |
|
||||
| `TreasureMap` | `TreasureMapObjectiveTask` | Trouver des emplacements de tresor |
|
||||
| `ReachLocation` | `ReachLocationTask` | Atteindre des coordonnees specifiques |
|
||||
|
||||
### Exemple d'Asset de Tache
|
||||
|
||||
```yaml
|
||||
# Objectives/collect_wood.json
|
||||
{
|
||||
"Id": "collect_wood",
|
||||
"Category": "tutorial",
|
||||
"TaskSets": [
|
||||
{
|
||||
"Tasks": [
|
||||
{
|
||||
"Type": "Gather",
|
||||
"Item": "oak_log",
|
||||
"Amount": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Completions": [
|
||||
{
|
||||
"Type": "GiveItems",
|
||||
"Items": [
|
||||
{ "Item": "gold_coin", "Amount": 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Gestionnaires de Completion
|
||||
|
||||
Quand tous les TaskSets sont completes, les gestionnaires de completion s'executent:
|
||||
|
||||
| Type | Classe | Description |
|
||||
|------|--------|-------------|
|
||||
| `GiveItems` | `GiveItemsCompletion` | Donner des items au joueur |
|
||||
| `ClearObjectiveItems` | `ClearObjectiveItemsCompletion` | Retirer les items lies a l'objectif |
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
### Demarrer des Objectifs
|
||||
|
||||
```java
|
||||
ObjectivePlugin plugin = ObjectivePlugin.get();
|
||||
|
||||
// Demarrer un objectif pour un joueur
|
||||
plugin.startObjective(player, "collect_wood");
|
||||
|
||||
// Avec des donnees personnalisees
|
||||
Objective objective = plugin.startObjective(player, "collect_wood", customData);
|
||||
```
|
||||
|
||||
### Suivre la Progression
|
||||
|
||||
```java
|
||||
// Obtenir les objectifs actifs du joueur
|
||||
ObjectiveHistoryComponent history = store.getComponent(
|
||||
playerRef,
|
||||
ObjectiveHistoryComponent.getComponentType()
|
||||
);
|
||||
|
||||
// Verifier le statut de l'objectif
|
||||
ObjectiveHistoryData data = history.getObjectiveData("collect_wood");
|
||||
if (data != null && data.isCompleted()) {
|
||||
// Deja complete
|
||||
}
|
||||
```
|
||||
|
||||
### Completer des Objectifs
|
||||
|
||||
```java
|
||||
// Marquer l'objectif comme complete (declenche les completions)
|
||||
objective.complete(objectivePlugin);
|
||||
|
||||
// Annuler l'objectif sans recompenses
|
||||
objective.cancel(objectivePlugin);
|
||||
```
|
||||
|
||||
## Conditions de Declenchement
|
||||
|
||||
Les objectifs peuvent avoir des conditions de declenchement pour l'activation:
|
||||
|
||||
```java
|
||||
// Declencheur base sur la meteo
|
||||
public class WeatherTriggerCondition { /* ... */ }
|
||||
|
||||
// Declencheur base sur la position
|
||||
public class ObjectiveLocationTriggerCondition { /* ... */ }
|
||||
|
||||
// Declencheur base sur l'heure
|
||||
public class HourRangeTriggerCondition { /* ... */ }
|
||||
```
|
||||
|
||||
## Marqueurs de Position
|
||||
|
||||
Utilisez les marqueurs pour creer des objectifs spatiaux:
|
||||
|
||||
```java
|
||||
// Definir une zone de marqueur
|
||||
public interface ObjectiveLocationMarkerArea {
|
||||
boolean isInArea(Vector3d position);
|
||||
}
|
||||
|
||||
// Types de zones integres
|
||||
public class ObjectiveLocationAreaRadius { /* Zone spherique */ }
|
||||
public class ObjectiveLocationAreaBox { /* Zone boite */ }
|
||||
```
|
||||
|
||||
## Commandes
|
||||
|
||||
| Commande | Description |
|
||||
|----------|-------------|
|
||||
| `/objective start <id> [joueur]` | Demarrer un objectif |
|
||||
| `/objective complete <id> [joueur]` | Forcer la completion |
|
||||
| `/objective history [joueur]` | Voir l'historique |
|
||||
| `/objective panel` | Ouvrir le panneau admin |
|
||||
|
||||
## Evenements
|
||||
|
||||
```java
|
||||
// Coffre au tresor ouvert pendant l'objectif
|
||||
public class TreasureChestOpeningEvent implements IEvent<UUID> {
|
||||
public TreasureChestState getChestState();
|
||||
public Player getPlayer();
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Les assets sont charges depuis le repertoire `Objectives/` avec support d'heritage:
|
||||
|
||||
```yaml
|
||||
# Objectives/config.yaml
|
||||
ObjectiveGameplayConfig:
|
||||
DefaultObjectives:
|
||||
- tutorial_quest
|
||||
- welcome_message
|
||||
```
|
||||
241
content/gameplay-systems/reputation.en.md
Normal file
241
content/gameplay-systems/reputation.en.md
Normal file
@@ -0,0 +1,241 @@
|
||||
---
|
||||
title: Reputation
|
||||
type: docs
|
||||
weight: 7
|
||||
---
|
||||
|
||||
The reputation system provides faction standing mechanics that affect NPC behavior and interactions.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.reputation`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ReputationPlugin
|
||||
├── Assets
|
||||
│ ├── ReputationGroup - Faction definitions
|
||||
│ └── ReputationRank - Standing level definitions
|
||||
├── Components
|
||||
│ └── ReputationGroupComponent - NPC faction assignment
|
||||
├── Storage
|
||||
│ ├── ReputationDataResource - World-level storage
|
||||
│ └── PlayerConfigData - Player-level storage
|
||||
├── Requirements
|
||||
│ └── ReputationRequirement - Choice requirement
|
||||
├── Configuration
|
||||
│ └── ReputationGameplayConfig
|
||||
└── Commands
|
||||
└── ReputationCommand (value, set, add, rank)
|
||||
```
|
||||
|
||||
## Reputation Groups
|
||||
|
||||
Factions are defined as `ReputationGroup` assets:
|
||||
|
||||
```java
|
||||
public class ReputationGroup implements JsonAssetWithMap<String, DefaultAssetMap<String, ReputationGroup>> {
|
||||
protected String id;
|
||||
protected String[] npcGroups; // NPC groups in this faction
|
||||
protected int initialReputationValue; // Starting reputation
|
||||
}
|
||||
```
|
||||
|
||||
**Asset Location:** `NPC/Reputation/Groups/`
|
||||
|
||||
### Group Configuration
|
||||
|
||||
```yaml
|
||||
# NPC/Reputation/Groups/village_faction.json
|
||||
{
|
||||
"Id": "village_faction",
|
||||
"NPCGroups": ["villagers", "guards"],
|
||||
"InitialReputationValue": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Reputation Ranks
|
||||
|
||||
Standing levels defined as `ReputationRank` assets:
|
||||
|
||||
```java
|
||||
public class ReputationRank implements JsonAssetWithMap<String, DefaultAssetMap<String, ReputationRank>> {
|
||||
protected String id;
|
||||
protected int minValue;
|
||||
protected int maxValue;
|
||||
protected Attitude attitude; // NPC attitude at this rank
|
||||
}
|
||||
```
|
||||
|
||||
**Asset Location:** `NPC/Reputation/Ranks/`
|
||||
|
||||
### Rank Configuration
|
||||
|
||||
```yaml
|
||||
# NPC/Reputation/Ranks/friendly.json
|
||||
{
|
||||
"Id": "friendly",
|
||||
"MinValue": 50,
|
||||
"MaxValue": 100,
|
||||
"Attitude": "friendly"
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Modes
|
||||
|
||||
Reputation can be stored per-player or per-world:
|
||||
|
||||
```java
|
||||
public enum ReputationStorageType {
|
||||
PerPlayer, // Each player has own reputation
|
||||
PerWorld // Shared world reputation
|
||||
}
|
||||
```
|
||||
|
||||
### Per-Player Storage
|
||||
|
||||
```java
|
||||
PlayerConfigData playerConfigData = player.getPlayerConfigData();
|
||||
Object2IntMap<String> reputationData = playerConfigData.getReputationData();
|
||||
```
|
||||
|
||||
### Per-World Storage
|
||||
|
||||
```java
|
||||
ReputationDataResource resource = world.getEntityStore().getStore()
|
||||
.getResource(ReputationPlugin.get().getReputationDataResourceType());
|
||||
Object2IntMap<String> worldReputation = resource.getReputationStats();
|
||||
```
|
||||
|
||||
## NPC Faction Assignment
|
||||
|
||||
### ReputationGroupComponent
|
||||
|
||||
Assigns NPCs to factions:
|
||||
|
||||
```java
|
||||
ComponentType<EntityStore, ReputationGroupComponent> type =
|
||||
ReputationPlugin.get().getReputationGroupComponentType();
|
||||
|
||||
ReputationGroupComponent comp = store.getComponent(npcRef, type);
|
||||
String factionId = comp.getReputationGroupId();
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Changing Reputation
|
||||
|
||||
```java
|
||||
ReputationPlugin rep = ReputationPlugin.get();
|
||||
|
||||
// Change reputation with faction (returns new value)
|
||||
int newValue = rep.changeReputation(player, "village_faction", 10, componentAccessor);
|
||||
|
||||
// Change reputation via NPC reference
|
||||
int newValue = rep.changeReputation(player, npcRef, -5, componentAccessor);
|
||||
|
||||
// World-level reputation change
|
||||
int newValue = rep.changeReputation(world, "village_faction", 15);
|
||||
```
|
||||
|
||||
### Getting Reputation
|
||||
|
||||
```java
|
||||
// Get reputation value
|
||||
int value = rep.getReputationValue(store, playerRef, "village_faction");
|
||||
int value = rep.getReputationValue(store, playerRef, npcRef);
|
||||
|
||||
// Get reputation rank
|
||||
ReputationRank rank = rep.getReputationRank(store, playerRef, "village_faction");
|
||||
ReputationRank rank = rep.getReputationRankFromValue(value);
|
||||
|
||||
// Get NPC attitude based on reputation
|
||||
Attitude attitude = rep.getAttitude(store, playerRef, npcRef);
|
||||
```
|
||||
|
||||
## Choice Requirements
|
||||
|
||||
Use reputation as requirement for dialogue choices:
|
||||
|
||||
```java
|
||||
ChoiceRequirement.CODEC.register(
|
||||
"Reputation",
|
||||
ReputationRequirement.class,
|
||||
ReputationRequirement.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Choice Configuration
|
||||
|
||||
```yaml
|
||||
{
|
||||
"Type": "Reputation",
|
||||
"FactionId": "village_faction",
|
||||
"MinRank": "friendly"
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/reputation` | Base reputation command |
|
||||
| `/reputation value <faction> [player]` | Show reputation value |
|
||||
| `/reputation set <faction> <value> [player]` | Set reputation |
|
||||
| `/reputation add <faction> <amount> [player]` | Add reputation |
|
||||
| `/reputation rank <faction> [player]` | Show current rank |
|
||||
|
||||
## Configuration
|
||||
|
||||
### Gameplay Config
|
||||
|
||||
```java
|
||||
public class ReputationGameplayConfig {
|
||||
ReputationStorageType reputationStorageType;
|
||||
|
||||
public static ReputationGameplayConfig getOrDefault(GameplayConfig config);
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# gameplay_config.json
|
||||
{
|
||||
"Reputation": {
|
||||
"ReputationStorageType": "PerPlayer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related Plugins
|
||||
|
||||
### ObjectiveReputationPlugin
|
||||
|
||||
Rewards reputation on objective completion:
|
||||
|
||||
```java
|
||||
public class ReputationCompletion implements ObjectiveCompletion {
|
||||
// Awards reputation when objective completes
|
||||
}
|
||||
```
|
||||
|
||||
### NPCReputationPlugin
|
||||
|
||||
NPC attitude based on reputation:
|
||||
|
||||
```java
|
||||
public class ReputationAttitudeSystem {
|
||||
// Updates NPC attitude based on player reputation
|
||||
}
|
||||
```
|
||||
|
||||
### ShopReputationPlugin
|
||||
|
||||
Reputation-gated shop items.
|
||||
|
||||
## Validation
|
||||
|
||||
Ranks are validated on plugin start:
|
||||
|
||||
```java
|
||||
// Warns if gaps between rank ranges
|
||||
// Warns if rank ranges overlap
|
||||
```
|
||||
241
content/gameplay-systems/reputation.fr.md
Normal file
241
content/gameplay-systems/reputation.fr.md
Normal file
@@ -0,0 +1,241 @@
|
||||
---
|
||||
title: Reputation
|
||||
type: docs
|
||||
weight: 7
|
||||
---
|
||||
|
||||
Le systeme de reputation fournit des mecaniques de standing de faction qui affectent le comportement et les interactions des NPCs.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.reputation`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ReputationPlugin
|
||||
├── Assets
|
||||
│ ├── ReputationGroup - Definitions de factions
|
||||
│ └── ReputationRank - Definitions de niveaux de standing
|
||||
├── Composants
|
||||
│ └── ReputationGroupComponent - Assignation faction NPC
|
||||
├── Stockage
|
||||
│ ├── ReputationDataResource - Stockage niveau monde
|
||||
│ └── PlayerConfigData - Stockage niveau joueur
|
||||
├── Requirements
|
||||
│ └── ReputationRequirement - Requirement de choix
|
||||
├── Configuration
|
||||
│ └── ReputationGameplayConfig
|
||||
└── Commandes
|
||||
└── ReputationCommand (value, set, add, rank)
|
||||
```
|
||||
|
||||
## Groupes de Reputation
|
||||
|
||||
Les factions sont definies comme assets `ReputationGroup`:
|
||||
|
||||
```java
|
||||
public class ReputationGroup implements JsonAssetWithMap<String, DefaultAssetMap<String, ReputationGroup>> {
|
||||
protected String id;
|
||||
protected String[] npcGroups; // Groupes NPC dans cette faction
|
||||
protected int initialReputationValue; // Reputation de depart
|
||||
}
|
||||
```
|
||||
|
||||
**Emplacement Asset:** `NPC/Reputation/Groups/`
|
||||
|
||||
### Configuration de Groupe
|
||||
|
||||
```yaml
|
||||
# NPC/Reputation/Groups/village_faction.json
|
||||
{
|
||||
"Id": "village_faction",
|
||||
"NPCGroups": ["villagers", "guards"],
|
||||
"InitialReputationValue": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Rangs de Reputation
|
||||
|
||||
Niveaux de standing definis comme assets `ReputationRank`:
|
||||
|
||||
```java
|
||||
public class ReputationRank implements JsonAssetWithMap<String, DefaultAssetMap<String, ReputationRank>> {
|
||||
protected String id;
|
||||
protected int minValue;
|
||||
protected int maxValue;
|
||||
protected Attitude attitude; // Attitude NPC a ce rang
|
||||
}
|
||||
```
|
||||
|
||||
**Emplacement Asset:** `NPC/Reputation/Ranks/`
|
||||
|
||||
### Configuration de Rang
|
||||
|
||||
```yaml
|
||||
# NPC/Reputation/Ranks/friendly.json
|
||||
{
|
||||
"Id": "friendly",
|
||||
"MinValue": 50,
|
||||
"MaxValue": 100,
|
||||
"Attitude": "friendly"
|
||||
}
|
||||
```
|
||||
|
||||
## Modes de Stockage
|
||||
|
||||
La reputation peut etre stockee par-joueur ou par-monde:
|
||||
|
||||
```java
|
||||
public enum ReputationStorageType {
|
||||
PerPlayer, // Chaque joueur a sa propre reputation
|
||||
PerWorld // Reputation monde partagee
|
||||
}
|
||||
```
|
||||
|
||||
### Stockage Par-Joueur
|
||||
|
||||
```java
|
||||
PlayerConfigData playerConfigData = player.getPlayerConfigData();
|
||||
Object2IntMap<String> reputationData = playerConfigData.getReputationData();
|
||||
```
|
||||
|
||||
### Stockage Par-Monde
|
||||
|
||||
```java
|
||||
ReputationDataResource resource = world.getEntityStore().getStore()
|
||||
.getResource(ReputationPlugin.get().getReputationDataResourceType());
|
||||
Object2IntMap<String> worldReputation = resource.getReputationStats();
|
||||
```
|
||||
|
||||
## Assignation Faction NPC
|
||||
|
||||
### ReputationGroupComponent
|
||||
|
||||
Assigne les NPCs aux factions:
|
||||
|
||||
```java
|
||||
ComponentType<EntityStore, ReputationGroupComponent> type =
|
||||
ReputationPlugin.get().getReputationGroupComponentType();
|
||||
|
||||
ReputationGroupComponent comp = store.getComponent(npcRef, type);
|
||||
String factionId = comp.getReputationGroupId();
|
||||
```
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
### Changer la Reputation
|
||||
|
||||
```java
|
||||
ReputationPlugin rep = ReputationPlugin.get();
|
||||
|
||||
// Changer reputation avec faction (retourne nouvelle valeur)
|
||||
int newValue = rep.changeReputation(player, "village_faction", 10, componentAccessor);
|
||||
|
||||
// Changer reputation via reference NPC
|
||||
int newValue = rep.changeReputation(player, npcRef, -5, componentAccessor);
|
||||
|
||||
// Changement reputation niveau monde
|
||||
int newValue = rep.changeReputation(world, "village_faction", 15);
|
||||
```
|
||||
|
||||
### Obtenir la Reputation
|
||||
|
||||
```java
|
||||
// Obtenir valeur reputation
|
||||
int value = rep.getReputationValue(store, playerRef, "village_faction");
|
||||
int value = rep.getReputationValue(store, playerRef, npcRef);
|
||||
|
||||
// Obtenir rang reputation
|
||||
ReputationRank rank = rep.getReputationRank(store, playerRef, "village_faction");
|
||||
ReputationRank rank = rep.getReputationRankFromValue(value);
|
||||
|
||||
// Obtenir attitude NPC basee sur reputation
|
||||
Attitude attitude = rep.getAttitude(store, playerRef, npcRef);
|
||||
```
|
||||
|
||||
## Requirements de Choix
|
||||
|
||||
Utiliser la reputation comme requirement pour les choix de dialogue:
|
||||
|
||||
```java
|
||||
ChoiceRequirement.CODEC.register(
|
||||
"Reputation",
|
||||
ReputationRequirement.class,
|
||||
ReputationRequirement.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Configuration de Choix
|
||||
|
||||
```yaml
|
||||
{
|
||||
"Type": "Reputation",
|
||||
"FactionId": "village_faction",
|
||||
"MinRank": "friendly"
|
||||
}
|
||||
```
|
||||
|
||||
## Commandes
|
||||
|
||||
| Commande | Description |
|
||||
|----------|-------------|
|
||||
| `/reputation` | Commande reputation de base |
|
||||
| `/reputation value <faction> [joueur]` | Afficher valeur reputation |
|
||||
| `/reputation set <faction> <valeur> [joueur]` | Definir reputation |
|
||||
| `/reputation add <faction> <montant> [joueur]` | Ajouter reputation |
|
||||
| `/reputation rank <faction> [joueur]` | Afficher rang actuel |
|
||||
|
||||
## Configuration
|
||||
|
||||
### Config Gameplay
|
||||
|
||||
```java
|
||||
public class ReputationGameplayConfig {
|
||||
ReputationStorageType reputationStorageType;
|
||||
|
||||
public static ReputationGameplayConfig getOrDefault(GameplayConfig config);
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# gameplay_config.json
|
||||
{
|
||||
"Reputation": {
|
||||
"ReputationStorageType": "PerPlayer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Plugins Lies
|
||||
|
||||
### ObjectiveReputationPlugin
|
||||
|
||||
Recompense reputation a la completion d'objectif:
|
||||
|
||||
```java
|
||||
public class ReputationCompletion implements ObjectiveCompletion {
|
||||
// Attribue reputation quand objectif complete
|
||||
}
|
||||
```
|
||||
|
||||
### NPCReputationPlugin
|
||||
|
||||
Attitude NPC basee sur reputation:
|
||||
|
||||
```java
|
||||
public class ReputationAttitudeSystem {
|
||||
// Met a jour attitude NPC basee sur reputation joueur
|
||||
}
|
||||
```
|
||||
|
||||
### ShopReputationPlugin
|
||||
|
||||
Items de boutique conditionnes par reputation.
|
||||
|
||||
## Validation
|
||||
|
||||
Les rangs sont valides au demarrage du plugin:
|
||||
|
||||
```java
|
||||
// Avertit si ecarts entre plages de rangs
|
||||
// Avertit si plages de rangs se chevauchent
|
||||
```
|
||||
195
content/gameplay-systems/shop.en.md
Normal file
195
content/gameplay-systems/shop.en.md
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: Shop
|
||||
type: docs
|
||||
weight: 5
|
||||
---
|
||||
|
||||
The shop system provides trading and commerce through NPC shops and barter systems.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.shop`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ShopPlugin
|
||||
├── Shop Types
|
||||
│ ├── ShopAsset - Static shop definitions
|
||||
│ └── BarterShopAsset - Dynamic barter shops
|
||||
├── Elements
|
||||
│ ├── ShopElement - Shop item entry
|
||||
│ └── ChoiceElement integration
|
||||
├── Interactions
|
||||
│ └── GiveItemInteraction
|
||||
├── Pages
|
||||
│ ├── ShopPage - Shop UI page
|
||||
│ ├── ShopPageSupplier
|
||||
│ └── BarterPage - Barter UI page
|
||||
└── State
|
||||
└── BarterShopState - Persistent barter state
|
||||
```
|
||||
|
||||
## Shop Types
|
||||
|
||||
### ShopAsset
|
||||
|
||||
Static shops with fixed content:
|
||||
|
||||
```java
|
||||
public class ShopAsset implements JsonAssetWithMap<String, DefaultAssetMap<String, ShopAsset>> {
|
||||
protected String id;
|
||||
protected ChoiceElement[] elements; // Shop items
|
||||
|
||||
public String getId();
|
||||
public ChoiceElement[] getElements();
|
||||
}
|
||||
```
|
||||
|
||||
**Asset Location:** `Shops/`
|
||||
|
||||
### BarterShopAsset
|
||||
|
||||
Dynamic shops with rotating trades:
|
||||
|
||||
```java
|
||||
public class BarterShopAsset implements JsonAssetWithMap<String, DefaultAssetMap<String, BarterShopAsset>> {
|
||||
// Barter-based trading with refresh mechanics
|
||||
}
|
||||
```
|
||||
|
||||
**Asset Location:** `BarterShops/`
|
||||
|
||||
## Shop Configuration
|
||||
|
||||
### Static Shop Example
|
||||
|
||||
```yaml
|
||||
# Shops/general_store.json
|
||||
{
|
||||
"Id": "general_store",
|
||||
"Content": [
|
||||
{
|
||||
"Type": "ShopElement",
|
||||
"Item": "bread",
|
||||
"Price": 5,
|
||||
"Currency": "gold_coin"
|
||||
},
|
||||
{
|
||||
"Type": "ShopElement",
|
||||
"Item": "torch",
|
||||
"Price": 2,
|
||||
"Currency": "gold_coin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Barter Shop Components
|
||||
|
||||
```java
|
||||
public class BarterShopState {
|
||||
// Persistent state for barter shops
|
||||
public static void initialize(Path dataDirectory);
|
||||
public static void shutdown();
|
||||
}
|
||||
|
||||
public class TradeSlot { /* Base trade slot */ }
|
||||
public class FixedTradeSlot extends TradeSlot { /* Fixed trade */ }
|
||||
public class PoolTradeSlot extends TradeSlot { /* Random from pool */ }
|
||||
public class WeightedTrade { /* Weighted random trade */ }
|
||||
|
||||
public class BarterItemStack { /* Items for barter */ }
|
||||
public class BarterTrade { /* Complete trade definition */ }
|
||||
public class RefreshInterval { /* Trade refresh timing */ }
|
||||
```
|
||||
|
||||
## Shop Elements
|
||||
|
||||
### ShopElement
|
||||
|
||||
Registered as a choice element type:
|
||||
|
||||
```java
|
||||
getCodecRegistry(ChoiceElement.CODEC).register("ShopElement", ShopElement.class, ShopElement.CODEC);
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
### GiveItemInteraction
|
||||
|
||||
Choice interaction that gives items to player:
|
||||
|
||||
```java
|
||||
getCodecRegistry(ChoiceInteraction.CODEC).register("GiveItem", GiveItemInteraction.class, GiveItemInteraction.CODEC);
|
||||
```
|
||||
|
||||
## UI Integration
|
||||
|
||||
### Shop Page
|
||||
|
||||
Custom UI page for shops:
|
||||
|
||||
```java
|
||||
getCodecRegistry(OpenCustomUIInteraction.PAGE_CODEC).register(
|
||||
"Shop",
|
||||
ShopPageSupplier.class,
|
||||
ShopPageSupplier.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Opening a Shop
|
||||
|
||||
```yaml
|
||||
# Interaction definition
|
||||
{
|
||||
"Type": "OpenCustomUI",
|
||||
"Page": {
|
||||
"Type": "Shop",
|
||||
"ShopId": "general_store"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Getting Shop Assets
|
||||
|
||||
```java
|
||||
// Get shop asset store
|
||||
AssetStore<String, ShopAsset, DefaultAssetMap<String, ShopAsset>> store =
|
||||
ShopAsset.getAssetStore();
|
||||
|
||||
// Get specific shop
|
||||
ShopAsset shop = ShopAsset.getAssetMap().getAsset("general_store");
|
||||
|
||||
// Get barter shop
|
||||
BarterShopAsset barterShop = BarterShopAsset.getAssetMap().getAsset("traveling_merchant");
|
||||
```
|
||||
|
||||
### Shop Plugin Instance
|
||||
|
||||
```java
|
||||
ShopPlugin shop = ShopPlugin.get();
|
||||
```
|
||||
|
||||
## Barter State Persistence
|
||||
|
||||
Barter shop state is saved/loaded automatically:
|
||||
|
||||
```java
|
||||
// Called in start()
|
||||
BarterShopState.initialize(this.getDataDirectory());
|
||||
|
||||
// Called in shutdown()
|
||||
BarterShopState.shutdown();
|
||||
```
|
||||
|
||||
## Asset Dependencies
|
||||
|
||||
Shops load after items:
|
||||
|
||||
```java
|
||||
HytaleAssetStore.builder(ShopAsset.class, new DefaultAssetMap())
|
||||
.setPath("Shops")
|
||||
.loadsAfter(Item.class) // Ensure items are loaded first
|
||||
.build();
|
||||
```
|
||||
195
content/gameplay-systems/shop.fr.md
Normal file
195
content/gameplay-systems/shop.fr.md
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: Boutique
|
||||
type: docs
|
||||
weight: 5
|
||||
---
|
||||
|
||||
Le systeme de boutique fournit le commerce via des boutiques NPC et des systemes de troc.
|
||||
|
||||
**Package:** `com.hypixel.hytale.builtin.adventure.shop`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ShopPlugin
|
||||
├── Types de Boutique
|
||||
│ ├── ShopAsset - Definitions boutique statiques
|
||||
│ └── BarterShopAsset - Boutiques de troc dynamiques
|
||||
├── Elements
|
||||
│ ├── ShopElement - Entree item boutique
|
||||
│ └── Integration ChoiceElement
|
||||
├── Interactions
|
||||
│ └── GiveItemInteraction
|
||||
├── Pages
|
||||
│ ├── ShopPage - Page UI boutique
|
||||
│ ├── ShopPageSupplier
|
||||
│ └── BarterPage - Page UI troc
|
||||
└── Etat
|
||||
└── BarterShopState - Etat troc persistant
|
||||
```
|
||||
|
||||
## Types de Boutique
|
||||
|
||||
### ShopAsset
|
||||
|
||||
Boutiques statiques avec contenu fixe:
|
||||
|
||||
```java
|
||||
public class ShopAsset implements JsonAssetWithMap<String, DefaultAssetMap<String, ShopAsset>> {
|
||||
protected String id;
|
||||
protected ChoiceElement[] elements; // Items boutique
|
||||
|
||||
public String getId();
|
||||
public ChoiceElement[] getElements();
|
||||
}
|
||||
```
|
||||
|
||||
**Emplacement Asset:** `Shops/`
|
||||
|
||||
### BarterShopAsset
|
||||
|
||||
Boutiques dynamiques avec echanges rotatifs:
|
||||
|
||||
```java
|
||||
public class BarterShopAsset implements JsonAssetWithMap<String, DefaultAssetMap<String, BarterShopAsset>> {
|
||||
// Trading base sur le troc avec mecanique de rafraichissement
|
||||
}
|
||||
```
|
||||
|
||||
**Emplacement Asset:** `BarterShops/`
|
||||
|
||||
## Configuration de Boutique
|
||||
|
||||
### Exemple Boutique Statique
|
||||
|
||||
```yaml
|
||||
# Shops/general_store.json
|
||||
{
|
||||
"Id": "general_store",
|
||||
"Content": [
|
||||
{
|
||||
"Type": "ShopElement",
|
||||
"Item": "bread",
|
||||
"Price": 5,
|
||||
"Currency": "gold_coin"
|
||||
},
|
||||
{
|
||||
"Type": "ShopElement",
|
||||
"Item": "torch",
|
||||
"Price": 2,
|
||||
"Currency": "gold_coin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Composants Boutique Troc
|
||||
|
||||
```java
|
||||
public class BarterShopState {
|
||||
// Etat persistant pour boutiques de troc
|
||||
public static void initialize(Path dataDirectory);
|
||||
public static void shutdown();
|
||||
}
|
||||
|
||||
public class TradeSlot { /* Slot d'echange de base */ }
|
||||
public class FixedTradeSlot extends TradeSlot { /* Echange fixe */ }
|
||||
public class PoolTradeSlot extends TradeSlot { /* Aleatoire depuis pool */ }
|
||||
public class WeightedTrade { /* Echange aleatoire pondere */ }
|
||||
|
||||
public class BarterItemStack { /* Items pour troc */ }
|
||||
public class BarterTrade { /* Definition complete d'echange */ }
|
||||
public class RefreshInterval { /* Timing de rafraichissement */ }
|
||||
```
|
||||
|
||||
## Elements de Boutique
|
||||
|
||||
### ShopElement
|
||||
|
||||
Enregistre comme type d'element de choix:
|
||||
|
||||
```java
|
||||
getCodecRegistry(ChoiceElement.CODEC).register("ShopElement", ShopElement.class, ShopElement.CODEC);
|
||||
```
|
||||
|
||||
## Interactions
|
||||
|
||||
### GiveItemInteraction
|
||||
|
||||
Interaction de choix qui donne des items au joueur:
|
||||
|
||||
```java
|
||||
getCodecRegistry(ChoiceInteraction.CODEC).register("GiveItem", GiveItemInteraction.class, GiveItemInteraction.CODEC);
|
||||
```
|
||||
|
||||
## Integration UI
|
||||
|
||||
### Page Boutique
|
||||
|
||||
Page UI personnalisee pour boutiques:
|
||||
|
||||
```java
|
||||
getCodecRegistry(OpenCustomUIInteraction.PAGE_CODEC).register(
|
||||
"Shop",
|
||||
ShopPageSupplier.class,
|
||||
ShopPageSupplier.CODEC
|
||||
);
|
||||
```
|
||||
|
||||
### Ouvrir une Boutique
|
||||
|
||||
```yaml
|
||||
# Definition d'interaction
|
||||
{
|
||||
"Type": "OpenCustomUI",
|
||||
"Page": {
|
||||
"Type": "Shop",
|
||||
"ShopId": "general_store"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Utilisation de l'API
|
||||
|
||||
### Obtenir les Assets Boutique
|
||||
|
||||
```java
|
||||
// Obtenir le store d'assets boutique
|
||||
AssetStore<String, ShopAsset, DefaultAssetMap<String, ShopAsset>> store =
|
||||
ShopAsset.getAssetStore();
|
||||
|
||||
// Obtenir boutique specifique
|
||||
ShopAsset shop = ShopAsset.getAssetMap().getAsset("general_store");
|
||||
|
||||
// Obtenir boutique troc
|
||||
BarterShopAsset barterShop = BarterShopAsset.getAssetMap().getAsset("traveling_merchant");
|
||||
```
|
||||
|
||||
### Instance Plugin Boutique
|
||||
|
||||
```java
|
||||
ShopPlugin shop = ShopPlugin.get();
|
||||
```
|
||||
|
||||
## Persistance Etat Troc
|
||||
|
||||
L'etat des boutiques de troc est sauvegarde/charge automatiquement:
|
||||
|
||||
```java
|
||||
// Appele dans start()
|
||||
BarterShopState.initialize(this.getDataDirectory());
|
||||
|
||||
// Appele dans shutdown()
|
||||
BarterShopState.shutdown();
|
||||
```
|
||||
|
||||
## Dependances d'Assets
|
||||
|
||||
Les boutiques chargent apres les items:
|
||||
|
||||
```java
|
||||
HytaleAssetStore.builder(ShopAsset.class, new DefaultAssetMap())
|
||||
.setPath("Shops")
|
||||
.loadsAfter(Item.class) // Assurer que items sont charges d'abord
|
||||
.build();
|
||||
```
|
||||
14
content/getting-started/_index.en.md
Normal file
14
content/getting-started/_index.en.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Getting Started
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Welcome to Hytale plugin development! This section will guide you through setting up your development environment and creating your first plugin.
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="prerequisites" title="Prerequisites" subtitle="Set up Java 25, Gradle, and your IDE" >}}
|
||||
{{< card link="building-and-running" title="Building & Running" subtitle="Build your plugin and deploy it" >}}
|
||||
{{< card link="creating-a-plugin" title="Creating a Plugin" subtitle="Project structure and manifest.json" >}}
|
||||
{{< card link="plugin-lifecycle" title="Plugin Lifecycle" subtitle="Understanding setup, start, and shutdown" >}}
|
||||
{{< /cards >}}
|
||||
14
content/getting-started/_index.fr.md
Normal file
14
content/getting-started/_index.fr.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Premiers Pas
|
||||
type: docs
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Bienvenue dans le développement de plugins Hytale ! Cette section vous guidera dans la configuration de votre environnement de développement et la création de votre premier plugin.
|
||||
|
||||
{{< cards >}}
|
||||
{{< card link="prerequisites" title="Prérequis" subtitle="Configurer Java 25, Gradle et votre IDE" >}}
|
||||
{{< card link="building-and-running" title="Build et Exécution" subtitle="Compiler et déployer votre plugin" >}}
|
||||
{{< card link="creating-a-plugin" title="Créer un Plugin" subtitle="Structure du projet et manifest.json" >}}
|
||||
{{< card link="plugin-lifecycle" title="Cycle de Vie" subtitle="Comprendre setup, start et shutdown" >}}
|
||||
{{< /cards >}}
|
||||
246
content/getting-started/building-and-running.en.md
Normal file
246
content/getting-started/building-and-running.en.md
Normal file
@@ -0,0 +1,246 @@
|
||||
---
|
||||
title: Building and Running
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
This guide covers how to set up your development environment, build your plugin, and deploy it to a Hytale server.
|
||||
|
||||
## Quick Start with example-mod
|
||||
|
||||
The easiest way to start plugin development is using the [example-mod](https://github.com/hytale-france/example-mod) template which includes an automated setup script.
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hytale-france/example-mod.git
|
||||
cd example-mod
|
||||
```
|
||||
|
||||
Or download the latest release from [GitHub Releases](https://github.com/hytale-france/example-mod/releases).
|
||||
|
||||
### 2. Download Tools (Once)
|
||||
|
||||
Download the required tools (hytale-downloader and CFR decompiler):
|
||||
|
||||
```bash
|
||||
./setup.sh --download
|
||||
```
|
||||
|
||||
This creates a `.bin/` directory with:
|
||||
- `hytale-downloader-*` - Official Hytale server downloader
|
||||
- `cfr-0.152.jar` - Java decompiler for reference sources
|
||||
|
||||
### 3. Download the Server
|
||||
|
||||
Download and extract the Hytale server:
|
||||
|
||||
```bash
|
||||
./setup.sh --setup
|
||||
```
|
||||
|
||||
The server will be available in `server/Server/HytaleServer.jar`.
|
||||
|
||||
### 4. Generate Reference Sources (Optional)
|
||||
|
||||
Decompile the server JAR to get API reference sources:
|
||||
|
||||
```bash
|
||||
./setup.sh --decompile
|
||||
```
|
||||
|
||||
This creates `src-ref/` with decompiled Java sources for IDE autocompletion and documentation.
|
||||
|
||||
## Project Structure
|
||||
|
||||
After setup, your project will look like this:
|
||||
|
||||
```
|
||||
example-mod/
|
||||
├── app/
|
||||
│ ├── src/main/java/org/example/
|
||||
│ │ └── ExamplePlugin.java
|
||||
│ ├── src/main/resources/
|
||||
│ │ └── manifest.json
|
||||
│ └── build.gradle.kts
|
||||
├── server/
|
||||
│ ├── Server/
|
||||
│ │ └── HytaleServer.jar
|
||||
│ └── Assets.zip
|
||||
├── src-ref/ # Decompiled reference sources
|
||||
├── .bin/ # Downloaded tools
|
||||
├── setup.sh # Setup automation script
|
||||
├── build.gradle.kts
|
||||
├── settings.gradle.kts
|
||||
└── gradlew
|
||||
```
|
||||
|
||||
## Building the Plugin
|
||||
|
||||
### 1. Build with Gradle
|
||||
|
||||
```bash
|
||||
# On Linux/macOS
|
||||
./gradlew build
|
||||
|
||||
# On Windows
|
||||
gradlew.bat build
|
||||
```
|
||||
|
||||
### 2. Locate the Built JAR
|
||||
|
||||
After a successful build, your plugin JAR will be in:
|
||||
|
||||
```
|
||||
app/build/libs/ExamplePlugin-1.0.0.jar
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
### 1. Initial Server Setup
|
||||
|
||||
Start the server for the first time:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
java -jar Server/HytaleServer.jar --assets Assets.zip
|
||||
```
|
||||
|
||||
### 2. Authenticate
|
||||
|
||||
Once the server console is ready, run the authentication command:
|
||||
|
||||
```
|
||||
/auth login device
|
||||
```
|
||||
|
||||
Follow the instructions to authenticate with your Hytale account.
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Authentication Required:** If no server tokens are configured, you will see a warning at startup:
|
||||
```
|
||||
No server tokens configured. Use /auth login to authenticate.
|
||||
```
|
||||
You must authenticate before players can join the server in non-singleplayer mode.
|
||||
{{< /callout >}}
|
||||
|
||||
### 3. Stop the Server
|
||||
|
||||
After authentication, you can stop the server:
|
||||
|
||||
- **Linux/macOS:** Press `Ctrl+C`
|
||||
- **Windows:** Press `Ctrl+C` or close the terminal
|
||||
|
||||
## Deploying Your Plugin
|
||||
|
||||
### 1. Copy the JAR
|
||||
|
||||
Copy the built JAR to the `mods` folder:
|
||||
|
||||
```bash
|
||||
cp app/build/libs/ExamplePlugin-1.0.0.jar server/mods/
|
||||
```
|
||||
|
||||
### 2. Start the Server
|
||||
|
||||
```bash
|
||||
cd server
|
||||
java -jar Server/HytaleServer.jar --assets Assets.zip
|
||||
```
|
||||
|
||||
### 3. Verify Loading
|
||||
|
||||
You should see your plugin loading in the console:
|
||||
|
||||
```
|
||||
[INFO] [PluginManager] Loading pending plugins from directory: mods
|
||||
[INFO] [PluginManager] - org.example:ExamplePlugin from path ExamplePlugin-1.0.0.jar
|
||||
```
|
||||
|
||||
## Updating the Server
|
||||
|
||||
When a new Hytale version is released:
|
||||
|
||||
```bash
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
This will download the latest server version and update your installation.
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Java Version Mismatch
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Error:** `Unsupported class file major version 69`
|
||||
{{< /callout >}}
|
||||
|
||||
Ensure you're using Java 25:
|
||||
|
||||
```bash
|
||||
java --version
|
||||
```
|
||||
|
||||
### Manifest Not Found
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Error:** `Failed to load manifest file!`
|
||||
{{< /callout >}}
|
||||
|
||||
Make sure `manifest.json` is in `src/main/resources/` and included in the JAR.
|
||||
|
||||
### Setup Script Not Found
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Error:** `./setup.sh: Permission denied`
|
||||
{{< /callout >}}
|
||||
|
||||
Make the script executable:
|
||||
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
## Manual Setup (Alternative)
|
||||
|
||||
If you prefer manual setup without the script:
|
||||
|
||||
### Locating Hytale Installation
|
||||
|
||||
Find your Hytale installation directory:
|
||||
|
||||
{{< tabs items="Windows,Linux,macOS" >}}
|
||||
{{< tab >}}
|
||||
```
|
||||
%appdata%\Hytale\install\release\package\game\latest
|
||||
```
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
```
|
||||
$XDG_DATA_HOME/Hytale/install/release/package/game/latest
|
||||
```
|
||||
Or typically:
|
||||
```
|
||||
~/.local/share/Hytale/install/release/package/game/latest
|
||||
```
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
```
|
||||
~/Application Support/Hytale/install/release/package/game/latest
|
||||
```
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### Manual Server Setup
|
||||
|
||||
1. Copy `Server/` and `Assets.zip` from your Hytale installation
|
||||
2. Download CFR from [https://www.benf.org/other/cfr/](https://www.benf.org/other/cfr/)
|
||||
3. Decompile: `java -jar cfr.jar HytaleServer.jar --outputdir src-ref`
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you can build and run your plugin, explore:
|
||||
|
||||
- [Plugin Lifecycle](../plugin-lifecycle) - Understand setup, start, and shutdown
|
||||
- [Commands](../../commands) - Create custom commands
|
||||
- [Events](../../events) - Listen to game events
|
||||
246
content/getting-started/building-and-running.fr.md
Normal file
246
content/getting-started/building-and-running.fr.md
Normal file
@@ -0,0 +1,246 @@
|
||||
---
|
||||
title: Compilation et Exécution
|
||||
type: docs
|
||||
weight: 2
|
||||
---
|
||||
|
||||
Ce guide explique comment configurer votre environnement de développement, compiler votre plugin et le déployer sur un serveur Hytale.
|
||||
|
||||
## Démarrage Rapide avec example-mod
|
||||
|
||||
La façon la plus simple de commencer le développement de plugins est d'utiliser le template [example-mod](https://github.com/hytale-france/example-mod) qui inclut un script de configuration automatisé.
|
||||
|
||||
### 1. Cloner le Dépôt
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hytale-france/example-mod.git
|
||||
cd example-mod
|
||||
```
|
||||
|
||||
Ou téléchargez la dernière release depuis [GitHub Releases](https://github.com/hytale-france/example-mod/releases).
|
||||
|
||||
### 2. Télécharger les Outils (Une fois)
|
||||
|
||||
Téléchargez les outils requis (hytale-downloader et décompilateur CFR) :
|
||||
|
||||
```bash
|
||||
./setup.sh --download
|
||||
```
|
||||
|
||||
Cela crée un répertoire `.bin/` avec :
|
||||
- `hytale-downloader-*` - Téléchargeur officiel du serveur Hytale
|
||||
- `cfr-0.152.jar` - Décompilateur Java pour les sources de référence
|
||||
|
||||
### 3. Télécharger le Serveur
|
||||
|
||||
Téléchargez et extrayez le serveur Hytale :
|
||||
|
||||
```bash
|
||||
./setup.sh --setup
|
||||
```
|
||||
|
||||
Le serveur sera disponible dans `server/Server/HytaleServer.jar`.
|
||||
|
||||
### 4. Générer les Sources de Référence (Optionnel)
|
||||
|
||||
Décompilez le JAR serveur pour obtenir les sources de référence de l'API :
|
||||
|
||||
```bash
|
||||
./setup.sh --decompile
|
||||
```
|
||||
|
||||
Cela crée `src-ref/` avec les sources Java décompilées pour l'autocomplétion IDE et la documentation.
|
||||
|
||||
## Structure du Projet
|
||||
|
||||
Après la configuration, votre projet ressemblera à ceci :
|
||||
|
||||
```
|
||||
example-mod/
|
||||
├── app/
|
||||
│ ├── src/main/java/org/example/
|
||||
│ │ └── ExamplePlugin.java
|
||||
│ ├── src/main/resources/
|
||||
│ │ └── manifest.json
|
||||
│ └── build.gradle.kts
|
||||
├── server/
|
||||
│ ├── Server/
|
||||
│ │ └── HytaleServer.jar
|
||||
│ └── Assets.zip
|
||||
├── src-ref/ # Sources de référence décompilées
|
||||
├── .bin/ # Outils téléchargés
|
||||
├── setup.sh # Script d'automatisation
|
||||
├── build.gradle.kts
|
||||
├── settings.gradle.kts
|
||||
└── gradlew
|
||||
```
|
||||
|
||||
## Compilation du Plugin
|
||||
|
||||
### 1. Compiler avec Gradle
|
||||
|
||||
```bash
|
||||
# Sur Linux/macOS
|
||||
./gradlew build
|
||||
|
||||
# Sur Windows
|
||||
gradlew.bat build
|
||||
```
|
||||
|
||||
### 2. Localiser le JAR Compilé
|
||||
|
||||
Après une compilation réussie, votre JAR de plugin sera dans :
|
||||
|
||||
```
|
||||
app/build/libs/ExamplePlugin-1.0.0.jar
|
||||
```
|
||||
|
||||
## Exécution du Serveur
|
||||
|
||||
### 1. Configuration Initiale du Serveur
|
||||
|
||||
Démarrez le serveur pour la première fois :
|
||||
|
||||
```bash
|
||||
cd server
|
||||
java -jar Server/HytaleServer.jar --assets Assets.zip
|
||||
```
|
||||
|
||||
### 2. S'Authentifier
|
||||
|
||||
Une fois la console du serveur prête, exécutez la commande d'authentification :
|
||||
|
||||
```
|
||||
/auth login device
|
||||
```
|
||||
|
||||
Suivez les instructions pour vous authentifier avec votre compte Hytale.
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
**Authentification Requise :** Si aucun token serveur n'est configuré, vous verrez un avertissement au démarrage :
|
||||
```
|
||||
No server tokens configured. Use /auth login to authenticate.
|
||||
```
|
||||
Vous devez vous authentifier avant que les joueurs puissent rejoindre le serveur en mode non-singleplayer.
|
||||
{{< /callout >}}
|
||||
|
||||
### 3. Arrêter le Serveur
|
||||
|
||||
Après l'authentification, vous pouvez arrêter le serveur :
|
||||
|
||||
- **Linux/macOS:** Appuyez sur `Ctrl+C`
|
||||
- **Windows:** Appuyez sur `Ctrl+C` ou fermez le terminal
|
||||
|
||||
## Déploiement du Plugin
|
||||
|
||||
### 1. Copier le JAR
|
||||
|
||||
Copiez le JAR compilé dans le dossier `mods` :
|
||||
|
||||
```bash
|
||||
cp app/build/libs/ExamplePlugin-1.0.0.jar server/mods/
|
||||
```
|
||||
|
||||
### 2. Démarrer le Serveur
|
||||
|
||||
```bash
|
||||
cd server
|
||||
java -jar Server/HytaleServer.jar --assets Assets.zip
|
||||
```
|
||||
|
||||
### 3. Vérifier le Chargement
|
||||
|
||||
Vous devriez voir votre plugin se charger dans la console :
|
||||
|
||||
```
|
||||
[INFO] [PluginManager] Loading pending plugins from directory: mods
|
||||
[INFO] [PluginManager] - org.example:ExamplePlugin from path ExamplePlugin-1.0.0.jar
|
||||
```
|
||||
|
||||
## Mise à Jour du Serveur
|
||||
|
||||
Lorsqu'une nouvelle version d'Hytale est publiée :
|
||||
|
||||
```bash
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
Cela téléchargera la dernière version du serveur et mettra à jour votre installation.
|
||||
|
||||
## Problèmes Courants
|
||||
|
||||
### Version Java Incompatible
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Erreur :** `Unsupported class file major version 69`
|
||||
{{< /callout >}}
|
||||
|
||||
Assurez-vous d'utiliser Java 25 :
|
||||
|
||||
```bash
|
||||
java --version
|
||||
```
|
||||
|
||||
### Manifest Non Trouvé
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Erreur :** `Failed to load manifest file!`
|
||||
{{< /callout >}}
|
||||
|
||||
Assurez-vous que `manifest.json` est dans `src/main/resources/` et inclus dans le JAR.
|
||||
|
||||
### Script de Configuration Non Trouvé
|
||||
|
||||
{{< callout type="error" >}}
|
||||
**Erreur :** `./setup.sh: Permission denied`
|
||||
{{< /callout >}}
|
||||
|
||||
Rendez le script exécutable :
|
||||
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
## Configuration Manuelle (Alternative)
|
||||
|
||||
Si vous préférez une configuration manuelle sans le script :
|
||||
|
||||
### Localiser l'Installation Hytale
|
||||
|
||||
Trouvez votre répertoire d'installation Hytale :
|
||||
|
||||
{{< tabs items="Windows,Linux,macOS" >}}
|
||||
{{< tab >}}
|
||||
```
|
||||
%appdata%\Hytale\install\release\package\game\latest
|
||||
```
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
```
|
||||
$XDG_DATA_HOME/Hytale/install/release/package/game/latest
|
||||
```
|
||||
Ou généralement :
|
||||
```
|
||||
~/.local/share/Hytale/install/release/package/game/latest
|
||||
```
|
||||
{{< /tab >}}
|
||||
{{< tab >}}
|
||||
```
|
||||
~/Application Support/Hytale/install/release/package/game/latest
|
||||
```
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
### Configuration Manuelle du Serveur
|
||||
|
||||
1. Copiez `Server/` et `Assets.zip` depuis votre installation Hytale
|
||||
2. Téléchargez CFR depuis [https://www.benf.org/other/cfr/](https://www.benf.org/other/cfr/)
|
||||
3. Décompilez : `java -jar cfr.jar HytaleServer.jar --outputdir src-ref`
|
||||
|
||||
## Étapes Suivantes
|
||||
|
||||
Maintenant que vous pouvez compiler et exécuter votre plugin, explorez :
|
||||
|
||||
- [Cycle de Vie du Plugin](../plugin-lifecycle) - Comprendre setup, start et shutdown
|
||||
- [Commandes](../../commands) - Créer des commandes personnalisées
|
||||
- [Événements](../../events) - Écouter les événements du jeu
|
||||
196
content/getting-started/creating-a-plugin.en.md
Normal file
196
content/getting-started/creating-a-plugin.en.md
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
title: Creating a Plugin
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
This guide walks you through creating your first Hytale plugin from scratch.
|
||||
|
||||
## Project Structure
|
||||
|
||||
A typical Hytale plugin project has this structure:
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── app/
|
||||
│ ├── build.gradle.kts
|
||||
│ └── src/
|
||||
│ ├── main/
|
||||
│ │ ├── java/
|
||||
│ │ │ └── com/example/myplugin/
|
||||
│ │ │ └── MyPlugin.java
|
||||
│ │ └── resources/
|
||||
│ │ └── manifest.json
|
||||
│ └── test/
|
||||
│ └── java/
|
||||
├── gradle/
|
||||
│ ├── libs.versions.toml
|
||||
│ └── wrapper/
|
||||
├── build.gradle.kts
|
||||
├── settings.gradle.kts
|
||||
└── gradlew
|
||||
```
|
||||
|
||||
## The manifest.json
|
||||
|
||||
Every plugin requires a `manifest.json` file in `src/main/resources/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Group": "com.example",
|
||||
"Name": "MyPlugin",
|
||||
"Version": "1.0.0",
|
||||
"Description": "My first Hytale plugin",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Your Name"
|
||||
}
|
||||
],
|
||||
"Main": "com.example.myplugin.MyPlugin",
|
||||
"ServerVersion": "*",
|
||||
"Dependencies": {},
|
||||
"OptionalDependencies": {},
|
||||
"DisabledByDefault": false,
|
||||
"IncludesAssetPack": false,
|
||||
"SubPlugins": []
|
||||
}
|
||||
```
|
||||
|
||||
### Manifest Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `Group` | Yes | Package group (e.g., "com.example") |
|
||||
| `Name` | Yes | Plugin name (used for identification) |
|
||||
| `Version` | Yes | Semantic version (e.g., "1.0.0") |
|
||||
| `Description` | No | Brief description of the plugin |
|
||||
| `Authors` | No | List of authors with `Name` field |
|
||||
| `Main` | Yes | Fully qualified main class name |
|
||||
| `ServerVersion` | Yes | Server version compatibility ("*" for any) |
|
||||
| `Dependencies` | No | Required plugin dependencies |
|
||||
| `OptionalDependencies` | No | Optional plugin dependencies |
|
||||
| `DisabledByDefault` | No | Whether plugin is disabled by default |
|
||||
| `IncludesAssetPack` | No | Whether plugin includes assets |
|
||||
| `SubPlugins` | No | List of sub-plugins |
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
All manifest field names use **PascalCase** (e.g., `ServerVersion`, not `serverVersion`).
|
||||
{{< /callout >}}
|
||||
|
||||
## Main Plugin Class
|
||||
|
||||
Create your main plugin class extending `JavaPlugin`:
|
||||
|
||||
```java
|
||||
package com.example.myplugin;
|
||||
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MyPlugin extends JavaPlugin {
|
||||
|
||||
public MyPlugin(JavaPluginInit init) {
|
||||
super(init);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup() {
|
||||
// Called during plugin initialization
|
||||
// Register configs, prepare resources
|
||||
getLogger().at(Level.INFO).log("MyPlugin is setting up!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
// Called when the plugin starts
|
||||
// Register commands, events, entities
|
||||
getLogger().at(Level.INFO).log("MyPlugin has started!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Called when the plugin is stopping
|
||||
// Clean up resources
|
||||
getLogger().at(Level.INFO).log("MyPlugin is shutting down!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
The constructor with `JavaPluginInit` parameter is required. Always call `super(init)`.
|
||||
{{< /callout >}}
|
||||
|
||||
## Logger API
|
||||
|
||||
Hytale uses a Flogger-based logging API:
|
||||
|
||||
```java
|
||||
import java.util.logging.Level;
|
||||
|
||||
// Basic logging
|
||||
getLogger().at(Level.INFO).log("Information message");
|
||||
getLogger().at(Level.WARNING).log("Warning message");
|
||||
getLogger().at(Level.SEVERE).log("Error message");
|
||||
|
||||
// With formatting
|
||||
getLogger().at(Level.INFO).log("Player %s joined the game", playerName);
|
||||
```
|
||||
|
||||
## build.gradle.kts
|
||||
|
||||
Configure your Gradle build file (Kotlin DSL):
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
java
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "1.0.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Hytale Server API - compile only since it's provided at runtime
|
||||
compileOnly(files("../../../HytaleServer.jar"))
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
archiveBaseName.set("MyPlugin")
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
The path to `HytaleServer.jar` depends on where your plugin source is located relative to the game directory.
|
||||
{{< /callout >}}
|
||||
|
||||
## Available Registries
|
||||
|
||||
Your plugin has access to several registries through the base class:
|
||||
|
||||
| Method | Registry Type | Purpose |
|
||||
|--------|--------------|---------|
|
||||
| `getEventRegistry()` | EventRegistry | Register event listeners |
|
||||
| `getCommandRegistry()` | CommandRegistry | Register commands |
|
||||
| `getEntityRegistry()` | EntityRegistry | Register custom entities |
|
||||
| `getBlockStateRegistry()` | BlockStateRegistry | Register block states |
|
||||
| `getTaskRegistry()` | TaskRegistry | Schedule tasks |
|
||||
| `getAssetRegistry()` | AssetRegistry | Register assets |
|
||||
| `getEntityStoreRegistry()` | EntityStoreRegistry | Register entity components |
|
||||
| `getChunkStoreRegistry()` | ChunkStoreRegistry | Register chunk components |
|
||||
|
||||
## Next Steps
|
||||
|
||||
Learn about the [Plugin Lifecycle](../plugin-lifecycle) to understand when each method is called.
|
||||
196
content/getting-started/creating-a-plugin.fr.md
Normal file
196
content/getting-started/creating-a-plugin.fr.md
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
title: Créer un Plugin
|
||||
type: docs
|
||||
weight: 3
|
||||
---
|
||||
|
||||
Ce guide vous accompagne dans la création de votre premier plugin Hytale.
|
||||
|
||||
## Structure du Projet
|
||||
|
||||
Un projet de plugin Hytale typique a cette structure :
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── app/
|
||||
│ ├── build.gradle.kts
|
||||
│ └── src/
|
||||
│ ├── main/
|
||||
│ │ ├── java/
|
||||
│ │ │ └── com/example/myplugin/
|
||||
│ │ │ └── MyPlugin.java
|
||||
│ │ └── resources/
|
||||
│ │ └── manifest.json
|
||||
│ └── test/
|
||||
│ └── java/
|
||||
├── gradle/
|
||||
│ ├── libs.versions.toml
|
||||
│ └── wrapper/
|
||||
├── build.gradle.kts
|
||||
├── settings.gradle.kts
|
||||
└── gradlew
|
||||
```
|
||||
|
||||
## Le manifest.json
|
||||
|
||||
Chaque plugin nécessite un fichier `manifest.json` dans `src/main/resources/` :
|
||||
|
||||
```json
|
||||
{
|
||||
"Group": "com.example",
|
||||
"Name": "MyPlugin",
|
||||
"Version": "1.0.0",
|
||||
"Description": "Mon premier plugin Hytale",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Votre Nom"
|
||||
}
|
||||
],
|
||||
"Main": "com.example.myplugin.MyPlugin",
|
||||
"ServerVersion": "*",
|
||||
"Dependencies": {},
|
||||
"OptionalDependencies": {},
|
||||
"DisabledByDefault": false,
|
||||
"IncludesAssetPack": false,
|
||||
"SubPlugins": []
|
||||
}
|
||||
```
|
||||
|
||||
### Champs du Manifest
|
||||
|
||||
| Champ | Requis | Description |
|
||||
|-------|--------|-------------|
|
||||
| `Group` | Oui | Groupe de package (ex: "com.example") |
|
||||
| `Name` | Oui | Nom du plugin (utilisé pour l'identification) |
|
||||
| `Version` | Oui | Version sémantique (ex: "1.0.0") |
|
||||
| `Description` | Non | Brève description du plugin |
|
||||
| `Authors` | Non | Liste des auteurs avec champ `Name` |
|
||||
| `Main` | Oui | Nom complet de la classe principale |
|
||||
| `ServerVersion` | Oui | Compatibilité version serveur ("*" pour toutes) |
|
||||
| `Dependencies` | Non | Dépendances de plugins requises |
|
||||
| `OptionalDependencies` | Non | Dépendances de plugins optionnelles |
|
||||
| `DisabledByDefault` | Non | Si le plugin est désactivé par défaut |
|
||||
| `IncludesAssetPack` | Non | Si le plugin inclut des assets |
|
||||
| `SubPlugins` | Non | Liste des sous-plugins |
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
Tous les noms de champs du manifest utilisent le **PascalCase** (ex: `ServerVersion`, pas `serverVersion`).
|
||||
{{< /callout >}}
|
||||
|
||||
## Classe Principale du Plugin
|
||||
|
||||
Créez votre classe principale en étendant `JavaPlugin` :
|
||||
|
||||
```java
|
||||
package com.example.myplugin;
|
||||
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MyPlugin extends JavaPlugin {
|
||||
|
||||
public MyPlugin(JavaPluginInit init) {
|
||||
super(init);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup() {
|
||||
// Appelé pendant l'initialisation du plugin
|
||||
// Enregistrer les configs, préparer les ressources
|
||||
getLogger().at(Level.INFO).log("MyPlugin s'initialise !");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
// Appelé quand le plugin démarre
|
||||
// Enregistrer les commandes, événements, entités
|
||||
getLogger().at(Level.INFO).log("MyPlugin a démarré !");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Appelé quand le plugin s'arrête
|
||||
// Nettoyer les ressources
|
||||
getLogger().at(Level.INFO).log("MyPlugin s'arrête !");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Le constructeur avec le paramètre `JavaPluginInit` est obligatoire. Appelez toujours `super(init)`.
|
||||
{{< /callout >}}
|
||||
|
||||
## API de Logging
|
||||
|
||||
Hytale utilise une API de logging basée sur Flogger :
|
||||
|
||||
```java
|
||||
import java.util.logging.Level;
|
||||
|
||||
// Logging basique
|
||||
getLogger().at(Level.INFO).log("Message d'information");
|
||||
getLogger().at(Level.WARNING).log("Message d'avertissement");
|
||||
getLogger().at(Level.SEVERE).log("Message d'erreur");
|
||||
|
||||
// Avec formatage
|
||||
getLogger().at(Level.INFO).log("Le joueur %s a rejoint la partie", playerName);
|
||||
```
|
||||
|
||||
## build.gradle.kts
|
||||
|
||||
Configurez votre fichier de build Gradle (DSL Kotlin) :
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
java
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "1.0.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// API Serveur Hytale - compile only car fournie à l'exécution
|
||||
compileOnly(files("../../../HytaleServer.jar"))
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
archiveBaseName.set("MyPlugin")
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Le chemin vers `HytaleServer.jar` dépend de l'emplacement de votre source de plugin par rapport au répertoire du jeu.
|
||||
{{< /callout >}}
|
||||
|
||||
## Registres Disponibles
|
||||
|
||||
Votre plugin a accès à plusieurs registres via la classe de base :
|
||||
|
||||
| Méthode | Type de Registre | Utilité |
|
||||
|---------|-----------------|---------|
|
||||
| `getEventRegistry()` | EventRegistry | Enregistrer des écouteurs d'événements |
|
||||
| `getCommandRegistry()` | CommandRegistry | Enregistrer des commandes |
|
||||
| `getEntityRegistry()` | EntityRegistry | Enregistrer des entités personnalisées |
|
||||
| `getBlockStateRegistry()` | BlockStateRegistry | Enregistrer des états de blocs |
|
||||
| `getTaskRegistry()` | TaskRegistry | Planifier des tâches |
|
||||
| `getAssetRegistry()` | AssetRegistry | Enregistrer des assets |
|
||||
| `getEntityStoreRegistry()` | EntityStoreRegistry | Enregistrer des composants d'entité |
|
||||
| `getChunkStoreRegistry()` | ChunkStoreRegistry | Enregistrer des composants de chunk |
|
||||
|
||||
## Étapes Suivantes
|
||||
|
||||
Découvrez le [Cycle de Vie du Plugin](../plugin-lifecycle) pour comprendre quand chaque méthode est appelée.
|
||||
186
content/getting-started/plugin-lifecycle.en.md
Normal file
186
content/getting-started/plugin-lifecycle.en.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: Plugin Lifecycle
|
||||
type: docs
|
||||
weight: 4
|
||||
---
|
||||
|
||||
Understanding the plugin lifecycle is essential for properly initializing and cleaning up your plugin resources.
|
||||
|
||||
## Lifecycle States
|
||||
|
||||
A plugin goes through several states during its lifetime:
|
||||
|
||||
```
|
||||
NONE → SETUP → START → ENABLED → SHUTDOWN → DISABLED
|
||||
```
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| `NONE` | Initial state before any initialization |
|
||||
| `SETUP` | Plugin is in the setup phase |
|
||||
| `START` | Plugin is starting |
|
||||
| `ENABLED` | Plugin is fully operational |
|
||||
| `SHUTDOWN` | Plugin is shutting down |
|
||||
| `DISABLED` | Plugin has been disabled |
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
### Constructor
|
||||
|
||||
The constructor is called when the plugin is instantiated. Use this for:
|
||||
|
||||
- Registering configuration files with `withConfig()` (recommended)
|
||||
- Storing the init reference if needed
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.util.Config;
|
||||
|
||||
private Config<MyConfig> config;
|
||||
|
||||
public MyPlugin(JavaPluginInit init) {
|
||||
super(init);
|
||||
// Register config in constructor - loaded asynchronously during preLoad()
|
||||
config = withConfig(MyConfig.CODEC);
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Registering configs in the constructor ensures they are loaded asynchronously during `preLoad()`, before `setup()` is called. This is the recommended approach.
|
||||
{{< /callout >}}
|
||||
|
||||
### setup()
|
||||
|
||||
Called during the initial setup phase, after configs are loaded. Use this for:
|
||||
|
||||
- Preparing resources that don't depend on other plugins
|
||||
- Early initialization logic
|
||||
- Accessing loaded configuration values
|
||||
|
||||
```java
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Override
|
||||
public void setup() {
|
||||
// Config is already loaded and available
|
||||
getLogger().at(Level.INFO).log("Setup complete!");
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
To create configuration classes with `CODEC`, see the [Codecs documentation]({{< ref "core-concepts/codecs#buildercodec" >}}).
|
||||
{{< /callout >}}
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
Do not register commands or events in `setup()`. Other plugins may not be loaded yet.
|
||||
{{< /callout >}}
|
||||
|
||||
### start()
|
||||
|
||||
Called after all plugins have completed setup. Use this for:
|
||||
|
||||
- Registering commands
|
||||
- Registering event listeners
|
||||
- Registering entities
|
||||
- Interacting with other plugins
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
// Register commands
|
||||
getCommandRegistry().registerCommand(new MyCommand());
|
||||
|
||||
// Register events
|
||||
getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect);
|
||||
|
||||
getLogger().at(Level.INFO).log("Plugin started!");
|
||||
}
|
||||
|
||||
private void onPlayerConnect(PlayerConnectEvent event) {
|
||||
// See PlayerRef documentation for thread-safe player references
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
getLogger().at(Level.INFO).log("Player connecting: " + playerRef.getUsername());
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
See the [Registries documentation]({{< ref "core-concepts/registries" >}}) for a complete list of available registries and their usage.
|
||||
{{< /callout >}}
|
||||
|
||||
### shutdown()
|
||||
|
||||
Called when the plugin is being disabled or the server is stopping. Use this for:
|
||||
|
||||
- Saving data
|
||||
- Cleaning up resources
|
||||
- Canceling scheduled tasks
|
||||
|
||||
```java
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Save any pending data
|
||||
savePlayerData();
|
||||
|
||||
getLogger().at(Level.INFO).log("Plugin shutdown complete!");
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Registries are automatically cleaned up after `shutdown()`. You don't need to manually unregister commands or events.
|
||||
{{< /callout >}}
|
||||
|
||||
## Lifecycle Flow Diagram
|
||||
|
||||
{{< steps >}}
|
||||
|
||||
### Plugin Loading
|
||||
The server discovers your plugin JAR and reads `manifest.json`
|
||||
|
||||
### Constructor Called
|
||||
`MyPlugin(JavaPluginInit init)` is instantiated
|
||||
|
||||
### preLoad()
|
||||
Configuration files registered with `withConfig()` are loaded asynchronously
|
||||
|
||||
### setup()
|
||||
Your `setup()` method is called
|
||||
|
||||
### start()
|
||||
Your `start()` method is called after all plugins are set up
|
||||
|
||||
### Running
|
||||
Plugin is now in `ENABLED` state and fully operational
|
||||
|
||||
### shutdown()
|
||||
Called when server stops or plugin is disabled
|
||||
|
||||
### Cleanup
|
||||
All registries are automatically cleaned up
|
||||
|
||||
{{< /steps >}}
|
||||
|
||||
## Checking Plugin State
|
||||
|
||||
You can check if your plugin is enabled:
|
||||
|
||||
```java
|
||||
if (isEnabled()) {
|
||||
// Plugin is running
|
||||
}
|
||||
|
||||
if (isDisabled()) {
|
||||
// Plugin is not running
|
||||
}
|
||||
|
||||
// Get current state
|
||||
PluginState state = getState();
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Learn how to [Build and Run](../building-and-running) your plugin.
|
||||
186
content/getting-started/plugin-lifecycle.fr.md
Normal file
186
content/getting-started/plugin-lifecycle.fr.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: Cycle de Vie du Plugin
|
||||
type: docs
|
||||
weight: 4
|
||||
---
|
||||
|
||||
Comprendre le cycle de vie du plugin est essentiel pour initialiser et nettoyer correctement les ressources de votre plugin.
|
||||
|
||||
## États du Cycle de Vie
|
||||
|
||||
Un plugin passe par plusieurs états durant sa vie :
|
||||
|
||||
```
|
||||
NONE → SETUP → START → ENABLED → SHUTDOWN → DISABLED
|
||||
```
|
||||
|
||||
| État | Description |
|
||||
|------|-------------|
|
||||
| `NONE` | État initial avant toute initialisation |
|
||||
| `SETUP` | Le plugin est en phase de configuration |
|
||||
| `START` | Le plugin démarre |
|
||||
| `ENABLED` | Le plugin est pleinement opérationnel |
|
||||
| `SHUTDOWN` | Le plugin s'arrête |
|
||||
| `DISABLED` | Le plugin a été désactivé |
|
||||
|
||||
## Méthodes du Cycle de Vie
|
||||
|
||||
### Constructeur
|
||||
|
||||
Le constructeur est appelé lorsque le plugin est instancié. Utilisez ceci pour :
|
||||
|
||||
- Enregistrer les fichiers de configuration avec `withConfig()` (recommandé)
|
||||
- Stocker la référence init si nécessaire
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.util.Config;
|
||||
|
||||
private Config<MyConfig> config;
|
||||
|
||||
public MyPlugin(JavaPluginInit init) {
|
||||
super(init);
|
||||
// Enregistrer la config dans le constructeur - chargée de façon asynchrone pendant preLoad()
|
||||
config = withConfig(MyConfig.CODEC);
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Enregistrer les configs dans le constructeur garantit qu'elles sont chargées de façon asynchrone pendant `preLoad()`, avant que `setup()` ne soit appelé. C'est l'approche recommandée.
|
||||
{{< /callout >}}
|
||||
|
||||
### setup()
|
||||
|
||||
Appelé pendant la phase initiale de configuration, après le chargement des configs. Utilisez ceci pour :
|
||||
|
||||
- Préparer les ressources qui ne dépendent pas d'autres plugins
|
||||
- Logique d'initialisation précoce
|
||||
- Accéder aux valeurs de configuration chargées
|
||||
|
||||
```java
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Override
|
||||
public void setup() {
|
||||
// La config est déjà chargée et disponible
|
||||
getLogger().at(Level.INFO).log("Configuration terminée !");
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Pour créer des classes de configuration avec `CODEC`, voir la [documentation des Codecs]({{< ref "core-concepts/codecs#buildercodec" >}}).
|
||||
{{< /callout >}}
|
||||
|
||||
{{< callout type="warning" >}}
|
||||
N'enregistrez pas de commandes ou d'événements dans `setup()`. D'autres plugins peuvent ne pas être encore chargés.
|
||||
{{< /callout >}}
|
||||
|
||||
### start()
|
||||
|
||||
Appelé après que tous les plugins aient terminé leur configuration. Utilisez ceci pour :
|
||||
|
||||
- Enregistrer les commandes
|
||||
- Enregistrer les écouteurs d'événements
|
||||
- Enregistrer les entités
|
||||
- Interagir avec d'autres plugins
|
||||
|
||||
```java
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
// Enregistrer les commandes
|
||||
getCommandRegistry().registerCommand(new MyCommand());
|
||||
|
||||
// Enregistrer les événements
|
||||
getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect);
|
||||
|
||||
getLogger().at(Level.INFO).log("Plugin démarré !");
|
||||
}
|
||||
|
||||
private void onPlayerConnect(PlayerConnectEvent event) {
|
||||
// Voir la documentation PlayerRef pour les références thread-safe aux joueurs
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
getLogger().at(Level.INFO).log("Joueur en connexion : " + playerRef.getUsername());
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Voir la [documentation des Registres]({{< ref "core-concepts/registries" >}}) pour une liste complète des registres disponibles et leur utilisation.
|
||||
{{< /callout >}}
|
||||
|
||||
### shutdown()
|
||||
|
||||
Appelé quand le plugin est désactivé ou le serveur s'arrête. Utilisez ceci pour :
|
||||
|
||||
- Sauvegarder les données
|
||||
- Nettoyer les ressources
|
||||
- Annuler les tâches planifiées
|
||||
|
||||
```java
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Sauvegarder les données en attente
|
||||
savePlayerData();
|
||||
|
||||
getLogger().at(Level.INFO).log("Arrêt du plugin terminé !");
|
||||
}
|
||||
```
|
||||
|
||||
{{< callout type="info" >}}
|
||||
Les registres sont automatiquement nettoyés après `shutdown()`. Vous n'avez pas besoin de désinscrire manuellement les commandes ou événements.
|
||||
{{< /callout >}}
|
||||
|
||||
## Diagramme du Flux de Vie
|
||||
|
||||
{{< steps >}}
|
||||
|
||||
### Chargement du Plugin
|
||||
Le serveur découvre votre JAR de plugin et lit `manifest.json`
|
||||
|
||||
### Appel du Constructeur
|
||||
`MyPlugin(JavaPluginInit init)` est instancié
|
||||
|
||||
### preLoad()
|
||||
Les fichiers de configuration enregistrés avec `withConfig()` sont chargés de façon asynchrone
|
||||
|
||||
### setup()
|
||||
Votre méthode `setup()` est appelée
|
||||
|
||||
### start()
|
||||
Votre méthode `start()` est appelée après que tous les plugins soient configurés
|
||||
|
||||
### En Cours d'Exécution
|
||||
Le plugin est maintenant en état `ENABLED` et pleinement opérationnel
|
||||
|
||||
### shutdown()
|
||||
Appelé quand le serveur s'arrête ou le plugin est désactivé
|
||||
|
||||
### Nettoyage
|
||||
Tous les registres sont automatiquement nettoyés
|
||||
|
||||
{{< /steps >}}
|
||||
|
||||
## Vérifier l'État du Plugin
|
||||
|
||||
Vous pouvez vérifier si votre plugin est activé :
|
||||
|
||||
```java
|
||||
if (isEnabled()) {
|
||||
// Le plugin fonctionne
|
||||
}
|
||||
|
||||
if (isDisabled()) {
|
||||
// Le plugin ne fonctionne pas
|
||||
}
|
||||
|
||||
// Obtenir l'état actuel
|
||||
PluginState state = getState();
|
||||
```
|
||||
|
||||
## Étapes Suivantes
|
||||
|
||||
Apprenez à [Compiler et Exécuter](../building-and-running) votre plugin.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user