commit b16a40e431f369426fd3bc9e8d797b9d618311cf Author: RS_COME_BACK Date: Tue Jan 20 20:33:59 2026 +0100 Init diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml new file mode 100644 index 0000000..a888734 --- /dev/null +++ b/.github/workflows/hugo.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..792daf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +public/ +.hugo_build.lock +resources/_gen/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bde0d9e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "themes/hextra"] + path = themes/hextra + url = git@github.com:imfing/hextra.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bb2ac1c --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000..6b6a0d6 --- /dev/null +++ b/README.fr.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..86610d6 --- /dev/null +++ b/README.md @@ -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. diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/content/_index.en.md b/content/_index.en.md new file mode 100644 index 0000000..8accf84 --- /dev/null +++ b/content/_index.en.md @@ -0,0 +1,100 @@ +--- +title: Hytale Plugin Documentation +layout: hextra-home +--- + +{{< hextra/hero-badge >}} + Hytale Server API +{{< /hextra/hero-badge >}} + +
+{{< hextra/hero-headline >}} + Build Plugins for Hytale +{{< /hextra/hero-headline >}} +
+ +
+{{< hextra/hero-subtitle >}} + Comprehensive documentation for creating Hytale server plugins. 
From your first plugin to advanced features. +{{< /hextra/hero-subtitle >}} +
+ +
 
+ +
+{{< hextra/hero-button text="Join Discord" link="https://discord.gg/4UPCz84Nst" icon="discord" >}} +
+ +
 
+ +## Explore the Documentation + +
 
+ +{{< 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 >}} + +
 
+ +## 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!"); + } +} +``` diff --git a/content/_index.fr.md b/content/_index.fr.md new file mode 100644 index 0000000..acaff6b --- /dev/null +++ b/content/_index.fr.md @@ -0,0 +1,100 @@ +--- +title: Documentation des Plugins Hytale +layout: hextra-home +--- + +{{< hextra/hero-badge >}} + API Serveur Hytale +{{< /hextra/hero-badge >}} + +
+{{< hextra/hero-headline >}} + Créez des Plugins pour Hytale +{{< /hextra/hero-headline >}} +
+ +
+{{< hextra/hero-subtitle >}} + Documentation complète pour la création de plugins serveur Hytale. 
De votre premier plugin aux fonctionnalités avancées. +{{< /hextra/hero-subtitle >}} +
+ +
 
+ +
+{{< hextra/hero-button text="Rejoindre Discord" link="https://discord.gg/4UPCz84Nst" icon="discord" >}} +
+ +
 
+ +## Explorer la Documentation + +
 
+ +{{< 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 >}} + +
 
+ +## 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 !"); + } +} +``` diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..87aeb84 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,21 @@ +--- +title: "Redirecting..." +layout: "redirect" +--- + + + + + +

Redirecting... English | Français

diff --git a/content/advanced/_index.en.md b/content/advanced/_index.en.md new file mode 100644 index 0000000..5518bd0 --- /dev/null +++ b/content/advanced/_index.en.md @@ -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)); +``` diff --git a/content/advanced/_index.fr.md b/content/advanced/_index.fr.md new file mode 100644 index 0000000..d126300 --- /dev/null +++ b/content/advanced/_index.fr.md @@ -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)); +``` diff --git a/content/advanced/effects/_index.en.md b/content/advanced/effects/_index.en.md new file mode 100644 index 0000000..82662aa --- /dev/null +++ b/content/advanced/effects/_index.en.md @@ -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 >}} diff --git a/content/advanced/effects/_index.fr.md b/content/advanced/effects/_index.fr.md new file mode 100644 index 0000000..64658bd --- /dev/null +++ b/content/advanced/effects/_index.fr.md @@ -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 >}} diff --git a/content/advanced/effects/dynamic-lights.en.md b/content/advanced/effects/dynamic-lights.en.md new file mode 100644 index 0000000..6020196 --- /dev/null +++ b/content/advanced/effects/dynamic-lights.en.md @@ -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 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 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, Integer> pulsingEntities = new ConcurrentHashMap<>(); + +// Call this method each tick +public void onTick(float deltaTime) { + for (Map.Entry, Integer> entry : pulsingEntities.entrySet()) { + Ref 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 entityRef) { + pulsingEntities.put(entityRef, 0); +} + +public void stopPulsingLight(Ref 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 >}} diff --git a/content/advanced/effects/dynamic-lights.fr.md b/content/advanced/effects/dynamic-lights.fr.md new file mode 100644 index 0000000..de986bf --- /dev/null +++ b/content/advanced/effects/dynamic-lights.fr.md @@ -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 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 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, Integer> pulsingEntities = new ConcurrentHashMap<>(); + +// Appeler cette méthode à chaque tick +public void onTick(float deltaTime) { + for (Map.Entry, Integer> entry : pulsingEntities.entrySet()) { + Ref 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 entityRef) { + pulsingEntities.put(entityRef, 0); +} + +public void stopPulsingLight(Ref 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 >}} diff --git a/content/advanced/effects/entity-effects.en.md b/content/advanced/effects/entity-effects.en.md new file mode 100644 index 0000000..144795c --- /dev/null +++ b/content/advanced/effects/entity-effects.en.md @@ -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 playerArg; + private final RequiredArg effectArg; + private final RequiredArg durationArg; + + // Would be set during plugin initialization + private Store store; + private ComponentAccessor 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 execute(CommandContext ctx) { + PlayerRef target = playerArg.get(ctx); + String effectId = effectArg.get(ctx); + int duration = durationArg.get(ctx); + + Ref 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 store, + ComponentAccessor componentAccessor) { + EntityEffect poison = EntityEffect.getAssetMap().getAsset("poison"); + + Ref 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 store, + ComponentAccessor componentAccessor) { + EntityEffect invuln = EntityEffect.getAssetMap().getAsset("invulnerability"); + + Ref 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 store, + ComponentAccessor componentAccessor) { + Ref 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 store, + ComponentAccessor componentAccessor) { + Ref 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 >}} diff --git a/content/advanced/effects/entity-effects.fr.md b/content/advanced/effects/entity-effects.fr.md new file mode 100644 index 0000000..ba6cac3 --- /dev/null +++ b/content/advanced/effects/entity-effects.fr.md @@ -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 playerArg; + private final RequiredArg effectArg; + private final RequiredArg durationArg; + + // Serait défini lors de l'initialisation du plugin + private Store store; + private ComponentAccessor 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 execute(CommandContext ctx) { + PlayerRef target = playerArg.get(ctx); + String effectId = effectArg.get(ctx); + int duration = durationArg.get(ctx); + + Ref 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 store, + ComponentAccessor componentAccessor) { + EntityEffect poison = EntityEffect.getAssetMap().getAsset("poison"); + + Ref 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 store, + ComponentAccessor componentAccessor) { + EntityEffect invuln = EntityEffect.getAssetMap().getAsset("invulnerability"); + + Ref 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 store, + ComponentAccessor componentAccessor) { + Ref 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 store, + ComponentAccessor componentAccessor) { + Ref 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 >}} diff --git a/content/advanced/effects/particles.en.md b/content/advanced/effects/particles.en.md new file mode 100644 index 0000000..6877d37 --- /dev/null +++ b/content/advanced/effects/particles.en.md @@ -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> + 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 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 store, + ComponentAccessor componentAccessor) { + if (hasTrailEffect(player)) { + // Get position from TransformComponent + Ref 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 activeAuras = new HashMap<>(); + private Store store; + private ComponentAccessor 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 entry : activeAuras.entrySet()) { + if (entry.getValue()) { + Player player = getPlayer(entry.getKey()); + if (player != null) { + // Get position from TransformComponent + Ref 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 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 diff --git a/content/advanced/effects/particles.fr.md b/content/advanced/effects/particles.fr.md new file mode 100644 index 0000000..3322b68 --- /dev/null +++ b/content/advanced/effects/particles.fr.md @@ -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> + 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 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 store, + ComponentAccessor componentAccessor) { + if (hasTrailEffect(player)) { + // Obtenir la position via TransformComponent + Ref 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 activeAuras = new HashMap<>(); + private Store store; + private ComponentAccessor 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 entry : activeAuras.entrySet()) { + if (entry.getValue()) { + Player player = getPlayer(entry.getKey()); + if (player != null) { + // Obtenir la position via TransformComponent + Ref 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 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 diff --git a/content/advanced/networking/_index.en.md b/content/advanced/networking/_index.en.md new file mode 100644 index 0000000..b503997 --- /dev/null +++ b/content/advanced/networking/_index.en.md @@ -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 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 diff --git a/content/advanced/networking/_index.fr.md b/content/advanced/networking/_index.fr.md new file mode 100644 index 0000000..a8c099e --- /dev/null +++ b/content/advanced/networking/_index.fr.md @@ -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 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 diff --git a/content/advanced/networking/packets.en.md b/content/advanced/networking/packets.en.md new file mode 100644 index 0000000..34f2f2a --- /dev/null +++ b/content/advanced/networking/packets.en.md @@ -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 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. +}); +``` diff --git a/content/advanced/networking/packets.fr.md b/content/advanced/networking/packets.fr.md new file mode 100644 index 0000000..939d772 --- /dev/null +++ b/content/advanced/networking/packets.fr.md @@ -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 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. +}); +``` diff --git a/content/advanced/networking/sync.en.md b/content/advanced/networking/sync.en.md new file mode 100644 index 0000000..378a690 --- /dev/null +++ b/content/advanced/networking/sync.en.md @@ -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 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 + } +} +``` diff --git a/content/advanced/networking/sync.fr.md b/content/advanced/networking/sync.fr.md new file mode 100644 index 0000000..378a690 --- /dev/null +++ b/content/advanced/networking/sync.fr.md @@ -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 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 + } +} +``` diff --git a/content/core-concepts/_index.en.md b/content/core-concepts/_index.en.md new file mode 100644 index 0000000..9dfbd0b --- /dev/null +++ b/content/core-concepts/_index.en.md @@ -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 >}} diff --git a/content/core-concepts/_index.fr.md b/content/core-concepts/_index.fr.md new file mode 100644 index 0000000..77d6242 --- /dev/null +++ b/content/core-concepts/_index.fr.md @@ -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 >}} diff --git a/content/core-concepts/assets.en.md b/content/core-concepts/assets.en.md new file mode 100644 index 0000000..f5c015f --- /dev/null +++ b/content/core-concepts/assets.en.md @@ -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 >}} diff --git a/content/core-concepts/assets.fr.md b/content/core-concepts/assets.fr.md new file mode 100644 index 0000000..853d8f0 --- /dev/null +++ b/content/core-concepts/assets.fr.md @@ -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 >}} diff --git a/content/core-concepts/codecs.en.md b/content/core-concepts/codecs.en.md new file mode 100644 index 0000000..1dfe22b --- /dev/null +++ b/content/core-concepts/codecs.en.md @@ -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 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 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 mainConfig = withConfig("main", MyConfig.CODEC); +Config 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 stringArray = new ArrayCodec<>(Codec.STRING, String[]::new); + +// Map with string keys +Codec> 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 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 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 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 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 >}} diff --git a/content/core-concepts/codecs.fr.md b/content/core-concepts/codecs.fr.md new file mode 100644 index 0000000..2551d75 --- /dev/null +++ b/content/core-concepts/codecs.fr.md @@ -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 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 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 mainConfig = withConfig("main", MyConfig.CODEC); +Config 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 stringArray = new ArrayCodec<>(Codec.STRING, String[]::new); + +// Map avec clés string +Codec> 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 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 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 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 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 >}} diff --git a/content/core-concepts/commands/_index.en.md b/content/core-concepts/commands/_index.en.md new file mode 100644 index 0000000..5788ae4 --- /dev/null +++ b/content/core-concepts/commands/_index.en.md @@ -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 >}} diff --git a/content/core-concepts/commands/_index.fr.md b/content/core-concepts/commands/_index.fr.md new file mode 100644 index 0000000..d40b6bc --- /dev/null +++ b/content/core-concepts/commands/_index.fr.md @@ -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 >}} diff --git a/content/core-concepts/commands/argument-types.en.md b/content/core-concepts/commands/argument-types.en.md new file mode 100644 index 0000000..20fd3ac --- /dev/null +++ b/content/core-concepts/commands/argument-types.en.md @@ -0,0 +1,1065 @@ +--- +title: Argument Types +type: docs +weight: 2 +--- + +Hytale provides a comprehensive set of argument types through the `ArgTypes` class. These types handle parsing, validation, and tab completion automatically, ensuring a consistent and user-friendly command experience. + +**Package:** `com.hypixel.hytale.server.core.command.system.arguments.types` + +{{< cards cols="3" >}} + {{< card link="#primitive-types" title="Primitives" subtitle="Boolean, Integer, Float, Double, String" icon="hashtag" >}} + {{< card link="#player-and-entity-types" title="Player & Entity" subtitle="PlayerRef, UUID, EntityId" icon="user" >}} + {{< card link="#asset-types" title="Assets" subtitle="Items, Blocks, Models, Effects" icon="cube" >}} + {{< card link="#position-types" title="Positions" subtitle="Absolute and relative coordinates" icon="map" >}} + {{< card link="#vector-types" title="Vectors" subtitle="2D/3D vectors and rotations" icon="arrows-expand" >}} + {{< card link="#special-types" title="Special" subtitle="Colors, Patterns, Ranges" icon="sparkles" >}} +{{< /cards >}} + +--- + +## Primitive Types + +Basic data types for simple values. These are the foundation of most command arguments. + +{{< tabs items="Overview,Examples,Methods" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.BOOLEAN` | `Boolean` | True or false value | `true`, `false` | +| `ArgTypes.INTEGER` | `Integer` | 32-bit whole numbers | `-1`, `0`, `56346`, `2147483647` | +| `ArgTypes.FLOAT` | `Float` | Single precision decimals | `3.14159`, `-2.5`, `7` | +| `ArgTypes.DOUBLE` | `Double` | Double precision decimals | `-3.14`, `0.0`, `3.141596` | +| `ArgTypes.STRING` | `String` | Single word text | `hello`, `world`, `test123` | + +{{< /tab >}} +{{< tab >}} + +```java +public class MathCommand extends AbstractCommand { + private final RequiredArg countArg; + private final RequiredArg multiplierArg; + private final RequiredArg verboseArg; + private final OptionalArg labelArg; + + public MathCommand() { + super("math", "Perform mathematical operations"); + countArg = withRequiredArg("count", "Number of iterations", ArgTypes.INTEGER); + multiplierArg = withRequiredArg("multiplier", "Multiplication factor", ArgTypes.DOUBLE); + verboseArg = withRequiredArg("verbose", "Show detailed output", ArgTypes.BOOLEAN); + labelArg = withOptionalArg("label", "Optional label", ArgTypes.STRING); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int count = context.get(countArg); + double mult = context.get(multiplierArg); + boolean verbose = context.get(verboseArg); + String label = context.get(labelArg); // null if not provided + + double result = count * mult; + + if (verbose) { + String output = (label != null ? label + ": " : "") + + count + " × " + mult + " = " + result; + context.sender().sendMessage(Message.raw(output)); + } + + return null; + } +} +``` + +**Usage:** `/math 5 2.5 true --label result` → `result: 5 × 2.5 = 12.5` + +{{< /tab >}} +{{< tab >}} + +**Common methods for primitive arguments:** + +| Method | Description | +|--------|-------------| +| `withRequiredArg(name, desc, type)` | Creates a required argument | +| `withOptionalArg(name, desc, type)` | Creates an optional argument | +| `context.get(arg)` | Gets the parsed value | +| `context.provided(arg)` | Checks if optional arg was provided | + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Player and Entity Types + +Types for referencing players and entities in the game world. + +{{< tabs items="Overview,PlayerRef,Entity,UUID" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.PLAYER_REF` | `PlayerRef` | Online player reference | `john_doe`, `user123` | +| `ArgTypes.PLAYER_UUID` | `UUID` | Player UUID (by name or UUID) | `john_doe`, `550e8400-e29b-...` | +| `ArgTypes.UUID` | `UUID` | Any valid UUID | `550e8400-e29b-41d4-a716-...` | +| `ArgTypes.ENTITY_ID` | `UUID` (wrapped) | Entity by UUID | Entity UUID | + +{{< callout type="warning" >}} +**PLAYER_REF** only matches currently online players and returns a `PlayerRef`. **PLAYER_UUID** can match by name or UUID and returns a `UUID` directly. +{{< /callout >}} + +{{< /tab >}} +{{< tab >}} + +{{< callout type="info" >}} +`PlayerRef` provides player identity (`getUsername()`, `getUuid()`) and ECS access via `getReference()`. To check if a player is online, verify `getReference()` returns a valid reference. +{{< /callout >}} + +```java +public class TeleportCommand extends AbstractCommand { + private final RequiredArg targetArg; + private final OptionalArg destinationArg; + + public TeleportCommand() { + super("tp", "Teleport players"); + targetArg = withRequiredArg("target", "Player to teleport", ArgTypes.PLAYER_REF); + destinationArg = withOptionalArg("destination", "Destination player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef targetRef = context.get(targetArg); + + // Check if player is online via ECS reference + Ref targetEntity = targetRef.getReference(); + if (targetEntity == null || !targetEntity.isValid()) { + context.sendMessage(Message.raw("Player is no longer online!")); + return null; + } + + if (context.provided(destinationArg)) { + PlayerRef destRef = context.get(destinationArg); + Ref destEntity = destRef.getReference(); + if (destEntity != null && destEntity.isValid()) { + // Teleport target to destination player using ECS + // Access TransformComponent from both entities for teleportation + } + } + + return null; + } +} +``` + +**Usage:** `/tp john_doe --destination steve` → Teleports john_doe to steve + +{{< /tab >}} +{{< tab >}} + +```java +public class EntityInfoCommand extends AbstractCommand { + private final RequiredArg entityArg; + + public EntityInfoCommand() { + super("entityinfo", "Get entity information"); + entityArg = withRequiredArg("entity", "Entity UUID", ArgTypes.UUID); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + UUID entityId = context.get(entityArg); + + // Find entity in worlds + for (World world : Universe.get().getWorlds().values()) { + // Search for entity by UUID + // Entity lookup depends on world API + } + + context.sender().sendMessage(Message.raw("Entity lookup complete")); + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +```java +public class UuidCommand extends AbstractCommand { + private final RequiredArg uuidArg; + + public UuidCommand() { + super("uuid", "Parse and display UUID"); + uuidArg = withRequiredArg("uuid", "UUID to parse", ArgTypes.UUID); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + UUID uuid = context.get(uuidArg); + context.sender().sendMessage(Message.raw( + "UUID: " + uuid.toString() + "\nVersion: " + uuid.version() + )); + return null; + } +} +``` + +**Valid UUID formats:** +- Full: `550e8400-e29b-41d4-a716-446655440000` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## World Types + +Types for referencing game worlds. + +{{< tabs items="Overview,Example" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.WORLD` | `World` | World by name | `default`, `creative`, `pvp_arena` | + +{{< callout type="info" >}} +World names are case-insensitive and support tab completion for all loaded worlds. +{{< /callout >}} + +{{< /tab >}} +{{< tab >}} + +```java +public class WorldTeleportCommand extends AbstractCommand { + private final RequiredArg worldArg; + private final OptionalArg positionArg; + + public WorldTeleportCommand() { + super("wtp", "Teleport to another world"); + worldArg = withRequiredArg("world", "Destination world", ArgTypes.WORLD); + positionArg = withOptionalArg("position", "Spawn position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + context.sender().sendMessage(Message.raw("Only players can use this command!")); + return null; + } + + World targetWorld = context.get(worldArg); + + Vector3d spawnPos; + if (context.provided(positionArg)) { + spawnPos = context.get(positionArg).resolve(player.getPosition()); + } else { + spawnPos = targetWorld.getSpawnPosition(); + } + + player.teleport(targetWorld, spawnPos); + player.sendMessage(Message.raw("Teleported to " + targetWorld.getName())); + + return null; + } +} +``` + +**Usage:** `/wtp creative` or `/wtp creative 100 64 200` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Asset Types + +Types for referencing game assets loaded from data packs. + +{{< tabs items="Overview,Items,Blocks,Effects,Other" >}} +{{< tab >}} + +| Type | Java Type | Description | +|------|-----------|-------------| +| `ArgTypes.ITEM_ASSET` | `Item` | Item definition | +| `ArgTypes.BLOCK_TYPE_ASSET` | `BlockType` | Block type definition | +| `ArgTypes.MODEL_ASSET` | `Model` | 3D model asset | +| `ArgTypes.WEATHER_ASSET` | `Weather` | Weather configuration | +| `ArgTypes.EFFECT_ASSET` | `EntityEffect` | Entity effect (buffs/debuffs) | +| `ArgTypes.ENVIRONMENT_ASSET` | `Environment` | Environment settings | +| `ArgTypes.PARTICLE_SYSTEM` | `ParticleSystem` | Particle system definition | +| `ArgTypes.SOUND_EVENT_ASSET` | `SoundEvent` | Sound event | +| `ArgTypes.AMBIENCE_FX_ASSET` | `AmbienceFX` | Ambient sound/effect | +| `ArgTypes.INTERACTION_ASSET` | `Interaction` | Interaction definition | + +{{< callout type="info" >}} +Asset identifiers use the format `namespace:path` (e.g., `hytale:sword_iron`). Tab completion lists all available assets. +{{< /callout >}} + +{{< /tab >}} +{{< tab >}} + +```java +public class GiveCommand extends AbstractCommand { + private final RequiredArg itemArg; + private final OptionalArg amountArg; + private final OptionalArg targetArg; + + public GiveCommand() { + super("give", "Give items to players"); + itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET); + amountArg = withOptionalArg("amount", "Stack size", ArgTypes.INTEGER); + targetArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Item item = context.get(itemArg); + int amount = context.provided(amountArg) ? context.get(amountArg) : 1; + + // Get target player - use PlayerRef for identity, sender for direct access + String targetName; + if (context.provided(targetArg)) { + PlayerRef ref = context.get(targetArg); + targetName = ref.getUsername(); + // Use ref.getReference() for ECS operations + } else if (context.sender() instanceof Player p) { + targetName = p.getDisplayName(); + // p.getInventory() for inventory operations + } else { + context.sendMessage(Message.raw("Specify a player!")); + return null; + } + + // Create ItemStack and add to inventory + ItemStack stack = new ItemStack(item.getId(), amount); + // Add to player inventory via ECS... + + context.sendMessage(Message.raw( + "Gave " + amount + "x " + item.getId() + " to " + targetName + )); + + return null; + } +} +``` + +**Usage:** `/give hytale:sword_iron 1 john_doe` + +{{< /tab >}} +{{< tab >}} + +```java +public class SetBlockCommand extends AbstractCommand { + private final RequiredArg blockArg; + private final RequiredArg positionArg; + + public SetBlockCommand() { + super("setblock", "Set a block at position"); + blockArg = withRequiredArg("block", "Block type", ArgTypes.BLOCK_TYPE_ASSET); + positionArg = withRequiredArg("position", "Target position", ArgTypes.RELATIVE_BLOCK_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + BlockType blockType = context.get(blockArg); + Vector3i pos = context.get(positionArg).resolve(player.getBlockPosition()); + + player.getWorld().setBlock(pos, blockType); + + context.sender().sendMessage( + Message.raw("Set block to " + blockType.getIdentifier() + " at " + pos) + ); + + return null; + } +} +``` + +**Usage:** `/setblock hytale:rock_stone ~ ~-1 ~` + +{{< /tab >}} +{{< tab >}} + +```java +public class EffectCommand extends AbstractCommand { + private final RequiredArg effectArg; + private final OptionalArg durationArg; + private final OptionalArg amplifierArg; + + public EffectCommand() { + super("effect", "Apply effect to player"); + effectArg = withRequiredArg("effect", "Effect to apply", ArgTypes.EFFECT_ASSET); + durationArg = withOptionalArg("duration", "Duration in ticks", ArgTypes.INTEGER); + amplifierArg = withOptionalArg("amplifier", "Effect level", ArgTypes.INTEGER); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + EntityEffect effect = context.get(effectArg); + int duration = context.provided(durationArg) ? context.get(durationArg) : 600; // 30 seconds + int amplifier = context.provided(amplifierArg) ? context.get(amplifierArg) : 0; + + player.addEffect(effect, duration, amplifier); + + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +```java +public class ParticleCommand extends AbstractCommand { + private final RequiredArg particleArg; + private final OptionalArg positionArg; + + public ParticleCommand() { + super("particle", "Spawn particle effect"); + particleArg = withRequiredArg("particle", "Particle system", ArgTypes.PARTICLE_SYSTEM); + positionArg = withOptionalArg("position", "Spawn position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + ParticleSystem particle = context.get(particleArg); + Vector3d pos = context.provided(positionArg) + ? context.get(positionArg).resolve(player.getPosition()) + : player.getPosition(); + + player.getWorld().spawnParticle(particle, pos); + + return null; + } +} +``` + +**Usage:** `/particle hytale:explosion ~ ~2 ~` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Position Types + +Types for specifying locations in the game world. Supports both absolute and relative coordinates. + +### Relative Notation + +Positions support relative coordinates using the `~` prefix: + +| Syntax | Meaning | Example | +|--------|---------|---------| +| `100` | Absolute value 100 | Exact coordinate | +| `~` | Current position | Same as `~0` | +| `~5` | Current + 5 | 5 blocks in positive direction | +| `~-5` | Current - 5 | 5 blocks in negative direction | + +{{< tabs items="3D Positions,Block Positions,Single Coords" >}} +{{< tab >}} + +| Type | Java Type | Precision | Input Examples | +|------|-----------|-----------|----------------| +| `ArgTypes.RELATIVE_POSITION` | `RelativeDoublePosition` | Double (sub-block) | `100.5 64.0 -200.5`, `~ ~ ~`, `~5 ~10 ~-5` | +| `ArgTypes.RELATIVE_BLOCK_POSITION` | `RelativeIntPosition` | Integer (block) | `100 64 -200`, `~ ~ ~`, `~5 ~10 ~-5` | +| `ArgTypes.RELATIVE_CHUNK_POSITION` | `RelativeChunkPosition` | Chunk (X, Z) | `5 10`, `~2 ~-3` | + +```java +public class TeleportPosCommand extends AbstractCommand { + private final RequiredArg posArg; + + public TeleportPosCommand() { + super("tppos", "Teleport to coordinates"); + posArg = withRequiredArg("position", "Target position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + RelativeDoublePosition relPos = context.get(posArg); + + // Resolve relative coordinates based on player's current position + Vector3d absolutePos = relPos.resolve(player.getPosition()); + + player.teleport(absolutePos); + player.sendMessage(Message.raw("Teleported to " + absolutePos)); + + return null; + } +} +``` + +**Usage Examples:** +- `/tppos 100 64 200` → Teleport to exact coordinates +- `/tppos ~ ~10 ~` → Teleport 10 blocks up +- `/tppos ~5 ~ ~-5` → Teleport 5 blocks east and 5 blocks north + +{{< /tab >}} +{{< tab >}} + +```java +public class FillCommand extends AbstractCommand { + private final RequiredArg pos1Arg; + private final RequiredArg pos2Arg; + private final RequiredArg blockArg; + + public FillCommand() { + super("fill", "Fill region with blocks"); + pos1Arg = withRequiredArg("from", "First corner", ArgTypes.RELATIVE_BLOCK_POSITION); + pos2Arg = withRequiredArg("to", "Second corner", ArgTypes.RELATIVE_BLOCK_POSITION); + blockArg = withRequiredArg("block", "Block type", ArgTypes.BLOCK_TYPE_ASSET); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Vector3i playerPos = player.getBlockPosition(); + Vector3i from = context.get(pos1Arg).resolve(playerPos); + Vector3i to = context.get(pos2Arg).resolve(playerPos); + BlockType block = context.get(blockArg); + + int count = 0; + World world = player.getWorld(); + + int minX = Math.min(from.x(), to.x()); + int maxX = Math.max(from.x(), to.x()); + int minY = Math.min(from.y(), to.y()); + int maxY = Math.max(from.y(), to.y()); + int minZ = Math.min(from.z(), to.z()); + int maxZ = Math.max(from.z(), to.z()); + + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + world.setBlock(new Vector3i(x, y, z), block); + count++; + } + } + } + + player.sendMessage(Message.raw("Filled " + count + " blocks")); + return null; + } +} +``` + +**Usage:** `/fill ~-5 ~ ~-5 ~5 ~10 ~5 hytale:rock_stone` → Create a 11x11x11 cube around player + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.RELATIVE_DOUBLE_COORD` | `Coord` | Single coordinate (double) | `5.0`, `~-2.3`, `~` | +| `ArgTypes.RELATIVE_INT_COORD` | `IntCoord` | Single coordinate (integer) | `5`, `~-2`, `~` | +| `ArgTypes.RELATIVE_INTEGER` | `RelativeInteger` | Relative integer value | `5`, `~-2` | +| `ArgTypes.RELATIVE_FLOAT` | `RelativeFloat` | Relative float value | `90.0`, `~-45.5`, `~` | + +```java +public class SetYCommand extends AbstractCommand { + private final RequiredArg yArg; + + public SetYCommand() { + super("sety", "Teleport to Y level"); + yArg = withRequiredArg("y", "Y coordinate", ArgTypes.RELATIVE_DOUBLE_COORD); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Coord relY = context.get(yArg); + // Coord.resolveXZ() or Coord.resolveYAtWorldCoords() for resolution + double newY = relY.resolveXZ(player.getPosition().y); + + player.teleport(new Vector3d( + player.getPosition().x(), + newY, + player.getPosition().z() + )); + + return null; + } +} +``` + +**Usage:** `/sety 64` or `/sety ~10` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Vector Types + +Types for directional and rotational data. + +{{< tabs items="Overview,Examples" >}} +{{< tab >}} + +| Type | Java Type | Components | Input Examples | +|------|-----------|------------|----------------| +| `ArgTypes.VECTOR2I` | `Vector2i` | X, Z (2D integer) | `124 232`, `5 -3` | +| `ArgTypes.VECTOR3I` | `Vector3i` | X, Y, Z (3D integer) | `124 232 234` | +| `ArgTypes.RELATIVE_VECTOR3I` | `RelativeVector3i` | X, Y, Z (relative 3D) | `124 ~232 234`, `~5 0 ~-3` | +| `ArgTypes.ROTATION` | `Vector3f` | Pitch, Yaw, Roll | `45.0 90.0 0.0` | + +{{< /tab >}} +{{< tab >}} + +```java +public class LookCommand extends AbstractCommand { + private final RequiredArg rotationArg; + + public LookCommand() { + super("look", "Set player rotation"); + rotationArg = withRequiredArg("rotation", "Pitch Yaw Roll", ArgTypes.ROTATION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Vector3f rotation = context.get(rotationArg); + // rotation.x = pitch, rotation.y = yaw, rotation.z = roll + + player.setRotation(rotation); + player.sendMessage(Message.raw( + "Rotation set to Pitch: " + rotation.x + + ", Yaw: " + rotation.y + + ", Roll: " + rotation.z + )); + + return null; + } +} +``` + +**Rotation values:** +- **Pitch:** -90 (up) to 90 (down) +- **Yaw:** 0-360 degrees (north = 0, east = 90, south = 180, west = 270) +- **Roll:** Usually 0 for players + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Range Types + +Types for specifying value ranges. + +{{< tabs items="Overview,Example" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.INT_RANGE` | `Pair` | Integer range (min, max) | `-2 8`, `5 5`, `1 100` | +| `ArgTypes.RELATIVE_INT_RANGE` | `Pair` | Relative integer range | `~-2 ~8`, `~5 ~5` | + +{{< /tab >}} +{{< tab >}} + +```java +public class RandomCommand extends AbstractCommand { + private final RequiredArg> rangeArg; + + public RandomCommand() { + super("random", "Generate random number in range"); + rangeArg = withRequiredArg("range", "Min and max values", ArgTypes.INT_RANGE); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Pair range = context.get(rangeArg); + int min = range.left(); + int max = range.right(); + + if (min > max) { + context.sender().sendMessage(Message.raw("Min must be less than max!")); + return null; + } + + int result = min + (int)(Math.random() * (max - min + 1)); + + context.sender().sendMessage( + Message.raw("Random number between " + min + " and " + max + ": " + result) + ); + + return null; + } +} +``` + +**Usage:** `/random 1 100` → Random number between 1 and 100 + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Special Types + +Specialized types for specific game mechanics. + +{{< tabs items="Color,Game Mode,Tick Rate,Block Patterns" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.COLOR` | `Integer` | ARGB color value | `#FF0000`, `#80FF0000`, `16711680`, `0xFF0000` | + +**Supported formats:** +- Hex RGB: `#FF0000` (red) +- Hex ARGB: `#80FF0000` (50% transparent red) +- Decimal: `16711680` (red) +- Hex literal: `0xFF0000` (red) + +```java +public class ColorCommand extends AbstractCommand { + private final RequiredArg colorArg; + + public ColorCommand() { + super("color", "Set display color"); + colorArg = withRequiredArg("color", "Color value", ArgTypes.COLOR); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int color = context.get(colorArg); + + int alpha = (color >> 24) & 0xFF; + int red = (color >> 16) & 0xFF; + int green = (color >> 8) & 0xFF; + int blue = color & 0xFF; + + context.sender().sendMessage( + Message.raw("Color: ARGB(" + alpha + ", " + red + ", " + green + ", " + blue + ")") + ); + + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.GAME_MODE` | `GameMode` | Player game mode | `Adventure`, `Creative` | + +{{< callout type="info" >}} +Hytale currently supports two game modes: **Adventure** and **Creative**. +{{< /callout >}} + +```java +public class GameModeCommand extends AbstractCommand { + private final RequiredArg modeArg; + private final OptionalArg playerArg; + + public GameModeCommand() { + super("gamemode", "Change game mode"); + modeArg = withRequiredArg("mode", "Game mode", ArgTypes.GAME_MODE); + playerArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + GameMode mode = context.get(modeArg); + + // Get target player name + String targetName; + if (context.provided(playerArg)) { + PlayerRef ref = context.get(playerArg); + targetName = ref.getUsername(); + // Use ref.getReference() for ECS game mode operations + } else if (context.sender() instanceof Player p) { + targetName = p.getDisplayName(); + // Use Player.setGameMode() static method for game mode changes + } else { + context.sendMessage(Message.raw("Specify a player!")); + return null; + } + + // Set game mode using Player.setGameMode() static method with ECS ref + context.sendMessage(Message.raw("Game mode changed to " + mode.name() + " for " + targetName)); + + return null; + } +} +``` + +**Usage:** `/gamemode Creative` or `/gamemode Adventure --player john_doe` + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.TICK_RATE` | `Integer` | Tick rate in TPS | `30tps`, `33ms`, `60` | + +**Supported formats:** +- TPS (ticks per second): `30tps`, `60tps` +- Milliseconds per tick: `33ms`, `16ms` (converted to TPS) +- Numeric TPS: `30`, `60` + +{{< callout type="info" >}} +`TICK_RATE` returns an `Integer` representing the ticks per second (TPS). Millisecond input is automatically converted to TPS. +{{< /callout >}} + +```java +public class TickRateCommand extends AbstractCommand { + private final RequiredArg rateArg; + + public TickRateCommand() { + super("tickrate", "Set world tick rate"); + rateArg = withRequiredArg("rate", "Tick rate", ArgTypes.TICK_RATE); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + int tps = context.get(rateArg); // Returns Integer (TPS) + // Set tick rate on world... + + player.sendMessage(Message.raw("Tick rate set to " + tps + " TPS")); + + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.BLOCK_PATTERN` | `BlockPattern` | Weighted block selection | `[Rock_Stone]`, `[20%Rock_Stone, 80%Rock_Shale]` | +| `ArgTypes.BLOCK_MASK` | `BlockMask` | Block filter | `[!Fluid_Water, !^Fluid_Lava]` | + +**Block Pattern Syntax:** +- Single block: `[Rock_Stone]` +- Weighted: `[20%Rock_Stone, 80%Rock_Shale]` +- Equal weights: `[Rock_Stone, Rock_Shale]` (50% each) + +**Block Mask Syntax:** +- Include: `[Rock_Stone]` +- Exclude: `[!Rock_Stone]` +- Exclude tag: `[!^Fluid]` (excludes all blocks with "Fluid" tag) + +```java +public class FillPatternCommand extends AbstractCommand { + private final RequiredArg pos1Arg; + private final RequiredArg pos2Arg; + private final RequiredArg patternArg; + private final OptionalArg maskArg; + + public FillPatternCommand() { + super("fillpattern", "Fill with pattern"); + pos1Arg = withRequiredArg("from", "First corner", ArgTypes.RELATIVE_BLOCK_POSITION); + pos2Arg = withRequiredArg("to", "Second corner", ArgTypes.RELATIVE_BLOCK_POSITION); + patternArg = withRequiredArg("pattern", "Block pattern", ArgTypes.BLOCK_PATTERN); + maskArg = withOptionalArg("mask", "Replace mask", ArgTypes.BLOCK_MASK); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Implementation using weighted random selection from pattern + // Only replace blocks matching mask if provided + return null; + } +} +``` + +**Usage:** `/fillpattern ~-5 ~ ~-5 ~5 ~5 ~5 [30%Rock_Stone, 70%Rock_Shale]` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Custom Argument Types + +### Creating Enum Arguments + +Use `ArgTypes.forEnum()` to create arguments for custom enums with automatic tab completion: + +```java +public enum Difficulty { + EASY, NORMAL, HARD, EXTREME +} + +public class DifficultyCommand extends AbstractCommand { + private final RequiredArg difficultyArg; + + public DifficultyCommand() { + super("difficulty", "Set server difficulty"); + difficultyArg = withRequiredArg( + "level", + "Difficulty level", + ArgTypes.forEnum("Difficulty", Difficulty.class) + ); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Difficulty level = context.get(difficultyArg); + // Apply difficulty setting + context.sender().sendMessage( + Message.raw("Difficulty set to " + level.name()) + ); + return null; + } +} +``` + +### List Arguments + +Create list arguments for any type: + +```java +public class MultiTeleportCommand extends AbstractCommand { + private final RequiredArg> playersArg; + private final RequiredArg positionArg; + + public MultiTeleportCommand() { + super("multitp", "Teleport multiple players"); + playersArg = withListRequiredArg("players", "Target players", ArgTypes.PLAYER_REF); + positionArg = withRequiredArg("position", "Destination", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + List playerRefs = context.get(playersArg); + RelativeDoublePosition relPos = context.get(positionArg); + + for (PlayerRef ref : playerRefs) { + // Use getReference() to access ECS entity + Ref entityRef = ref.getReference(); + if (entityRef != null && entityRef.isValid()) { + // Access TransformComponent for position/teleportation + // Vector3d pos = relPos.resolve(currentPosition); + // Set new position via ECS + } + } + + context.sendMessage( + Message.raw("Teleported " + playerRefs.size() + " players") + ); + + return null; + } +} +``` + +**Usage:** `/multitp [player1, player2, player3] 100 64 200` + +{{< callout type="info" >}} +List arguments use square bracket syntax: `[item1, item2, item3]`. Each item is parsed according to the base argument type. +{{< /callout >}} + +--- + +## Argument Validation + +Arguments are automatically validated during parsing. For custom validation, use the `validate()` method: + +```java +public class AgeCommand extends AbstractCommand { + private final RequiredArg ageArg; + + public AgeCommand() { + super("setage", "Set player age"); + ageArg = withRequiredArg("age", "Age value (1-120)", ArgTypes.INTEGER) + .validate(age -> { + if (age < 1 || age > 120) { + throw new CommandException("Age must be between 1 and 120"); + } + }); + } +} +``` + +--- + +## Summary Reference + +{{< details title="Complete ArgTypes Reference" >}} + +| Category | Type | Java Type | +|----------|------|-----------| +| **Primitive** | `BOOLEAN` | `Boolean` | +| | `INTEGER` | `Integer` | +| | `FLOAT` | `Float` | +| | `DOUBLE` | `Double` | +| | `STRING` | `String` | +| **Player/Entity** | `PLAYER_REF` | `PlayerRef` | +| | `PLAYER_UUID` | `UUID` | +| | `UUID` | `UUID` | +| | `ENTITY_ID` | `UUID` (wrapped) | +| **World** | `WORLD` | `World` | +| **Assets** | `ITEM_ASSET` | `Item` | +| | `BLOCK_TYPE_ASSET` | `BlockType` | +| | `MODEL_ASSET` | `ModelAsset` | +| | `WEATHER_ASSET` | `Weather` | +| | `EFFECT_ASSET` | `EntityEffect` | +| | `ENVIRONMENT_ASSET` | `Environment` | +| | `PARTICLE_SYSTEM` | `ParticleSystem` | +| | `SOUND_EVENT_ASSET` | `SoundEvent` | +| | `AMBIENCE_FX_ASSET` | `AmbienceFX` | +| | `INTERACTION_ASSET` | `Interaction` | +| | `ROOT_INTERACTION_ASSET` | `RootInteraction` | +| **Position** | `RELATIVE_POSITION` | `RelativeDoublePosition` | +| | `RELATIVE_BLOCK_POSITION` | `RelativeIntPosition` | +| | `RELATIVE_CHUNK_POSITION` | `RelativeChunkPosition` | +| | `RELATIVE_DOUBLE_COORD` | `Coord` | +| | `RELATIVE_INT_COORD` | `IntCoord` | +| | `RELATIVE_INTEGER` | `RelativeInteger` | +| | `RELATIVE_FLOAT` | `RelativeFloat` | +| **Vector** | `VECTOR2I` | `Vector2i` | +| | `VECTOR3I` | `Vector3i` | +| | `RELATIVE_VECTOR3I` | `RelativeVector3i` | +| | `ROTATION` | `Vector3f` | +| **Range** | `INT_RANGE` | `Pair` | +| | `RELATIVE_INT_RANGE` | `RelativeIntegerRange` | +| **Special** | `COLOR` | `Integer` | +| | `GAME_MODE` | `GameMode` (Adventure, Creative) | +| | `SOUND_CATEGORY` | `SoundCategory` | +| | `TICK_RATE` | `Integer` (TPS) | +| | `BLOCK_PATTERN` | `BlockPattern` | +| | `BLOCK_MASK` | `BlockMask` | +| **Advanced** | `HITBOX_COLLISION_CONFIG` | `HitboxCollisionConfig` | +| | `REPULSION_CONFIG` | `RepulsionConfig` | +| | `INTEGER_COMPARISON_OPERATOR` | `IntegerComparisonOperator` | +| | `INTEGER_OPERATION` | `IntegerOperation` | + +{{< /details >}} diff --git a/content/core-concepts/commands/argument-types.fr.md b/content/core-concepts/commands/argument-types.fr.md new file mode 100644 index 0000000..20fd3ac --- /dev/null +++ b/content/core-concepts/commands/argument-types.fr.md @@ -0,0 +1,1065 @@ +--- +title: Argument Types +type: docs +weight: 2 +--- + +Hytale provides a comprehensive set of argument types through the `ArgTypes` class. These types handle parsing, validation, and tab completion automatically, ensuring a consistent and user-friendly command experience. + +**Package:** `com.hypixel.hytale.server.core.command.system.arguments.types` + +{{< cards cols="3" >}} + {{< card link="#primitive-types" title="Primitives" subtitle="Boolean, Integer, Float, Double, String" icon="hashtag" >}} + {{< card link="#player-and-entity-types" title="Player & Entity" subtitle="PlayerRef, UUID, EntityId" icon="user" >}} + {{< card link="#asset-types" title="Assets" subtitle="Items, Blocks, Models, Effects" icon="cube" >}} + {{< card link="#position-types" title="Positions" subtitle="Absolute and relative coordinates" icon="map" >}} + {{< card link="#vector-types" title="Vectors" subtitle="2D/3D vectors and rotations" icon="arrows-expand" >}} + {{< card link="#special-types" title="Special" subtitle="Colors, Patterns, Ranges" icon="sparkles" >}} +{{< /cards >}} + +--- + +## Primitive Types + +Basic data types for simple values. These are the foundation of most command arguments. + +{{< tabs items="Overview,Examples,Methods" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.BOOLEAN` | `Boolean` | True or false value | `true`, `false` | +| `ArgTypes.INTEGER` | `Integer` | 32-bit whole numbers | `-1`, `0`, `56346`, `2147483647` | +| `ArgTypes.FLOAT` | `Float` | Single precision decimals | `3.14159`, `-2.5`, `7` | +| `ArgTypes.DOUBLE` | `Double` | Double precision decimals | `-3.14`, `0.0`, `3.141596` | +| `ArgTypes.STRING` | `String` | Single word text | `hello`, `world`, `test123` | + +{{< /tab >}} +{{< tab >}} + +```java +public class MathCommand extends AbstractCommand { + private final RequiredArg countArg; + private final RequiredArg multiplierArg; + private final RequiredArg verboseArg; + private final OptionalArg labelArg; + + public MathCommand() { + super("math", "Perform mathematical operations"); + countArg = withRequiredArg("count", "Number of iterations", ArgTypes.INTEGER); + multiplierArg = withRequiredArg("multiplier", "Multiplication factor", ArgTypes.DOUBLE); + verboseArg = withRequiredArg("verbose", "Show detailed output", ArgTypes.BOOLEAN); + labelArg = withOptionalArg("label", "Optional label", ArgTypes.STRING); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int count = context.get(countArg); + double mult = context.get(multiplierArg); + boolean verbose = context.get(verboseArg); + String label = context.get(labelArg); // null if not provided + + double result = count * mult; + + if (verbose) { + String output = (label != null ? label + ": " : "") + + count + " × " + mult + " = " + result; + context.sender().sendMessage(Message.raw(output)); + } + + return null; + } +} +``` + +**Usage:** `/math 5 2.5 true --label result` → `result: 5 × 2.5 = 12.5` + +{{< /tab >}} +{{< tab >}} + +**Common methods for primitive arguments:** + +| Method | Description | +|--------|-------------| +| `withRequiredArg(name, desc, type)` | Creates a required argument | +| `withOptionalArg(name, desc, type)` | Creates an optional argument | +| `context.get(arg)` | Gets the parsed value | +| `context.provided(arg)` | Checks if optional arg was provided | + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Player and Entity Types + +Types for referencing players and entities in the game world. + +{{< tabs items="Overview,PlayerRef,Entity,UUID" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.PLAYER_REF` | `PlayerRef` | Online player reference | `john_doe`, `user123` | +| `ArgTypes.PLAYER_UUID` | `UUID` | Player UUID (by name or UUID) | `john_doe`, `550e8400-e29b-...` | +| `ArgTypes.UUID` | `UUID` | Any valid UUID | `550e8400-e29b-41d4-a716-...` | +| `ArgTypes.ENTITY_ID` | `UUID` (wrapped) | Entity by UUID | Entity UUID | + +{{< callout type="warning" >}} +**PLAYER_REF** only matches currently online players and returns a `PlayerRef`. **PLAYER_UUID** can match by name or UUID and returns a `UUID` directly. +{{< /callout >}} + +{{< /tab >}} +{{< tab >}} + +{{< callout type="info" >}} +`PlayerRef` provides player identity (`getUsername()`, `getUuid()`) and ECS access via `getReference()`. To check if a player is online, verify `getReference()` returns a valid reference. +{{< /callout >}} + +```java +public class TeleportCommand extends AbstractCommand { + private final RequiredArg targetArg; + private final OptionalArg destinationArg; + + public TeleportCommand() { + super("tp", "Teleport players"); + targetArg = withRequiredArg("target", "Player to teleport", ArgTypes.PLAYER_REF); + destinationArg = withOptionalArg("destination", "Destination player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + PlayerRef targetRef = context.get(targetArg); + + // Check if player is online via ECS reference + Ref targetEntity = targetRef.getReference(); + if (targetEntity == null || !targetEntity.isValid()) { + context.sendMessage(Message.raw("Player is no longer online!")); + return null; + } + + if (context.provided(destinationArg)) { + PlayerRef destRef = context.get(destinationArg); + Ref destEntity = destRef.getReference(); + if (destEntity != null && destEntity.isValid()) { + // Teleport target to destination player using ECS + // Access TransformComponent from both entities for teleportation + } + } + + return null; + } +} +``` + +**Usage:** `/tp john_doe --destination steve` → Teleports john_doe to steve + +{{< /tab >}} +{{< tab >}} + +```java +public class EntityInfoCommand extends AbstractCommand { + private final RequiredArg entityArg; + + public EntityInfoCommand() { + super("entityinfo", "Get entity information"); + entityArg = withRequiredArg("entity", "Entity UUID", ArgTypes.UUID); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + UUID entityId = context.get(entityArg); + + // Find entity in worlds + for (World world : Universe.get().getWorlds().values()) { + // Search for entity by UUID + // Entity lookup depends on world API + } + + context.sender().sendMessage(Message.raw("Entity lookup complete")); + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +```java +public class UuidCommand extends AbstractCommand { + private final RequiredArg uuidArg; + + public UuidCommand() { + super("uuid", "Parse and display UUID"); + uuidArg = withRequiredArg("uuid", "UUID to parse", ArgTypes.UUID); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + UUID uuid = context.get(uuidArg); + context.sender().sendMessage(Message.raw( + "UUID: " + uuid.toString() + "\nVersion: " + uuid.version() + )); + return null; + } +} +``` + +**Valid UUID formats:** +- Full: `550e8400-e29b-41d4-a716-446655440000` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## World Types + +Types for referencing game worlds. + +{{< tabs items="Overview,Example" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.WORLD` | `World` | World by name | `default`, `creative`, `pvp_arena` | + +{{< callout type="info" >}} +World names are case-insensitive and support tab completion for all loaded worlds. +{{< /callout >}} + +{{< /tab >}} +{{< tab >}} + +```java +public class WorldTeleportCommand extends AbstractCommand { + private final RequiredArg worldArg; + private final OptionalArg positionArg; + + public WorldTeleportCommand() { + super("wtp", "Teleport to another world"); + worldArg = withRequiredArg("world", "Destination world", ArgTypes.WORLD); + positionArg = withOptionalArg("position", "Spawn position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + context.sender().sendMessage(Message.raw("Only players can use this command!")); + return null; + } + + World targetWorld = context.get(worldArg); + + Vector3d spawnPos; + if (context.provided(positionArg)) { + spawnPos = context.get(positionArg).resolve(player.getPosition()); + } else { + spawnPos = targetWorld.getSpawnPosition(); + } + + player.teleport(targetWorld, spawnPos); + player.sendMessage(Message.raw("Teleported to " + targetWorld.getName())); + + return null; + } +} +``` + +**Usage:** `/wtp creative` or `/wtp creative 100 64 200` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Asset Types + +Types for referencing game assets loaded from data packs. + +{{< tabs items="Overview,Items,Blocks,Effects,Other" >}} +{{< tab >}} + +| Type | Java Type | Description | +|------|-----------|-------------| +| `ArgTypes.ITEM_ASSET` | `Item` | Item definition | +| `ArgTypes.BLOCK_TYPE_ASSET` | `BlockType` | Block type definition | +| `ArgTypes.MODEL_ASSET` | `Model` | 3D model asset | +| `ArgTypes.WEATHER_ASSET` | `Weather` | Weather configuration | +| `ArgTypes.EFFECT_ASSET` | `EntityEffect` | Entity effect (buffs/debuffs) | +| `ArgTypes.ENVIRONMENT_ASSET` | `Environment` | Environment settings | +| `ArgTypes.PARTICLE_SYSTEM` | `ParticleSystem` | Particle system definition | +| `ArgTypes.SOUND_EVENT_ASSET` | `SoundEvent` | Sound event | +| `ArgTypes.AMBIENCE_FX_ASSET` | `AmbienceFX` | Ambient sound/effect | +| `ArgTypes.INTERACTION_ASSET` | `Interaction` | Interaction definition | + +{{< callout type="info" >}} +Asset identifiers use the format `namespace:path` (e.g., `hytale:sword_iron`). Tab completion lists all available assets. +{{< /callout >}} + +{{< /tab >}} +{{< tab >}} + +```java +public class GiveCommand extends AbstractCommand { + private final RequiredArg itemArg; + private final OptionalArg amountArg; + private final OptionalArg targetArg; + + public GiveCommand() { + super("give", "Give items to players"); + itemArg = withRequiredArg("item", "Item to give", ArgTypes.ITEM_ASSET); + amountArg = withOptionalArg("amount", "Stack size", ArgTypes.INTEGER); + targetArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Item item = context.get(itemArg); + int amount = context.provided(amountArg) ? context.get(amountArg) : 1; + + // Get target player - use PlayerRef for identity, sender for direct access + String targetName; + if (context.provided(targetArg)) { + PlayerRef ref = context.get(targetArg); + targetName = ref.getUsername(); + // Use ref.getReference() for ECS operations + } else if (context.sender() instanceof Player p) { + targetName = p.getDisplayName(); + // p.getInventory() for inventory operations + } else { + context.sendMessage(Message.raw("Specify a player!")); + return null; + } + + // Create ItemStack and add to inventory + ItemStack stack = new ItemStack(item.getId(), amount); + // Add to player inventory via ECS... + + context.sendMessage(Message.raw( + "Gave " + amount + "x " + item.getId() + " to " + targetName + )); + + return null; + } +} +``` + +**Usage:** `/give hytale:sword_iron 1 john_doe` + +{{< /tab >}} +{{< tab >}} + +```java +public class SetBlockCommand extends AbstractCommand { + private final RequiredArg blockArg; + private final RequiredArg positionArg; + + public SetBlockCommand() { + super("setblock", "Set a block at position"); + blockArg = withRequiredArg("block", "Block type", ArgTypes.BLOCK_TYPE_ASSET); + positionArg = withRequiredArg("position", "Target position", ArgTypes.RELATIVE_BLOCK_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + BlockType blockType = context.get(blockArg); + Vector3i pos = context.get(positionArg).resolve(player.getBlockPosition()); + + player.getWorld().setBlock(pos, blockType); + + context.sender().sendMessage( + Message.raw("Set block to " + blockType.getIdentifier() + " at " + pos) + ); + + return null; + } +} +``` + +**Usage:** `/setblock hytale:rock_stone ~ ~-1 ~` + +{{< /tab >}} +{{< tab >}} + +```java +public class EffectCommand extends AbstractCommand { + private final RequiredArg effectArg; + private final OptionalArg durationArg; + private final OptionalArg amplifierArg; + + public EffectCommand() { + super("effect", "Apply effect to player"); + effectArg = withRequiredArg("effect", "Effect to apply", ArgTypes.EFFECT_ASSET); + durationArg = withOptionalArg("duration", "Duration in ticks", ArgTypes.INTEGER); + amplifierArg = withOptionalArg("amplifier", "Effect level", ArgTypes.INTEGER); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + EntityEffect effect = context.get(effectArg); + int duration = context.provided(durationArg) ? context.get(durationArg) : 600; // 30 seconds + int amplifier = context.provided(amplifierArg) ? context.get(amplifierArg) : 0; + + player.addEffect(effect, duration, amplifier); + + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +```java +public class ParticleCommand extends AbstractCommand { + private final RequiredArg particleArg; + private final OptionalArg positionArg; + + public ParticleCommand() { + super("particle", "Spawn particle effect"); + particleArg = withRequiredArg("particle", "Particle system", ArgTypes.PARTICLE_SYSTEM); + positionArg = withOptionalArg("position", "Spawn position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + ParticleSystem particle = context.get(particleArg); + Vector3d pos = context.provided(positionArg) + ? context.get(positionArg).resolve(player.getPosition()) + : player.getPosition(); + + player.getWorld().spawnParticle(particle, pos); + + return null; + } +} +``` + +**Usage:** `/particle hytale:explosion ~ ~2 ~` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Position Types + +Types for specifying locations in the game world. Supports both absolute and relative coordinates. + +### Relative Notation + +Positions support relative coordinates using the `~` prefix: + +| Syntax | Meaning | Example | +|--------|---------|---------| +| `100` | Absolute value 100 | Exact coordinate | +| `~` | Current position | Same as `~0` | +| `~5` | Current + 5 | 5 blocks in positive direction | +| `~-5` | Current - 5 | 5 blocks in negative direction | + +{{< tabs items="3D Positions,Block Positions,Single Coords" >}} +{{< tab >}} + +| Type | Java Type | Precision | Input Examples | +|------|-----------|-----------|----------------| +| `ArgTypes.RELATIVE_POSITION` | `RelativeDoublePosition` | Double (sub-block) | `100.5 64.0 -200.5`, `~ ~ ~`, `~5 ~10 ~-5` | +| `ArgTypes.RELATIVE_BLOCK_POSITION` | `RelativeIntPosition` | Integer (block) | `100 64 -200`, `~ ~ ~`, `~5 ~10 ~-5` | +| `ArgTypes.RELATIVE_CHUNK_POSITION` | `RelativeChunkPosition` | Chunk (X, Z) | `5 10`, `~2 ~-3` | + +```java +public class TeleportPosCommand extends AbstractCommand { + private final RequiredArg posArg; + + public TeleportPosCommand() { + super("tppos", "Teleport to coordinates"); + posArg = withRequiredArg("position", "Target position", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + RelativeDoublePosition relPos = context.get(posArg); + + // Resolve relative coordinates based on player's current position + Vector3d absolutePos = relPos.resolve(player.getPosition()); + + player.teleport(absolutePos); + player.sendMessage(Message.raw("Teleported to " + absolutePos)); + + return null; + } +} +``` + +**Usage Examples:** +- `/tppos 100 64 200` → Teleport to exact coordinates +- `/tppos ~ ~10 ~` → Teleport 10 blocks up +- `/tppos ~5 ~ ~-5` → Teleport 5 blocks east and 5 blocks north + +{{< /tab >}} +{{< tab >}} + +```java +public class FillCommand extends AbstractCommand { + private final RequiredArg pos1Arg; + private final RequiredArg pos2Arg; + private final RequiredArg blockArg; + + public FillCommand() { + super("fill", "Fill region with blocks"); + pos1Arg = withRequiredArg("from", "First corner", ArgTypes.RELATIVE_BLOCK_POSITION); + pos2Arg = withRequiredArg("to", "Second corner", ArgTypes.RELATIVE_BLOCK_POSITION); + blockArg = withRequiredArg("block", "Block type", ArgTypes.BLOCK_TYPE_ASSET); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Vector3i playerPos = player.getBlockPosition(); + Vector3i from = context.get(pos1Arg).resolve(playerPos); + Vector3i to = context.get(pos2Arg).resolve(playerPos); + BlockType block = context.get(blockArg); + + int count = 0; + World world = player.getWorld(); + + int minX = Math.min(from.x(), to.x()); + int maxX = Math.max(from.x(), to.x()); + int minY = Math.min(from.y(), to.y()); + int maxY = Math.max(from.y(), to.y()); + int minZ = Math.min(from.z(), to.z()); + int maxZ = Math.max(from.z(), to.z()); + + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + world.setBlock(new Vector3i(x, y, z), block); + count++; + } + } + } + + player.sendMessage(Message.raw("Filled " + count + " blocks")); + return null; + } +} +``` + +**Usage:** `/fill ~-5 ~ ~-5 ~5 ~10 ~5 hytale:rock_stone` → Create a 11x11x11 cube around player + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.RELATIVE_DOUBLE_COORD` | `Coord` | Single coordinate (double) | `5.0`, `~-2.3`, `~` | +| `ArgTypes.RELATIVE_INT_COORD` | `IntCoord` | Single coordinate (integer) | `5`, `~-2`, `~` | +| `ArgTypes.RELATIVE_INTEGER` | `RelativeInteger` | Relative integer value | `5`, `~-2` | +| `ArgTypes.RELATIVE_FLOAT` | `RelativeFloat` | Relative float value | `90.0`, `~-45.5`, `~` | + +```java +public class SetYCommand extends AbstractCommand { + private final RequiredArg yArg; + + public SetYCommand() { + super("sety", "Teleport to Y level"); + yArg = withRequiredArg("y", "Y coordinate", ArgTypes.RELATIVE_DOUBLE_COORD); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Coord relY = context.get(yArg); + // Coord.resolveXZ() or Coord.resolveYAtWorldCoords() for resolution + double newY = relY.resolveXZ(player.getPosition().y); + + player.teleport(new Vector3d( + player.getPosition().x(), + newY, + player.getPosition().z() + )); + + return null; + } +} +``` + +**Usage:** `/sety 64` or `/sety ~10` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Vector Types + +Types for directional and rotational data. + +{{< tabs items="Overview,Examples" >}} +{{< tab >}} + +| Type | Java Type | Components | Input Examples | +|------|-----------|------------|----------------| +| `ArgTypes.VECTOR2I` | `Vector2i` | X, Z (2D integer) | `124 232`, `5 -3` | +| `ArgTypes.VECTOR3I` | `Vector3i` | X, Y, Z (3D integer) | `124 232 234` | +| `ArgTypes.RELATIVE_VECTOR3I` | `RelativeVector3i` | X, Y, Z (relative 3D) | `124 ~232 234`, `~5 0 ~-3` | +| `ArgTypes.ROTATION` | `Vector3f` | Pitch, Yaw, Roll | `45.0 90.0 0.0` | + +{{< /tab >}} +{{< tab >}} + +```java +public class LookCommand extends AbstractCommand { + private final RequiredArg rotationArg; + + public LookCommand() { + super("look", "Set player rotation"); + rotationArg = withRequiredArg("rotation", "Pitch Yaw Roll", ArgTypes.ROTATION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + Vector3f rotation = context.get(rotationArg); + // rotation.x = pitch, rotation.y = yaw, rotation.z = roll + + player.setRotation(rotation); + player.sendMessage(Message.raw( + "Rotation set to Pitch: " + rotation.x + + ", Yaw: " + rotation.y + + ", Roll: " + rotation.z + )); + + return null; + } +} +``` + +**Rotation values:** +- **Pitch:** -90 (up) to 90 (down) +- **Yaw:** 0-360 degrees (north = 0, east = 90, south = 180, west = 270) +- **Roll:** Usually 0 for players + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Range Types + +Types for specifying value ranges. + +{{< tabs items="Overview,Example" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.INT_RANGE` | `Pair` | Integer range (min, max) | `-2 8`, `5 5`, `1 100` | +| `ArgTypes.RELATIVE_INT_RANGE` | `Pair` | Relative integer range | `~-2 ~8`, `~5 ~5` | + +{{< /tab >}} +{{< tab >}} + +```java +public class RandomCommand extends AbstractCommand { + private final RequiredArg> rangeArg; + + public RandomCommand() { + super("random", "Generate random number in range"); + rangeArg = withRequiredArg("range", "Min and max values", ArgTypes.INT_RANGE); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Pair range = context.get(rangeArg); + int min = range.left(); + int max = range.right(); + + if (min > max) { + context.sender().sendMessage(Message.raw("Min must be less than max!")); + return null; + } + + int result = min + (int)(Math.random() * (max - min + 1)); + + context.sender().sendMessage( + Message.raw("Random number between " + min + " and " + max + ": " + result) + ); + + return null; + } +} +``` + +**Usage:** `/random 1 100` → Random number between 1 and 100 + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Special Types + +Specialized types for specific game mechanics. + +{{< tabs items="Color,Game Mode,Tick Rate,Block Patterns" >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.COLOR` | `Integer` | ARGB color value | `#FF0000`, `#80FF0000`, `16711680`, `0xFF0000` | + +**Supported formats:** +- Hex RGB: `#FF0000` (red) +- Hex ARGB: `#80FF0000` (50% transparent red) +- Decimal: `16711680` (red) +- Hex literal: `0xFF0000` (red) + +```java +public class ColorCommand extends AbstractCommand { + private final RequiredArg colorArg; + + public ColorCommand() { + super("color", "Set display color"); + colorArg = withRequiredArg("color", "Color value", ArgTypes.COLOR); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + int color = context.get(colorArg); + + int alpha = (color >> 24) & 0xFF; + int red = (color >> 16) & 0xFF; + int green = (color >> 8) & 0xFF; + int blue = color & 0xFF; + + context.sender().sendMessage( + Message.raw("Color: ARGB(" + alpha + ", " + red + ", " + green + ", " + blue + ")") + ); + + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.GAME_MODE` | `GameMode` | Player game mode | `Adventure`, `Creative` | + +{{< callout type="info" >}} +Hytale currently supports two game modes: **Adventure** and **Creative**. +{{< /callout >}} + +```java +public class GameModeCommand extends AbstractCommand { + private final RequiredArg modeArg; + private final OptionalArg playerArg; + + public GameModeCommand() { + super("gamemode", "Change game mode"); + modeArg = withRequiredArg("mode", "Game mode", ArgTypes.GAME_MODE); + playerArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + GameMode mode = context.get(modeArg); + + // Get target player name + String targetName; + if (context.provided(playerArg)) { + PlayerRef ref = context.get(playerArg); + targetName = ref.getUsername(); + // Use ref.getReference() for ECS game mode operations + } else if (context.sender() instanceof Player p) { + targetName = p.getDisplayName(); + // Use Player.setGameMode() static method for game mode changes + } else { + context.sendMessage(Message.raw("Specify a player!")); + return null; + } + + // Set game mode using Player.setGameMode() static method with ECS ref + context.sendMessage(Message.raw("Game mode changed to " + mode.name() + " for " + targetName)); + + return null; + } +} +``` + +**Usage:** `/gamemode Creative` or `/gamemode Adventure --player john_doe` + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.TICK_RATE` | `Integer` | Tick rate in TPS | `30tps`, `33ms`, `60` | + +**Supported formats:** +- TPS (ticks per second): `30tps`, `60tps` +- Milliseconds per tick: `33ms`, `16ms` (converted to TPS) +- Numeric TPS: `30`, `60` + +{{< callout type="info" >}} +`TICK_RATE` returns an `Integer` representing the ticks per second (TPS). Millisecond input is automatically converted to TPS. +{{< /callout >}} + +```java +public class TickRateCommand extends AbstractCommand { + private final RequiredArg rateArg; + + public TickRateCommand() { + super("tickrate", "Set world tick rate"); + rateArg = withRequiredArg("rate", "Tick rate", ArgTypes.TICK_RATE); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + if (!(context.sender() instanceof Player player)) { + return null; + } + + int tps = context.get(rateArg); // Returns Integer (TPS) + // Set tick rate on world... + + player.sendMessage(Message.raw("Tick rate set to " + tps + " TPS")); + + return null; + } +} +``` + +{{< /tab >}} +{{< tab >}} + +| Type | Java Type | Description | Input Examples | +|------|-----------|-------------|----------------| +| `ArgTypes.BLOCK_PATTERN` | `BlockPattern` | Weighted block selection | `[Rock_Stone]`, `[20%Rock_Stone, 80%Rock_Shale]` | +| `ArgTypes.BLOCK_MASK` | `BlockMask` | Block filter | `[!Fluid_Water, !^Fluid_Lava]` | + +**Block Pattern Syntax:** +- Single block: `[Rock_Stone]` +- Weighted: `[20%Rock_Stone, 80%Rock_Shale]` +- Equal weights: `[Rock_Stone, Rock_Shale]` (50% each) + +**Block Mask Syntax:** +- Include: `[Rock_Stone]` +- Exclude: `[!Rock_Stone]` +- Exclude tag: `[!^Fluid]` (excludes all blocks with "Fluid" tag) + +```java +public class FillPatternCommand extends AbstractCommand { + private final RequiredArg pos1Arg; + private final RequiredArg pos2Arg; + private final RequiredArg patternArg; + private final OptionalArg maskArg; + + public FillPatternCommand() { + super("fillpattern", "Fill with pattern"); + pos1Arg = withRequiredArg("from", "First corner", ArgTypes.RELATIVE_BLOCK_POSITION); + pos2Arg = withRequiredArg("to", "Second corner", ArgTypes.RELATIVE_BLOCK_POSITION); + patternArg = withRequiredArg("pattern", "Block pattern", ArgTypes.BLOCK_PATTERN); + maskArg = withOptionalArg("mask", "Replace mask", ArgTypes.BLOCK_MASK); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + // Implementation using weighted random selection from pattern + // Only replace blocks matching mask if provided + return null; + } +} +``` + +**Usage:** `/fillpattern ~-5 ~ ~-5 ~5 ~5 ~5 [30%Rock_Stone, 70%Rock_Shale]` + +{{< /tab >}} +{{< /tabs >}} + +--- + +## Custom Argument Types + +### Creating Enum Arguments + +Use `ArgTypes.forEnum()` to create arguments for custom enums with automatic tab completion: + +```java +public enum Difficulty { + EASY, NORMAL, HARD, EXTREME +} + +public class DifficultyCommand extends AbstractCommand { + private final RequiredArg difficultyArg; + + public DifficultyCommand() { + super("difficulty", "Set server difficulty"); + difficultyArg = withRequiredArg( + "level", + "Difficulty level", + ArgTypes.forEnum("Difficulty", Difficulty.class) + ); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + Difficulty level = context.get(difficultyArg); + // Apply difficulty setting + context.sender().sendMessage( + Message.raw("Difficulty set to " + level.name()) + ); + return null; + } +} +``` + +### List Arguments + +Create list arguments for any type: + +```java +public class MultiTeleportCommand extends AbstractCommand { + private final RequiredArg> playersArg; + private final RequiredArg positionArg; + + public MultiTeleportCommand() { + super("multitp", "Teleport multiple players"); + playersArg = withListRequiredArg("players", "Target players", ArgTypes.PLAYER_REF); + positionArg = withRequiredArg("position", "Destination", ArgTypes.RELATIVE_POSITION); + } + + @Override + protected CompletableFuture execute(CommandContext context) { + List playerRefs = context.get(playersArg); + RelativeDoublePosition relPos = context.get(positionArg); + + for (PlayerRef ref : playerRefs) { + // Use getReference() to access ECS entity + Ref entityRef = ref.getReference(); + if (entityRef != null && entityRef.isValid()) { + // Access TransformComponent for position/teleportation + // Vector3d pos = relPos.resolve(currentPosition); + // Set new position via ECS + } + } + + context.sendMessage( + Message.raw("Teleported " + playerRefs.size() + " players") + ); + + return null; + } +} +``` + +**Usage:** `/multitp [player1, player2, player3] 100 64 200` + +{{< callout type="info" >}} +List arguments use square bracket syntax: `[item1, item2, item3]`. Each item is parsed according to the base argument type. +{{< /callout >}} + +--- + +## Argument Validation + +Arguments are automatically validated during parsing. For custom validation, use the `validate()` method: + +```java +public class AgeCommand extends AbstractCommand { + private final RequiredArg ageArg; + + public AgeCommand() { + super("setage", "Set player age"); + ageArg = withRequiredArg("age", "Age value (1-120)", ArgTypes.INTEGER) + .validate(age -> { + if (age < 1 || age > 120) { + throw new CommandException("Age must be between 1 and 120"); + } + }); + } +} +``` + +--- + +## Summary Reference + +{{< details title="Complete ArgTypes Reference" >}} + +| Category | Type | Java Type | +|----------|------|-----------| +| **Primitive** | `BOOLEAN` | `Boolean` | +| | `INTEGER` | `Integer` | +| | `FLOAT` | `Float` | +| | `DOUBLE` | `Double` | +| | `STRING` | `String` | +| **Player/Entity** | `PLAYER_REF` | `PlayerRef` | +| | `PLAYER_UUID` | `UUID` | +| | `UUID` | `UUID` | +| | `ENTITY_ID` | `UUID` (wrapped) | +| **World** | `WORLD` | `World` | +| **Assets** | `ITEM_ASSET` | `Item` | +| | `BLOCK_TYPE_ASSET` | `BlockType` | +| | `MODEL_ASSET` | `ModelAsset` | +| | `WEATHER_ASSET` | `Weather` | +| | `EFFECT_ASSET` | `EntityEffect` | +| | `ENVIRONMENT_ASSET` | `Environment` | +| | `PARTICLE_SYSTEM` | `ParticleSystem` | +| | `SOUND_EVENT_ASSET` | `SoundEvent` | +| | `AMBIENCE_FX_ASSET` | `AmbienceFX` | +| | `INTERACTION_ASSET` | `Interaction` | +| | `ROOT_INTERACTION_ASSET` | `RootInteraction` | +| **Position** | `RELATIVE_POSITION` | `RelativeDoublePosition` | +| | `RELATIVE_BLOCK_POSITION` | `RelativeIntPosition` | +| | `RELATIVE_CHUNK_POSITION` | `RelativeChunkPosition` | +| | `RELATIVE_DOUBLE_COORD` | `Coord` | +| | `RELATIVE_INT_COORD` | `IntCoord` | +| | `RELATIVE_INTEGER` | `RelativeInteger` | +| | `RELATIVE_FLOAT` | `RelativeFloat` | +| **Vector** | `VECTOR2I` | `Vector2i` | +| | `VECTOR3I` | `Vector3i` | +| | `RELATIVE_VECTOR3I` | `RelativeVector3i` | +| | `ROTATION` | `Vector3f` | +| **Range** | `INT_RANGE` | `Pair` | +| | `RELATIVE_INT_RANGE` | `RelativeIntegerRange` | +| **Special** | `COLOR` | `Integer` | +| | `GAME_MODE` | `GameMode` (Adventure, Creative) | +| | `SOUND_CATEGORY` | `SoundCategory` | +| | `TICK_RATE` | `Integer` (TPS) | +| | `BLOCK_PATTERN` | `BlockPattern` | +| | `BLOCK_MASK` | `BlockMask` | +| **Advanced** | `HITBOX_COLLISION_CONFIG` | `HitboxCollisionConfig` | +| | `REPULSION_CONFIG` | `RepulsionConfig` | +| | `INTEGER_COMPARISON_OPERATOR` | `IntegerComparisonOperator` | +| | `INTEGER_OPERATION` | `IntegerOperation` | + +{{< /details >}} diff --git a/content/core-concepts/commands/command-context.en.md b/content/core-concepts/commands/command-context.en.md new file mode 100644 index 0000000..c126cac --- /dev/null +++ b/content/core-concepts/commands/command-context.en.md @@ -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 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 playerArg; +private final RequiredArg 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 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 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 targetArg; + +@Override +protected CompletableFuture 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 worldArg; + +@Override +protected CompletableFuture 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 execute(CommandContext context) { + PlayerRef playerRef = context.get(playerArg); + + // Check if player is online via ECS reference + Ref 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 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 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 itemArg; + private final OptionalArg targetArg; + private final DefaultArg 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 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 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; + } +} +``` diff --git a/content/core-concepts/commands/command-context.fr.md b/content/core-concepts/commands/command-context.fr.md new file mode 100644 index 0000000..c126cac --- /dev/null +++ b/content/core-concepts/commands/command-context.fr.md @@ -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 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 playerArg; +private final RequiredArg 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 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 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 targetArg; + +@Override +protected CompletableFuture 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 worldArg; + +@Override +protected CompletableFuture 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 execute(CommandContext context) { + PlayerRef playerRef = context.get(playerArg); + + // Check if player is online via ECS reference + Ref 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 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 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 itemArg; + private final OptionalArg targetArg; + private final DefaultArg 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 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 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; + } +} +``` diff --git a/content/core-concepts/commands/creating-commands.en.md b/content/core-concepts/commands/creating-commands.en.md new file mode 100644 index 0000000..7966ebb --- /dev/null +++ b/content/core-concepts/commands/creating-commands.en.md @@ -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 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 targetArg; + private final RequiredArg 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 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 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 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 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 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 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 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> playersArg; + +public KickAllCommand() { + super("kickall", "Kick multiple players"); + + playersArg = withListRequiredArg("players", "Players to kick", ArgTypes.STRING); +} + +@Override +protected CompletableFuture execute(CommandContext context) { + List 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 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"); +} +``` diff --git a/content/core-concepts/commands/creating-commands.fr.md b/content/core-concepts/commands/creating-commands.fr.md new file mode 100644 index 0000000..394f5a7 --- /dev/null +++ b/content/core-concepts/commands/creating-commands.fr.md @@ -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 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 targetArg; + private final RequiredArg 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 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 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 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 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 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 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 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> playersArg; + +public KickAllCommand() { + super("kickall", "Expulser plusieurs joueurs"); + + playersArg = withListRequiredArg("players", "Joueurs à expulser", ArgTypes.STRING); +} + +@Override +protected CompletableFuture execute(CommandContext context) { + List 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 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"); +} +``` diff --git a/content/core-concepts/commands/subcommands.en.md b/content/core-concepts/commands/subcommands.en.md new file mode 100644 index 0000000..e873406 --- /dev/null +++ b/content/core-concepts/commands/subcommands.en.md @@ -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 execute(CommandContext context) { + // This runs when no subcommand is specified + context.sender().sendMessage(Message.raw("Usage: /admin ")); + return null; + } +} +``` + +### Subcommand Implementation + +```java +public class KickSubCommand extends AbstractCommand { + + private final RequiredArg playerArg; + private final OptionalArg 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 execute(CommandContext context) { + PlayerRef target = context.get(playerArg); + String reason = context.get(reasonArg); + + // Check if player is online via ECS reference + Ref 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 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 targetArg; + + public TpCommand() { + super("tp", "Teleport command"); + + // Main variant: /tp + targetArg = withRequiredArg("target", "Player to teleport to", ArgTypes.PLAYER_REF); + + // Add variant: /tp + addUsageVariant(new TpToPlayerVariant()); + + // Add variant: /tp + addUsageVariant(new TpToPositionVariant()); + } + + @Override + protected CompletableFuture 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 playerArg; + private final RequiredArg 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 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"); +} +``` diff --git a/content/core-concepts/commands/subcommands.fr.md b/content/core-concepts/commands/subcommands.fr.md new file mode 100644 index 0000000..e873406 --- /dev/null +++ b/content/core-concepts/commands/subcommands.fr.md @@ -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 execute(CommandContext context) { + // This runs when no subcommand is specified + context.sender().sendMessage(Message.raw("Usage: /admin ")); + return null; + } +} +``` + +### Subcommand Implementation + +```java +public class KickSubCommand extends AbstractCommand { + + private final RequiredArg playerArg; + private final OptionalArg 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 execute(CommandContext context) { + PlayerRef target = context.get(playerArg); + String reason = context.get(reasonArg); + + // Check if player is online via ECS reference + Ref 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 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 targetArg; + + public TpCommand() { + super("tp", "Teleport command"); + + // Main variant: /tp + targetArg = withRequiredArg("target", "Player to teleport to", ArgTypes.PLAYER_REF); + + // Add variant: /tp + addUsageVariant(new TpToPlayerVariant()); + + // Add variant: /tp + addUsageVariant(new TpToPositionVariant()); + } + + @Override + protected CompletableFuture 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 playerArg; + private final RequiredArg 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 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"); +} +``` diff --git a/content/core-concepts/events/_index.en.md b/content/core-concepts/events/_index.en.md new file mode 100644 index 0000000..3d14942 --- /dev/null +++ b/content/core-concepts/events/_index.en.md @@ -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 >}} diff --git a/content/core-concepts/events/_index.fr.md b/content/core-concepts/events/_index.fr.md new file mode 100644 index 0000000..842f29b --- /dev/null +++ b/content/core-concepts/events/_index.fr.md @@ -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 >}} diff --git a/content/core-concepts/events/async-events.en.md b/content/core-concepts/events/async-events.en.md new file mode 100644 index 0000000..fbadcb1 --- /dev/null +++ b/content/core-concepts/events/async-events.en.md @@ -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` support asynchronous handling: + +```java +public interface IAsyncEvent extends IBaseEvent { + // 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` - 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 spamCheckFuture = + CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent())); + + CompletableFuture 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` and `ICancellable`: + +```java +public class PlayerChatEvent implements IAsyncEvent, ICancellable { + // sender: PlayerRef + // targets: List + // 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 filterContent( + CompletableFuture 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 processChat( + CompletableFuture 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 logChat( + CompletableFuture 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; + }); +}); +``` diff --git a/content/core-concepts/events/async-events.fr.md b/content/core-concepts/events/async-events.fr.md new file mode 100644 index 0000000..fbadcb1 --- /dev/null +++ b/content/core-concepts/events/async-events.fr.md @@ -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` support asynchronous handling: + +```java +public interface IAsyncEvent extends IBaseEvent { + // 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` - 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 spamCheckFuture = + CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent())); + + CompletableFuture 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` and `ICancellable`: + +```java +public class PlayerChatEvent implements IAsyncEvent, ICancellable { + // sender: PlayerRef + // targets: List + // 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 filterContent( + CompletableFuture 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 processChat( + CompletableFuture 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 logChat( + CompletableFuture 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; + }); +}); +``` diff --git a/content/core-concepts/events/cancellable-events.en.md b/content/core-concepts/events/cancellable-events.en.md new file mode 100644 index 0000000..8a42fa2 --- /dev/null +++ b/content/core-concepts/events/cancellable-events.en.md @@ -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 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 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 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; + } +} +``` diff --git a/content/core-concepts/events/cancellable-events.fr.md b/content/core-concepts/events/cancellable-events.fr.md new file mode 100644 index 0000000..8a42fa2 --- /dev/null +++ b/content/core-concepts/events/cancellable-events.fr.md @@ -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 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 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 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; + } +} +``` diff --git a/content/core-concepts/events/event-priorities.en.md b/content/core-concepts/events/event-priorities.en.md new file mode 100644 index 0000000..f8c72cd --- /dev/null +++ b/content/core-concepts/events/event-priorities.en.md @@ -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 >}} diff --git a/content/core-concepts/events/event-priorities.fr.md b/content/core-concepts/events/event-priorities.fr.md new file mode 100644 index 0000000..4293466 --- /dev/null +++ b/content/core-concepts/events/event-priorities.fr.md @@ -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()); + } +}); +``` diff --git a/content/core-concepts/events/event-reference/_index.en.md b/content/core-concepts/events/event-reference/_index.en.md new file mode 100644 index 0000000..8115fa2 --- /dev/null +++ b/content/core-concepts/events/event-reference/_index.en.md @@ -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 diff --git a/content/core-concepts/events/event-reference/_index.fr.md b/content/core-concepts/events/event-reference/_index.fr.md new file mode 100644 index 0000000..006b002 --- /dev/null +++ b/content/core-concepts/events/event-reference/_index.fr.md @@ -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 diff --git a/content/core-concepts/events/event-reference/block-events.en.md b/content/core-concepts/events/event-reference/block-events.en.md new file mode 100644 index 0000000..8e79607 --- /dev/null +++ b/content/core-concepts/events/event-reference/block-events.en.md @@ -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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/block-events.fr.md b/content/core-concepts/events/event-reference/block-events.fr.md new file mode 100644 index 0000000..0f2310b --- /dev/null +++ b/content/core-concepts/events/event-reference/block-events.fr.md @@ -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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/entity-events.en.md b/content/core-concepts/events/event-reference/entity-events.en.md new file mode 100644 index 0000000..ca835b2 --- /dev/null +++ b/content/core-concepts/events/event-reference/entity-events.en.md @@ -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` | 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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/entity-events.fr.md b/content/core-concepts/events/event-reference/entity-events.fr.md new file mode 100644 index 0000000..5194119 --- /dev/null +++ b/content/core-concepts/events/event-reference/entity-events.fr.md @@ -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` | 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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/permission-events.en.md b/content/core-concepts/events/event-reference/permission-events.en.md new file mode 100644 index 0000000..2984bc9 --- /dev/null +++ b/content/core-concepts/events/event-reference/permission-events.en.md @@ -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` | 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 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` | 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 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` | 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 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` | 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 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 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 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> 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 perms = cachedPermissions.get(event.getPlayerUuid()); + if (perms != null) { + perms.removeAll(event.getRemovedPermissions()); + } + }); + } + + public Set 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 >}} diff --git a/content/core-concepts/events/event-reference/permission-events.fr.md b/content/core-concepts/events/event-reference/permission-events.fr.md new file mode 100644 index 0000000..f6fdbd6 --- /dev/null +++ b/content/core-concepts/events/event-reference/permission-events.fr.md @@ -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> tempPerms = new HashMap<>(); + + @Override + public void start() { + getEventRegistry().register(PermissionCheckEvent.class, event -> { + UUID uuid = event.getPlayer().getUuid(); + Set 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 perms = tempPerms.get(player); + if (perms != null) { + perms.remove(permission); + } + } +} +``` diff --git a/content/core-concepts/events/event-reference/player-events.en.md b/content/core-concepts/events/event-reference/player-events.en.md new file mode 100644 index 0000000..ddab357 --- /dev/null +++ b/content/core-concepts/events/event-reference/player-events.en.md @@ -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` | 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` | 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` | 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` | 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` | 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)` - 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` | 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` | 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 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 bannedWords = new HashSet<>(); + private final Map 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; + } + } + }); + } +} +``` diff --git a/content/core-concepts/events/event-reference/player-events.fr.md b/content/core-concepts/events/event-reference/player-events.fr.md new file mode 100644 index 0000000..ddab357 --- /dev/null +++ b/content/core-concepts/events/event-reference/player-events.fr.md @@ -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` | 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` | 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` | 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` | 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` | 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)` - 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` | 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` | 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 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 bannedWords = new HashSet<>(); + private final Map 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; + } + } + }); + } +} +``` diff --git a/content/core-concepts/events/event-reference/server-events.en.md b/content/core-concepts/events/event-reference/server-events.en.md new file mode 100644 index 0000000..2048325 --- /dev/null +++ b/content/core-concepts/events/event-reference/server-events.en.md @@ -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 loadedDependencies = new HashMap<>(); + private final Set 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> 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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/server-events.fr.md b/content/core-concepts/events/event-reference/server-events.fr.md new file mode 100644 index 0000000..89e0b27 --- /dev/null +++ b/content/core-concepts/events/event-reference/server-events.fr.md @@ -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 loadedDependencies = new HashMap<>(); + private final Set 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> 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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/world-events.en.md b/content/core-concepts/events/event-reference/world-events.en.md new file mode 100644 index 0000000..7923965 --- /dev/null +++ b/content/core-concepts/events/event-reference/world-events.en.md @@ -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` | All loaded worlds | +{{< /tab >}} +{{< tab >}} +- `getWorlds()` - Returns list of all loaded worlds +{{< /tab >}} +{{< tab >}} +```java +getEventRegistry().register(AllWorldsLoadedEvent.class, event -> { + List 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 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 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 >}} diff --git a/content/core-concepts/events/event-reference/world-events.fr.md b/content/core-concepts/events/event-reference/world-events.fr.md new file mode 100644 index 0000000..63507c7 --- /dev/null +++ b/content/core-concepts/events/event-reference/world-events.fr.md @@ -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` | 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 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 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 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 >}} diff --git a/content/core-concepts/events/event-system.en.md b/content/core-concepts/events/event-system.en.md new file mode 100644 index 0000000..2661230 --- /dev/null +++ b/content/core-concepts/events/event-system.en.md @@ -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 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` | Base interface for all events | +| `IEvent` | Synchronous events | +| `IAsyncEvent` | 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>> eventClasses = + HytaleServer.get().getEventBus().getRegisteredEventClasses(); + +// Get registered event names +Set 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 +``` diff --git a/content/core-concepts/events/event-system.fr.md b/content/core-concepts/events/event-system.fr.md new file mode 100644 index 0000000..2661230 --- /dev/null +++ b/content/core-concepts/events/event-system.fr.md @@ -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 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` | Base interface for all events | +| `IEvent` | Synchronous events | +| `IAsyncEvent` | 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>> eventClasses = + HytaleServer.get().getEventBus().getRegisteredEventClasses(); + +// Get registered event names +Set 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 +``` diff --git a/content/core-concepts/registries.en.md b/content/core-concepts/registries.en.md new file mode 100644 index 0000000..7214bf7 --- /dev/null +++ b/content/core-concepts/registries.en.md @@ -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 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) 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. diff --git a/content/core-concepts/registries.fr.md b/content/core-concepts/registries.fr.md new file mode 100644 index 0000000..95515fa --- /dev/null +++ b/content/core-concepts/registries.fr.md @@ -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 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) 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. diff --git a/content/core-concepts/tasks/_index.en.md b/content/core-concepts/tasks/_index.en.md new file mode 100644 index 0000000..702cf43 --- /dev/null +++ b/content/core-concepts/tasks/_index.en.md @@ -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)); + }); +}); +``` diff --git a/content/core-concepts/tasks/_index.fr.md b/content/core-concepts/tasks/_index.fr.md new file mode 100644 index 0000000..d2434aa --- /dev/null +++ b/content/core-concepts/tasks/_index.fr.md @@ -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)); + }); +}); +``` diff --git a/content/core-concepts/tasks/async-operations.en.md b/content/core-concepts/tasks/async-operations.en.md new file mode 100644 index 0000000..aade53c --- /dev/null +++ b/content/core-concepts/tasks/async-operations.en.md @@ -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 ref = playerRef.getReference(); + if (ref != null) { + applyData(ref, data); + } + }); + }); +}); +``` + +### Thread-Safe Collections + +```java +// Use concurrent collections for shared data +private final Map playerData = new ConcurrentHashMap<>(); +private final Set 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 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 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 players = new ArrayList<>(); + world.getEntityStore().forEach((ref, store) -> { + PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType()); + if (playerRef != null) { + players.add(playerRef); + } + }); + + CompletableFuture.runAsync(() -> { + Map results = new HashMap<>(); + + for (PlayerRef playerRef : players) { + results.put(playerRef.getUuid(), computeResult(playerRef)); + } + + world.execute(() -> { + for (PlayerRef playerRef : players) { + Ref 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` +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 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 >}} diff --git a/content/core-concepts/tasks/async-operations.fr.md b/content/core-concepts/tasks/async-operations.fr.md new file mode 100644 index 0000000..ab13594 --- /dev/null +++ b/content/core-concepts/tasks/async-operations.fr.md @@ -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 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 playerData = new ConcurrentHashMap<>(); +private final Set 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 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 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 players = new ArrayList<>(); + world.getEntityStore().forEach((ref, store) -> { + PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType()); + if (playerRef != null) { + players.add(playerRef); + } + }); + + CompletableFuture.runAsync(() -> { + Map results = new HashMap<>(); + + for (PlayerRef playerRef : players) { + results.put(playerRef.getUuid(), computeResult(playerRef)); + } + + world.execute(() -> { + for (PlayerRef playerRef : players) { + Ref 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` +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 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 >}} diff --git a/content/core-concepts/tasks/task-registry.en.md b/content/core-concepts/tasks/task-registry.en.md new file mode 100644 index 0000000..be6aed8 --- /dev/null +++ b/content/core-concepts/tasks/task-registry.en.md @@ -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 { + // Register a CompletableFuture task + public TaskRegistration registerTask(CompletableFuture task); + + // Register a ScheduledFuture task + public TaskRegistration registerTask(ScheduledFuture task); +} +``` + +The TaskRegistry tracks tasks for proper cleanup during plugin shutdown. + +## Asynchronous Tasks + +Use `CompletableFuture` for async operations: + +```java +// Run async operation +CompletableFuture 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 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 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) 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 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 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` +- 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 +// }); +``` diff --git a/content/core-concepts/tasks/task-registry.fr.md b/content/core-concepts/tasks/task-registry.fr.md new file mode 100644 index 0000000..15e2d73 --- /dev/null +++ b/content/core-concepts/tasks/task-registry.fr.md @@ -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 { + // Enregistrer une tâche CompletableFuture + public TaskRegistration registerTask(CompletableFuture task); + + // Enregistrer une tâche ScheduledFuture + public TaskRegistration registerTask(ScheduledFuture 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 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 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 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) 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 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 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` +- 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 +// }); +``` diff --git a/content/core-concepts/threading.en.md b/content/core-concepts/threading.en.md new file mode 100644 index 0000000..15f5616 --- /dev/null +++ b/content/core-concepts/threading.en.md @@ -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 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 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}` diff --git a/content/core-concepts/threading.fr.md b/content/core-concepts/threading.fr.md new file mode 100644 index 0000000..9d9bc00 --- /dev/null +++ b/content/core-concepts/threading.fr.md @@ -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 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 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}` diff --git a/content/gameplay-systems/_index.en.md b/content/gameplay-systems/_index.en.md new file mode 100644 index 0000000..3b814fa --- /dev/null +++ b/content/gameplay-systems/_index.en.md @@ -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 diff --git a/content/gameplay-systems/_index.fr.md b/content/gameplay-systems/_index.fr.md new file mode 100644 index 0000000..e88fdbd --- /dev/null +++ b/content/gameplay-systems/_index.fr.md @@ -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 diff --git a/content/gameplay-systems/camera-effects.en.md b/content/gameplay-systems/camera-effects.en.md new file mode 100644 index 0000000..b7216b0 --- /dev/null +++ b/content/gameplay-systems/camera-effects.en.md @@ -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 [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 { + // Generates network packets for camera shake +} + +public class ViewBobbingPacketGenerator implements PacketGenerator { + // 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. diff --git a/content/gameplay-systems/camera-effects.fr.md b/content/gameplay-systems/camera-effects.fr.md new file mode 100644 index 0000000..36e61dc --- /dev/null +++ b/content/gameplay-systems/camera-effects.fr.md @@ -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 [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 { + // Genere paquets reseau pour tremblement +} + +public class ViewBobbingPacketGenerator implements PacketGenerator { + // 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. diff --git a/content/gameplay-systems/farming.en.md b/content/gameplay-systems/farming.en.md new file mode 100644 index 0000000..54be0ee --- /dev/null +++ b/content/gameplay-systems/farming.en.md @@ -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 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 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 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 diff --git a/content/gameplay-systems/farming.fr.md b/content/gameplay-systems/farming.fr.md new file mode 100644 index 0000000..1c0b55a --- /dev/null +++ b/content/gameplay-systems/farming.fr.md @@ -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 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 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 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 diff --git a/content/gameplay-systems/memories.en.md b/content/gameplay-systems/memories.en.md new file mode 100644 index 0000000..181c3ab --- /dev/null +++ b/content/gameplay-systems/memories.en.md @@ -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 CODEC; +} +``` + +### MemoryProvider + +Provides memory instances and configuration: + +```java +public interface MemoryProvider { + String getId(); + BuilderCodec getCodec(); + Map> getAllMemories(); +} +``` + +### NPCMemory + +Memories discovered from NPCs: + +```java +public class NPCMemory implements Memory { + // Collected when player approaches NPC within radius +} + +public class NPCMemoryProvider implements MemoryProvider { + public double getCollectionRadius(); // From config +} +``` + +## Player Memories Component + +```java +ComponentType 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 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 ` | 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 collectionRadius; + + public Object2DoubleMap 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> allMemories = memories.getAllMemories(); + +// Register custom memory provider +memories.registerMemoryProvider(new CustomMemoryProvider()); + +// Check player memories component +PlayerMemories playerMemories = store.getComponent(ref, PlayerMemories.getComponentType()); +``` diff --git a/content/gameplay-systems/memories.fr.md b/content/gameplay-systems/memories.fr.md new file mode 100644 index 0000000..611944e --- /dev/null +++ b/content/gameplay-systems/memories.fr.md @@ -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 CODEC; +} +``` + +### MemoryProvider + +Fournit les instances et configuration de memoire: + +```java +public interface MemoryProvider { + String getId(); + BuilderCodec getCodec(); + Map> 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 { + public double getCollectionRadius(); // Depuis config +} +``` + +## Composant Player Memories + +```java +ComponentType 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 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 ` | 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 collectionRadius; + + public Object2DoubleMap 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> allMemories = memories.getAllMemories(); + +// Enregistrer fournisseur de memoire personnalise +memories.registerMemoryProvider(new CustomMemoryProvider()); + +// Verifier composant memoires joueur +PlayerMemories playerMemories = store.getComponent(ref, PlayerMemories.getComponentType()); +``` diff --git a/content/gameplay-systems/npc-objectives.en.md b/content/gameplay-systems/npc-objectives.en.md new file mode 100644 index 0000000..2060aed --- /dev/null +++ b/content/gameplay-systems/npc-objectives.en.md @@ -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 store, + Ref 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 store) { + // Get spawn position from asset configuration + Vector3i spawnPosition = getAsset().getWorldLocationProvider() + .runCondition(world, objectivePosition); + + // Spawn the bounty NPC + Pair, 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 store, Ref 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 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); +``` diff --git a/content/gameplay-systems/npc-objectives.fr.md b/content/gameplay-systems/npc-objectives.fr.md new file mode 100644 index 0000000..7099fc8 --- /dev/null +++ b/content/gameplay-systems/npc-objectives.fr.md @@ -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 store, + Ref 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 store) { + // Obtenir la position de spawn depuis la configuration + Vector3i spawnPosition = getAsset().getWorldLocationProvider() + .runCondition(world, objectivePosition); + + // Faire apparaitre le NPC bounty + Pair, 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 store, Ref 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 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); +``` diff --git a/content/gameplay-systems/objectives.en.md b/content/gameplay-systems/objectives.en.md new file mode 100644 index 0000000..cbdede5 --- /dev/null +++ b/content/gameplay-systems/objectives.en.md @@ -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 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 [player]` | Start an objective | +| `/objective complete [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 { + 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 +``` diff --git a/content/gameplay-systems/objectives.fr.md b/content/gameplay-systems/objectives.fr.md new file mode 100644 index 0000000..3b0b9c1 --- /dev/null +++ b/content/gameplay-systems/objectives.fr.md @@ -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 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 [joueur]` | Demarrer un objectif | +| `/objective complete [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 { + 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 +``` diff --git a/content/gameplay-systems/reputation.en.md b/content/gameplay-systems/reputation.en.md new file mode 100644 index 0000000..1d567f3 --- /dev/null +++ b/content/gameplay-systems/reputation.en.md @@ -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> { + 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> { + 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 reputationData = playerConfigData.getReputationData(); +``` + +### Per-World Storage + +```java +ReputationDataResource resource = world.getEntityStore().getStore() + .getResource(ReputationPlugin.get().getReputationDataResourceType()); +Object2IntMap worldReputation = resource.getReputationStats(); +``` + +## NPC Faction Assignment + +### ReputationGroupComponent + +Assigns NPCs to factions: + +```java +ComponentType 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 [player]` | Show reputation value | +| `/reputation set [player]` | Set reputation | +| `/reputation add [player]` | Add reputation | +| `/reputation rank [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 +``` diff --git a/content/gameplay-systems/reputation.fr.md b/content/gameplay-systems/reputation.fr.md new file mode 100644 index 0000000..b8613e3 --- /dev/null +++ b/content/gameplay-systems/reputation.fr.md @@ -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> { + 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> { + 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 reputationData = playerConfigData.getReputationData(); +``` + +### Stockage Par-Monde + +```java +ReputationDataResource resource = world.getEntityStore().getStore() + .getResource(ReputationPlugin.get().getReputationDataResourceType()); +Object2IntMap worldReputation = resource.getReputationStats(); +``` + +## Assignation Faction NPC + +### ReputationGroupComponent + +Assigne les NPCs aux factions: + +```java +ComponentType 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 [joueur]` | Afficher valeur reputation | +| `/reputation set [joueur]` | Definir reputation | +| `/reputation add [joueur]` | Ajouter reputation | +| `/reputation rank [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 +``` diff --git a/content/gameplay-systems/shop.en.md b/content/gameplay-systems/shop.en.md new file mode 100644 index 0000000..c317c6f --- /dev/null +++ b/content/gameplay-systems/shop.en.md @@ -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> { + 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> { + // 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> 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(); +``` diff --git a/content/gameplay-systems/shop.fr.md b/content/gameplay-systems/shop.fr.md new file mode 100644 index 0000000..e2ea0be --- /dev/null +++ b/content/gameplay-systems/shop.fr.md @@ -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> { + 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> { + // 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> 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(); +``` diff --git a/content/getting-started/_index.en.md b/content/getting-started/_index.en.md new file mode 100644 index 0000000..dd9493d --- /dev/null +++ b/content/getting-started/_index.en.md @@ -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 >}} diff --git a/content/getting-started/_index.fr.md b/content/getting-started/_index.fr.md new file mode 100644 index 0000000..206522b --- /dev/null +++ b/content/getting-started/_index.fr.md @@ -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 >}} diff --git a/content/getting-started/building-and-running.en.md b/content/getting-started/building-and-running.en.md new file mode 100644 index 0000000..e5eb42a --- /dev/null +++ b/content/getting-started/building-and-running.en.md @@ -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 diff --git a/content/getting-started/building-and-running.fr.md b/content/getting-started/building-and-running.fr.md new file mode 100644 index 0000000..64637ea --- /dev/null +++ b/content/getting-started/building-and-running.fr.md @@ -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 diff --git a/content/getting-started/creating-a-plugin.en.md b/content/getting-started/creating-a-plugin.en.md new file mode 100644 index 0000000..1f66a09 --- /dev/null +++ b/content/getting-started/creating-a-plugin.en.md @@ -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. diff --git a/content/getting-started/creating-a-plugin.fr.md b/content/getting-started/creating-a-plugin.fr.md new file mode 100644 index 0000000..8d19fd6 --- /dev/null +++ b/content/getting-started/creating-a-plugin.fr.md @@ -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. diff --git a/content/getting-started/plugin-lifecycle.en.md b/content/getting-started/plugin-lifecycle.en.md new file mode 100644 index 0000000..640e7db --- /dev/null +++ b/content/getting-started/plugin-lifecycle.en.md @@ -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 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. diff --git a/content/getting-started/plugin-lifecycle.fr.md b/content/getting-started/plugin-lifecycle.fr.md new file mode 100644 index 0000000..16beb6c --- /dev/null +++ b/content/getting-started/plugin-lifecycle.fr.md @@ -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 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. diff --git a/content/getting-started/prerequisites.en.md b/content/getting-started/prerequisites.en.md new file mode 100644 index 0000000..07b5c70 --- /dev/null +++ b/content/getting-started/prerequisites.en.md @@ -0,0 +1,72 @@ +--- +title: Prerequisites +type: docs +weight: 1 +--- + +Before you start developing Hytale plugins, you need to set up your development environment. + +## Java 25 + +Hytale plugins require **Java 25** or later. Download and install the JDK from: + +- [Eclipse Temurin](https://adoptium.net/) (Recommended) +- [Oracle JDK](https://www.oracle.com/java/technologies/downloads/) + +Verify your installation: + +```bash +java --version +# Should output: openjdk 25.x.x or similar +``` + +## Gradle + +Hytale plugins use **Gradle** as the build system. You can either: + +1. Use the Gradle Wrapper (recommended) - included in the project template +2. Install Gradle globally from [gradle.org](https://gradle.org/install/) + +## IDE (Optional) + +We recommend using one of these IDEs: + +{{< tabs items="IntelliJ IDEA,Eclipse,VS Code" >}} +{{< tab >}} +**IntelliJ IDEA** (Recommended) + +1. Download [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community or Ultimate) +2. Open your plugin project folder +3. IntelliJ will automatically detect the Gradle project +4. Wait for the project to sync + +{{< /tab >}} +{{< tab >}} +**Eclipse** + +1. Download [Eclipse IDE for Java Developers](https://www.eclipse.org/downloads/) +2. Install the Buildship Gradle Integration plugin +3. Import as a Gradle project + +{{< /tab >}} +{{< tab >}} +**VS Code** + +1. Install [VS Code](https://code.visualstudio.com/) +2. Install the Extension Pack for Java +3. Install the Gradle for Java extension +4. Open the project folder + +{{< /tab >}} +{{< /tabs >}} + +## Hytale Game + +You need the Hytale game installed: + +1. Download and install the [Hytale Launcher](https://hytale.com/) +2. Launch the game at least once to download all files + +## Next Steps + +Once your environment is set up, proceed to [Building and Running](../building-and-running) to set up the server and build your first plugin. diff --git a/content/getting-started/prerequisites.fr.md b/content/getting-started/prerequisites.fr.md new file mode 100644 index 0000000..ebd7c87 --- /dev/null +++ b/content/getting-started/prerequisites.fr.md @@ -0,0 +1,72 @@ +--- +title: Prérequis +type: docs +weight: 1 +--- + +Avant de commencer à développer des plugins Hytale, vous devez configurer votre environnement de développement. + +## Java 25 + +Les plugins Hytale nécessitent **Java 25** ou supérieur. Téléchargez et installez le JDK depuis : + +- [Eclipse Temurin](https://adoptium.net/) (Recommandé) +- [Oracle JDK](https://www.oracle.com/java/technologies/downloads/) + +Vérifiez votre installation : + +```bash +java --version +# Devrait afficher : openjdk 25.x.x ou similaire +``` + +## Gradle + +Les plugins Hytale utilisent **Gradle** comme système de build. Vous pouvez soit : + +1. Utiliser le Gradle Wrapper (recommandé) - inclus dans le template de projet +2. Installer Gradle globalement depuis [gradle.org](https://gradle.org/install/) + +## IDE (Optionnel) + +Nous recommandons l'utilisation d'un de ces IDEs : + +{{< tabs items="IntelliJ IDEA,Eclipse,VS Code" >}} +{{< tab >}} +**IntelliJ IDEA** (Recommandé) + +1. Téléchargez [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community ou Ultimate) +2. Ouvrez le dossier de votre projet plugin +3. IntelliJ détectera automatiquement le projet Gradle +4. Attendez la synchronisation du projet + +{{< /tab >}} +{{< tab >}} +**Eclipse** + +1. Téléchargez [Eclipse IDE for Java Developers](https://www.eclipse.org/downloads/) +2. Installez le plugin Buildship Gradle Integration +3. Importez comme projet Gradle + +{{< /tab >}} +{{< tab >}} +**VS Code** + +1. Installez [VS Code](https://code.visualstudio.com/) +2. Installez l'Extension Pack for Java +3. Installez l'extension Gradle for Java +4. Ouvrez le dossier du projet + +{{< /tab >}} +{{< /tabs >}} + +## Jeu Hytale + +Vous avez besoin du jeu Hytale installé : + +1. Téléchargez et installez le [Hytale Launcher](https://hytale.com/) +2. Lancez le jeu au moins une fois pour télécharger tous les fichiers + +## Étapes Suivantes + +Une fois votre environnement configuré, passez à [Compilation et Exécution](../building-and-running) pour configurer le serveur et compiler votre premier plugin. diff --git a/content/patch-note/2026.01.13-50e69c385-2026.01.15-c04fdfe10-patch-note.md b/content/patch-note/2026.01.13-50e69c385-2026.01.15-c04fdfe10-patch-note.md new file mode 100644 index 0000000..26c88b6 --- /dev/null +++ b/content/patch-note/2026.01.13-50e69c385-2026.01.15-c04fdfe10-patch-note.md @@ -0,0 +1,761 @@ +# Patch Notes: Hytale Server + +## Version 2026.01.13-50e69c385 → 2026.01.15-c04fdfe10 + +**Date de release** : 15 janvier 2026 +**Build précédent** : `50e69c385` (13 janvier 2026) +**Build actuel** : `c04fdfe10` (15 janvier 2026) + +--- + +### Statistiques + +| Métrique | Valeur | +|----------|--------| +| Fichiers modifiés | 9 | +| Insertions | +55 | +| Suppressions | -18 | +| Delta net | +37 lignes | + +--- + +## Table des matières + +1. [Authentification & Sécurité JWT](#1-authentification--sécurité-jwt) +2. [Gestion d'échec d'authentification serveur](#2-gestion-déchec-dauthentification-serveur) +3. [Transport QUIC - Optimisations réseau](#3-transport-quic---optimisations-réseau) +4. [Timeout Setup Handler](#4-timeout-setup-handler) +5. [Physique des Items - Collisions latérales](#5-physique-des-items---collisions-latérales) +6. [Nouvelle raison d'arrêt MISSING_ASSETS](#6-nouvelle-raison-darrêt-missing_assets) +7. [Validation des Asset Packs](#7-validation-des-asset-packs) +8. [Télémétrie Sentry enrichie](#8-télémétrie-sentry-enrichie) +9. [Fix NullPointerException CraftingManager](#9-fix-nullpointerexception-craftingmanager) + +--- + +## 1. Authentification & Sécurité JWT + +**Fichier** : `com/hypixel/hytale/server/core/auth/JWTValidator.java` +**Lignes modifiées** : 31, 87-99, 293-309, 355-372 + +### 1.1 Nouvelle constante (non utilisée) + +```java +private static final long CLOCK_SKEW_SECONDS = 300L; +``` + +Une constante a été ajoutée pour centraliser la valeur du clock skew, mais elle n'est **pas encore utilisée** dans le code - les valeurs `300L` sont hardcodées directement dans les méthodes de validation. + +### 1.2 Augmentation du Clock Skew : 60s → 300s + +Le **clock skew** (tolérance de désynchronisation d'horloge entre client et serveur) passe de **1 minute à 5 minutes**. Cette modification affecte la validation de trois types de tokens JWT : + +#### Méthode `validateToken()` (lignes 87-99) + +| Claim | Avant | Après | Comportement | +|-------|-------|-------|--------------| +| `exp` (expires at) | `now >= exp + 60` | `now >= exp + 300` | Token accepté jusqu'à 5 min après expiration | +| `nbf` (not before) | `now < nbf - 60` | `now < nbf - 300` | Token accepté 5 min avant validité | +| `iat` (issued at) | **Non vérifié** | `iat > now + 300` | **NOUVEAU** : Rejette tokens du futur | + +**Code ajouté** (lignes 96-99) : +```java +if (claims.issuedAt != null && claims.issuedAt > nowSeconds + 300L) { + LOGGER.at(Level.WARNING).log("Token issued in the future (iat: %d, now: %d)", + (Object)claims.issuedAt, nowSeconds); + return null; +} +``` + +#### Méthode `validateIdentityToken()` (lignes 293-309) + +| Claim | Avant | Après | +|-------|-------|-------| +| `exp` | `now >= exp + 60` | `now >= exp + 300` | +| `nbf` | `now < nbf - 60` | `now < nbf - 300` | +| `iat` | `iat > now + 60` | `iat > now + 300` | + +La validation `iat` existait déjà pour les identity tokens, seule la valeur du clock skew a changé. + +#### Méthode `validateSessionToken()` (lignes 355-372) + +| Claim | Avant | Après | Comportement | +|-------|-------|-------|--------------| +| `exp` | `now >= exp + 60` | `now >= exp + 300` | Token accepté jusqu'à 5 min après expiration | +| `nbf` | `now < nbf - 60` | `now < nbf - 300` | Token accepté 5 min avant validité | +| `iat` | **Non vérifié** | `iat > now + 300` | **NOUVEAU** : Rejette tokens du futur | + +**Code ajouté** (lignes 368-371) : +```java +if (claims.issuedAt != null && claims.issuedAt > nowSeconds + 300L) { + LOGGER.at(Level.WARNING).log("Session token issued in the future (iat: %d, now: %d)", + (Object)claims.issuedAt, nowSeconds); + return null; +} +``` + +### 1.3 Impact + +| Aspect | Effet | +|--------|-------| +| **Compatibilité** | Meilleure tolérance pour clients avec horloge désynchronisée | +| **Sécurité** | Protection contre tokens pré-générés pour utilisation future | +| **Stabilité** | Réduction des erreurs de validation dues au décalage horaire | + +### 1.4 Contexte technique + +Le `JWTValidator` gère trois types de tokens avec des rôles distincts : + +1. **Auth Token (JWTClaims)** : Authentification client, contient username, IP, certificate fingerprint +2. **Identity Token (IdentityTokenClaims)** : Identité du joueur, requiert scopes `hytale:client` ou `hytale:editor` +3. **Session Token (SessionTokenClaims)** : Sessions serveur, validation minimale + +Le système utilise un cache JWKS (JSON Web Key Set) avec durée de 1 heure et mécanisme de retry intelligent en cas d'échec de vérification de signature. + +--- + +## 2. Gestion d'échec d'authentification serveur + +**Fichier** : `com/hypixel/hytale/server/core/io/handlers/login/HandshakeHandler.java` +**Lignes modifiées** : 291-298 (dans le callback `exceptionally`) + +### 2.1 Modification du comportement + +Dans la méthode `exchangeServerAuthGrant()`, le callback `.exceptionally()` (gestion des erreurs async) a été modifié. Lorsqu'une exception se produit pendant l'échange de server token (`AuthState.EXCHANGING_SERVER_TOKEN`) : + +**Avant** : +```java +.exceptionally(ex -> { + LOGGER.at(Level.WARNING).withCause(ex).log("Error exchanging server auth grant"); + this.channel.eventLoop().execute(() -> { + if (this.authState != AuthState.EXCHANGING_SERVER_TOKEN) { + return; + } + byte[] passwordChallenge = this.generatePasswordChallengeIfNeeded(); + this.completeAuthentication(passwordChallenge); + }); + return null; +}); +``` + +**Après** : +```java +.exceptionally(ex -> { + LOGGER.at(Level.WARNING).withCause(ex).log("Error exchanging server auth grant"); + this.channel.eventLoop().execute(() -> { + if (this.authState != AuthState.EXCHANGING_SERVER_TOKEN) { + return; + } + this.disconnect("Server authentication failed - please try again later"); + }); + return null; +}); +``` + +### 2.2 Impact sur la sécurité + +| Aspect | Avant | Après | +|--------|-------|-------| +| Échec server token exchange | Authentification continuait silencieusement | Déconnexion immédiate | +| Bypass potentiel | Possible dans certains cas de timeout | Impossible | +| Message utilisateur | Aucun | Message explicite | + +### 2.3 Contexte : Flow d'authentification mutuelle + +Le `HandshakeHandler` orchestre un flow d'authentification mutuelle (mTLS) avec les états suivants : + +``` +REQUESTING_AUTH_GRANT (30s timeout) + ↓ +AWAITING_AUTH_TOKEN (30s timeout) + ↓ +PROCESSING_AUTH_TOKEN + ↓ +EXCHANGING_SERVER_TOKEN (15s timeout) ← Changement ici + ↓ +AUTHENTICATED +``` + +**Étapes détaillées** : + +1. **Validation Identity Token** : Vérifie le token du client, UUID, scopes +2. **Demande Auth Grant** : Récupère le server session token, appel async au `SessionServiceClient` +3. **Traitement Token Client** : Vérifie JWT signature, certificate binding, UUID/username +4. **Échange Server Token** : Échange auth grant contre token avec fingerprint serveur +5. **Complétion** : Crée `PlayerAuthentication`, passe au `SetupPacketHandler` + +Le changement garantit qu'un échec à l'étape 4 ne permet plus de compléter l'authentification. + +--- + +## 3. Transport QUIC - Optimisations réseau + +**Fichier** : `com/hypixel/hytale/server/core/io/transport/QUICTransport.java` +**Lignes modifiées** : 30, 128 + +### 3.1 Nouvel import + +```java +import io.netty.handler.codec.quic.QuicCongestionControlAlgorithm; +``` + +### 3.2 Activation de PMTU Discovery + +**Ajout** : `.discoverPmtu(true)` + +Le **Path MTU Discovery** permet au serveur de : + +| Fonctionnalité | Description | +|----------------|-------------| +| Détection automatique | Découvre la taille maximale de paquet sur chaque chemin réseau | +| Évitement fragmentation | Prévient la fragmentation IP qui dégrade les performances | +| Optimisation throughput | Utilise des paquets de taille optimale par connexion | + +### 3.3 Changement d'algorithme de congestion : BBR + +**Ajout** : `.congestionControlAlgorithm(QuicCongestionControlAlgorithm.BBR)` + +#### Comparaison des algorithmes + +| Aspect | Cubic (implicite avant) | BBR (nouveau) | +|--------|-------------------------|---------------| +| **Approche** | Loss-based | Model-based | +| **Réaction aux pertes** | Réduit agressivement le débit | Maintient le débit estimé | +| **Latence** | Variable, peut augmenter | Minimisée activement | +| **Réseaux lossy** | Performances dégradées | Meilleures performances | +| **Bufferbloat** | Peut remplir les buffers | Évite le bufferbloat | +| **WiFi/Mobile** | Sous-optimal | Optimisé | + +**BBR (Bottleneck Bandwidth and Round-trip propagation time)** est l'algorithme développé par Google qui : +- Estime continuellement la bande passante disponible +- Maintient un RTT minimal +- Offre de meilleures performances sur réseaux modernes + +### 3.4 Configuration QUIC complète après modification + +```java +QuicServerCodecBuilder() + .sslContext(sslContext) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .maxIdleTimeout(playTimeout.toMillis(), TimeUnit.MILLISECONDS) + .ackDelayExponent(3L) // 8ms entre acks + .initialMaxData(524288L) // 512 KB total + .initialMaxStreamDataUnidirectional(0L) // Désactivé + .initialMaxStreamsUnidirectional(0L) // Désactivé + .initialMaxStreamDataBidirectionalLocal(131072L) // 128 KB par stream + .initialMaxStreamDataBidirectionalRemote(131072L) // 128 KB par stream + .initialMaxStreamsBidirectional(1L) // 1 stream + .discoverPmtu(true) // NOUVEAU + .congestionControlAlgorithm(QuicCongestionControlAlgorithm.BBR) // NOUVEAU +``` + +### 3.5 Contexte technique + +Le `QUICTransport` configure également : +- **mTLS obligatoire** : `ClientAuth.REQUIRE` +- **Certificat auto-signé** : Généré au démarrage +- **IPv4/IPv6 dual-stack** : Bootstraps séparés +- **Options socket** : `SO_REUSEADDR`, `IP_DONTFRAGMENT` +- **Certificate fingerprinting** : Pour validation JWT + +--- + +## 4. Timeout Setup Handler + +**Fichier** : `com/hypixel/hytale/server/core/io/handlers/SetupPacketHandler.java` +**Ligne modifiée** : 102 + +### 4.1 Modification du timeout + +```java +// Avant +this.setTimeout("send-world-settings", () -> this.assets != null, 1L, TimeUnit.SECONDS); + +// Après +this.setTimeout("send-world-settings", () -> this.assets != null, 10L, TimeUnit.SECONDS); +``` + +### 4.2 Contexte + +Ce timeout attend que les assets soient prêts (`this.assets != null`) avant d'envoyer les paramètres du monde (`WorldSettings`) au client. + +### 4.3 Raisons du changement + +| Scénario | Impact avec 1s | Impact avec 10s | +|----------|----------------|-----------------| +| Connexion haute latence | Timeout fréquent | Toléré | +| Serveur avec nombreux asset packs | Timeout possible | Toléré | +| Charge serveur élevée | Timeout possible | Toléré | +| Démarrage lent des assets | Déconnexion | Attente correcte | + +### 4.4 Autres timeouts dans le SetupPacketHandler + +| Timeout | Durée | Condition | Usage | +|---------|-------|-----------|-------| +| `send-world-settings` | **10s** (modifié) | `this.assets != null` | Attente chargement assets | +| `receive-assets-request` | 120s | Packet `RequestAssets` reçu | Négociation d'assets | +| `send-assets` | 120s | `SendCommonAssetsEvent` terminé | Envoi assets + translations | +| `add-to-universe` | 60s | `Universe.addPlayer()` terminé | Création joueur dans monde | + +### 4.5 Phases du Setup Handler + +``` +[Post-Auth] → registered0() + ↓ +[WorldSettings + ServerInfo envoyés] + ↓ +[Client envoie RequestAssets] (120s max) + ↓ +[Envoi CommonAssets + Translations] (120s max) + ↓ +[Client envoie PlayerOptions] + ↓ +[AddToUniverse] (60s max) + ↓ +[Joueur connecté au monde] +``` + +--- + +## 5. Physique des Items - Collisions latérales + +**Fichier** : `com/hypixel/hytale/server/core/modules/entity/item/ItemPhysicsSystem.java` +**Lignes modifiées** : 84-97 + +### 5.1 Architecture du système + +| Élément | Description | +|---------|-------------| +| **Classe parente** | `EntityTickingSystem` | +| **Pattern** | Entity Component System (ECS) | +| **Tick rate** | Appelé à chaque tick du monde | + +### 5.2 Composants utilisés + +| Composant | Rôle | +|-----------|------| +| `ItemPhysicsComponent` | État physique spécifique aux items | +| `BoundingBox` | Boîte de collision 3D | +| `Velocity` (VelocityComponent) | Vecteur de vélocité | +| `TransformComponent` | Position et rotation | + +### 5.3 Modification de la gestion des collisions + +**Avant** : Seules les collisions verticales (sol) étaient gérées + +```java +BlockCollisionData blockCollisionData = collisionResult.getFirstBlockCollision(); +if (blockCollisionData != null && blockCollisionData.collisionNormal.equals(Vector3d.UP)) { + velocityComponent.setZero(); + position.assign(blockCollisionData.collisionPoint); +} else { + velocityComponent.assignVelocityTo(scaledVelocity).scale(dt); + position.add(scaledVelocity); +} +``` + +**Après** : Gestion complète de toutes les directions de collision + +```java +BlockCollisionData blockCollisionData = collisionResult.getFirstBlockCollision(); +if (blockCollisionData != null) { + if (blockCollisionData.collisionNormal.equals(Vector3d.UP)) { + // Collision avec le sol : arrêt complet + velocityComponent.setZero(); + position.assign(blockCollisionData.collisionPoint); + } else { + // Collision latérale : annulation de la composante perpendiculaire + Vector3d velocity = velocityComponent.getVelocity(); + double dot = velocity.dot(blockCollisionData.collisionNormal); + Vector3d velocityToCancel = blockCollisionData.collisionNormal.clone().scale(dot); + velocity.subtract(velocityToCancel); + } +} else { + velocityComponent.assignVelocityTo(scaledVelocity).scale(dt); + position.add(scaledVelocity); +} +``` + +### 5.4 Explication mathématique + +Pour une collision latérale (mur, escalier, pente) : + +1. **Calcul du produit scalaire** : `dot = velocity · normal` + - Mesure la composante de vélocité perpendiculaire à la surface + +2. **Calcul du vecteur à annuler** : `velocityToCancel = normal × dot` + - Vecteur perpendiculaire à la surface avec magnitude égale à la composante perpendiculaire + +3. **Soustraction** : `velocity = velocity - velocityToCancel` + - Projette la vélocité sur le plan tangent à la surface + +**Formule vectorielle** : `v' = v - (v · n) × n` + +### 5.5 Impact sur le gameplay + +| Comportement | Avant | Après | +|--------------|-------|-------| +| Item lancé contre un mur | Traversait ou s'arrêtait net | Glisse le long du mur | +| Item tombant sur une pente | Comportement incohérent | Roule correctement | +| Item dans un coin | Pouvait se bloquer/traverser | Réagit physiquement | + +### 5.6 Méthodes de détection de collision + +Le système utilise deux méthodes selon la magnitude de vitesse : + +| Méthode | Condition | Usage | +|---------|-----------|-------| +| `findBlockCollisionsShortDistance()` | Mouvement faible | Optimisation pour petits mouvements | +| `findBlockCollisionsIterative()` | Mouvement important | Précision pour grandes vitesses | + +--- + +## 6. Nouvelle raison d'arrêt MISSING_ASSETS + +**Fichier** : `com/hypixel/hytale/server/core/ShutdownReason.java` +**Ligne ajoutée** : 17 + +### 6.1 Nouvelle constante + +```java +public static final ShutdownReason MISSING_ASSETS = new ShutdownReason(7); +``` + +### 6.2 Liste complète des ShutdownReason + +| Constante | Exit Code | Description | Utilisation | +|-----------|-----------|-------------|-------------| +| `SIGINT` | 130 | Signal d'interruption | Ctrl+C ou SIGINT reçu | +| `SHUTDOWN` | 0 | Arrêt gracieux | Commande `/stop` ou shutdown normal | +| `CRASH` | 1 | Crash serveur | Erreur non gérée pendant le boot | +| `AUTH_FAILED` | 2 | Échec authentification | Impossible de s'authentifier auprès des services | +| `WORLD_GEN` | 3 | Erreur génération monde | WorldGenLoadException | +| `CLIENT_GONE` | 4 | Client singleplayer disparu | Mode singleplayer, client déconnecté | +| `MISSING_REQUIRED_PLUGIN` | 5 | Plugin core manquant | Plugin requis non trouvé | +| `VALIDATE_ERROR` | 6 | Validation assets échouée | Erreur de validation des assets | +| **`MISSING_ASSETS`** | **7** | **Aucun asset pack** | **NOUVEAU : Aucun pack chargé** | + +### 6.3 Pattern d'utilisation + +```java +// Utilisation simple +HytaleServer.get().shutdownServer(ShutdownReason.MISSING_ASSETS); + +// Avec message personnalisé +HytaleServer.get().shutdownServer( + ShutdownReason.MISSING_ASSETS.withMessage("Failed to load any asset packs") +); +``` + +La méthode `withMessage(String)` crée une nouvelle instance de `ShutdownReason` avec le même code de sortie mais un message additionnel pour les logs. + +--- + +## 7. Validation des Asset Packs + +**Fichier** : `com/hypixel/hytale/server/core/asset/AssetModule.java` +**Lignes modifiées** : 24, 104-106 + +### 7.1 Nouvel import + +```java +import com.hypixel.hytale.server.core.ShutdownReason; +``` + +### 7.2 Nouvelle vérification + +Après le chargement des asset packs depuis tous les répertoires de mods : + +```java +for (Path modsPath : Options.getOptionSet().valuesOf(Options.MODS_DIRECTORIES)) { + this.loadPacksFromDirectory(modsPath); +} + +// NOUVEAU : Vérification des assets +if (this.assetPacks.isEmpty()) { + HytaleServer.get().shutdownServer( + ShutdownReason.MISSING_ASSETS.withMessage("Failed to load any asset packs") + ); + return; +} +``` + +### 7.3 Cycle de vie de l'AssetModule + +``` +setup() + ↓ +[Création AssetMonitor si --disable-file-watcher absent] + ↓ +[Chargement packs CLI] + ↓ +[Chargement packs mods/] + ↓ +[NOUVEAU: Vérification assetPacks.isEmpty()] ← Shutdown si vide + ↓ +[Registration event listeners] + ↓ +LoadAssetEvent dispatch + ↓ +[AssetRegistryLoader charge tous les assets] + ↓ +BootEvent + ↓ +[Log nombre total d'assets] +``` + +### 7.4 Sources de chargement des asset packs + +| Source | Méthode | Description | +|--------|---------|-------------| +| CLI `--assets` | `loadPacksFromDirectory()` | Chemins spécifiés en ligne de commande | +| Dossier `mods/` | `loadPacksFromDirectory()` | Packs dans le dossier mods par défaut | +| Autres `--mods-directories` | `loadPacksFromDirectory()` | Répertoires additionnels | + +### 7.5 Types de packs supportés + +| Type | Détection | Immutabilité | +|------|-----------|--------------| +| ZIP/JAR | Contient `manifest.json` | Toujours immutable | +| Répertoire | Contient `manifest.json` | Immutable si `CommonAssetsIndex.hashes` présent | +| Répertoire sans hash | Contient `manifest.json` | Mutable (rechargement possible) | + +### 7.6 AssetMonitor (rechargement temps réel) + +Si `--disable-file-watcher` n'est pas passé, un `AssetMonitor` est créé pour : +- Surveiller les modifications de fichiers dans les packs mutables +- Déclencher un rechargement automatique des assets modifiés +- Permettre le développement itératif sans redémarrage serveur + +--- + +## 8. Télémétrie Sentry enrichie + +**Fichier** : `com/hypixel/hytale/server/core/HytaleServer.java` +**Lignes modifiées** : 30, 51, 155-170 + +### 8.1 Nouveaux imports + +```java +import com.hypixel.hytale.server.core.auth.SessionServiceClient; +import io.sentry.protocol.User; +``` + +### 8.2 Nouvelles données utilisateur collectées + +Dans le callback `beforeSend` de Sentry, un objet `User` est maintenant créé et attaché à chaque événement : + +```java +User user = new User(); +HashMap unknown = new HashMap(); +user.setUnknown(unknown); + +// 1. Hardware UUID +UUID hardwareUUID = HardwareUtil.getUUID(); +if (hardwareUUID != null) { + unknown.put("hardware-uuid", hardwareUUID.toString()); +} + +// 2. Mode d'authentification +ServerAuthManager authManager = ServerAuthManager.getInstance(); +unknown.put("auth-mode", authManager.getAuthMode().toString()); + +// 3. Profil utilisateur (si authentifié) +SessionServiceClient.GameProfile profile = authManager.getSelectedProfile(); +if (profile != null) { + user.setUsername(profile.username); + user.setId(profile.uuid.toString()); +} + +// 4. Adresse IP (auto-détection Sentry) +user.setIpAddress("{{auto}}"); + +event.setUser(user); +``` + +### 8.3 Données collectées par Sentry + +#### Données utilisateur (nouvelles) + +| Champ Sentry | Source | Type | Description | +|--------------|--------|------|-------------| +| `user.username` | `GameProfile.username` | String | Nom d'utilisateur du compte | +| `user.id` | `GameProfile.uuid` | UUID String | Identifiant unique du compte | +| `user.ip_address` | Auto-détecté | IP | Adresse IP du serveur | +| `user.unknown.hardware-uuid` | `HardwareUtil.getUUID()` | UUID String | Identifiant matériel unique | +| `user.unknown.auth-mode` | `ServerAuthManager.getAuthMode()` | String | Mode d'auth (ONLINE/OFFLINE/etc.) | + +#### Données contextuelles (existantes) + +| Contexte | Données | +|----------|---------| +| **server** | Nom, max-players, listeners | +| **universe** | Chemin, nombre joueurs, liste worlds | +| **plugins** | Version et état de chaque plugin | + +### 8.4 Conditions de collecte + +Les données Sentry sont collectées **uniquement si** : +- Aucun early plugin avec transformer n'est chargé +- Le flag `--disable-sentry` n'est pas passé +- Une erreur se produit (beforeSend callback) + +### 8.5 Filtrage des erreurs + +Les erreurs provenant de plugins tiers sont filtrées avant envoi : + +```java +if (PluginClassLoader.isFromThirdPartyPlugin(event.getThrowable())) { + return null; // Ne pas envoyer +} +``` + +### 8.6 Configuration Sentry + +| Paramètre | Valeur | +|-----------|--------| +| DSN | `https://...@sentry.hytale.com/4` | +| Environnement | `release` | +| Tags | `patchline` | +| Server name | Hostname du serveur | + +--- + +## 9. Fix NullPointerException CraftingManager + +**Fichier** : `com/hypixel/hytale/builtin/crafting/component/CraftingManager.java` +**Ligne modifiée** : 500 + +### 9.1 Modification + +**Avant** : +```java +if (!itemResourceType.id.equals(slotCraftingMaterial.getResourceTypeId())) continue; +``` + +**Après** : +```java +if (!slotCraftingMaterial.getResourceTypeId().equals(itemResourceType.id)) continue; +``` + +### 9.2 Contexte du code + +Cette ligne se trouve dans une méthode de comparaison des matériaux de craft, dans une boucle imbriquée : + +```java +// Ligne ~497-502 +if (slotCraftingMaterial.getResourceTypeId() == null || + slotItemStack.getItem().getResourceTypes() == null) continue; + +for (ItemResourceType itemResourceType : slotItemStack.getItem().getResourceTypes()) { + if (!slotCraftingMaterial.getResourceTypeId().equals(itemResourceType.id)) continue; + return true; +} +``` + +### 9.3 Analyse du problème + +| Variable | Peut être null ? | Raison | +|----------|------------------|--------| +| `slotCraftingMaterial.getResourceTypeId()` | Non | Vérifié 2 lignes avant | +| `itemResourceType.id` | **Oui** | Pas de vérification préalable | + +**Problème** : Appeler `.equals()` sur `itemResourceType.id` quand celui-ci est `null` lance un `NullPointerException`. + +**Solution** : Inverser l'ordre de comparaison pour appeler `.equals()` sur la valeur garantie non-null. + +### 9.4 Architecture du CraftingManager + +| Élément | Description | +|---------|-------------| +| **Interface** | `Component` (ECS) | +| **Attaché à** | Joueurs | +| **Rôle** | Gestion complète du crafting | + +### 9.5 Classes internes + +| Classe | Rôle | +|--------|------| +| `CraftingJob` | Représente un travail de crafting en cours | +| `BenchUpgradingJob` | Représente une amélioration d'établi | + +### 9.6 Flow de crafting + +``` +queueCraft() + ↓ +[Job ajouté à BlockingQueue] + ↓ +tick() [appelé chaque tick] + ↓ +[Vérifie matériaux disponibles] ← Fix NPE ici + ↓ +[Retire matériaux de l'inventaire] + ↓ +[Accumule temps] + ↓ +[Si terminé: giveOutput()] + ↓ +[Items craftés donnés au joueur] +``` + +### 9.7 Impact + +| Aspect | Avant | Après | +|--------|-------|-------| +| Items avec `ResourceType.id = null` | Crash du crafting | Comparaison correcte | +| Stabilité serveur | NPE possible | Stable | +| Crafting avec ressources custom | Potentiellement cassé | Fonctionnel | + +--- + +## Résumé des impacts + +### Performance + +| Changement | Impact | +|------------|--------| +| BBR congestion control | Amélioration significative du débit réseau | +| PMTU Discovery | Réduction de la fragmentation, meilleur throughput | +| Timeout setup 10s | Négligeable | + +### Sécurité + +| Changement | Impact | +|------------|--------| +| Clock skew 300s | Plus permissif mais toujours sécurisé | +| Validation iat | Protection contre tokens futurs | +| Échec auth → disconnect | Fermeture d'une faille potentielle | + +### Stabilité + +| Changement | Impact | +|------------|--------| +| MISSING_ASSETS shutdown | Erreur claire au lieu de comportement indéfini | +| Fix NPE CraftingManager | Élimination d'un crash potentiel | +| Collisions latérales items | Comportement physique cohérent | + +### Télémétrie + +| Changement | Impact | +|------------|--------| +| User data Sentry | Meilleur debugging des erreurs | +| Hardware UUID | Identification des problèmes matériels | +| Auth mode tracking | Corrélation erreurs/mode d'auth | + +--- + +## Fichiers modifiés + +| Fichier | Insertions | Suppressions | +|---------|------------|--------------| +| `server/core/auth/JWTValidator.java` | +11 | -6 | +| `server/core/HytaleServer.java` | +18 | 0 | +| `server/core/ShutdownReason.java` | +1 | 0 | +| `server/core/asset/AssetModule.java` | +5 | 0 | +| `server/core/io/handlers/SetupPacketHandler.java` | +1 | -1 | +| `server/core/io/handlers/login/HandshakeHandler.java` | +1 | -2 | +| `server/core/io/transport/QUICTransport.java` | +2 | -1 | +| `server/core/modules/entity/item/ItemPhysicsSystem.java` | +11 | -3 | +| `builtin/crafting/component/CraftingManager.java` | +1 | -1 | +| **Total** | **+55** | **-18** | diff --git a/content/patch-note/2026.01.15-c04fdfe10-2026.01.16-c508b9acd-prerelease-patch-note.md b/content/patch-note/2026.01.15-c04fdfe10-2026.01.16-c508b9acd-prerelease-patch-note.md new file mode 100644 index 0000000..077b0bc --- /dev/null +++ b/content/patch-note/2026.01.15-c04fdfe10-2026.01.16-c508b9acd-prerelease-patch-note.md @@ -0,0 +1,1219 @@ +# Patch Notes: Hytale Server (Pre-release) + +## Version 2026.01.15-c04fdfe10 → 2026.01.16-c508b9acd + +**Date de release** : 16 janvier 2026 (Pre-release) +**Build précédent** : `c04fdfe10` (15 janvier 2026) +**Build actuel** : `c508b9acd` (16 janvier 2026) + +--- + +## Statistiques + +| Métrique | Valeur | +|----------|--------| +| Fichiers modifiés | 169 | +| Insertions | +1604 | +| Suppressions | -994 | +| Delta net | +610 lignes | + +--- + +## Table des matières + +1. [Authentification - ProfileServiceClient (NOUVEAU)](#1-authentification---profileserviceclient-nouveau) +2. [Authentification - ServerAuthManager Refactoring](#2-authentification---serverauthmanager-refactoring) +3. [Système de Commandes - GAME_PROFILE_LOOKUP (NOUVEAU)](#3-système-de-commandes---game_profile_lookup-nouveau) +4. [Commandes Access Control Refactorisées](#4-commandes-access-control-refactorisées) +5. [Système de Téléportation Refactorisé](#5-système-de-téléportation-refactorisé) +6. [WorldGen - BlockMaskCondition](#6-worldgen---blockmaskcondition) +7. [PrefabSaver - Async Chunk Loading](#7-prefabsaver---async-chunk-loading) +8. [ChunkStore - Thread Safety Generator](#8-chunkstore---thread-safety-generator) +9. [InteractionManager - Timing Refactoring](#9-interactionmanager---timing-refactoring) +10. [InteractionContext - Amélioration Sélection Inventaire](#10-interactioncontext---amélioration-sélection-inventaire) +11. [Système Memories - Nouvelles Fonctionnalités](#11-système-memories---nouvelles-fonctionnalités) +12. [Configuration Serveur - Suppression LocalCompression](#12-configuration-serveur---suppression-localcompression) +13. [Holder - Nouvelle méthode cloneSerializable](#13-holder---nouvelle-méthode-cloneserializable) +14. [BlockSelection - Tri et Filler Fix](#14-blockselection---tri-et-filler-fix) +15. [Corrections de Bugs](#15-corrections-de-bugs) +16. [Migration Annotations checkerframework → javax](#16-migration-annotations-checkerframework--javax) +17. [Liste complète des fichiers modifiés](#17-liste-complète-des-fichiers-modifiés) + +--- + +## 1. Authentification - ProfileServiceClient (NOUVEAU) + +**Nouveau fichier** : `com/hypixel/hytale/server/core/auth/ProfileServiceClient.java` + +### 1.1 Description + +Nouveau client HTTP pour le service de profils Hytale. Permet de récupérer les profils publics de joueurs par UUID ou par username. + +**URL du service** : `https://account-data.hytale.com` + +### 1.2 API Complète + +```java +public class ProfileServiceClient { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(5L); + + public ProfileServiceClient(@Nonnull String profileServiceUrl); + + // Synchrone + @Nullable + public PublicGameProfile getProfileByUuid(@Nonnull UUID uuid, @Nonnull String bearerToken); + + @Nullable + public PublicGameProfile getProfileByUsername(@Nonnull String username, @Nonnull String bearerToken); + + // Asynchrone + public CompletableFuture getProfileByUuidAsync(@Nonnull UUID uuid, @Nonnull String bearerToken); + + public CompletableFuture getProfileByUsernameAsync(@Nonnull String username, @Nonnull String bearerToken); + + // Classe interne + public static class PublicGameProfile { + public static final BuilderCodec CODEC; + private UUID uuid; + private String username; + + public UUID getUuid(); + public String getUsername(); + } +} +``` + +### 1.3 Endpoints + +| Endpoint | Méthode | Description | +|----------|---------|-------------| +| `/profile/uuid/{uuid}` | GET | Récupère un profil par UUID | +| `/profile/username/{username}` | GET | Récupère un profil par username (URL encoded) | + +### 1.4 Headers HTTP + +``` +Accept: application/json +Authorization: Bearer {token} +User-Agent: {AuthConfig.USER_AGENT} +``` + +### 1.5 Gestion des erreurs + +- Timeout de 5 secondes +- Retourne `null` si HTTP status != 200 +- Logging des erreurs IO, interruptions et exceptions + +--- + +## 2. Authentification - ServerAuthManager Refactoring + +**Fichier** : `com/hypixel/hytale/server/core/auth/ServerAuthManager.java` + +### 2.1 Nouvelle intégration ProfileServiceClient + +```java +private volatile ProfileServiceClient profileServiceClient; + +@Nonnull +public ProfileServiceClient getProfileServiceClient() { + // Lazy initialization avec double-checked locking + if (this.profileServiceClient == null) { + synchronized (this) { + if (this.profileServiceClient == null) { + this.profileServiceClient = new ProfileServiceClient("https://account-data.hytale.com"); + } + } + } + return this.profileServiceClient; +} +``` + +### 2.2 Refactoring gestion d'expiration des tokens + +**Nouvelles méthodes privées** : + +```java +// Calcule l'expiry effective (minimum entre session et identity token) +@Nullable +private Instant getEffectiveExpiry(@Nullable SessionServiceClient.GameSessionResponse session) { + Instant sessionExpiry = session != null ? session.getExpiresAtInstant() : null; + Instant identityExpiry = this.parseIdentityTokenExpiry(session != null ? session.identityToken : this.getIdentityToken()); + if (sessionExpiry != null && identityExpiry != null) { + return sessionExpiry.isBefore(identityExpiry) ? sessionExpiry : identityExpiry; + } + return sessionExpiry != null ? sessionExpiry : identityExpiry; +} + +// Parse l'expiry depuis le JWT identity token +@Nullable +private Instant parseIdentityTokenExpiry(@Nullable String idToken); + +// Méthode unifiée pour définir l'expiry et planifier le refresh +private void setExpiryAndScheduleRefresh(@Nonnull Instant expiry); +``` + +### 2.3 Changements de signature + +**Avant** : +```java +private void scheduleRefresh(int expiresInSeconds); +``` + +**Après** : +```java +private void setExpiryAndScheduleRefresh(@Nonnull Instant expiry); +``` + +### 2.4 Réorganisation des champs + +```java +// Champs déplacés pour meilleure organisation +private volatile SessionServiceClient.GameProfile[] pendingProfiles; +private volatile AuthMode pendingAuthMode; +// ... maintenant au début de la classe +``` + +### 2.5 Fix typo + +```java +// Avant +LOGGER.at(Level.INFO).log("Auto-selected profile from stroage: %s (%s)", ...); + +// Après +LOGGER.at(Level.INFO).log("Auto-selected profile from storage: %s (%s)", ...); +``` + +### 2.6 Suppression de dépendance + +```java +// Supprimé +import org.checkerframework.checker.nullness.compatqual.NullableDecl; +``` + +--- + +## 3. Système de Commandes - GAME_PROFILE_LOOKUP (NOUVEAU) + +**Fichier** : `com/hypixel/hytale/server/core/command/system/arguments/types/ArgTypes.java` + +### 3.1 Nouveaux types d'arguments + +```java +// Version asynchrone +public static final SingleArgumentType> GAME_PROFILE_LOOKUP_ASYNC; + +// Version synchrone (bloquante) +public static final SingleArgumentType GAME_PROFILE_LOOKUP; +``` + +### 3.2 Logique de résolution + +1. **Recherche locale** : Vérifie d'abord les joueurs connectés sur tous les mondes +2. **Parsing UUID** : Tente de parser l'input comme UUID +3. **Lookup distant** : Utilise `ProfileServiceClient` pour résoudre par UUID ou username + +```java +// Pseudo-code de la logique +for (World world : Universe.get().getWorlds().values()) { + PlayerRef playerRef = NameMatching.DEFAULT.find(world.getPlayerRefs(), input, PlayerRef::getUsername); + if (playerRef != null) { + return new PublicGameProfile(playerRef.getUuid(), playerRef.getUsername()); + } +} +// Fallback: lookup distant via ProfileServiceClient +``` + +### 3.3 Messages d'erreur + +| Clé de traduction | Cas | +|-------------------|-----| +| `server.commands.parsing.argtype.playerUuidLookup.noAuth` | Pas de session token disponible | +| `server.commands.parsing.argtype.playerUuidLookup.notFound` | Joueur introuvable | +| `server.commands.parsing.argtype.playerUuidLookup.lookupError` | Erreur lors du lookup | + +--- + +## 4. Commandes Access Control Refactorisées + +### 4.1 BanCommand + +**Fichier** : `com/hypixel/hytale/server/core/modules/accesscontrol/commands/BanCommand.java` + +**Avant** : +```java +private final RequiredArg usernameArg = this.withRequiredArg("username", "...", ArgTypes.STRING); +private final OptionalArg reasonArg = this.withOptionalArg("reason", "...", ArgTypes.STRING); + +// Utilisation +String username = this.usernameArg.get(context); +AuthUtil.lookupUuid(username).thenCompose(uuid -> { ... }); +``` + +**Après** : +```java +private final RequiredArg playerArg = + this.withRequiredArg("player", "server.commands.ban.player.desc", ArgTypes.GAME_PROFILE_LOOKUP); + +// Utilisation +ProfileServiceClient.PublicGameProfile profile = this.playerArg.get(context); +UUID uuid = profile.getUuid(); +Message displayMessage = Message.raw(profile.getUsername()).bold(true); +``` + +**Changements clés** : +- Suppression de `AuthUtil.lookupUuid()` (lookup intégré dans `GAME_PROFILE_LOOKUP`) +- Affichage du nom en **gras** dans les messages +- Simplification du code (pas de chaîne `thenCompose`) + +### 4.2 UnbanCommand + +**Fichier** : `com/hypixel/hytale/server/core/modules/accesscontrol/commands/UnbanCommand.java` + +Même refactoring que BanCommand. + +### 4.3 WhitelistAddCommand + +**Fichier** : `com/hypixel/hytale/server/core/modules/accesscontrol/commands/WhitelistAddCommand.java` + +Même refactoring que BanCommand. + +### 4.4 WhitelistRemoveCommand + +**Fichier** : `com/hypixel/hytale/server/core/modules/accesscontrol/commands/WhitelistRemoveCommand.java` + +Même refactoring que BanCommand. + +--- + +## 5. Système de Téléportation Refactorisé + +### 5.1 Teleport.java + +**Fichier** : `com/hypixel/hytale/server/core/modules/entity/teleport/Teleport.java` + +#### Nouveaux factory methods + +```java +// Pour les joueurs - sépare rotation du corps et de la tête +@Nonnull +public static Teleport createForPlayer(@Nullable World world, @Nonnull Transform transform) { + Vector3f headRotation = transform.getRotation(); + Vector3f bodyRotation = new Vector3f(0.0f, headRotation.getYaw(), 0.0f); + return new Teleport(world, transform.getPosition(), bodyRotation).setHeadRotation(headRotation); +} + +@Nonnull +public static Teleport createForPlayer(@Nullable World world, @Nonnull Vector3d position, @Nonnull Vector3f rotation); + +@Nonnull +public static Teleport createForPlayer(@Nonnull Vector3d position, @Nonnull Vector3f rotation); + +@Nonnull +public static Teleport createForPlayer(@Nonnull Transform transform); + +// Pour téléportation exacte (contrôle précis des rotations) +@Nonnull +public static Teleport createExact(@Nonnull Vector3d position, @Nonnull Vector3f bodyRotation, @Nonnull Vector3f headRotation); + +@Nonnull +public static Teleport createExact(@Nonnull Vector3d position, @Nonnull Vector3f bodyRotation); +``` + +#### Constructeurs supprimés + +```java +// Supprimés - remplacés par factory methods +public Teleport(@Nullable World world, @Nonnull Transform transform); +public Teleport(@Nonnull Transform transform); +``` + +#### Nouvelles méthodes de mutation + +```java +public void setPosition(@Nonnull Vector3d position); +public void setRotation(@Nonnull Vector3f rotation); + +// Renommé (anciennement withHeadRotation) +@Nonnull +public Teleport setHeadRotation(@Nonnull Vector3f headRotation) { + this.headRotation = headRotation.clone(); // Note: clone() ajouté + return this; +} +``` + +#### Méthodes supprimées + +```java +// Supprimé +public Teleport withResetRoll(); +``` + +### 5.2 Warp.java + +**Fichier** : `com/hypixel/hytale/builtin/teleport/Warp.java` + +#### Constructeur simplifié + +**Avant** : +```java +public Warp(double locX, double locY, double locZ, float yaw, float pitch, float roll, + String id, @Nonnull World world, String creator, Instant creationDate) { + this.transform = new Transform(locX, locY, locZ, pitch, yaw, roll); + // ... +} +``` + +**Après** : +```java +public Warp(@Nonnull Transform transform, @Nonnull String id, @Nonnull World world, + @Nonnull String creator, @Nonnull Instant creationDate) { + this.transform = transform; + // ... +} +``` + +#### Utilisation de Teleport.createForPlayer + +```java +// Avant +return new Teleport(worldInstance, this.transform); + +// Après +return Teleport.createForPlayer(worldInstance, this.transform); +``` + +### 5.3 Commandes de téléportation mises à jour + +| Fichier | Changement | +|---------|------------| +| `SpawnCommand.java` | `Teleport.createForPlayer()` | +| `SpawnSetCommand.java` | `Teleport.createForPlayer()` | +| `TeleportAllCommand.java` | `Teleport.createForPlayer()` | +| `TeleportHomeCommand.java` | `Teleport.createForPlayer()` | +| `TeleportTopCommand.java` | `Teleport.createForPlayer()` | +| `TeleportWorldCommand.java` | `Teleport.createForPlayer()` | +| `TeleportOtherToPlayerCommand.java` | `Teleport.createForPlayer()` | +| `TeleportPlayerToCoordinatesCommand.java` | `Teleport.createForPlayer()` | +| `TeleportToCoordinatesCommand.java` | `Teleport.createForPlayer()` | +| `TeleportToPlayerCommand.java` | `Teleport.createForPlayer()` | +| `WarpCommand.java` | `Teleport.createForPlayer()` | +| `WarpSetCommand.java` | `Teleport.createForPlayer()` | + +--- + +## 6. WorldGen - BlockMaskCondition + +**Fichier** : `com/hypixel/hytale/server/worldgen/util/condition/BlockMaskCondition.java` + +### 6.1 Changement d'architecture + +**Avant** : Interface fonctionnelle simple +```java +@FunctionalInterface +public interface BlockMaskCondition { + public boolean eval(int var1, int var2, BlockFluidEntry var3); +} +``` + +**Après** : Classe complète avec système de masques (+156 lignes) +```java +public class BlockMaskCondition { + public static final Mask DEFAULT_MASK; + public static final BlockMaskCondition DEFAULT_TRUE; + public static final BlockMaskCondition DEFAULT_FALSE; + + @Nonnull + private Mask defaultMask = DEFAULT_MASK; + @Nonnull + private Long2ObjectMap specificMasks = Long2ObjectMaps.emptyMap(); + + public void set(@Nonnull Mask defaultMask, @Nonnull Long2ObjectMap specificMasks); + + public boolean eval(int currentBlock, int currentFluid, int nextBlockId, int nextFluidId); +} +``` + +### 6.2 Classes internes + +#### Mask + +```java +public static class Mask { + private final boolean matchEmpty; + private final MaskEntry[] entries; + + public Mask(@Nonnull MaskEntry[] entries); + private Mask(boolean matchEmpty, @Nonnull MaskEntry[] entries); + + public boolean shouldReplace(int current, int fluid); +} +``` + +#### MaskEntry + +Classe pour définir les conditions de remplacement basées sur : +- ID de bloc +- ID de fluide +- Tags de bloc +- Action (WHITELIST ou BLACKLIST) + +### 6.3 Fichiers impactés + +- `BlockPlacementMaskJsonLoader.java` (+36/-12) +- `BlockPlacementMaskRegistry.java` (+28/-8) +- `PrefabPasteUtil.java` (+8/-4) + +--- + +## 7. PrefabSaver - Async Chunk Loading + +**Fichier** : `com/hypixel/hytale/builtin/buildertools/prefabeditor/saving/PrefabSaver.java` + +### 7.1 Nouveau flow asynchrone + +**Avant** : +```java +@Nonnull +public static CompletableFuture savePrefab(...) { + return CompletableFuture.supplyAsync(() -> { + BlockSelection blockSelection = PrefabSaver.copyBlocks(...); + if (blockSelection == null) return false; + return PrefabSaver.save(...); + }, world); +} +``` + +**Après** : +```java +@Nonnull +public static CompletableFuture savePrefab(...) { + return PrefabSaver.copyBlocksAsync(...) + .thenApplyAsync(blockSelection -> { + if (blockSelection == null) return false; + return PrefabSaver.save(...); + }, (Executor)world); +} +``` + +### 7.2 Nouvelle méthode de préchargement + +```java +@Nonnull +private static CompletableFuture>> preloadChunksInSelectionAsync( + @Nonnull ChunkStore chunkStore, + @Nonnull Vector3i minPoint, + @Nonnull Vector3i maxPoint +); +``` + +### 7.3 Méthode refactorisée + +```java +// Avant (synchrone) +@Nullable +private static BlockSelection copyBlocks(...); + +// Après (2 méthodes) +@Nonnull +private static CompletableFuture copyBlocksAsync(...); + +@Nullable +private static BlockSelection copyBlocksWithLoadedChunks(..., @Nonnull Long2ObjectMap> loadedChunks, ...); +``` + +--- + +## 8. ChunkStore - Thread Safety Generator + +**Fichier** : `com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java` + +### 8.1 Nouveau lock pour le générateur + +```java +private final StampedLock generatorLock = new StampedLock(); +``` + +### 8.2 Accès thread-safe au générateur + +**Avant** : +```java +@Nullable +public IWorldGen getGenerator() { + return this.generator; +} + +public void setGenerator(@Nullable IWorldGen generator) { + if (this.generator != null) { + this.generator.shutdown(); + } + this.generator = generator; + // ... +} +``` + +**Après** : +```java +@Nullable +public IWorldGen getGenerator() { + long readStamp = this.generatorLock.readLock(); + try { + return this.generator; + } finally { + this.generatorLock.unlockRead(readStamp); + } +} + +public void setGenerator(@Nullable IWorldGen generator) { + long writeStamp = this.generatorLock.writeLock(); + try { + if (this.generator != null) { + this.generator.shutdown(); + } + this.generator = generator; + // ... + } finally { + this.generatorLock.unlockWrite(writeStamp); + } +} +``` + +### 8.3 Nouvelle méthode + +```java +public void shutdownGenerator() { + this.setGenerator(null); +} +``` + +### 8.4 Accès protégé dans getChunkReferenceAsync + +Tous les accès à `this.generator` dans la méthode `getChunkReferenceAsync` sont maintenant protégés par `generatorLock.readLock()`. + +--- + +## 9. InteractionManager - Timing Refactoring + +**Fichier** : `com/hypixel/hytale/server/core/entity/InteractionManager.java` + +### 9.1 Nouveau champ de temps + +```java +private long currentTime = 1L; +``` + +### 9.2 Mise à jour du temps + +```java +public void tick(@Nonnull Ref ref, @Nonnull CommandBuffer commandBuffer, float dt) { + this.currentTime += (long)commandBuffer.getExternalData().getWorld().getTickStepNanos(); + // ... +} +``` + +### 9.3 Remplacement de System.nanoTime() + +Toutes les occurrences de `System.nanoTime()` ont été remplacées par `this.currentTime` : + +| Contexte | Avant | Après | +|----------|-------|-------| +| packetQueueTime | `System.nanoTime()` | `this.currentTime` | +| waitingForClientFinished | `System.nanoTime()` | `this.currentTime` | +| waitingForServerFinished | `System.nanoTime()` | `this.currentTime` | +| waitingForSyncData | `System.nanoTime()` | `this.currentTime` | +| tickTime | `System.nanoTime()` | `this.currentTime` | +| getTimeInSeconds | `System.nanoTime()` | `this.currentTime` | + +**Avantage** : Le temps est maintenant basé sur les ticks du monde plutôt que sur l'horloge système, ce qui améliore la cohérence et la reproductibilité. + +--- + +## 10. InteractionContext - Amélioration Sélection Inventaire + +**Fichier** : `com/hypixel/hytale/server/core/entity/InteractionContext.java` + +### 10.1 Nouveau cas de fallback + +Quand les priorités d'interaction primaire et secondaire sont égales : + +```java +} else { + if (type == InteractionType.Primary && !primary.getItem().getInteractions().containsKey((Object)InteractionType.Primary)) { + selectedInventory = -5; + } + if (type == InteractionType.Secondary && !primary.getItem().getInteractions().containsKey((Object)InteractionType.Secondary)) { + selectedInventory = -5; + } +} +``` + +**Effet** : Si l'item primaire n'a pas d'interaction du type demandé, le système utilise l'inventaire secondaire (-5). + +--- + +## 11. Système Memories - Nouvelles Fonctionnalités + +### 11.1 MemoriesPlugin.java + +**Fichier** : `com/hypixel/hytale/builtin/adventure/memories/MemoriesPlugin.java` + +#### Nouvelles pages UI + +```java +// Nouvelle registration +OpenCustomUIInteraction.registerCustomPageSupplier(this, MemoriesUnlockedPage.class, "MemoriesUnlocked", new MemoriesUnlockedPageSuplier()); +``` + +#### Protection shutdown + +```java +@Override +protected void shutdown() { + if (!this.hasInitializedMemories) { + return; // Nouveau: évite NPE si pas initialisé + } + // ... +} +``` + +#### Nouvelle méthode setRecordedMemoriesCount + +```java +public int setRecordedMemoriesCount(int count) { + if (count < 0) count = 0; + this.recordedMemories.lock.writeLock().lock(); + try { + this.recordedMemories.memories.clear(); + ObjectArrayList allAvailableMemories = new ObjectArrayList(); + for (Map.Entry> entry : this.allMemories.entrySet()) { + allAvailableMemories.addAll(entry.getValue()); + } + int actualCount = Math.min(count, allAvailableMemories.size()); + for (int i = 0; i < actualCount; ++i) { + this.recordedMemories.memories.add(allAvailableMemories.get(i)); + } + BsonUtil.writeSync(Constants.UNIVERSE_PATH.resolve("memories.json"), RecordedMemories.CODEC, this.recordedMemories, this.getLogger()); + return actualCount; + } finally { + this.recordedMemories.lock.writeLock().unlock(); + } +} +``` + +#### Fix calcul getMemoriesLevel + +**Avant** (bug: incrémentation incorrecte): +```java +for (int i = 0; i < memoriesAmountPerLevel.length && recordedMemoriesCount >= memoriesAmountPerLevel[i]; ++i) { + memoriesLevel += i + 1; +} +``` + +**Après** : +```java +for (int i = memoriesAmountPerLevel.length - 1; i >= 0; --i) { + if (recordedMemoriesCount < memoriesAmountPerLevel[i]) continue; + return i + 2; +} +return memoriesLevel; +``` + +#### Suppression méthode + +```java +// Supprimé +public int getMemoriesForNextLevel(@Nonnull GameplayConfig gameplayConfig); +``` + +### 11.2 NPCMemory.java + +**Fichier** : `com/hypixel/hytale/builtin/adventure/memories/memories/npc/NPCMemory.java` + +#### Nouvelle méthode displayCatchEntityParticles + +```java +private static void displayCatchEntityParticles(MemoriesGameplayConfig memoriesGameplayConfig, Vector3d targetPosition, Ref targetRef, @Nonnull CommandBuffer commandBuffer) { + ModelParticle particle = memoriesGameplayConfig.getMemoriesCatchEntityParticle(); + if (particle == null) return; + + NetworkId networkIdComponent = commandBuffer.getComponent(targetRef, NetworkId.getComponentType()); + if (networkIdComponent == null) return; + + // Crée et envoie le packet de particules à tous les joueurs proches + SpawnModelParticles packet = new SpawnModelParticles(networkIdComponent.getId(), modelParticlesProtocol); + SpatialResource, EntityStore> spatialResource = commandBuffer.getResource(EntityModule.get().getPlayerSpatialResourceType()); + SpatialStructure> spatialStructure = spatialResource.getSpatialStructure(); + + spatialStructure.ordered(targetPosition, memoriesGameplayConfig.getMemoriesCatchParticleViewDistance(), results); + for (Ref ref : results) { + PlayerRef playerRefComponent = commandBuffer.getComponent(ref, PlayerRef.getComponentType()); + playerRefComponent.getPacketHandler().write((Packet)packet); + } +} +``` + +#### Appel dans GatherMemoriesSystem + +```java +// Après création de l'item mémoire +commandBuffer.addEntity(memoryItemHolder, AddReason.SPAWN); +GatherMemoriesSystem.displayCatchEntityParticles(memoriesGameplayConfig, memoryItemHolderPosition, ref, commandBuffer); +``` + +### 11.3 Nouveaux fichiers + +| Fichier | Description | +|---------|-------------| +| `page/MemoriesUnlockedPage.java` | Page UI pour afficher les mémoires débloquées | +| `page/MemoriesUnlockedPageSuplier.java` | Supplier pour la page | + +--- + +## 12. Configuration Serveur - Suppression LocalCompression + +**Fichier** : `com/hypixel/hytale/server/core/HytaleServerConfig.java` + +### 12.1 Champ supprimé + +```java +// Supprimé +private boolean localCompressionEnabled; +``` + +### 12.2 Méthodes supprimées + +```java +// Supprimés +public boolean isLocalCompressionEnabled(); +public void setLocalCompressionEnabled(boolean localCompression); +``` + +### 12.3 Codec mis à jour + +Le champ `LocalCompressionEnabled` a été retiré du CODEC de configuration. + +### 12.4 Impact sur SetupPacketHandler + +**Fichier** : `com/hypixel/hytale/server/core/io/handlers/SetupPacketHandler.java` + +```java +// Supprimé +HytaleServerConfig serverConfig = HytaleServer.get().getConfig(); +boolean enableCompression = !serverConfig.isLocalCompressionEnabled() ? !oldHandler.isLocalConnection() : true; +oldHandler.setCompressionEnabled(enableCompression); +``` + +La configuration du serveur est maintenant récupérée plus tard, juste avant l'envoi de `ServerInfo`. + +--- + +## 13. Holder - Nouvelle méthode cloneSerializable + +**Fichier** : `com/hypixel/hytale/component/Holder.java` + +### 13.1 Nouvelle méthode + +```java +public Holder cloneSerializable(@Nonnull ComponentRegistry.Data data) { + assert (this.archetype != null); + assert (this.components != null); + assert (this.registry != null); + + long stamp = this.lock.readLock(); + try { + Archetype serializableArchetype = this.archetype.getSerializableArchetype(data); + Component[] componentsClone = new Component[serializableArchetype.length()]; + + for (int i = serializableArchetype.getMinIndex(); i < serializableArchetype.length(); ++i) { + ComponentType componentType = serializableArchetype.get(i); + if (componentType == null) continue; + componentsClone[i] = this.components[i].cloneSerializable(); + } + + return this.registry.newHolder(serializableArchetype, componentsClone); + } finally { + this.lock.unlockRead(stamp); + } +} +``` + +**Usage** : Clone un holder en ne gardant que les composants sérialisables. + +--- + +## 14. BlockSelection - Tri et Filler Fix + +**Fichier** : `com/hypixel/hytale/server/core/prefab/selection/standard/BlockSelection.java` + +### 14.1 Nouvelle méthode sortEntitiesByPosition + +```java +public void sortEntitiesByPosition() { + this.entitiesLock.writeLock().lock(); + try { + ComponentType transformType = TransformComponent.getComponentType(); + this.entities.sort((a, b) -> { + TransformComponent ta = a.getComponent(transformType); + TransformComponent tb = b.getComponent(transformType); + // Comparaison par X, puis Y, puis Z + // Gestion des cas null + }); + } finally { + this.entitiesLock.writeLock().unlock(); + } +} +``` + +### 14.2 Fix filler blocks + +```java +// Nouveau cas dans withBlockComponents +if (holder != null && b.filler != 0) { + return new BlockHolder(b.blockId(), b.rotation(), b.filler(), b.supportValue(), null); +} +``` + +**Effet** : Les blocs filler ne perdent plus leurs données lors de la copie. + +--- + +## 15. Corrections de Bugs + +### 15.1 PickupItemSystem - Fix NPE + +**Fichier** : `com/hypixel/hytale/server/core/modules/entity/item/PickupItemSystem.java` + +```java +// Avant +if (!targetRef.isValid()) { + +// Après +if (targetRef == null || !targetRef.isValid()) { +``` + +### 15.2 World - Fix coordonnée Y dans blockValidator + +**Fichier** : `com/hypixel/hytale/server/core/universe/world/World.java` + +```java +// Avant (bug: utilisait y au lieu de worldY) +blockValidator.accept(worldX, y, worldZ, blockId, 0, 1.0f, holder, ...); + +// Après +blockValidator.accept(worldX, worldY, worldZ, blockId, 0, 1.0f, holder, ...); +``` + +### 15.3 CoopBlock - ArrayList → ObjectArrayList + +**Fichier** : `com/hypixel/hytale/builtin/adventure/farming/states/CoopBlock.java` + +```java +// Avant +ArrayList remainder = new ArrayList(); + +// Après +ObjectArrayList remainder = new ObjectArrayList(); +``` + +### 15.4 EnterBedSystem - Ajout assertion + +**Fichier** : `com/hypixel/hytale/builtin/beds/sleep/systems/player/EnterBedSystem.java` + +```java +PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType()); +assert (playerRef != null); // Nouveau +``` + +### 15.5 UpdateSleepPacketSystem - Ajout assertions + +**Fichier** : `com/hypixel/hytale/builtin/beds/sleep/systems/player/UpdateSleepPacketSystem.java` + +```java +SleepTracker sleepTrackerComponent = archetypeChunk.getComponent(index, SleepTracker.getComponentType()); +assert (sleepTrackerComponent != null); // Nouveau + +PlayerRef playerRefComponent = archetypeChunk.getComponent(index, PlayerRef.getComponentType()); +assert (playerRefComponent != null); // Nouveau + +PlayerSomnolence playerSomnolenceComponent = archetypeChunk.getComponent(index, PlayerSomnolence.getComponentType()); +assert (playerSomnolenceComponent != null); // Nouveau +``` + +### 15.6 WeatherSystem - Ajout assertion + +**Fichier** : `com/hypixel/hytale/builtin/weather/systems/WeatherSystem.java` + +```java +WeatherTracker weatherTrackerComponent = commandBuffer.getComponent(ref, WeatherTracker.getComponentType()); +assert (weatherTrackerComponent != null); // Nouveau +weatherTrackerComponent.clear(); +``` + +--- + +## 16. Migration Annotations checkerframework → javax + +### 16.1 Annotations migrées + +| Avant | Après | +|-------|-------| +| `@NonNullDecl` | `@Nonnull` | +| `@NullableDecl` | `@Nullable` | + +### 16.2 Import supprimé + +```java +// Supprimé de tous les fichiers concernés +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; +``` + +### 16.3 Fichiers impactés (liste partielle) + +- `BedInteraction.java` +- `ServerAuthManager.java` +- `FarmingSystems.java` +- `FarmingUtil.java` +- `CoopResidentComponent.java` +- `UseCaptureCrateInteraction.java` +- `UseCoopInteraction.java` +- `CoopBlock.java` +- `PlayerSomnolence.java` +- `SleepTracker.java` +- `WorldSomnolence.java` +- `EnterBedSystem.java` +- `RegisterTrackerSystem.java` +- `UpdateSleepPacketSystem.java` +- `WakeUpOnDismountSystem.java` +- `StartSlumberSystem.java` +- `UpdateWorldSlumberSystem.java` +- `WeatherSystem.java` +- Et de nombreuses classes d'interaction... + +--- + +## 17. Liste complète des fichiers modifiés + +
+Cliquer pour afficher les 169 fichiers + +``` +com/hypixel/hytale/assetstore/AssetValidationResults.java +com/hypixel/hytale/builtin/camera/command/CameraEffectCommand.java +com/hypixel/hytale/builtin/adventure/farming/FarmingSystems.java +com/hypixel/hytale/builtin/adventure/farming/FarmingUtil.java +com/hypixel/hytale/builtin/adventure/farming/component/CoopResidentComponent.java +com/hypixel/hytale/builtin/adventure/farming/interactions/UseCaptureCrateInteraction.java +com/hypixel/hytale/builtin/adventure/farming/interactions/UseCoopInteraction.java +com/hypixel/hytale/builtin/adventure/farming/states/CoopBlock.java +com/hypixel/hytale/builtin/adventure/memories/MemoriesGameplayConfig.java +com/hypixel/hytale/builtin/adventure/memories/MemoriesPlugin.java +com/hypixel/hytale/builtin/adventure/memories/commands/MemoriesCommand.java +com/hypixel/hytale/builtin/adventure/memories/interactions/MemoriesConditionInteraction.java +com/hypixel/hytale/builtin/adventure/memories/memories/npc/NPCMemory.java +com/hypixel/hytale/builtin/adventure/memories/memories/npc/NPCMemoryProvider.java +com/hypixel/hytale/builtin/adventure/memories/page/MemoriesPage.java +com/hypixel/hytale/builtin/adventure/memories/page/MemoriesPageSupplier.java +com/hypixel/hytale/builtin/adventure/memories/temple/TempleRespawnPlayersSystem.java +com/hypixel/hytale/builtin/adventure/teleporter/component/Teleporter.java +com/hypixel/hytale/builtin/adventure/teleporter/systems/CreateWarpWhenTeleporterPlacedSystem.java +com/hypixel/hytale/builtin/asseteditor/AssetEditorPlugin.java +com/hypixel/hytale/builtin/beds/interactions/BedInteraction.java +com/hypixel/hytale/builtin/beds/sleep/components/PlayerSomnolence.java +com/hypixel/hytale/builtin/beds/sleep/components/SleepTracker.java +com/hypixel/hytale/builtin/beds/sleep/resources/WorldSomnolence.java +com/hypixel/hytale/builtin/beds/sleep/systems/player/EnterBedSystem.java +com/hypixel/hytale/builtin/beds/sleep/systems/player/RegisterTrackerSystem.java +com/hypixel/hytale/builtin/beds/sleep/systems/player/UpdateSleepPacketSystem.java +com/hypixel/hytale/builtin/beds/sleep/systems/player/WakeUpOnDismountSystem.java +com/hypixel/hytale/builtin/beds/sleep/systems/world/StartSlumberSystem.java +com/hypixel/hytale/builtin/beds/sleep/systems/world/UpdateWorldSlumberSystem.java +com/hypixel/hytale/builtin/blockphysics/WorldValidationUtil.java +com/hypixel/hytale/builtin/buildertools/commands/ReplaceCommand.java +com/hypixel/hytale/builtin/buildertools/prefabeditor/PrefabEditSessionManager.java +com/hypixel/hytale/builtin/buildertools/prefabeditor/saving/PrefabSaver.java +com/hypixel/hytale/builtin/buildertools/prefabeditor/ui/PrefabEditorSaveSettingsPage.java +com/hypixel/hytale/builtin/buildertools/prefabeditor/ui/PrefabTeleportPage.java +com/hypixel/hytale/builtin/crafting/window/StructuralCraftingWindow.java +com/hypixel/hytale/builtin/hub/interactions/HubPortalInteraction.java +com/hypixel/hytale/builtin/interaction/SpawnDeployableAtHitLocationInteraction.java +com/hypixel/hytale/builtin/interaction/SpawnDeployableFromRaycastInteraction.java +com/hypixel/hytale/builtin/instances/InstancesPlugin.java +com/hypixel/hytale/builtin/instances/command/InstanceEditLoadCommand.java +com/hypixel/hytale/builtin/instances/page/InstanceListPage.java +com/hypixel/hytale/builtin/mounts/interactions/SeatingInteraction.java +com/hypixel/hytale/builtin/path/PrefabPathSystems.java +com/hypixel/hytale/builtin/path/commands/WorldPathBuilderCommand.java +com/hypixel/hytale/builtin/singleplayer/systems/CloseWorldWhenBreakingDeviceSystems.java +com/hypixel/hytale/builtin/teleport/Warp.java +com/hypixel/hytale/builtin/teleport/commands/teleport/SpawnCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/SpawnSetCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/TeleportAllCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/TeleportHomeCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/TeleportTopCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/TeleportWorldCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/variant/TeleportOtherToPlayerCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/variant/TeleportPlayerToCoordinatesCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/variant/TeleportToCoordinatesCommand.java +com/hypixel/hytale/builtin/teleport/commands/teleport/variant/TeleportToPlayerCommand.java +com/hypixel/hytale/builtin/teleport/commands/warp/WarpCommand.java +com/hypixel/hytale/builtin/teleport/commands/warp/WarpSetCommand.java +com/hypixel/hytale/builtin/teleport/components/TeleportHistory.java +com/hypixel/hytale/builtin/weather/systems/WeatherSystem.java +com/hypixel/hytale/component/ComponentRegistry.java +com/hypixel/hytale/component/Holder.java +com/hypixel/hytale/server/cell/evaluator/SkipCellPointEvaluator.java +com/hypixel/hytale/server/core/HytaleServerConfig.java +com/hypixel/hytale/server/core/Message.java +com/hypixel/hytale/server/core/asset/type/fluid/DefaultFluidTicker.java +com/hypixel/hytale/server/core/asset/type/fluid/FluidTicker.java +com/hypixel/hytale/server/core/asset/type/gameplay/CraftingConfig.java +com/hypixel/hytale/server/core/asset/type/gameplay/respawn/HomeOrSpawnPoint.java +com/hypixel/hytale/server/core/asset/type/gameplay/respawn/WorldSpawnPoint.java +com/hypixel/hytale/server/core/asset/type/item/DroplistCommand.java +com/hypixel/hytale/server/core/asset/type/item/config/Item.java +com/hypixel/hytale/server/core/auth/AuthConfig.java +com/hypixel/hytale/server/core/auth/ServerAuthManager.java +com/hypixel/hytale/server/core/auth/oauth/OAuthClient.java +com/hypixel/hytale/server/core/commands/debug/DebugPlayerPositionCommand.java +com/hypixel/hytale/server/core/commands/player/inventory/GiveCommand.java +com/hypixel/hytale/server/core/commands/world/worldgen/WorldGenBenchmarkCommand.java +com/hypixel/hytale/server/core/command/system/arguments/types/ArgTypes.java +com/hypixel/hytale/server/core/entity/InteractionContext.java +com/hypixel/hytale/server/core/entity/InteractionManager.java +com/hypixel/hytale/server/core/entity/player/data/UniqueItemUsagesComponent.java +com/hypixel/hytale/server/core/io/handlers/SetupPacketHandler.java +com/hypixel/hytale/server/core/io/handlers/game/GamePacketHandler.java +com/hypixel/hytale/server/core/modules/accesscontrol/commands/BanCommand.java +com/hypixel/hytale/server/core/modules/accesscontrol/commands/UnbanCommand.java +com/hypixel/hytale/server/core/modules/accesscontrol/commands/WhitelistAddCommand.java +com/hypixel/hytale/server/core/modules/accesscontrol/commands/WhitelistRemoveCommand.java +com/hypixel/hytale/server/core/modules/accesscontrol/provider/HytaleWhitelistProvider.java +com/hypixel/hytale/server/core/modules/entity/EntityModule.java +com/hypixel/hytale/server/core/modules/entity/LegacyProjectileSystems.java +com/hypixel/hytale/server/core/modules/entity/component/DisplayNameComponent.java +com/hypixel/hytale/server/core/modules/entity/damage/DamageSystems.java +com/hypixel/hytale/server/core/modules/entity/item/PickupItemComponent.java +com/hypixel/hytale/server/core/modules/entity/item/PickupItemSystem.java +com/hypixel/hytale/server/core/modules/entity/player/PlayerSystems.java +com/hypixel/hytale/server/core/modules/entity/system/RespondToHitSystems.java +com/hypixel/hytale/server/core/modules/entity/system/UpdateLocationSystems.java +com/hypixel/hytale/server/core/modules/entity/teleport/Teleport.java +com/hypixel/hytale/server/core/modules/entitystats/EntityStatValue.java +com/hypixel/hytale/server/core/modules/interaction/config/SimpleInstantInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/SimpleInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/ApplyForceInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/ChainingInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/ChargingInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/FirstClickInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/MovementConditionInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/PickBlockInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/PlaceBlockInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/SimpleBlockInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/client/WieldingInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/BuilderToolInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/CameraInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/ChangeActiveSlotInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/ConditionInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/ParallelInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/RepeatInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/ReplaceInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/SelectInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/none/SerialInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/selector/HorizontalSelector.java +com/hypixel/hytale/server/core/modules/interaction/config/selector/StabSelector.java +com/hypixel/hytale/server/core/modules/interaction/config/server/CheckUniqueItemUsageInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/server/DamageEntityInteraction.java +com/hypixel/hytale/server/core/modules/interaction/config/server/IncreaseBackpackCapacityInteraction.java +com/hypixel/hytale/server/core/permissions/commands/op/OpAddCommand.java +com/hypixel/hytale/server/core/permissions/commands/op/OpRemoveCommand.java +com/hypixel/hytale/server/core/permissions/commands/op/OpSelfCommand.java +com/hypixel/hytale/server/core/prefab/PrefabCopyableComponent.java +com/hypixel/hytale/server/core/prefab/config/SelectionPrefabSerializer.java +com/hypixel/hytale/server/core/prefab/selection/standard/BlockSelection.java +com/hypixel/hytale/server/core/universe/world/World.java +com/hypixel/hytale/server/core/universe/world/accessor/IChunkAccessorSync.java +com/hypixel/hytale/server/core/universe/world/chunk/BlockComponentChunk.java +com/hypixel/hytale/server/core/universe/world/chunk/EntityChunk.java +com/hypixel/hytale/server/core/universe/world/chunk/WorldChunk.java +com/hypixel/hytale/server/core/universe/world/commands/world/perf/WorldPerfResetCommand.java +com/hypixel/hytale/server/core/universe/world/connectedblocks/builtin/RoofConnectedBlockRuleSet.java +com/hypixel/hytale/server/core/universe/world/meta/BlockState.java +com/hypixel/hytale/server/core/universe/world/path/SimplePathWaypoint.java +com/hypixel/hytale/server/core/universe/world/spawn/FitToHeightMapSpawnProvider.java +com/hypixel/hytale/server/core/universe/world/spawn/GlobalSpawnProvider.java +com/hypixel/hytale/server/core/universe/world/spawn/IndividualSpawnProvider.java +com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java +com/hypixel/hytale/server/core/universe/world/storage/component/ChunkSavingSystems.java +com/hypixel/hytale/server/core/universe/world/storage/provider/DefaultChunkStorageProvider.java +com/hypixel/hytale/server/core/universe/world/storage/provider/EmptyChunkStorageProvider.java +com/hypixel/hytale/server/core/universe/world/storage/provider/MigrationChunkStorageProvider.java +com/hypixel/hytale/server/core/util/PrefabUtil.java +com/hypixel/hytale/server/movement/BodyMotionTeleport.java +com/hypixel/hytale/server/npc/role/builders/BuilderRole.java +com/hypixel/hytale/server/npc/role/support/StateSupport.java +com/hypixel/hytale/server/npc/systems/NPCSystems.java +com/hypixel/hytale/server/worldgen/cave/shape/AbstractCaveNodeShape.java +com/hypixel/hytale/server/worldgen/cave/shape/DistortedCaveNodeShape.java +com/hypixel/hytale/server/worldgen/loader/cave/CavePrefabConfigJsonLoader.java +com/hypixel/hytale/server/worldgen/loader/cave/CaveTypeJsonLoader.java +com/hypixel/hytale/server/worldgen/loader/cave/PrefabCaveNodeShapeGeneratorJsonLoader.java +com/hypixel/hytale/server/worldgen/loader/prefab/BlockPlacementMaskJsonLoader.java +com/hypixel/hytale/server/worldgen/loader/prefab/BlockPlacementMaskRegistry.java +com/hypixel/hytale/server/worldgen/loader/prefab/PrefabPatternGeneratorJsonLoader.java +com/hypixel/hytale/server/worldgen/loader/prefab/UniquePrefabConfigurationJsonLoader.java +com/hypixel/hytale/server/worldgen/prefab/PrefabPasteUtil.java +com/hypixel/hytale/server/worldgen/util/cache/ConcurrentSizedTimeoutCache.java +com/hypixel/hytale/server/worldgen/util/condition/BlockMaskCondition.java +org/jline/reader/impl/LineReaderImpl.java +summary.txt +``` + +
+ +--- + +## Résumé des impacts + +### Performance + +| Changement | Impact | +|------------|--------| +| PrefabSaver async chunk loading | Sauvegarde de prefabs non-bloquante | +| ProfileServiceClient async | Récupération de profils sans bloquer | +| ChunkStore generator lock | Thread safety améliorée | +| InteractionManager currentTime | Timing plus cohérent basé sur les ticks | + +### Stabilité + +| Changement | Impact | +|------------|--------| +| PickupItemSystem NPE fix | Évite crash sur ref null | +| World blockValidator fix | Coordonnées Y correctes | +| Assertions ajoutées | Détection précoce des erreurs | +| ChunkStore thread safety | Évite race conditions sur generator | + +### API + +| Changement | Impact | +|------------|--------| +| Teleport factory methods | API plus claire pour la téléportation | +| BlockMaskCondition | Nouveau système de masques pour worldgen | +| ProfileServiceClient | Nouvelle API pour récupérer les profils | +| GAME_PROFILE_LOOKUP | Nouveau type d'argument pour les commandes | +| Holder.cloneSerializable() | Nouvelle méthode de clonage | +| BlockSelection.sortEntitiesByPosition() | Nouvelle méthode de tri | + +### Qualité du code + +| Changement | Impact | +|------------|--------| +| Migration annotations | Standardisation sur javax.annotation | +| ServerAuthManager refactoring | Code plus maintenable | +| Suppression LocalCompression | Simplification de la configuration | +| Typo fix "stroage" → "storage" | Meilleurs logs | + +--- + +*Généré à partir du diff git entre les versions `c04fdfe10` et `c508b9acd`* +*169 fichiers analysés exhaustivement* diff --git a/content/reference/_index.en.md b/content/reference/_index.en.md new file mode 100644 index 0000000..501662e --- /dev/null +++ b/content/reference/_index.en.md @@ -0,0 +1,83 @@ +--- +title: Reference +type: docs +weight: 7 +--- + +Complete API reference and documentation for Hytale plugin development. + +{{< cards >}} + {{< card link="manifest-schema" title="Manifest Schema" subtitle="Complete plugin.json specification" >}} + {{< card link="all-registries" title="All Registries" subtitle="Complete registry listing" >}} + {{< card link="buildertools" title="Builder Tools" subtitle="Creative mode construction tools" >}} +{{< /cards >}} + +## API Overview + +Hytale provides a comprehensive Java API for plugin development: + +### Core Systems +- **PluginBase** - Base class for all plugins +- **Universe/World** - World management +- **Entity/Player** - Entity handling +- **Events** - Event system +- **Commands** - Command framework + +### Registries +Access game systems through typed registries: + +```java +public class MyPlugin extends PluginBase { + @Override + public void start() { + // All registries accessible via PluginBase + EventRegistry events = getEventRegistry(); + CommandRegistry commands = getCommandRegistry(); + TaskRegistry tasks = getTaskRegistry(); + AssetRegistry assets = getAssetRegistry(); + BlockStateRegistry blocks = getBlockStateRegistry(); + CodecRegistry codecs = getCodecRegistry(); + } +} +``` + +## Quick Reference + +### Plugin Lifecycle + +| Method | Called When | +|--------|-------------| +| `setup()` | Before start, for pre-initialization | +| `start()` | Plugin is enabled | +| `shutdown()` | Plugin is disabled | + +### Event Priorities + +| Priority | Value | Use Case | +|----------|-------|----------| +| FIRST | -21844 | Pre-processing, logging | +| EARLY | -10922 | Early modifications | +| NORMAL | 0 | Standard handlers | +| LATE | 10922 | Late modifications | +| LAST | 21844 | Final processing, cleanup | + +### Common ArgTypes + +| Type | Description | +|------|-------------| +| BOOLEAN | true/false | +| INTEGER | Whole numbers | +| DOUBLE | Decimal numbers | +| STRING | Single word text | +| GREEDY_STRING | Multi-word text | +| PLAYER_REF | Online player | +| WORLD | World name | +| ENTITY_ID | Entity UUID | + +See [Command Arguments](/commands/arguments/) for the complete list. + +## External Resources + +- Hytale Official Website +- Plugin Development Forum +- Community Discord diff --git a/content/reference/_index.fr.md b/content/reference/_index.fr.md new file mode 100644 index 0000000..c45aa43 --- /dev/null +++ b/content/reference/_index.fr.md @@ -0,0 +1,81 @@ +--- +title: Référence +type: docs +weight: 7 +--- + +Référence API complète et documentation pour le développement de plugins Hytale. + +{{< cards >}} + {{< card link="manifest-schema" title="Schéma du Manifeste" subtitle="Spécification complète de plugin.json" >}} + {{< card link="all-registries" title="Tous les Registres" subtitle="Liste complète des registres" >}} + {{< card link="buildertools" title="Outils de Construction" subtitle="Outils de construction mode creatif" >}} +{{< /cards >}} + +## Aperçu de l'API + +Hytale fournit une API Java complète pour le développement de plugins : + +### Systèmes Principaux +- **PluginBase** - Classe de base pour tous les plugins +- **Universe/World** - Gestion des mondes +- **Entity/Player** - Gestion des entités +- **Events** - Système d'événements +- **Commands** - Framework de commandes + +### Registres +Accédez aux systèmes du jeu via des registres typés : + +```java +public class MyPlugin extends PluginBase { + @Override + public void start() { + // Tous les registres accessibles via PluginBase + EventRegistry events = getEventRegistry(); + CommandRegistry commands = getCommandRegistry(); + TaskRegistry tasks = getTaskRegistry(); + AssetRegistry assets = getAssetRegistry(); + BlockStateRegistry blocks = getBlockStateRegistry(); + CodecRegistry codecs = getCodecRegistry(); + } +} +``` + +## Référence Rapide + +### Cycle de Vie du Plugin + +| Méthode | Appelée Quand | +|---------|---------------| +| `setup()` | Avant start, pour la pré-initialisation | +| `start()` | Le plugin est activé | +| `shutdown()` | Le plugin est désactivé | + +### Priorités d'Événements + +| Priorité | Valeur | Cas d'Usage | +|----------|--------|-------------| +| FIRST | -21844 | Pré-traitement, logging | +| EARLY | -10922 | Modifications précoces | +| NORMAL | 0 | Handlers standards | +| LATE | 10922 | Modifications tardives | +| LAST | 21844 | Traitement final, nettoyage | + +### ArgTypes Courants + +| Type | Description | +|------|-------------| +| BOOLEAN | true/false | +| INTEGER | Nombres entiers | +| DOUBLE | Nombres décimaux | +| STRING | Texte d'un mot | +| GREEDY_STRING | Texte multi-mots | +| PLAYER_REF | Joueur en ligne | +| WORLD | Nom du monde | +| ENTITY | Sélecteur d'entité | + +## Ressources Externes + +- Site Officiel Hytale +- Forum de Développement de Plugins +- Discord Communautaire diff --git a/content/reference/all-registries.en.md b/content/reference/all-registries.en.md new file mode 100644 index 0000000..ba7738e --- /dev/null +++ b/content/reference/all-registries.en.md @@ -0,0 +1,379 @@ +--- +title: All Registries +type: docs +weight: 2 +--- + +Complete reference of all registries available in the Hytale Plugin API. + +## Registry Overview + +Registries are accessed through `PluginBase` methods: + +```java +public class MyPlugin extends PluginBase { + @Override + public void start() { + EventRegistry events = getEventRegistry(); + CommandRegistry commands = getCommandRegistry(); + TaskRegistry tasks = getTaskRegistry(); + AssetRegistry assets = getAssetRegistry(); + BlockStateRegistry blocks = getBlockStateRegistry(); + CodecRegistry codecs = getCodecRegistry(); + EntityRegistry entities = getEntityRegistry(); + } +} +``` + +## EventRegistry + +Manages event listeners and dispatching. + +### Methods + +| Method | Description | +|--------|-------------| +| `register(Class, Consumer)` | Register event handler | +| `register(EventPriority, Class, Consumer)` | Register with priority | +| `registerGlobal(Class, Consumer)` | Register global handler | +| `registerAsync(Class, Function)` | Register async handler | +| `unregister(listener)` | Remove listener | + +### Example + +```java +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.Message; +import java.util.logging.Level; + +EventRegistry events = getEventRegistry(); + +// Use getPlayerRef() - getPlayer() is deprecated +events.register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + playerRef.sendMessage(Message.raw("Welcome!")); +}); + +events.register(EventPriority.FIRST, PlayerDisconnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + getLogger().at(Level.INFO).log(playerRef.getUsername() + " left"); +}); +``` + +## CommandRegistry + +Manages command registration and execution. + +### Methods + +| Method | Description | +|--------|-------------| +| `registerCommand(AbstractCommand)` | Register a command | + +### Example + +```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.CommandRegistry; +import com.hypixel.hytale.server.core.Message; +import java.util.concurrent.CompletableFuture; + +CommandRegistry commands = getCommandRegistry(); + +// Register a command - AbstractCommand.execute() returns CompletableFuture +commands.registerCommand(new AbstractCommand("hello", "my_plugin.commands.hello.description") { + @Override + protected CompletableFuture execute(CommandContext ctx) { + ctx.sender().sendMessage(Message.raw("Hello, World!")); + return null; // Return null for synchronous completion + } +}); +``` + +{{< callout type="info" >}} +**Note:** Create a dedicated command class by extending `AbstractCommand` for complex commands. See [Creating Commands](/commands/creating-commands/) for details. +{{< /callout >}} + +## TaskRegistry + +Tracks asynchronous tasks for cleanup during plugin shutdown. + +{{< callout type="warning" >}} +**Important:** TaskRegistry does NOT have `runAsync()`, `runSync()`, `runLater()`, or `runRepeating()` methods. Use Java's standard concurrency APIs instead. +{{< /callout >}} + +### Methods + +| Method | Description | +|--------|-------------| +| `registerTask(CompletableFuture)` | Register a CompletableFuture for tracking | +| `registerTask(ScheduledFuture)` | Register a ScheduledFuture for tracking | + +### Example + +```java +// Async operation +CompletableFuture task = CompletableFuture.runAsync(() -> { + // Background work + Data result = heavyComputation(); + + world.execute(() -> { + playerRef.sendMessage(Message.raw("Done: " + result)); + }); +}); +getTaskRegistry().registerTask(task); + +// Delayed operation (3 seconds) +CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS) + .execute(() -> { + world.execute(() -> { + getLogger().at(Level.INFO).log("3 seconds passed!"); + }); + }); + +// Repeating operation +ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); +ScheduledFuture repeating = scheduler.scheduleAtFixedRate(() -> { + broadcastTime(); +}, 0, 5, TimeUnit.MINUTES); +getTaskRegistry().registerTask((ScheduledFuture) repeating); +``` + +## AssetRegistry + +Registers custom asset stores with the game. + +**Package:** `com.hypixel.hytale.server.core.plugin.registry` + +{{< callout type="warning" >}} +**Important:** `AssetRegistry` does NOT provide `getItem()`, `getBlockType()`, etc. methods. To access assets, use the asset class's static methods directly. +{{< /callout >}} + +### Methods + +| Method | Description | +|--------|-------------| +| `register(AssetStore)` | Register a custom AssetStore | + +### Accessing Assets + +Assets are accessed via their class's static methods, not through AssetRegistry: + +```java +import com.hypixel.hytale.assetstore.map.DefaultAssetMap; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; + +// Get items by string ID directly (DefaultAssetMap) +DefaultAssetMap itemMap = Item.getAssetMap(); +Item sword = itemMap.getAsset("iron_sword"); + +// Get block types +DefaultAssetMap blockMap = BlockType.getAssetMap(); +BlockType stone = blockMap.getAsset("stone"); +``` + +{{< callout type="info" >}} +**Note:** Most assets like `Item` and `BlockType` use `DefaultAssetMap` where you can access by string key directly. Some assets like `Interaction` use `IndexedLookupTableAssetMap` which requires getting the index first. +{{< /callout >}} + +### Custom Asset Store Registration + +```java +AssetRegistry assets = getAssetRegistry(); + +// Register a custom asset store +assets.register(MyCustomAsset.getAssetStore()); +``` + +## BlockStateRegistry + +Manages block states and their properties. + +### Methods + +| Method | Description | +|--------|-------------| +| `getBlockState(BlockType)` | Get default state | +| `getBlockState(BlockType, Properties)` | Get state with properties | + +### Example + +```java +BlockStateRegistry blocks = getBlockStateRegistry(); + +BlockState stone = blocks.getBlockState(stoneType); +world.setBlockState(x, y, z, stone); +``` + +## Codec Registration + +**Package:** `com.hypixel.hytale.server.core.plugin.registry` + +{{< callout type="warning" >}} +**Important:** There is no simple `CodecRegistry` class. Instead, use the typed `getCodecRegistry()` methods with codec map parameters. +{{< /callout >}} + +### Methods + +| Method | Description | +|--------|-------------| +| `getCodecRegistry(StringCodecMapCodec)` | Get CodecMapRegistry for string-keyed codecs | +| `getCodecRegistry(AssetCodecMapCodec)` | Get CodecMapRegistry.Assets for asset codecs | +| `getCodecRegistry(MapKeyMapCodec)` | Get MapKeyMapRegistry for map-keyed codecs | + +### Example + +```java +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.server.core.plugin.registry.CodecMapRegistry; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction; + +// Register a custom interaction type using the Interaction CODEC +CodecMapRegistry.Assets registry = + getCodecRegistry(Interaction.CODEC); + +registry.register("MyCustomInteraction", MyCustomInteraction.class, MyCustomInteraction.CODEC); +``` + +For simple data serialization, use `BuilderCodec` directly: + +```java +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.Codec; + +// Define a codec for your data class +public static final BuilderCodec CODEC = BuilderCodec.builder(PlayerData.class, PlayerData::new) + .append(new KeyedCodec<>("name", Codec.STRING), + (obj, val) -> obj.name = val, + obj -> obj.name) + .add() + .build(); +``` + +## EntityRegistry + +Registers custom entity types with the game. + +**Package:** `com.hypixel.hytale.server.core.modules.entity` + +{{< callout type="warning" >}} +**Important:** `EntityRegistry` is for registering custom entity types, NOT for spawning entities. To spawn entities, use `World.spawnEntity()`. +{{< /callout >}} + +### Methods + +| Method | Description | +|--------|-------------| +| `registerEntity(String, Class, Function, Codec)` | Register a custom entity type | + +### Registering Custom Entities + +```java +import com.hypixel.hytale.server.core.entity.Entity; +import com.hypixel.hytale.codec.DirectDecodeCodec; + +EntityRegistry entities = getEntityRegistry(); + +// Register a custom entity type +entities.registerEntity( + "my_custom_entity", + MyCustomEntity.class, + world -> new MyCustomEntity(world), + MyCustomEntity.CODEC +); +``` + +### Spawning Entities + +To spawn entities at runtime, use `World`: + +```java +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.entity.Entity; + +World world = player.getWorld(); + +// Spawn an entity +Entity entity = world.spawnEntity(entityClass, position); +``` + +## Registry Access Patterns + +### Lazy Initialization + +```java +public class MyPlugin extends PluginBase { + private EventRegistry events; + private CommandRegistry commands; + + @Override + public void start() { + this.events = getEventRegistry(); + this.commands = getCommandRegistry(); + + registerEvents(); + registerCommands(); + } + + private void registerEvents() { + events.register(PlayerConnectEvent.class, this::onJoin); + } + + private void registerCommands() { + commands.register(new MyCommand()); + } +} +``` + +### Helper Methods + +```java +public class MyPlugin extends JavaPlugin { + public void runOnWorldThread(World world, Runnable task) { + world.execute(task); + } + + public void runAsync(Runnable task) { + CompletableFuture.runAsync(task); + } + + public Item getItem(String id) { + return getAssetRegistry().getItem(id); + } +} +``` + +## Thread Safety + +{{< callout type="warning" >}} +**Registry Thread Safety:** +- Most registries are designed for world thread access +- Each World runs on its own dedicated thread +- Use `world.execute()` when accessing from async code +- TaskRegistry is thread-safe for task registration +{{< /callout >}} + +```java +// Safe async pattern +World world = player.getWorld(); +PlayerRef playerRef = player.getPlayerRef(); + +CompletableFuture.runAsync(() -> { + // Background work + Data result = compute(); + + // Return to world thread for game state changes + world.execute(() -> { + // Access other registries here safely + EventRegistry events = getEventRegistry(); + playerRef.sendMessage(Message.raw("Result: " + result)); + }); +}); +``` diff --git a/content/reference/all-registries.fr.md b/content/reference/all-registries.fr.md new file mode 100644 index 0000000..cacd2d2 --- /dev/null +++ b/content/reference/all-registries.fr.md @@ -0,0 +1,379 @@ +--- +title: Tous les Registres +type: docs +weight: 2 +--- + +Référence complète de tous les registres disponibles dans l'API Plugin Hytale. + +## Aperçu des Registres + +Les registres sont accessibles via les méthodes de `PluginBase` : + +```java +public class MyPlugin extends PluginBase { + @Override + public void start() { + EventRegistry events = getEventRegistry(); + CommandRegistry commands = getCommandRegistry(); + TaskRegistry tasks = getTaskRegistry(); + AssetRegistry assets = getAssetRegistry(); + BlockStateRegistry blocks = getBlockStateRegistry(); + CodecRegistry codecs = getCodecRegistry(); + EntityRegistry entities = getEntityRegistry(); + } +} +``` + +## EventRegistry + +Gère les écouteurs d'événements et la diffusion. + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `register(Class, Consumer)` | Enregistrer un handler d'événement | +| `register(EventPriority, Class, Consumer)` | Enregistrer avec priorité | +| `registerGlobal(Class, Consumer)` | Enregistrer un handler global | +| `registerAsync(Class, Function)` | Enregistrer un handler async | +| `unregister(listener)` | Supprimer un écouteur | + +### Exemple + +```java +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.Message; +import java.util.logging.Level; + +EventRegistry events = getEventRegistry(); + +// Utilisez getPlayerRef() - getPlayer() est déprécié +events.register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + playerRef.sendMessage(Message.raw("Bienvenue !")); +}); + +events.register(EventPriority.FIRST, PlayerDisconnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + getLogger().at(Level.INFO).log(playerRef.getUsername() + " est parti"); +}); +``` + +## CommandRegistry + +Gère l'enregistrement et l'exécution des commandes. + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `registerCommand(AbstractCommand)` | Enregistrer une commande | + +### Exemple + +```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.CommandRegistry; +import com.hypixel.hytale.server.core.Message; +import java.util.concurrent.CompletableFuture; + +CommandRegistry commands = getCommandRegistry(); + +// Enregistrer une commande - AbstractCommand.execute() retourne CompletableFuture +commands.registerCommand(new AbstractCommand("hello", "my_plugin.commands.hello.description") { + @Override + protected CompletableFuture execute(CommandContext ctx) { + ctx.sender().sendMessage(Message.raw("Bonjour, Monde !")); + return null; // Retourner null pour une complétion synchrone + } +}); +``` + +{{< callout type="info" >}} +**Note :** Créez une classe de commande dédiée en étendant `AbstractCommand` pour les commandes complexes. Voir [Créer des Commandes](/fr/commands/creating-commands/) pour les détails. +{{< /callout >}} + +## TaskRegistry + +Suit les tâches asynchrones pour le nettoyage lors de l'arrêt du plugin. + +{{< callout type="warning" >}} +**Important :** TaskRegistry n'a PAS de méthodes `runAsync()`, `runSync()`, `runLater()`, ou `runRepeating()`. Utilisez les APIs de concurrence standard de Java à la place. +{{< /callout >}} + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `registerTask(CompletableFuture)` | Enregistrer un CompletableFuture pour le suivi | +| `registerTask(ScheduledFuture)` | Enregistrer un ScheduledFuture pour le suivi | + +### Exemple + +```java +// Opération async +CompletableFuture task = CompletableFuture.runAsync(() -> { + // Travail en arrière-plan + Data result = heavyComputation(); + + world.execute(() -> { + playerRef.sendMessage(Message.raw("Terminé : " + result)); + }); +}); +getTaskRegistry().registerTask(task); + +// Opération différée (3 secondes) +CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS) + .execute(() -> { + world.execute(() -> { + getLogger().at(Level.INFO).log("3 secondes passées !"); + }); + }); + +// Opération répétitive +ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); +ScheduledFuture repeating = scheduler.scheduleAtFixedRate(() -> { + broadcastTime(); +}, 0, 5, TimeUnit.MINUTES); +getTaskRegistry().registerTask((ScheduledFuture) repeating); +``` + +## AssetRegistry + +Enregistre les stores d'assets personnalisés avec le jeu. + +**Package :** `com.hypixel.hytale.server.core.plugin.registry` + +{{< callout type="warning" >}} +**Important :** `AssetRegistry` ne fournit PAS de méthodes `getItem()`, `getBlockType()`, etc. Pour accéder aux assets, utilisez directement les méthodes statiques de la classe d'asset. +{{< /callout >}} + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `register(AssetStore)` | Enregistrer un AssetStore personnalisé | + +### Accéder aux Assets + +Les assets sont accessibles via les méthodes statiques de leur classe, pas via AssetRegistry : + +```java +import com.hypixel.hytale.assetstore.map.DefaultAssetMap; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; + +// Obtenir les objets par ID string directement (DefaultAssetMap) +DefaultAssetMap itemMap = Item.getAssetMap(); +Item sword = itemMap.getAsset("iron_sword"); + +// Obtenir les types de blocs +DefaultAssetMap blockMap = BlockType.getAssetMap(); +BlockType stone = blockMap.getAsset("stone"); +``` + +{{< callout type="info" >}} +**Note :** La plupart des assets comme `Item` et `BlockType` utilisent `DefaultAssetMap` où vous pouvez accéder directement par clé string. Certains assets comme `Interaction` utilisent `IndexedLookupTableAssetMap` qui nécessite d'obtenir l'index d'abord. +{{< /callout >}} + +### Enregistrement d'Asset Store Personnalisé + +```java +AssetRegistry assets = getAssetRegistry(); + +// Enregistrer un asset store personnalisé +assets.register(MyCustomAsset.getAssetStore()); +``` + +## BlockStateRegistry + +Gère les états de blocs et leurs propriétés. + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `getBlockState(BlockType)` | Obtenir l'état par défaut | +| `getBlockState(BlockType, Properties)` | Obtenir l'état avec propriétés | + +### Exemple + +```java +BlockStateRegistry blocks = getBlockStateRegistry(); + +BlockState stone = blocks.getBlockState(stoneType); +world.setBlockState(x, y, z, stone); +``` + +## Enregistrement de Codecs + +**Package :** `com.hypixel.hytale.server.core.plugin.registry` + +{{< callout type="warning" >}} +**Important :** Il n'y a pas de classe `CodecRegistry` simple. Utilisez plutôt les méthodes typées `getCodecRegistry()` avec des paramètres de codec map. +{{< /callout >}} + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `getCodecRegistry(StringCodecMapCodec)` | Obtenir CodecMapRegistry pour codecs à clés string | +| `getCodecRegistry(AssetCodecMapCodec)` | Obtenir CodecMapRegistry.Assets pour codecs d'assets | +| `getCodecRegistry(MapKeyMapCodec)` | Obtenir MapKeyMapRegistry pour codecs à clés map | + +### Exemple + +```java +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.server.core.plugin.registry.CodecMapRegistry; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction; + +// Enregistrer un type d'interaction personnalisé utilisant le CODEC d'Interaction +CodecMapRegistry.Assets registry = + getCodecRegistry(Interaction.CODEC); + +registry.register("MyCustomInteraction", MyCustomInteraction.class, MyCustomInteraction.CODEC); +``` + +Pour la sérialisation simple de données, utilisez `BuilderCodec` directement : + +```java +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.Codec; + +// Définir un codec pour votre classe de données +public static final BuilderCodec CODEC = BuilderCodec.builder(PlayerData.class, PlayerData::new) + .append(new KeyedCodec<>("name", Codec.STRING), + (obj, val) -> obj.name = val, + obj -> obj.name) + .add() + .build(); +``` + +## EntityRegistry + +Enregistre les types d'entités personnalisés avec le jeu. + +**Package :** `com.hypixel.hytale.server.core.modules.entity` + +{{< callout type="warning" >}} +**Important :** `EntityRegistry` sert à enregistrer des types d'entités personnalisés, PAS à faire apparaître des entités. Pour faire apparaître des entités, utilisez `World.spawnEntity()`. +{{< /callout >}} + +### Méthodes + +| Méthode | Description | +|---------|-------------| +| `registerEntity(String, Class, Function, Codec)` | Enregistrer un type d'entité personnalisé | + +### Enregistrer des Entités Personnalisées + +```java +import com.hypixel.hytale.server.core.entity.Entity; +import com.hypixel.hytale.codec.DirectDecodeCodec; + +EntityRegistry entities = getEntityRegistry(); + +// Enregistrer un type d'entité personnalisé +entities.registerEntity( + "my_custom_entity", + MyCustomEntity.class, + world -> new MyCustomEntity(world), + MyCustomEntity.CODEC +); +``` + +### Faire Apparaître des Entités + +Pour faire apparaître des entités à l'exécution, utilisez `World` : + +```java +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.entity.Entity; + +World world = player.getWorld(); + +// Faire apparaître une entité +Entity entity = world.spawnEntity(entityClass, position); +``` + +## Patterns d'Accès aux Registres + +### Initialisation Différée + +```java +public class MyPlugin extends PluginBase { + private EventRegistry events; + private CommandRegistry commands; + + @Override + public void start() { + this.events = getEventRegistry(); + this.commands = getCommandRegistry(); + + registerEvents(); + registerCommands(); + } + + private void registerEvents() { + events.register(PlayerConnectEvent.class, this::onJoin); + } + + private void registerCommands() { + commands.registerCommand(new MyCommand()); + } +} +``` + +### Méthodes Helper + +```java +public class MyPlugin extends JavaPlugin { + public void runOnWorldThread(World world, Runnable task) { + world.execute(task); + } + + public void runAsync(Runnable task) { + CompletableFuture.runAsync(task); + } + + public Item getItem(String id) { + return Item.getAssetMap().getAsset(id); + } +} +``` + +## Sécurité des Threads + +{{< callout type="warning" >}} +**Sécurité des Threads des Registres :** +- La plupart des registres sont conçus pour l'accès sur le thread du monde +- Chaque World s'exécute sur son propre thread dédié +- Utilisez `world.execute()` lors de l'accès depuis du code async +- TaskRegistry est thread-safe pour l'enregistrement des tâches +{{< /callout >}} + +```java +// Pattern async sûr +World world = player.getWorld(); +PlayerRef playerRef = player.getPlayerRef(); + +CompletableFuture.runAsync(() -> { + // Travail en arrière-plan + Data result = compute(); + + // Retourner au thread du monde pour les changements d'état du jeu + world.execute(() -> { + // Accéder aux autres registres ici en sécurité + EventRegistry events = getEventRegistry(); + playerRef.sendMessage(Message.raw("Résultat : " + result)); + }); +}); +``` diff --git a/content/reference/buildertools/_index.en.md b/content/reference/buildertools/_index.en.md new file mode 100644 index 0000000..68a17ea --- /dev/null +++ b/content/reference/buildertools/_index.en.md @@ -0,0 +1,126 @@ +--- +title: Builder Tools +type: docs +weight: 3 +--- + +The Builder Tools system provides world editing capabilities for creative mode, including selection operations, scripted brushes, and prefab editing. + +**Package:** `com.hypixel.hytale.builtin.buildertools` + +## Architecture + +``` +BuilderToolsPlugin +├── Commands (37+) +│ ├── Selection: /pos1, /pos2, /deselect, /expand, /contract, /shift +│ ├── Clipboard: /copy, /cut, /paste, /rotate, /flip +│ ├── Modification: /set, /fill, /replace, /walls, /hollow +│ ├── History: /undo, /redo, /clearhistory +│ └── Utilities: /stack, /move, /tint, /submerge +├── Scripted Brushes +│ ├── ScriptedBrushAsset - Brush definitions +│ ├── BrushOperation - Operation base classes +│ └── BrushConfigCommand - Brush configuration +├── Prefab Editor +│ ├── PrefabEditSession - Editing session +│ ├── PrefabAnchor - Anchor points +│ └── PrefabCommand - Prefab commands +├── Snapshots +│ ├── SelectionSnapshot - Selection history +│ ├── ClipboardContentsSnapshot - Clipboard data +│ └── EntityTransformSnapshot - Entity changes +├── Tool Operations +│ ├── PaintOperation - Paint blocks +│ └── ToolOperation - Base operations +└── Systems + ├── BuilderToolsSystems - Main processing + └── BuilderToolsUserDataSystem - Per-user data +``` + +## Commands Overview + +### Selection Commands + +| Command | Description | +|---------|-------------| +| `/pos1` | Set selection point 1 | +| `/pos2` | Set selection point 2 | +| `/deselect` | Clear selection | +| `/expand [direction]` | Expand selection | +| `/contract [direction]` | Contract selection | +| `/shift [direction]` | Move selection | +| `/selectchunk` | Select entire chunk | +| `/selectchunksection` | Select chunk section | + +### Clipboard Commands + +| Command | Description | +|---------|-------------| +| `/copy` | Copy selection to clipboard | +| `/cut` | Cut selection to clipboard | +| `/paste` | Paste clipboard | +| `/rotate ` | Rotate clipboard | +| `/flip ` | Flip clipboard | + +### Modification Commands + +| Command | Description | +|---------|-------------| +| `/set ` | Fill selection with blocks | +| `/fill ` | Fill selection | +| `/replace ` | Replace blocks | +| `/walls ` | Create walls around selection | +| `/hollow` | Hollow out selection | +| `/stack [direction]` | Stack selection | +| `/move ` | Move blocks | + +### History Commands + +| Command | Description | +|---------|-------------| +| `/undo [count]` | Undo operations | +| `/redo [count]` | Redo operations | +| `/clearedithistory` | Clear edit history | +| `/settoolhistorysize ` | Set history limit | + +### Utility Commands + +| Command | Description | +|---------|-------------| +| `/tint ` | Tint blocks | +| `/submerge ` | Submerge selection | +| `/clearblocks` | Clear blocks | +| `/clearentities` | Clear entities | +| `/environment` | Set environment | +| `/globalmask` | Set global mask | + +## Quick Start + +```java +// Builder tools are enabled in creative mode +// Players can use selection and editing commands + +// Programmatic access to builder tools data via ECS components +import com.hypixel.hytale.builtin.buildertools.BuilderToolsPlugin; +import com.hypixel.hytale.builtin.buildertools.BuilderToolsUserData; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; + +// Get the component type +BuilderToolsPlugin builderTools = BuilderToolsPlugin.get(); +ComponentType userDataType = builderTools.getUserDataComponentType(); + +// Access user data via command buffer +BuilderToolsUserData userData = commandBuffer.getComponent(playerRef, userDataType); +``` + +## Section Contents + +{{< cards >}} + {{< card link="scripted-brushes" title="Scripted Brushes" subtitle="Programmable brush operations" icon="code" >}} + {{< card link="builder-commands" title="Builder Commands" subtitle="All building commands reference" icon="terminal" >}} + {{< card link="prefab-editor" title="Prefab Editor" subtitle="Create and edit prefabs" icon="cube" >}} + {{< card link="tool-operations" title="Tool Operations" subtitle="Block painting and editing" icon="pencil" >}} + {{< card link="snapshots" title="Snapshots" subtitle="Undo/redo and history" icon="clock" >}} +{{< /cards >}} diff --git a/content/reference/buildertools/_index.fr.md b/content/reference/buildertools/_index.fr.md new file mode 100644 index 0000000..0b38534 --- /dev/null +++ b/content/reference/buildertools/_index.fr.md @@ -0,0 +1,126 @@ +--- +title: Outils de Construction +type: docs +weight: 3 +--- + +Le systeme d'outils de construction fournit des capacites d'edition de monde pour le mode creatif, incluant operations de selection, brosses scriptees et edition de prefabs. + +**Package:** `com.hypixel.hytale.builtin.buildertools` + +## Architecture + +``` +BuilderToolsPlugin +├── Commandes (37+) +│ ├── Selection: /pos1, /pos2, /deselect, /expand, /contract, /shift +│ ├── Presse-papiers: /copy, /cut, /paste, /rotate, /flip +│ ├── Modification: /set, /fill, /replace, /walls, /hollow +│ ├── Historique: /undo, /redo, /clearhistory +│ └── Utilitaires: /stack, /move, /tint, /submerge +├── Brosses Scriptees +│ ├── ScriptedBrushAsset - Definitions de brosses +│ ├── BrushOperation - Classes de base operations +│ └── BrushConfigCommand - Configuration brosses +├── Editeur Prefab +│ ├── PrefabEditSession - Session d'edition +│ ├── PrefabAnchor - Points d'ancrage +│ └── PrefabCommand - Commandes prefab +├── Snapshots +│ ├── SelectionSnapshot - Historique selection +│ ├── ClipboardContentsSnapshot - Donnees presse-papiers +│ └── EntityTransformSnapshot - Changements entites +├── Operations Outil +│ ├── PaintOperation - Peindre blocs +│ └── ToolOperation - Operations de base +└── Systemes + ├── BuilderToolsSystems - Traitement principal + └── BuilderToolsUserDataSystem - Donnees par utilisateur +``` + +## Apercu des Commandes + +### Commandes de Selection + +| Commande | Description | +|----------|-------------| +| `/pos1` | Definir point de selection 1 | +| `/pos2` | Definir point de selection 2 | +| `/deselect` | Effacer selection | +| `/expand [direction]` | Etendre selection | +| `/contract [direction]` | Contracter selection | +| `/shift [direction]` | Deplacer selection | +| `/selectchunk` | Selectionner chunk entier | +| `/selectchunksection` | Selectionner section chunk | + +### Commandes Presse-papiers + +| Commande | Description | +|----------|-------------| +| `/copy` | Copier selection vers presse-papiers | +| `/cut` | Couper selection vers presse-papiers | +| `/paste` | Coller presse-papiers | +| `/rotate ` | Pivoter presse-papiers | +| `/flip ` | Retourner presse-papiers | + +### Commandes de Modification + +| Commande | Description | +|----------|-------------| +| `/set ` | Remplir selection avec blocs | +| `/fill ` | Remplir selection | +| `/replace ` | Remplacer blocs | +| `/walls ` | Creer murs autour selection | +| `/hollow` | Evider selection | +| `/stack [direction]` | Empiler selection | +| `/move ` | Deplacer blocs | + +### Commandes Historique + +| Commande | Description | +|----------|-------------| +| `/undo [nombre]` | Annuler operations | +| `/redo [nombre]` | Refaire operations | +| `/clearedithistory` | Effacer historique edition | +| `/settoolhistorysize ` | Definir limite historique | + +### Commandes Utilitaires + +| Commande | Description | +|----------|-------------| +| `/tint ` | Teinter blocs | +| `/submerge ` | Submerger selection | +| `/clearblocks` | Effacer blocs | +| `/clearentities` | Effacer entites | +| `/environment` | Definir environnement | +| `/globalmask` | Definir masque global | + +## Demarrage Rapide + +```java +// Les outils de construction sont actives en mode creatif +// Les joueurs peuvent utiliser les commandes de selection et edition + +// Acces programmatique aux donnees outils construction via composants ECS +import com.hypixel.hytale.builtin.buildertools.BuilderToolsPlugin; +import com.hypixel.hytale.builtin.buildertools.BuilderToolsUserData; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; + +// Obtenir le type de composant +BuilderToolsPlugin builderTools = BuilderToolsPlugin.get(); +ComponentType userDataType = builderTools.getUserDataComponentType(); + +// Accéder aux données utilisateur via command buffer +BuilderToolsUserData userData = commandBuffer.getComponent(playerRef, userDataType); +``` + +## Contenu de la Section + +{{< cards >}} + {{< card link="scripted-brushes" title="Brosses Scriptees" subtitle="Operations de brosses programmables" icon="code" >}} + {{< card link="builder-commands" title="Commandes Construction" subtitle="Reference de toutes les commandes" icon="terminal" >}} + {{< card link="prefab-editor" title="Editeur Prefab" subtitle="Creer et editer prefabs" icon="cube" >}} + {{< card link="tool-operations" title="Operations Outil" subtitle="Peinture et edition blocs" icon="pencil" >}} + {{< card link="snapshots" title="Snapshots" subtitle="Annuler/refaire et historique" icon="clock" >}} +{{< /cards >}} diff --git a/content/reference/buildertools/builder-commands.en.md b/content/reference/buildertools/builder-commands.en.md new file mode 100644 index 0000000..5e62703 --- /dev/null +++ b/content/reference/buildertools/builder-commands.en.md @@ -0,0 +1,394 @@ +--- +title: Builder Commands +type: docs +weight: 2 +--- + +Complete reference for all builder tool commands available in creative mode. + +**Package:** `com.hypixel.hytale.builtin.buildertools.commands` + +## Selection Commands + +### /pos1, /pos2 + +Set selection corners at current position or specified coordinates. + +``` +/pos1 [ ] +/pos2 [ ] +``` + +**Examples:** +- `/pos1` - Set pos1 at current position +- `/pos1 100 64 200` - Set pos1 at coordinates +- `/pos2 ~10 ~ ~10` - Set pos2 relative to current position + +### /deselect + +Clear the current selection. + +``` +/deselect +``` + +### /expand + +Expand selection in a direction. + +``` +/expand [direction] +``` + +**Directions:** `up`, `down`, `north`, `south`, `east`, `west`, `all` + +**Examples:** +- `/expand 10 up` - Expand 10 blocks upward +- `/expand 5 all` - Expand 5 blocks in all directions + +### /contract + +Contract selection from a direction. + +``` +/contract [direction] +``` + +### /shift + +Move selection without affecting blocks. + +``` +/shift [direction] +``` + +### /selectchunk + +Select the entire chunk at current position. + +``` +/selectchunk +``` + +### /selectchunksection + +Select a 16x16x16 chunk section. + +``` +/selectchunksection +``` + +### /selectionhistory + +Navigate selection history. + +``` +/selectionhistory undo +/selectionhistory redo +``` + +### /updateselection + +Refresh selection display. + +``` +/updateselection +``` + +## Clipboard Commands + +### /copy + +Copy selection to clipboard. + +``` +/copy +``` + +### /cut + +Cut selection to clipboard (removes blocks). + +``` +/cut +``` + +### /paste + +Paste clipboard at current position. + +``` +/paste [flags] +``` + +**Flags:** +- `-a` - Ignore air blocks +- `-e` - Include entities +- `-o` - Paste at original location + +### /rotate + +Rotate clipboard contents. + +``` +/rotate +``` + +**Angles:** `90`, `180`, `270` + +### /flip + +Flip clipboard contents. + +``` +/flip +``` + +**Axes:** `x`, `y`, `z` + +## Modification Commands + +### /set + +Fill selection with a block pattern. + +``` +/set +``` + +**Examples:** +- `/set [Stone]` - Fill with stone +- `/set [50%Stone, 50%Dirt]` - Fill with weighted random +- `/set [Air]` - Clear selection + +### /fill + +Fill selection (alias for set). + +``` +/fill +``` + +### /replace + +Replace blocks matching a mask. + +``` +/replace +``` + +**Examples:** +- `/replace [Stone] [Dirt]` - Replace stone with dirt +- `/replace [!Air] [Glass]` - Replace all non-air with glass + +### /walls + +Create walls around selection. + +``` +/walls +``` + +### /hollow + +Hollow out the selection, leaving only walls. + +``` +/hollow [thickness] +``` + +### /stack + +Stack selection multiple times. + +``` +/stack [direction] +``` + +**Examples:** +- `/stack 5 up` - Stack 5 times upward +- `/stack 10 north` - Stack 10 times north + +### /move + +Move blocks in selection. + +``` +/move +``` + +## History Commands + +### /undo + +Undo previous operations. + +``` +/undo [count] +``` + +### /redo + +Redo undone operations. + +``` +/redo [count] +``` + +### /clearedithistory + +Clear all edit history. + +``` +/clearedithistory +``` + +### /settoolhistorysize + +Set maximum history size. + +``` +/settoolhistorysize +``` + +## Utility Commands + +### /tint + +Apply color tint to blocks. + +``` +/tint +``` + +### /submerge + +Submerge selection in fluid. + +``` +/submerge +``` + +### /clearblocks + +Clear all blocks in selection. + +``` +/clearblocks +``` + +### /clearentities + +Clear all entities in selection. + +``` +/clearentities +``` + +### /environment + +Set environment for area. + +``` +/environment +``` + +### /globalmask + +Set a global mask for all operations. + +``` +/globalmask +/globalmask clear +``` + +### /editline + +Create a line of blocks. + +``` +/editline +``` + +### /extendface + +Extend a face of selection. + +``` +/extendface +``` + +### /hotbarswitch + +Switch builder tool hotbar. + +``` +/hotbarswitch +``` + +### /repairfillers + +Repair filler blocks. + +``` +/repairfillers +``` + +## Import Commands + +### /imageimport + +Import image as blocks. + +``` +/imageimport +``` + +### /objimport + +Import OBJ model as blocks. + +``` +/objimport +``` + +## Prefab Commands + +### /prefab + +Manage prefabs. + +``` +/prefab save +/prefab load +/prefab list +/prefab delete +``` + +### /prefabedit + +Enter prefab editing mode. + +``` +/prefabedit +``` + +## Block Patterns + +Pattern syntax used by many commands: + +| Syntax | Description | Example | +|--------|-------------|---------| +| `[Block]` | Single block | `[Stone]` | +| `[Block1, Block2]` | Equal weights | `[Stone, Dirt]` | +| `[N%Block]` | Weighted | `[70%Stone, 30%Dirt]` | +| `[!Block]` | Exclude | `[!Air]` | +| `[!^Tag]` | Exclude tag | `[!^Fluid]` | + +## Permissions + +Builder commands require creative mode or appropriate permissions: + +``` +buildertools.command. +``` diff --git a/content/reference/buildertools/builder-commands.fr.md b/content/reference/buildertools/builder-commands.fr.md new file mode 100644 index 0000000..6be3ec7 --- /dev/null +++ b/content/reference/buildertools/builder-commands.fr.md @@ -0,0 +1,394 @@ +--- +title: Commandes Construction +type: docs +weight: 2 +--- + +Reference complete pour toutes les commandes d'outils de construction disponibles en mode creatif. + +**Package:** `com.hypixel.hytale.builtin.buildertools.commands` + +## Commandes de Selection + +### /pos1, /pos2 + +Definir les coins de selection a la position actuelle ou coordonnees specifiees. + +``` +/pos1 [ ] +/pos2 [ ] +``` + +**Exemples:** +- `/pos1` - Definir pos1 a la position actuelle +- `/pos1 100 64 200` - Definir pos1 aux coordonnees +- `/pos2 ~10 ~ ~10` - Definir pos2 relative a position actuelle + +### /deselect + +Effacer la selection actuelle. + +``` +/deselect +``` + +### /expand + +Etendre la selection dans une direction. + +``` +/expand [direction] +``` + +**Directions:** `up`, `down`, `north`, `south`, `east`, `west`, `all` + +**Exemples:** +- `/expand 10 up` - Etendre 10 blocs vers le haut +- `/expand 5 all` - Etendre 5 blocs dans toutes directions + +### /contract + +Contracter la selection depuis une direction. + +``` +/contract [direction] +``` + +### /shift + +Deplacer la selection sans affecter les blocs. + +``` +/shift [direction] +``` + +### /selectchunk + +Selectionner le chunk entier a la position actuelle. + +``` +/selectchunk +``` + +### /selectchunksection + +Selectionner une section de chunk 16x16x16. + +``` +/selectchunksection +``` + +### /selectionhistory + +Naviguer dans l'historique de selection. + +``` +/selectionhistory undo +/selectionhistory redo +``` + +### /updateselection + +Rafraichir l'affichage de selection. + +``` +/updateselection +``` + +## Commandes Presse-papiers + +### /copy + +Copier la selection vers le presse-papiers. + +``` +/copy +``` + +### /cut + +Couper la selection vers le presse-papiers (supprime les blocs). + +``` +/cut +``` + +### /paste + +Coller le presse-papiers a la position actuelle. + +``` +/paste [flags] +``` + +**Flags:** +- `-a` - Ignorer les blocs d'air +- `-e` - Inclure les entites +- `-o` - Coller a l'emplacement original + +### /rotate + +Pivoter le contenu du presse-papiers. + +``` +/rotate +``` + +**Angles:** `90`, `180`, `270` + +### /flip + +Retourner le contenu du presse-papiers. + +``` +/flip +``` + +**Axes:** `x`, `y`, `z` + +## Commandes de Modification + +### /set + +Remplir la selection avec un pattern de blocs. + +``` +/set +``` + +**Exemples:** +- `/set [Stone]` - Remplir avec pierre +- `/set [50%Stone, 50%Dirt]` - Remplir avec aleatoire pondere +- `/set [Air]` - Effacer selection + +### /fill + +Remplir la selection (alias pour set). + +``` +/fill +``` + +### /replace + +Remplacer les blocs correspondant a un masque. + +``` +/replace +``` + +**Exemples:** +- `/replace [Stone] [Dirt]` - Remplacer pierre par terre +- `/replace [!Air] [Glass]` - Remplacer tout non-air par verre + +### /walls + +Creer des murs autour de la selection. + +``` +/walls +``` + +### /hollow + +Evider la selection, ne laissant que les murs. + +``` +/hollow [epaisseur] +``` + +### /stack + +Empiler la selection plusieurs fois. + +``` +/stack [direction] +``` + +**Exemples:** +- `/stack 5 up` - Empiler 5 fois vers le haut +- `/stack 10 north` - Empiler 10 fois au nord + +### /move + +Deplacer les blocs dans la selection. + +``` +/move +``` + +## Commandes Historique + +### /undo + +Annuler les operations precedentes. + +``` +/undo [nombre] +``` + +### /redo + +Refaire les operations annulees. + +``` +/redo [nombre] +``` + +### /clearedithistory + +Effacer tout l'historique d'edition. + +``` +/clearedithistory +``` + +### /settoolhistorysize + +Definir la taille maximale de l'historique. + +``` +/settoolhistorysize +``` + +## Commandes Utilitaires + +### /tint + +Appliquer une teinte de couleur aux blocs. + +``` +/tint +``` + +### /submerge + +Submerger la selection dans un fluide. + +``` +/submerge +``` + +### /clearblocks + +Effacer tous les blocs dans la selection. + +``` +/clearblocks +``` + +### /clearentities + +Effacer toutes les entites dans la selection. + +``` +/clearentities +``` + +### /environment + +Definir l'environnement pour une zone. + +``` +/environment +``` + +### /globalmask + +Definir un masque global pour toutes les operations. + +``` +/globalmask +/globalmask clear +``` + +### /editline + +Creer une ligne de blocs. + +``` +/editline +``` + +### /extendface + +Etendre une face de la selection. + +``` +/extendface +``` + +### /hotbarswitch + +Changer de hotbar outil de construction. + +``` +/hotbarswitch +``` + +### /repairfillers + +Reparer les blocs de remplissage. + +``` +/repairfillers +``` + +## Commandes d'Import + +### /imageimport + +Importer une image en blocs. + +``` +/imageimport +``` + +### /objimport + +Importer un modele OBJ en blocs. + +``` +/objimport +``` + +## Commandes Prefab + +### /prefab + +Gerer les prefabs. + +``` +/prefab save +/prefab load +/prefab list +/prefab delete +``` + +### /prefabedit + +Entrer en mode edition prefab. + +``` +/prefabedit +``` + +## Patterns de Blocs + +Syntaxe de pattern utilisee par plusieurs commandes: + +| Syntaxe | Description | Exemple | +|---------|-------------|---------| +| `[Bloc]` | Bloc unique | `[Stone]` | +| `[Bloc1, Bloc2]` | Poids egaux | `[Stone, Dirt]` | +| `[N%Bloc]` | Pondere | `[70%Stone, 30%Dirt]` | +| `[!Bloc]` | Exclure | `[!Air]` | +| `[!^Tag]` | Exclure tag | `[!^Fluid]` | + +## Permissions + +Les commandes de construction necessitent le mode creatif ou permissions appropriees: + +``` +buildertools.command. +``` diff --git a/content/reference/buildertools/prefab-editor.en.md b/content/reference/buildertools/prefab-editor.en.md new file mode 100644 index 0000000..63633cf --- /dev/null +++ b/content/reference/buildertools/prefab-editor.en.md @@ -0,0 +1,229 @@ +--- +title: Prefab Editor +type: docs +weight: 3 +--- + +The prefab editor allows creating, editing, and managing prefabricated structures that can be placed in the world. + +**Package:** `com.hypixel.hytale.builtin.buildertools.prefabeditor` + +## Architecture + +``` +Prefab Editor +├── Session Management +│ ├── PrefabEditSession - Active edit session +│ └── PrefabEditSessionManager - Session coordinator +├── Components +│ ├── PrefabAnchor - Anchor point definitions +│ └── PrefabEditorCreationSettings +├── Interactions +│ ├── PrefabSelectionInteraction +│ └── PrefabSetAnchorInteraction +├── Systems +│ ├── PrefabDirtySystems - Track changes +│ └── PrefabMarkerProvider - Anchor markers +└── Commands + └── PrefabEditCommand +``` + +## Prefab Edit Session + +### Starting a Session + +``` +/prefabedit +``` + +This enters prefab editing mode where: +- Selection tools work within prefab bounds +- Changes are tracked separately +- Anchors can be set + +### PrefabEditSession + +```java +public class PrefabEditSession { + // Active editing session for a prefab + // Tracks modifications and anchors +} +``` + +### Session Manager + +```java +PrefabEditSessionManager manager = BuilderToolsPlugin.get().getPrefabEditSessionManager(); + +// Check if player is editing +boolean isEditing = manager.isEditing(player); + +// Get active session +PrefabEditSession session = manager.getSession(player); +``` + +## Prefab Anchors + +Anchors define special points within a prefab: + +### PrefabAnchor + +```java +public class PrefabAnchor { + // Position relative to prefab origin + // Name identifier + // Anchor type +} +``` + +### Setting Anchors + +Use the anchor interaction or command: + +``` +/prefab anchor set [x y z] +/prefab anchor remove +/prefab anchor list +``` + +### Anchor Types + +| Type | Description | +|------|-------------| +| `origin` | Prefab placement origin | +| `spawn` | Entity spawn point | +| `connection` | Connection to other prefabs | +| `custom` | User-defined anchor | + +## Prefab Commands + +### /prefab + +Main prefab management command: + +``` +/prefab save Save selection as prefab +/prefab load Load prefab to clipboard +/prefab list List available prefabs +/prefab delete Delete a prefab +/prefab info Show prefab information +``` + +### /prefabedit + +Enter prefab editing mode: + +``` +/prefabedit Edit existing prefab +/prefabedit exit Exit editing mode +/prefabedit save Save changes +/prefabedit discard Discard changes +``` + +## Interactions + +### PrefabSelectionInteraction + +Select prefabs in the world: + +```java +// Registered interaction type +getCodecRegistry(Interaction.CODEC).register( + "PrefabSelection", + PrefabSelectionInteraction.class, + PrefabSelectionInteraction.CODEC +); +``` + +### PrefabSetAnchorInteraction + +Set anchors via interaction: + +```java +// Allows clicking to place anchors +getCodecRegistry(Interaction.CODEC).register( + "PrefabSetAnchor", + PrefabSetAnchorInteraction.class, + PrefabSetAnchorInteraction.CODEC +); +``` + +## Creation Settings + +### PrefabEditorCreationSettings + +Configuration for creating new prefabs: + +```java +public class PrefabEditorCreationSettings { + // Settings for prefab creation + // Include entities flag + // Compression settings +} +``` + +## Systems + +### PrefabDirtySystems + +Tracks modifications to prefabs: + +```java +// Marks prefab as modified when changes occur +// Prompts save on exit +``` + +### PrefabMarkerProvider + +Provides visual markers for anchors: + +```java +// Displays anchor positions in creative mode +// Shows connection points +``` + +## API Usage + +### Save Selection as Prefab + +```java +BuilderToolsPlugin tools = BuilderToolsPlugin.get(); +// Selection must be defined first +// Use /prefab save command or API +``` + +### Load and Place Prefab + +```java +// Load to clipboard +// /prefab load + +// Paste at location +// /paste +``` + +### Check Edit Mode + +```java +PrefabEditSessionManager manager = + BuilderToolsPlugin.get().getPrefabEditSessionManager(); + +if (manager.isEditing(player)) { + PrefabEditSession session = manager.getSession(player); + // Work with session +} +``` + +## Prefab Storage + +Prefabs are stored in the world data: + +``` +worlds//prefabs/.prefab +``` + +Structure includes: +- Block data +- Entity data (if included) +- Anchor definitions +- Metadata diff --git a/content/reference/buildertools/prefab-editor.fr.md b/content/reference/buildertools/prefab-editor.fr.md new file mode 100644 index 0000000..70103da --- /dev/null +++ b/content/reference/buildertools/prefab-editor.fr.md @@ -0,0 +1,229 @@ +--- +title: Editeur Prefab +type: docs +weight: 3 +--- + +L'editeur de prefabs permet de creer, editer et gerer des structures prefabriquees qui peuvent etre placees dans le monde. + +**Package:** `com.hypixel.hytale.builtin.buildertools.prefabeditor` + +## Architecture + +``` +Editeur Prefab +├── Gestion Sessions +│ ├── PrefabEditSession - Session d'edition active +│ └── PrefabEditSessionManager - Coordinateur sessions +├── Composants +│ ├── PrefabAnchor - Definitions points d'ancrage +│ └── PrefabEditorCreationSettings +├── Interactions +│ ├── PrefabSelectionInteraction +│ └── PrefabSetAnchorInteraction +├── Systemes +│ ├── PrefabDirtySystems - Suivi changements +│ └── PrefabMarkerProvider - Marqueurs d'ancrage +└── Commandes + └── PrefabEditCommand +``` + +## Session Edition Prefab + +### Demarrer une Session + +``` +/prefabedit +``` + +Cela entre en mode edition prefab ou: +- Les outils de selection fonctionnent dans les limites du prefab +- Les changements sont suivis separement +- Les ancres peuvent etre definies + +### PrefabEditSession + +```java +public class PrefabEditSession { + // Session d'edition active pour un prefab + // Suit modifications et ancres +} +``` + +### Gestionnaire de Sessions + +```java +PrefabEditSessionManager manager = BuilderToolsPlugin.get().getPrefabEditSessionManager(); + +// Verifier si joueur edite +boolean isEditing = manager.isEditing(player); + +// Obtenir session active +PrefabEditSession session = manager.getSession(player); +``` + +## Ancres Prefab + +Les ancres definissent des points speciaux dans un prefab: + +### PrefabAnchor + +```java +public class PrefabAnchor { + // Position relative a l'origine prefab + // Identifiant nom + // Type d'ancre +} +``` + +### Definir des Ancres + +Utiliser l'interaction d'ancrage ou commande: + +``` +/prefab anchor set [x y z] +/prefab anchor remove +/prefab anchor list +``` + +### Types d'Ancres + +| Type | Description | +|------|-------------| +| `origin` | Origine placement prefab | +| `spawn` | Point de spawn entite | +| `connection` | Connexion vers autres prefabs | +| `custom` | Ancre definie par utilisateur | + +## Commandes Prefab + +### /prefab + +Commande principale gestion prefabs: + +``` +/prefab save Sauver selection comme prefab +/prefab load Charger prefab vers presse-papiers +/prefab list Lister prefabs disponibles +/prefab delete Supprimer un prefab +/prefab info Afficher informations prefab +``` + +### /prefabedit + +Entrer en mode edition prefab: + +``` +/prefabedit Editer prefab existant +/prefabedit exit Quitter mode edition +/prefabedit save Sauvegarder changements +/prefabedit discard Abandonner changements +``` + +## Interactions + +### PrefabSelectionInteraction + +Selectionner prefabs dans le monde: + +```java +// Type d'interaction enregistre +getCodecRegistry(Interaction.CODEC).register( + "PrefabSelection", + PrefabSelectionInteraction.class, + PrefabSelectionInteraction.CODEC +); +``` + +### PrefabSetAnchorInteraction + +Definir ancres via interaction: + +```java +// Permet de cliquer pour placer ancres +getCodecRegistry(Interaction.CODEC).register( + "PrefabSetAnchor", + PrefabSetAnchorInteraction.class, + PrefabSetAnchorInteraction.CODEC +); +``` + +## Parametres de Creation + +### PrefabEditorCreationSettings + +Configuration pour creer nouveaux prefabs: + +```java +public class PrefabEditorCreationSettings { + // Parametres pour creation prefab + // Flag inclure entites + // Parametres compression +} +``` + +## Systemes + +### PrefabDirtySystems + +Suit les modifications aux prefabs: + +```java +// Marque prefab comme modifie quand changements surviennent +// Demande sauvegarde a la sortie +``` + +### PrefabMarkerProvider + +Fournit marqueurs visuels pour ancres: + +```java +// Affiche positions d'ancrage en mode creatif +// Montre points de connexion +``` + +## Utilisation de l'API + +### Sauver Selection comme Prefab + +```java +BuilderToolsPlugin tools = BuilderToolsPlugin.get(); +// La selection doit etre definie d'abord +// Utiliser commande /prefab save ou API +``` + +### Charger et Placer Prefab + +```java +// Charger vers presse-papiers +// /prefab load + +// Coller a l'emplacement +// /paste +``` + +### Verifier Mode Edition + +```java +PrefabEditSessionManager manager = + BuilderToolsPlugin.get().getPrefabEditSessionManager(); + +if (manager.isEditing(player)) { + PrefabEditSession session = manager.getSession(player); + // Travailler avec session +} +``` + +## Stockage Prefab + +Les prefabs sont stockes dans les donnees du monde: + +``` +worlds//prefabs/.prefab +``` + +La structure inclut: +- Donnees de blocs +- Donnees d'entites (si incluses) +- Definitions d'ancres +- Metadonnees diff --git a/content/reference/buildertools/scripted-brushes.en.md b/content/reference/buildertools/scripted-brushes.en.md new file mode 100644 index 0000000..1fc0ca3 --- /dev/null +++ b/content/reference/buildertools/scripted-brushes.en.md @@ -0,0 +1,387 @@ +--- +title: Scripted Brushes +type: docs +weight: 1 +--- + +Scripted brushes allow creating complex, programmable brush operations through JSON asset definitions. + +**Package:** `com.hypixel.hytale.builtin.buildertools.scriptedbrushes` + +## Architecture + +``` +Scripted Brushes +├── Assets +│ └── ScriptedBrushAsset - Brush definitions +├── Operations +│ ├── BrushOperation - Base operation class +│ ├── SequenceBrushOperation - Sequential operations +│ └── GlobalBrushOperation - Global modifiers +├── Categories +│ ├── Shape Operations - Set, fill, shape +│ ├── Mask Operations - Mask, history mask +│ ├── Flow Control - Loops, jumps, conditions +│ ├── Material Operations - Pattern, replace +│ └── Transform Operations - Offset, dimensions +├── Config +│ ├── BrushConfig - Runtime configuration +│ └── BrushConfigEditStore - Edit state +└── Commands + └── BrushConfigCommand - /brushconfig +``` + +## ScriptedBrushAsset + +Brush definitions loaded from JSON: + +**Asset Location:** `BuilderTools/Brushes/` + +```yaml +# BuilderTools/Brushes/terrain_sphere.json +{ + "Id": "terrain_sphere", + "Operations": [ + { + "Type": "Shape", + "Shape": "Sphere" + }, + { + "Type": "Dimensions", + "Width": 5, + "Height": 5, + "Depth": 5 + }, + { + "Type": "Material", + "Pattern": "[Rock_Stone]" + }, + { + "Type": "Set" + } + ] +} +``` + +### Asset Access + +```java +// Get brush asset +ScriptedBrushAsset brush = ScriptedBrushAsset.get("terrain_sphere"); + +// Get operations +List operations = brush.getOperations(); + +// Load into executor +brush.loadIntoExecutor(executor); +``` + +## Operation Types + +### Shape Operations + +| Operation | Description | +|-----------|-------------| +| `Set` | Apply blocks at positions | +| `Shape` | Define brush shape (Sphere, Cube, Cylinder) | +| `Delete` | Remove blocks | +| `Smooth` | Smooth terrain | +| `Erode` | Erode terrain | +| `Lift` | Raise terrain | +| `Melt` | Melt terrain | + +```yaml +{ + "Type": "Shape", + "Shape": "Sphere" // Sphere, Cube, Cylinder +} +``` + +### Dimension Operations + +```yaml +{ + "Type": "Dimensions", + "Width": 10, + "Height": 5, + "Depth": 10 +} +``` + +```yaml +{ + "Type": "RandomizeDimensions", + "MinWidth": 3, + "MaxWidth": 7, + "MinHeight": 3, + "MaxHeight": 7 +} +``` + +### Material Operations + +```yaml +{ + "Type": "Material", + "Pattern": "[50%Rock_Stone, 50%Rock_Shale]" +} +``` + +```yaml +{ + "Type": "BlockPattern", + "Pattern": "[Rock_Stone]" +} +``` + +```yaml +{ + "Type": "Replace", + "From": "[Grass]", + "To": "[Dirt]" +} +``` + +### Mask Operations + +```yaml +{ + "Type": "Mask", + "Mask": "[!Air]" +} +``` + +```yaml +{ + "Type": "AppendMask", + "Mask": "[!Water]" +} +``` + +```yaml +{ + "Type": "HistoryMask" + // Only affect previously modified blocks +} +``` + +```yaml +{ + "Type": "UseBrushMask" + // Use mask from tool arguments +} +``` + +### Offset Operations + +```yaml +{ + "Type": "Offset", + "X": 0, + "Y": 5, + "Z": 0 +} +``` + +```yaml +{ + "Type": "RandomOffset", + "MinX": -2, + "MaxX": 2, + "MinY": 0, + "MaxY": 5, + "MinZ": -2, + "MaxZ": 2 +} +``` + +### Flow Control + +#### Loops + +```yaml +{ + "Type": "Loop", + "Count": 5, + "StartIndex": 2, + "EndIndex": 6 +} +``` + +```yaml +{ + "Type": "LoopRandom", + "MinCount": 3, + "MaxCount": 8 +} +``` + +```yaml +{ + "Type": "CircleOffsetAndLoop", + "Radius": 5, + "Count": 8 +} +``` + +#### Conditionals + +```yaml +{ + "Type": "JumpIfBlockType", + "BlockType": "Air", + "Index": 10, + "ElseIndex": 5 +} +``` + +```yaml +{ + "Type": "JumpIfClickType", + "ClickType": "Primary", + "Index": 3 +} +``` + +```yaml +{ + "Type": "JumpIfCompare", + "Variable": "height", + "Operator": ">", + "Value": 5, + "Index": 8 +} +``` + +```yaml +{ + "Type": "JumpToIndex", + "Index": 0 +} +``` + +```yaml +{ + "Type": "Exit" + // Stop brush execution +} +``` + +### Special Operations + +```yaml +{ + "Type": "Echo", + "Message": "Brush applied!" +} +``` + +```yaml +{ + "Type": "RunCommand", + "Command": "/fill [Stone]" +} +``` + +```yaml +{ + "Type": "PastePrefab", + "PrefabId": "tree_oak" +} +``` + +```yaml +{ + "Type": "HeightmapLayer", + "Layer": 0 +} +``` + +### Save/Load Operations + +```yaml +{ + "Type": "LoadOperationsFromAsset", + "AssetId": "common_setup" +} +``` + +```yaml +{ + "Type": "SaveBrushConfig", + "Key": "my_config" +} +``` + +```yaml +{ + "Type": "LoadBrushConfig", + "Key": "my_config" +} +``` + +## Global Operations + +Operations that affect the entire brush: + +```yaml +{ + "Type": "Debug", + "Enabled": true +} +``` + +```yaml +{ + "Type": "DisableHoldInteraction" + // Only trigger on click, not hold +} +``` + +```yaml +{ + "Type": "IgnoreExistingBrushData" +} +``` + +## Brush Config Command + +Manage brush configurations via command: + +| Command | Description | +|---------|-------------| +| `/brushconfig` | Base brush config command | +| `/brushconfig load ` | Load brush config | +| `/brushconfig list` | List available brushes | +| `/brushconfig clear` | Clear current config | +| `/brushconfig exit` | Exit brush mode | +| `/brushconfig debugstep` | Step through operations | + +## API Usage + +### Load Brush + +```java +ScriptedBrushAsset brush = ScriptedBrushAsset.get("terrain_sphere"); +``` + +### Execute Brush + +```java +BrushConfigCommandExecutor executor = new BrushConfigCommandExecutor(); +brush.loadIntoExecutor(executor); +// Execute operations sequentially +``` + +### Access Operations + +```java +List operations = brush.getOperations(); +for (BrushOperation op : operations) { + if (op instanceof SequenceBrushOperation) { + // Sequential operation + } else if (op instanceof GlobalBrushOperation) { + // Global modifier + } +} +``` diff --git a/content/reference/buildertools/scripted-brushes.fr.md b/content/reference/buildertools/scripted-brushes.fr.md new file mode 100644 index 0000000..7d8d448 --- /dev/null +++ b/content/reference/buildertools/scripted-brushes.fr.md @@ -0,0 +1,387 @@ +--- +title: Brosses Scriptees +type: docs +weight: 1 +--- + +Les brosses scriptees permettent de creer des operations de brosse complexes et programmables via definitions d'assets JSON. + +**Package:** `com.hypixel.hytale.builtin.buildertools.scriptedbrushes` + +## Architecture + +``` +Brosses Scriptees +├── Assets +│ └── ScriptedBrushAsset - Definitions de brosses +├── Operations +│ ├── BrushOperation - Classe operation de base +│ ├── SequenceBrushOperation - Operations sequentielles +│ └── GlobalBrushOperation - Modificateurs globaux +├── Categories +│ ├── Operations Forme - Set, fill, shape +│ ├── Operations Masque - Mask, history mask +│ ├── Controle Flux - Boucles, sauts, conditions +│ ├── Operations Materiau - Pattern, replace +│ └── Operations Transform - Offset, dimensions +├── Config +│ ├── BrushConfig - Configuration runtime +│ └── BrushConfigEditStore - Etat edition +└── Commandes + └── BrushConfigCommand - /brushconfig +``` + +## ScriptedBrushAsset + +Definitions de brosses chargees depuis JSON: + +**Emplacement Asset:** `BuilderTools/Brushes/` + +```yaml +# BuilderTools/Brushes/terrain_sphere.json +{ + "Id": "terrain_sphere", + "Operations": [ + { + "Type": "Shape", + "Shape": "Sphere" + }, + { + "Type": "Dimensions", + "Width": 5, + "Height": 5, + "Depth": 5 + }, + { + "Type": "Material", + "Pattern": "[Rock_Stone]" + }, + { + "Type": "Set" + } + ] +} +``` + +### Acces Asset + +```java +// Obtenir asset brosse +ScriptedBrushAsset brush = ScriptedBrushAsset.get("terrain_sphere"); + +// Obtenir operations +List operations = brush.getOperations(); + +// Charger dans executeur +brush.loadIntoExecutor(executor); +``` + +## Types d'Operations + +### Operations de Forme + +| Operation | Description | +|-----------|-------------| +| `Set` | Appliquer blocs aux positions | +| `Shape` | Definir forme brosse (Sphere, Cube, Cylinder) | +| `Delete` | Supprimer blocs | +| `Smooth` | Lisser terrain | +| `Erode` | Eroder terrain | +| `Lift` | Elever terrain | +| `Melt` | Fondre terrain | + +```yaml +{ + "Type": "Shape", + "Shape": "Sphere" // Sphere, Cube, Cylinder +} +``` + +### Operations de Dimension + +```yaml +{ + "Type": "Dimensions", + "Width": 10, + "Height": 5, + "Depth": 10 +} +``` + +```yaml +{ + "Type": "RandomizeDimensions", + "MinWidth": 3, + "MaxWidth": 7, + "MinHeight": 3, + "MaxHeight": 7 +} +``` + +### Operations de Materiau + +```yaml +{ + "Type": "Material", + "Pattern": "[50%Rock_Stone, 50%Rock_Shale]" +} +``` + +```yaml +{ + "Type": "BlockPattern", + "Pattern": "[Rock_Stone]" +} +``` + +```yaml +{ + "Type": "Replace", + "From": "[Grass]", + "To": "[Dirt]" +} +``` + +### Operations de Masque + +```yaml +{ + "Type": "Mask", + "Mask": "[!Air]" +} +``` + +```yaml +{ + "Type": "AppendMask", + "Mask": "[!Water]" +} +``` + +```yaml +{ + "Type": "HistoryMask" + // Affecter uniquement blocs precedemment modifies +} +``` + +```yaml +{ + "Type": "UseBrushMask" + // Utiliser masque des arguments outil +} +``` + +### Operations de Decalage + +```yaml +{ + "Type": "Offset", + "X": 0, + "Y": 5, + "Z": 0 +} +``` + +```yaml +{ + "Type": "RandomOffset", + "MinX": -2, + "MaxX": 2, + "MinY": 0, + "MaxY": 5, + "MinZ": -2, + "MaxZ": 2 +} +``` + +### Controle de Flux + +#### Boucles + +```yaml +{ + "Type": "Loop", + "Count": 5, + "StartIndex": 2, + "EndIndex": 6 +} +``` + +```yaml +{ + "Type": "LoopRandom", + "MinCount": 3, + "MaxCount": 8 +} +``` + +```yaml +{ + "Type": "CircleOffsetAndLoop", + "Radius": 5, + "Count": 8 +} +``` + +#### Conditionnels + +```yaml +{ + "Type": "JumpIfBlockType", + "BlockType": "Air", + "Index": 10, + "ElseIndex": 5 +} +``` + +```yaml +{ + "Type": "JumpIfClickType", + "ClickType": "Primary", + "Index": 3 +} +``` + +```yaml +{ + "Type": "JumpIfCompare", + "Variable": "height", + "Operator": ">", + "Value": 5, + "Index": 8 +} +``` + +```yaml +{ + "Type": "JumpToIndex", + "Index": 0 +} +``` + +```yaml +{ + "Type": "Exit" + // Arreter execution brosse +} +``` + +### Operations Speciales + +```yaml +{ + "Type": "Echo", + "Message": "Brosse appliquee!" +} +``` + +```yaml +{ + "Type": "RunCommand", + "Command": "/fill [Stone]" +} +``` + +```yaml +{ + "Type": "PastePrefab", + "PrefabId": "tree_oak" +} +``` + +```yaml +{ + "Type": "HeightmapLayer", + "Layer": 0 +} +``` + +### Operations Sauvegarde/Chargement + +```yaml +{ + "Type": "LoadOperationsFromAsset", + "AssetId": "common_setup" +} +``` + +```yaml +{ + "Type": "SaveBrushConfig", + "Key": "my_config" +} +``` + +```yaml +{ + "Type": "LoadBrushConfig", + "Key": "my_config" +} +``` + +## Operations Globales + +Operations qui affectent toute la brosse: + +```yaml +{ + "Type": "Debug", + "Enabled": true +} +``` + +```yaml +{ + "Type": "DisableHoldInteraction" + // Declencher uniquement au clic, pas au maintien +} +``` + +```yaml +{ + "Type": "IgnoreExistingBrushData" +} +``` + +## Commande Config Brosse + +Gerer les configurations de brosse via commande: + +| Commande | Description | +|----------|-------------| +| `/brushconfig` | Commande config brosse de base | +| `/brushconfig load ` | Charger config brosse | +| `/brushconfig list` | Lister brosses disponibles | +| `/brushconfig clear` | Effacer config actuelle | +| `/brushconfig exit` | Quitter mode brosse | +| `/brushconfig debugstep` | Executer pas a pas | + +## Utilisation de l'API + +### Charger une Brosse + +```java +ScriptedBrushAsset brush = ScriptedBrushAsset.get("terrain_sphere"); +``` + +### Executer une Brosse + +```java +BrushConfigCommandExecutor executor = new BrushConfigCommandExecutor(); +brush.loadIntoExecutor(executor); +// Executer operations sequentiellement +``` + +### Acceder aux Operations + +```java +List operations = brush.getOperations(); +for (BrushOperation op : operations) { + if (op instanceof SequenceBrushOperation) { + // Operation sequentielle + } else if (op instanceof GlobalBrushOperation) { + // Modificateur global + } +} +``` diff --git a/content/reference/buildertools/snapshots.en.md b/content/reference/buildertools/snapshots.en.md new file mode 100644 index 0000000..b69a725 --- /dev/null +++ b/content/reference/buildertools/snapshots.en.md @@ -0,0 +1,284 @@ +--- +title: Snapshots +type: docs +weight: 5 +--- + +The snapshot system provides undo/redo functionality for builder tools by capturing and restoring state. + +**Package:** `com.hypixel.hytale.builtin.buildertools.snapshot` + +## Architecture + +``` +Snapshots +├── Base +│ └── SelectionSnapshot - Snapshot interface +├── Block Snapshots +│ └── BlockSelectionSnapshot - Block selection state +├── Clipboard Snapshots +│ ├── ClipboardSnapshot - Clipboard interface +│ ├── ClipboardContentsSnapshot - Clipboard contents +│ └── ClipboardBoundsSnapshot - Selection bounds +└── Entity Snapshots + ├── EntitySnapshot - Entity interface + ├── EntityAddSnapshot - Track entity addition + ├── EntityRemoveSnapshot - Track entity removal + └── EntityTransformSnapshot - Track transform changes +``` + +## SelectionSnapshot Interface + +Base interface for all snapshots: + +```java +public interface SelectionSnapshot> { + // Restore to previous state + // Returns inverse snapshot for redo + T restore(Ref ref, Player player, World world, + ComponentAccessor accessor); +} +``` + +### How Undo/Redo Works + +The snapshot pattern creates inverse operations: +1. Snapshot captures current state before operation +2. Operation modifies the state +3. Calling `restore()` reverts changes +4. `restore()` returns new snapshot for redo + +## Block Selection Snapshots + +### BlockSelectionSnapshot + +Captures block selection state: + +```java +public class BlockSelectionSnapshot implements SelectionSnapshot { + private final BlockSelection selection; + + public BlockSelection getBlockSelection(); + + // Create copy of selection + public static BlockSelectionSnapshot copyOf(BlockSelection selection); +} +``` + +### Usage + +```java +// Before modifying selection +BlockSelectionSnapshot snapshot = BlockSelectionSnapshot.copyOf(selection); + +// Perform modifications... + +// To undo - restore returns redo snapshot +BlockSelectionSnapshot redoSnapshot = snapshot.restore(ref, player, world, accessor); +``` + +## Clipboard Snapshots + +### ClipboardSnapshot Interface + +Extended interface for clipboard operations: + +```java +public interface ClipboardSnapshot> + extends SelectionSnapshot { + + T restoreClipboard(Ref ref, Player player, World world, + BuilderToolsPlugin.BuilderState state, + ComponentAccessor accessor); +} +``` + +### ClipboardContentsSnapshot + +Captures clipboard contents: + +```java +public class ClipboardContentsSnapshot implements ClipboardSnapshot { + private final BlockSelection selection; + + // Create copy of clipboard contents + public static ClipboardContentsSnapshot copyOf(BlockSelection selection); +} +``` + +### ClipboardBoundsSnapshot + +Captures selection bounds only: + +```java +public class ClipboardBoundsSnapshot implements ClipboardSnapshot { + public static final ClipboardBoundsSnapshot EMPTY; + + private final Vector3i min; + private final Vector3i max; + + public ClipboardBoundsSnapshot(BlockSelection selection); + public ClipboardBoundsSnapshot(Vector3i min, Vector3i max); + + public Vector3i getMin(); + public Vector3i getMax(); +} +``` + +## Entity Snapshots + +### EntitySnapshot Interface + +Interface for entity-related snapshots: + +```java +public interface EntitySnapshot> + extends SelectionSnapshot { + + T restoreEntity(Player player, World world, + ComponentAccessor accessor); +} +``` + +Entity snapshots handle thread safety automatically, ensuring restoration runs on the world's tick thread. + +### EntityAddSnapshot + +Tracks entity creation (undo removes entity): + +```java +public class EntityAddSnapshot implements EntitySnapshot { + private final Ref entityRef; + + public Ref getEntityRef(); + + // Restore removes the entity + // Returns EntityRemoveSnapshot for redo (re-add) +} +``` + +### EntityRemoveSnapshot + +Tracks entity removal (undo adds entity back): + +```java +public class EntityRemoveSnapshot implements EntitySnapshot { + private final Holder holder; + + public Holder getHolder(); + + // Restore adds entity back + // Returns EntityAddSnapshot for redo (remove again) +} +``` + +The `Holder` stores a complete copy of the entity and its components. + +### EntityTransformSnapshot + +Tracks entity position and rotation: + +```java +public class EntityTransformSnapshot implements EntitySnapshot { + private final Ref ref; + private final Transform transform; // Position and rotation + private final Vector3f headRotation; // Head direction + + // Stores current transform state + public EntityTransformSnapshot(Ref ref, + ComponentAccessor accessor); + + // Restore returns snapshot of current state before restoration +} +``` + +## Snapshot Pairs + +Snapshots come in complementary pairs: + +| Add Operation | Undo (Remove) | Redo (Add) | +|---------------|---------------|------------| +| `EntityAddSnapshot` | Removes entity | `EntityRemoveSnapshot` | +| `EntityRemoveSnapshot` | Adds entity back | `EntityAddSnapshot` | + +| Modification | Undo | Redo | +|--------------|------|------| +| `EntityTransformSnapshot` | Restores previous transform | Returns new snapshot | +| `BlockSelectionSnapshot` | Restores previous blocks | Returns new snapshot | +| `ClipboardContentsSnapshot` | Restores previous contents | Returns new snapshot | +| `ClipboardBoundsSnapshot` | Restores previous bounds | Returns new snapshot | + +## Edit History Integration + +Snapshots integrate with the edit history system: + +```java +// History stores snapshots for each operation +// /undo command restores from history +// /redo command uses inverse snapshots + +// Commands related to history: +// /undo [count] +// /redo [count] +// /clearedithistory +// /settoolhistorysize +``` + +## Thread Safety + +Entity snapshots automatically handle world thread requirements: + +```java +// EntitySnapshot.restore() implementation +default T restore(...) { + if (!world.isInThread()) { + // Run on world tick thread + return CompletableFuture.supplyAsync( + () -> this.restoreEntity(player, world, store), + world + ).join(); + } + return this.restoreEntity(player, world, store); +} +``` + +## API Usage + +### Working with Block Snapshots + +```java +// Before operation +BlockSelectionSnapshot before = BlockSelectionSnapshot.copyOf(selection); + +// Perform operation that modifies blocks +// ... + +// Store snapshot in history for undo +``` + +### Working with Entity Snapshots + +```java +// Before adding entity +Ref newEntity = world.spawnEntity(...); +EntityAddSnapshot snapshot = new EntityAddSnapshot(newEntity); + +// To undo (remove the entity) +EntityRemoveSnapshot inverse = snapshot.restore(ref, player, world, accessor); + +// To redo (add entity back) +EntityAddSnapshot restored = inverse.restore(ref, player, world, accessor); +``` + +### Working with Transform Snapshots + +```java +// Before moving entity +EntityTransformSnapshot before = new EntityTransformSnapshot(entityRef, accessor); + +// Move entity +transformComponent.setPosition(newPosition); + +// To undo (restore original position) +EntityTransformSnapshot after = before.restore(player, world, accessor); +``` diff --git a/content/reference/buildertools/snapshots.fr.md b/content/reference/buildertools/snapshots.fr.md new file mode 100644 index 0000000..57d8dfb --- /dev/null +++ b/content/reference/buildertools/snapshots.fr.md @@ -0,0 +1,284 @@ +--- +title: Snapshots +type: docs +weight: 5 +--- + +Le systeme de snapshots fournit la fonctionnalite annuler/refaire pour les outils de construction en capturant et restaurant l'etat. + +**Package:** `com.hypixel.hytale.builtin.buildertools.snapshot` + +## Architecture + +``` +Snapshots +├── Base +│ └── SelectionSnapshot - Interface snapshot +├── Snapshots Blocs +│ └── BlockSelectionSnapshot - Etat selection blocs +├── Snapshots Presse-papiers +│ ├── ClipboardSnapshot - Interface presse-papiers +│ ├── ClipboardContentsSnapshot - Contenu presse-papiers +│ └── ClipboardBoundsSnapshot - Limites selection +└── Snapshots Entites + ├── EntitySnapshot - Interface entite + ├── EntityAddSnapshot - Suivi ajout entite + ├── EntityRemoveSnapshot - Suivi suppression entite + └── EntityTransformSnapshot - Suivi changements transform +``` + +## Interface SelectionSnapshot + +Interface de base pour tous les snapshots: + +```java +public interface SelectionSnapshot> { + // Restaurer etat precedent + // Retourne snapshot inverse pour refaire + T restore(Ref ref, Player player, World world, + ComponentAccessor accessor); +} +``` + +### Fonctionnement Annuler/Refaire + +Le pattern snapshot cree des operations inverses: +1. Le snapshot capture l'etat actuel avant l'operation +2. L'operation modifie l'etat +3. Appeler `restore()` annule les changements +4. `restore()` retourne nouveau snapshot pour refaire + +## Snapshots Selection Blocs + +### BlockSelectionSnapshot + +Capture l'etat de selection de blocs: + +```java +public class BlockSelectionSnapshot implements SelectionSnapshot { + private final BlockSelection selection; + + public BlockSelection getBlockSelection(); + + // Creer copie de selection + public static BlockSelectionSnapshot copyOf(BlockSelection selection); +} +``` + +### Utilisation + +```java +// Avant modification selection +BlockSelectionSnapshot snapshot = BlockSelectionSnapshot.copyOf(selection); + +// Effectuer modifications... + +// Pour annuler - restore retourne snapshot refaire +BlockSelectionSnapshot redoSnapshot = snapshot.restore(ref, player, world, accessor); +``` + +## Snapshots Presse-papiers + +### Interface ClipboardSnapshot + +Interface etendue pour operations presse-papiers: + +```java +public interface ClipboardSnapshot> + extends SelectionSnapshot { + + T restoreClipboard(Ref ref, Player player, World world, + BuilderToolsPlugin.BuilderState state, + ComponentAccessor accessor); +} +``` + +### ClipboardContentsSnapshot + +Capture le contenu du presse-papiers: + +```java +public class ClipboardContentsSnapshot implements ClipboardSnapshot { + private final BlockSelection selection; + + // Creer copie du contenu presse-papiers + public static ClipboardContentsSnapshot copyOf(BlockSelection selection); +} +``` + +### ClipboardBoundsSnapshot + +Capture uniquement les limites de selection: + +```java +public class ClipboardBoundsSnapshot implements ClipboardSnapshot { + public static final ClipboardBoundsSnapshot EMPTY; + + private final Vector3i min; + private final Vector3i max; + + public ClipboardBoundsSnapshot(BlockSelection selection); + public ClipboardBoundsSnapshot(Vector3i min, Vector3i max); + + public Vector3i getMin(); + public Vector3i getMax(); +} +``` + +## Snapshots Entites + +### Interface EntitySnapshot + +Interface pour snapshots lies aux entites: + +```java +public interface EntitySnapshot> + extends SelectionSnapshot { + + T restoreEntity(Player player, World world, + ComponentAccessor accessor); +} +``` + +Les snapshots d'entites gerent automatiquement la securite des threads, assurant que la restauration s'execute sur le thread de tick du monde. + +### EntityAddSnapshot + +Suit la creation d'entite (annuler supprime l'entite): + +```java +public class EntityAddSnapshot implements EntitySnapshot { + private final Ref entityRef; + + public Ref getEntityRef(); + + // Restore supprime l'entite + // Retourne EntityRemoveSnapshot pour refaire (re-ajouter) +} +``` + +### EntityRemoveSnapshot + +Suit la suppression d'entite (annuler ajoute l'entite): + +```java +public class EntityRemoveSnapshot implements EntitySnapshot { + private final Holder holder; + + public Holder getHolder(); + + // Restore ajoute l'entite + // Retourne EntityAddSnapshot pour refaire (supprimer a nouveau) +} +``` + +Le `Holder` stocke une copie complete de l'entite et ses composants. + +### EntityTransformSnapshot + +Suit la position et rotation d'entite: + +```java +public class EntityTransformSnapshot implements EntitySnapshot { + private final Ref ref; + private final Transform transform; // Position et rotation + private final Vector3f headRotation; // Direction tete + + // Stocke l'etat transform actuel + public EntityTransformSnapshot(Ref ref, + ComponentAccessor accessor); + + // Restore retourne snapshot de l'etat actuel avant restauration +} +``` + +## Paires de Snapshots + +Les snapshots viennent en paires complementaires: + +| Operation Ajout | Annuler (Supprimer) | Refaire (Ajouter) | +|-----------------|---------------------|-------------------| +| `EntityAddSnapshot` | Supprime entite | `EntityRemoveSnapshot` | +| `EntityRemoveSnapshot` | Ajoute entite | `EntityAddSnapshot` | + +| Modification | Annuler | Refaire | +|--------------|---------|---------| +| `EntityTransformSnapshot` | Restaure transform precedent | Retourne nouveau snapshot | +| `BlockSelectionSnapshot` | Restaure blocs precedents | Retourne nouveau snapshot | +| `ClipboardContentsSnapshot` | Restaure contenu precedent | Retourne nouveau snapshot | +| `ClipboardBoundsSnapshot` | Restaure limites precedentes | Retourne nouveau snapshot | + +## Integration Historique Edition + +Les snapshots s'integrent avec le systeme d'historique: + +```java +// L'historique stocke les snapshots pour chaque operation +// Commande /undo restaure depuis l'historique +// Commande /redo utilise les snapshots inverses + +// Commandes liees a l'historique: +// /undo [nombre] +// /redo [nombre] +// /clearedithistory +// /settoolhistorysize +``` + +## Securite des Threads + +Les snapshots d'entites gerent automatiquement les exigences de thread du monde: + +```java +// Implementation EntitySnapshot.restore() +default T restore(...) { + if (!world.isInThread()) { + // Executer sur thread tick du monde + return CompletableFuture.supplyAsync( + () -> this.restoreEntity(player, world, store), + world + ).join(); + } + return this.restoreEntity(player, world, store); +} +``` + +## Utilisation de l'API + +### Travailler avec Snapshots Blocs + +```java +// Avant operation +BlockSelectionSnapshot before = BlockSelectionSnapshot.copyOf(selection); + +// Effectuer operation qui modifie les blocs +// ... + +// Stocker snapshot dans historique pour annuler +``` + +### Travailler avec Snapshots Entites + +```java +// Avant ajout entite +Ref newEntity = world.spawnEntity(...); +EntityAddSnapshot snapshot = new EntityAddSnapshot(newEntity); + +// Pour annuler (supprimer l'entite) +EntityRemoveSnapshot inverse = snapshot.restore(ref, player, world, accessor); + +// Pour refaire (ajouter entite a nouveau) +EntityAddSnapshot restored = inverse.restore(ref, player, world, accessor); +``` + +### Travailler avec Snapshots Transform + +```java +// Avant deplacement entite +EntityTransformSnapshot before = new EntityTransformSnapshot(entityRef, accessor); + +// Deplacer entite +transformComponent.setPosition(newPosition); + +// Pour annuler (restaurer position originale) +EntityTransformSnapshot after = before.restore(player, world, accessor); +``` diff --git a/content/reference/buildertools/tool-operations.en.md b/content/reference/buildertools/tool-operations.en.md new file mode 100644 index 0000000..63b9147 --- /dev/null +++ b/content/reference/buildertools/tool-operations.en.md @@ -0,0 +1,205 @@ +--- +title: Tool Operations +type: docs +weight: 4 +--- + +Tool operations handle the actual block editing mechanics when using builder tools. + +**Package:** `com.hypixel.hytale.builtin.buildertools.tooloperations` + +## Architecture + +``` +Tool Operations +├── Base +│ └── ToolOperation - Operation base class +├── Operations +│ ├── PaintOperation - Block painting +│ └── EditOperation - Edit tracking +├── Materials +│ └── Material - Block material wrapper +└── Utilities + └── CopyCutSettings - Copy/cut configuration +``` + +## ToolOperation + +Base class for all tool operations: + +```java +public class ToolOperation { + // Base operation that modifies blocks + // Tracked for undo/redo +} +``` + +## PaintOperation + +Handles block painting with brush tools: + +```java +public class PaintOperation extends ToolOperation { + // Applies blocks in brush shape + // Supports patterns and masks +} +``` + +### Usage + +When a player uses a brush tool: +1. PaintOperation is created +2. Shape is calculated based on tool settings +3. Blocks are modified according to pattern +4. Operation is added to history + +## EditOperation + +Tracks changes for undo/redo: + +```java +public class EditOperation { + // Records block changes + // Enables reversal +} +``` + +## Material System + +### Material + +Wrapper for block materials: + +```java +public class Material { + // Block type reference + // Additional properties (rotation, tint) +} +``` + +### Block Patterns + +Patterns define how blocks are selected: + +| Pattern | Description | +|---------|-------------| +| `[Stone]` | Single block type | +| `[50%Stone, 50%Dirt]` | Weighted random | +| `[Stone, Dirt, Grass]` | Equal weights | + +### Block Masks + +Masks filter which blocks are affected: + +| Mask | Description | +|------|-------------| +| `[Stone]` | Only affect stone | +| `[!Air]` | Affect non-air blocks | +| `[!^Fluid]` | Exclude fluid tagged blocks | + +## Copy/Cut Settings + +### CopyCutSettings + +Configuration for copy and cut operations: + +```java +public class CopyCutSettings { + // Include entities flag + // Include biome data flag + // Compression level +} +``` + +## Tool Arguments + +Builder tools support configurable arguments: + +### Argument Types + +| Type | Class | Description | +|------|-------|-------------| +| Block | `BlockArg` | Block type selection | +| Bool | `BoolArg` | Boolean flag | +| Int | `IntArg` | Integer value | +| Float | `FloatArg` | Decimal value | +| Mask | `MaskArg` | Block mask | +| String | `StringArg` | Text value | +| BrushShape | `BrushShapeArg` | Sphere, Cube, etc. | +| BrushOrigin | `BrushOriginArg` | Center, Surface, etc. | + +### Tool Argument Packets + +Arguments sync via network packets: + +```java +// BuilderToolArgUpdate packet +// Syncs tool argument changes to server +``` + +## Brush Configuration + +### Brush Shape + +```java +public enum BrushShape { + Sphere, + Cube, + Cylinder, + // ... +} +``` + +### Brush Origin + +```java +public enum BrushOrigin { + Center, // Brush centered on click + Surface, // Brush on surface + Offset // Custom offset +} +``` + +### Brush Axis + +```java +public enum BrushAxis { + Y, // Always vertical + View, // Follow view direction + Surface // Normal to surface +} +``` + +## API Usage + +### Access Tool Settings + +```java +BuilderToolsPlugin tools = BuilderToolsPlugin.get(); +BuilderToolsUserData userData = tools.getUserData(player); +``` + +### Get Current Tool + +```java +BuilderTool currentTool = userData.getCurrentTool(); +BuilderToolData toolData = currentTool.getData(); +``` + +## Tool State + +### BuilderToolState + +Tracks current tool state: + +```java +// BuilderToolState packet +// Syncs tool state to client +``` + +### Brush Data + +```java +// BuilderToolBrushData packet +// Contains brush size, shape, pattern +``` diff --git a/content/reference/buildertools/tool-operations.fr.md b/content/reference/buildertools/tool-operations.fr.md new file mode 100644 index 0000000..476df73 --- /dev/null +++ b/content/reference/buildertools/tool-operations.fr.md @@ -0,0 +1,205 @@ +--- +title: Operations Outil +type: docs +weight: 4 +--- + +Les operations d'outil gerent les mecaniques d'edition de blocs reelles lors de l'utilisation des outils de construction. + +**Package:** `com.hypixel.hytale.builtin.buildertools.tooloperations` + +## Architecture + +``` +Operations Outil +├── Base +│ └── ToolOperation - Classe de base operation +├── Operations +│ ├── PaintOperation - Peinture de blocs +│ └── EditOperation - Suivi d'edition +├── Materiaux +│ └── Material - Wrapper materiau bloc +└── Utilitaires + └── CopyCutSettings - Configuration copier/couper +``` + +## ToolOperation + +Classe de base pour toutes les operations d'outil: + +```java +public class ToolOperation { + // Operation de base qui modifie les blocs + // Suivie pour annuler/refaire +} +``` + +## PaintOperation + +Gere la peinture de blocs avec outils brosse: + +```java +public class PaintOperation extends ToolOperation { + // Applique blocs dans forme brosse + // Supporte patterns et masques +} +``` + +### Utilisation + +Quand un joueur utilise un outil brosse: +1. PaintOperation est creee +2. La forme est calculee selon parametres outil +3. Les blocs sont modifies selon pattern +4. L'operation est ajoutee a l'historique + +## EditOperation + +Suit les changements pour annuler/refaire: + +```java +public class EditOperation { + // Enregistre changements de blocs + // Permet l'inversion +} +``` + +## Systeme de Materiaux + +### Material + +Wrapper pour materiaux de blocs: + +```java +public class Material { + // Reference type de bloc + // Proprietes additionnelles (rotation, teinte) +} +``` + +### Patterns de Blocs + +Les patterns definissent comment les blocs sont selectionnes: + +| Pattern | Description | +|---------|-------------| +| `[Stone]` | Type de bloc unique | +| `[50%Stone, 50%Dirt]` | Aleatoire pondere | +| `[Stone, Dirt, Grass]` | Poids egaux | + +### Masques de Blocs + +Les masques filtrent quels blocs sont affectes: + +| Masque | Description | +|--------|-------------| +| `[Stone]` | Affecter uniquement pierre | +| `[!Air]` | Affecter blocs non-air | +| `[!^Fluid]` | Exclure blocs tagues fluide | + +## Parametres Copier/Couper + +### CopyCutSettings + +Configuration pour operations copier et couper: + +```java +public class CopyCutSettings { + // Flag inclure entites + // Flag inclure donnees biome + // Niveau compression +} +``` + +## Arguments Outil + +Les outils de construction supportent des arguments configurables: + +### Types d'Arguments + +| Type | Classe | Description | +|------|--------|-------------| +| Block | `BlockArg` | Selection type bloc | +| Bool | `BoolArg` | Flag booleen | +| Int | `IntArg` | Valeur entiere | +| Float | `FloatArg` | Valeur decimale | +| Mask | `MaskArg` | Masque de blocs | +| String | `StringArg` | Valeur texte | +| BrushShape | `BrushShapeArg` | Sphere, Cube, etc. | +| BrushOrigin | `BrushOriginArg` | Center, Surface, etc. | + +### Paquets Arguments Outil + +Les arguments se synchronisent via paquets reseau: + +```java +// Paquet BuilderToolArgUpdate +// Synchronise changements arguments vers serveur +``` + +## Configuration Brosse + +### Forme Brosse + +```java +public enum BrushShape { + Sphere, + Cube, + Cylinder, + // ... +} +``` + +### Origine Brosse + +```java +public enum BrushOrigin { + Center, // Brosse centree sur clic + Surface, // Brosse sur surface + Offset // Decalage personnalise +} +``` + +### Axe Brosse + +```java +public enum BrushAxis { + Y, // Toujours vertical + View, // Suivre direction vue + Surface // Normal a la surface +} +``` + +## Utilisation de l'API + +### Acceder aux Parametres Outil + +```java +BuilderToolsPlugin tools = BuilderToolsPlugin.get(); +BuilderToolsUserData userData = tools.getUserData(player); +``` + +### Obtenir Outil Actuel + +```java +BuilderTool currentTool = userData.getCurrentTool(); +BuilderToolData toolData = currentTool.getData(); +``` + +## Etat Outil + +### BuilderToolState + +Suit l'etat actuel de l'outil: + +```java +// Paquet BuilderToolState +// Synchronise etat outil vers client +``` + +### Donnees Brosse + +```java +// Paquet BuilderToolBrushData +// Contient taille brosse, forme, pattern +``` diff --git a/content/reference/manifest-schema.en.md b/content/reference/manifest-schema.en.md new file mode 100644 index 0000000..8bcb6b5 --- /dev/null +++ b/content/reference/manifest-schema.en.md @@ -0,0 +1,212 @@ +--- +title: Manifest Schema +type: docs +weight: 1 +--- + +The `manifest.json` file defines your plugin's metadata and configuration. + +## Basic Structure + +```json +{ + "name": "MyPlugin", + "version": "1.0.0", + "main": "com.example.MyPlugin", + "dependencies": [] +} +``` + +## Required Fields + +### name +**Type:** `string` + +The unique identifier for your plugin. Used for logging and dependency resolution. + +```json +{ + "name": "MyAwesomePlugin" +} +``` + +### version +**Type:** `string` + +The plugin version following semantic versioning (recommended). + +```json +{ + "version": "1.0.0" +} +``` + +### main +**Type:** `string` + +The fully qualified class name of your main plugin class. This class must extend `JavaPlugin`. + +```json +{ + "main": "com.example.myplugin.MyPlugin" +} +``` + +## Optional Fields + +### dependencies +**Type:** `string[]` + +List of plugin names this plugin depends on. Dependent plugins will be loaded first. + +```json +{ + "dependencies": ["CoreLib", "DatabasePlugin"] +} +``` + +### description +**Type:** `string` + +A brief description of your plugin. + +```json +{ + "description": "Adds awesome features to the server" +} +``` + +### author +**Type:** `string` + +The plugin author or team name. + +```json +{ + "author": "YourName" +} +``` + +### authors +**Type:** `string[]` + +List of plugin authors for team projects. + +```json +{ + "authors": ["Developer1", "Developer2"] +} +``` + +### website +**Type:** `string` + +Plugin homepage or documentation URL. + +```json +{ + "website": "https://example.com/myplugin" +} +``` + +## Complete Example + +```json +{ + "name": "EconomyPlugin", + "version": "2.1.0", + "main": "com.example.economy.EconomyPlugin", + "description": "A comprehensive economy system for Hytale servers", + "author": "GameDev", + "authors": ["GameDev", "Contributor1"], + "website": "https://github.com/example/economy", + "dependencies": ["DatabaseLib"] +} +``` + +## File Location + +The `manifest.json` must be placed in the root of your plugin JAR file: + +``` +MyPlugin.jar +├── manifest.json +├── com/ +│ └── example/ +│ └── myplugin/ +│ └── MyPlugin.class +└── resources/ + └── config.yml +``` + +## Validation + +{{< callout type="warning" >}} +The server validates manifest.json on startup. Invalid manifests will prevent your plugin from loading. + +Common issues: +- Missing required fields (name, version, main) +- Invalid JSON syntax +- Main class not found in JAR +- Circular dependencies +{{< /callout >}} + +## Main Class Requirements + +Your main class specified in `main` must: + +1. Extend `JavaPlugin` +2. Have a public no-argument constructor +3. Be accessible in the JAR classpath + +```java +package com.example.myplugin; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; + +public class MyPlugin extends JavaPlugin { + @Override + public void start() { + // Plugin enabled - register events, commands, etc. + getLogger().at(Level.INFO).log("Plugin started!"); + } + + @Override + public void shutdown() { + // Plugin disabled - cleanup resources + } +} +``` + +## Dependency Resolution + +Dependencies are loaded in order: + +1. All dependencies must be present and valid +2. Circular dependencies cause load failure +3. Missing dependencies cause load failure + +``` +LoadOrder: +1. DatabaseLib (no dependencies) +2. CoreLib (depends on DatabaseLib) +3. MyPlugin (depends on CoreLib) +``` + +## Version Compatibility + +{{< callout type="info" >}} +Consider versioning your plugin API if other plugins depend on you: +- Major: Breaking changes +- Minor: New features, backward compatible +- Patch: Bug fixes +{{< /callout >}} + +```json +{ + "name": "MyAPI", + "version": "2.0.0" +} +``` + +Dependent plugins can then specify minimum versions in their documentation. diff --git a/content/reference/manifest-schema.fr.md b/content/reference/manifest-schema.fr.md new file mode 100644 index 0000000..58f1256 --- /dev/null +++ b/content/reference/manifest-schema.fr.md @@ -0,0 +1,212 @@ +--- +title: Schéma du Manifeste +type: docs +weight: 1 +--- + +Le fichier `manifest.json` définit les métadonnées et la configuration de votre plugin. + +## Structure de Base + +```json +{ + "name": "MyPlugin", + "version": "1.0.0", + "main": "com.example.MyPlugin", + "dependencies": [] +} +``` + +## Champs Requis + +### name +**Type :** `string` + +L'identifiant unique de votre plugin. Utilisé pour le logging et la résolution des dépendances. + +```json +{ + "name": "MyAwesomePlugin" +} +``` + +### version +**Type :** `string` + +La version du plugin suivant le versioning sémantique (recommandé). + +```json +{ + "version": "1.0.0" +} +``` + +### main +**Type :** `string` + +Le nom de classe pleinement qualifié de votre classe principale de plugin. Cette classe doit étendre `JavaPlugin`. + +```json +{ + "main": "com.example.myplugin.MyPlugin" +} +``` + +## Champs Optionnels + +### dependencies +**Type :** `string[]` + +Liste des noms de plugins dont ce plugin dépend. Les plugins dépendants seront chargés en premier. + +```json +{ + "dependencies": ["CoreLib", "DatabasePlugin"] +} +``` + +### description +**Type :** `string` + +Une brève description de votre plugin. + +```json +{ + "description": "Ajoute des fonctionnalités géniales au serveur" +} +``` + +### author +**Type :** `string` + +L'auteur du plugin ou le nom de l'équipe. + +```json +{ + "author": "VotreNom" +} +``` + +### authors +**Type :** `string[]` + +Liste des auteurs du plugin pour les projets d'équipe. + +```json +{ + "authors": ["Développeur1", "Développeur2"] +} +``` + +### website +**Type :** `string` + +Page d'accueil du plugin ou URL de documentation. + +```json +{ + "website": "https://example.com/myplugin" +} +``` + +## Exemple Complet + +```json +{ + "name": "EconomyPlugin", + "version": "2.1.0", + "main": "com.example.economy.EconomyPlugin", + "description": "Un système économique complet pour les serveurs Hytale", + "author": "GameDev", + "authors": ["GameDev", "Contributeur1"], + "website": "https://github.com/example/economy", + "dependencies": ["DatabaseLib"] +} +``` + +## Emplacement du Fichier + +Le `manifest.json` doit être placé à la racine de votre fichier JAR de plugin : + +``` +MyPlugin.jar +├── manifest.json +├── com/ +│ └── example/ +│ └── myplugin/ +│ └── MyPlugin.class +└── resources/ + └── config.yml +``` + +## Validation + +{{< callout type="warning" >}} +Le serveur valide manifest.json au démarrage. Les manifestes invalides empêcheront votre plugin de se charger. + +Problèmes courants : +- Champs requis manquants (name, version, main) +- Syntaxe JSON invalide +- Classe principale non trouvée dans le JAR +- Dépendances circulaires +{{< /callout >}} + +## Exigences de la Classe Principale + +Votre classe principale spécifiée dans `main` doit : + +1. Étendre `JavaPlugin` +2. Avoir un constructeur public sans argument +3. Être accessible dans le classpath du JAR + +```java +package com.example.myplugin; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; + +public class MyPlugin extends JavaPlugin { + @Override + public void start() { + // Plugin activé - enregistrer événements, commandes, etc. + getLogger().at(Level.INFO).log("Plugin démarré !"); + } + + @Override + public void shutdown() { + // Plugin désactivé - nettoyage ressources + } +} +``` + +## Résolution des Dépendances + +Les dépendances sont chargées dans l'ordre : + +1. Toutes les dépendances doivent être présentes et valides +2. Les dépendances circulaires causent un échec de chargement +3. Les dépendances manquantes causent un échec de chargement + +``` +Ordre de Chargement : +1. DatabaseLib (pas de dépendances) +2. CoreLib (dépend de DatabaseLib) +3. MyPlugin (dépend de CoreLib) +``` + +## Compatibilité des Versions + +{{< callout type="info" >}} +Considérez le versioning de l'API de votre plugin si d'autres plugins dépendent de vous : +- Majeur : Changements cassants +- Mineur : Nouvelles fonctionnalités, rétrocompatible +- Patch : Corrections de bugs +{{< /callout >}} + +```json +{ + "name": "MyAPI", + "version": "2.0.0" +} +``` + +Les plugins dépendants peuvent alors spécifier les versions minimales dans leur documentation. diff --git a/content/ui-systems/_index.en.md b/content/ui-systems/_index.en.md new file mode 100644 index 0000000..9e4718e --- /dev/null +++ b/content/ui-systems/_index.en.md @@ -0,0 +1,100 @@ +--- +title: UI Systems +type: docs +weight: 5 +--- + +The UI Systems section covers user interface, permissions, and custom pages. + +## Overview + +{{< cards cols="2" >}} + {{< card link="hud-manager" title="HUD Manager" subtitle="Control visibility of HUD components" icon="eye" >}} + {{< card link="custom-pages" title="Custom Pages" subtitle="Create custom UI overlays" icon="document-text" >}} + {{< card link="windows" title="Windows" subtitle="Inventory and crafting windows" icon="collection" >}} + {{< card link="ui-files" title="UI Files" subtitle="The .ui markup language" icon="code" >}} + {{< card link="ui-syntax" title="UI Syntax" subtitle="Variables, imports, templates" icon="document" >}} + {{< card link="ui-widgets" title="UI Widgets" subtitle="Available widget types" icon="cube" >}} + {{< card link="ui-properties" title="UI Properties" subtitle="Anchor, layout, padding" icon="adjustments" >}} + {{< card link="ui-styles" title="UI Styles" subtitle="Styling system" icon="color-swatch" >}} + {{< card link="permissions" title="Permissions" subtitle="Player permissions and access control" icon="shield-check" >}} +{{< /cards >}} + +## Key Concepts + +### HUD Components + +The HUD consists of built-in components that can be shown or hidden: + +- **Hotbar** - Item slots at the bottom of the screen +- **Health/Mana/Stamina** - Vital stat bars +- **Chat** - Text communication +- **Compass** - Direction indicator +- **Notifications** - Alert messages +- And many more... + +### Custom Pages + +Custom pages allow you to create overlay UIs using the `UICommandBuilder`: + +```java +public class MyPage extends CustomUIPage { + + public MyPage(PlayerRef ref) { + super(ref, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Build UI using builder.set(), builder.append(), etc. + builder.append("Pages/MyTemplate.ui"); + builder.set("#title", "Welcome!"); + } +} +``` + +### Page Manager + +Each player has a `PageManager` that handles custom pages: + +```java +Player player = ...; +PageManager pageManager = player.getPageManager(); + +// Open a custom page +pageManager.openCustomPage(ref, store, new MyPage(player.getPlayerRef())); + +// Close to normal view +pageManager.setPage(ref, store, Page.None); +``` + +## Accessing the HUD Manager + +```java +Player player = ...; +HudManager hudManager = player.getHudManager(); + +// Hide all HUD components +hudManager.setVisibleHudComponents(player.getPlayerRef()); + +// Show specific components +hudManager.showHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health +); +``` + +## Architecture + +``` +Player +├── HudManager → Controls HUD visibility +├── PageManager → Manages custom pages +│ └── CustomUIPage → Your custom UI +└── WindowManager → Manages windows (inventory, crafting) +``` + +{{< callout type="info" >}} +UI operations are typically performed through the `PlayerRef` or `Player` component. Access these through events or the command context. +{{< /callout >}} diff --git a/content/ui-systems/_index.fr.md b/content/ui-systems/_index.fr.md new file mode 100644 index 0000000..1986812 --- /dev/null +++ b/content/ui-systems/_index.fr.md @@ -0,0 +1,97 @@ +--- +title: Systèmes UI +type: docs +weight: 5 +--- + +La section Systèmes UI couvre l'interface utilisateur, les permissions et les pages personnalisées. + +## Vue d'Ensemble + +{{< cards cols="2" >}} + {{< card link="hud-manager" title="Gestionnaire HUD" subtitle="Contrôler la visibilité des éléments HUD" icon="eye" >}} + {{< card link="custom-pages" title="Pages Personnalisées" subtitle="Créer des interfaces interactives" icon="document" >}} + {{< card link="windows" title="Fenêtres" subtitle="Conteneurs d'inventaire et crafting" icon="collection" >}} + {{< card link="ui-files" title="Fichiers UI" subtitle="Le langage de balisage .ui" icon="code" >}} + {{< card link="ui-syntax" title="Syntaxe UI" subtitle="Variables, imports, templates" icon="document" >}} + {{< card link="ui-widgets" title="Widgets UI" subtitle="Types de widgets disponibles" icon="cube" >}} + {{< card link="ui-properties" title="Propriétés UI" subtitle="Anchor, layout, padding" icon="adjustments" >}} + {{< card link="ui-styles" title="Styles UI" subtitle="Système de styles" icon="color-swatch" >}} + {{< card link="permissions" title="Permissions" subtitle="Permissions joueur et contrôle d'accès" icon="shield-check" >}} +{{< /cards >}} + +### Architecture UI + +Le système UI se compose de trois couches principales : + +1. **HUD** - Éléments persistants à l'écran (barre de vie, hotbar, etc.) +2. **Pages** - Overlays plein écran (menus, interfaces personnalisées) +3. **Fenêtres** - Interfaces de conteneur (coffres, crafting) + +### Accéder au Système UI + +```java +Player player = ...; + +// Gestionnaire HUD - contrôle la visibilité des composants HUD +HudManager hudManager = player.getHudManager(); + +// Gestionnaire de Pages - gère les overlays et pages personnalisées +PageManager pageManager = player.getPageManager(); + +// Gestionnaire de Fenêtres - gère les interfaces de conteneur +WindowManager windowManager = player.getWindowManager(); +``` + +## Utilisation Rapide + +### Afficher/Masquer les Éléments HUD + +```java +// Masquer le compas et le speedomètre +hudManager.hideHudComponents(player.getPlayerRef(), + HudComponent.Compass, + HudComponent.Speedometer +); + +// N'afficher que les éléments essentiels +hudManager.setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health, + HudComponent.Reticle +); +``` + +### Ouvrir une Page Personnalisée + +```java +// Créer et ouvrir une page personnalisée +MyCustomPage page = new MyCustomPage(player.getPlayerRef()); +pageManager.openCustomPage( + player.getReference(), + player.getReference().getStore(), + page +); +``` + +### Ouvrir une Fenêtre de Conteneur + +```java +// Ouvrir un conteneur de coffre +ContainerWindow window = new ContainerWindow( + 1, // ID de fenêtre + WindowType.Container, + chestContainer +); +pageManager.setPageWithWindows(ref, store, Page.None, true, window); +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Directives UI :** +- Utilisez toujours `PlayerRef` lors de l'appel des méthodes HUD +- Nettoyez les pages et fenêtres lors de la déconnexion du joueur +- Utilisez `sendUpdate()` pour les petits changements, `rebuild()` pour les changements majeurs +- Testez les interactions UI dans différents modes de jeu +{{< /callout >}} diff --git a/content/ui-systems/custom-pages.en.md b/content/ui-systems/custom-pages.en.md new file mode 100644 index 0000000..5500110 --- /dev/null +++ b/content/ui-systems/custom-pages.en.md @@ -0,0 +1,341 @@ +--- +title: Custom Pages +type: docs +weight: 2 +--- + +Custom pages allow plugins to create interactive UI overlays that can receive user input and display dynamic content. + +**Package:** `com.hypixel.hytale.server.core.entity.entities.player.pages` + +## Page Lifetimes + +Control how pages can be closed: + +| Lifetime | Description | +|----------|-------------| +| `CustomPageLifetime.CantClose` | Page cannot be dismissed by the player | +| `CustomPageLifetime.CanDismiss` | Player can close with Escape key | +| `CustomPageLifetime.CanDismissOrCloseThroughInteraction` | Can close via Escape or interaction | + +## Creating a Custom Page + +Extend `CustomUIPage`: + +```java +public class WelcomePage extends CustomUIPage { + + public WelcomePage(PlayerRef playerRef) { + super(playerRef, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Load a UI template from assets (extension .ui is REQUIRED) + builder.append("Pages/WelcomeScreen.ui"); + + // Set dynamic values + builder.set("#player_name", playerRef.getUsername()); + builder.set("#server_name", "My Server"); + } +} +``` + +## UICommandBuilder + +Build UI content dynamically: + +### Append Content + +```java +// Append a UI template (extension .ui is REQUIRED) +builder.append("Pages/TemplatePath.ui"); + +// Append to a specific selector +builder.append("#container", "Pages/ChildTemplate.ui"); + +// Append inline YAML/JSON +builder.appendInline("#list", "{ type: text, content: 'Hello' }"); +``` + +### Set Values + +```java +// Set text +builder.set("#title", "Welcome!"); +builder.set("#description", Message.translation("my.key")); + +// Set numbers +builder.set("#score", 1500); +builder.set("#progress", 0.75f); + +// Set boolean +builder.set("#visible", true); + +// Set to null +builder.setNull("#optional_field"); +``` + +### Set Objects + +```java +// Set ItemStack +ItemStack item = ...; +builder.setObject("#item_display", item); + +// Set UI Area +Area area = new Area(0, 0, 100, 50); +builder.setObject("#bounds", area); + +// Set arrays/lists +builder.set("#items", itemStackArray); +builder.set("#options", stringList); +``` + +### Remove Content + +```java +// Clear children of an element +builder.clear("#container"); + +// Remove an element entirely +builder.remove("#element_id"); + +// Insert before an element +builder.insertBefore("#existing", "Pages/NewElement.ui"); +``` + +## UIEventBuilder + +Bind events to UI elements using `addEventBinding`: + +```java +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.ui.builder.EventData; + +@Override +public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("Pages/Menu.ui"); + + // Bind click events using addEventBinding + events.addEventBinding(CustomUIEventBindingType.Activating, "#button_start", + EventData.of("Action", "start_game")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#button_settings", + EventData.of("Action", "open_settings")); +} +``` + +### Event Binding Types + +| Type | Description | +|------|-------------| +| `Activating` | Button click/activation | +| `RightClicking` | Right-click action | +| `DoubleClicking` | Double-click action | +| `MouseEntered` | Mouse hover enter | +| `MouseExited` | Mouse hover exit | +| `ValueChanged` | Input value changed | +| `FocusGained` | Element focused | +| `FocusLost` | Element lost focus | + +## Handling Events + +Override `handleDataEvent` to process user interactions. The `rawData` parameter contains JSON with the event data: + +```java +public class MenuPage extends CustomUIPage { + + public MenuPage(PlayerRef ref) { + super(ref, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("Pages/MainMenu.ui"); + + events.addEventBinding(CustomUIEventBindingType.Activating, "#play_button", + EventData.of("Action", "play")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#quit_button", + EventData.of("Action", "quit")); + } + + @Override + public void handleDataEvent(Ref ref, Store store, String rawData) { + // rawData contains JSON like: {"Action":"play"} + if (rawData.contains("\"Action\":\"play\"")) { + startGame(); + } else if (rawData.contains("\"Action\":\"quit\"")) { + close(); + } + } + + private void startGame() { + // Game logic... + close(); // Close the page + } +} +``` + +{{< callout type="info" >}} +**Tip:** For more complex event handling, consider using a JSON parser library or extending `InteractiveCustomUIPage` with a typed data class. +{{< /callout >}} + +## Opening Pages + +Use `PageManager` to open pages: + +```java +Player player = ...; +PageManager pageManager = player.getPageManager(); + +// Open custom page +pageManager.openCustomPage( + player.getReference(), + player.getReference().getStore(), + new MenuPage(player.getPlayerRef()) +); +``` + +## Closing Pages + +```java +// From within the page +protected void close() { + // Inherited method - closes this page +} + +// From outside +pageManager.setPage(ref, store, Page.None); +``` + +## Updating Pages + +Update content without rebuilding: + +```java +// Send update with new commands +UICommandBuilder update = new UICommandBuilder(); +update.set("#score", newScore); +sendUpdate(update); + +// Full rebuild +rebuild(); + +// Clear and update +sendUpdate(update, true); // clear=true +``` + +## Page with Windows + +Open pages alongside inventory windows: + +```java +// Open page with windows +pageManager.openCustomPageWithWindows( + ref, store, + new CraftingPage(playerRef), + craftingWindow, + playerInventoryWindow +); +``` + +## Interactive Page Example + +```java +public class ShopPage extends CustomUIPage { + + private int selectedItem = -1; + private final List items; + + public ShopPage(PlayerRef ref, List items) { + super(ref, CustomPageLifetime.CanDismiss); + this.items = items; + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("Pages/Shop.ui"); + + // Populate items + for (int i = 0; i < items.size(); i++) { + ShopItem item = items.get(i); + builder.set("#item_" + i + "_name", item.getName()); + builder.set("#item_" + i + "_price", item.getPrice()); + events.addEventBinding(CustomUIEventBindingType.Activating, "#item_" + i, + EventData.of("Action", "select").append("Index", String.valueOf(i))); + } + + events.addEventBinding(CustomUIEventBindingType.Activating, "#buy_button", + EventData.of("Action", "buy")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#close_button", + EventData.of("Action", "close")); + } + + @Override + public void handleDataEvent(Ref ref, Store store, String rawData) { + // Parse JSON event data + if (rawData.contains("\"Action\":\"select\"")) { + // Extract index from JSON + int indexStart = rawData.indexOf("\"Index\":\"") + 9; + int indexEnd = rawData.indexOf("\"", indexStart); + selectedItem = Integer.parseInt(rawData.substring(indexStart, indexEnd)); + updateSelection(); + } else if (rawData.contains("\"Action\":\"buy\"") && selectedItem >= 0) { + purchaseItem(selectedItem); + } else if (rawData.contains("\"Action\":\"close\"")) { + close(); + } + } + + private void updateSelection() { + UICommandBuilder update = new UICommandBuilder(); + update.set("#selected_index", selectedItem); + update.set("#selected_name", items.get(selectedItem).getName()); + sendUpdate(update); + } + + private void purchaseItem(int index) { + // Purchase logic... + rebuild(); // Refresh UI + } +} +``` + +## Built-in Pages + +Available page types: + +| Page | Description | +|------|-------------| +| `Page.None` | No page open (close current page) | +| `Page.Bench` | Crafting/workbench interface | +| `Page.Inventory` | Player inventory | +| `Page.ToolsSettings` | Tools settings | +| `Page.Map` | World map | +| `Page.MachinimaEditor` | Machinima editor | +| `Page.ContentCreation` | Content creation tools | +| `Page.Custom` | Custom UI page | + +```java +// Open built-in page +pageManager.setPage(ref, store, Page.Inventory); +``` + +## Best Practices + +{{< callout type="info" >}} +**Custom Page Guidelines:** +- Use `CanDismiss` unless the page must be modal +- Handle the `onDismiss` callback for cleanup +- Use `sendUpdate()` for small changes, `rebuild()` for major changes +- Store page state in instance fields +- Close pages properly using `close()` or `setPage(Page.None)` +{{< /callout >}} + +{{< callout type="warning" >}} +**Important:** Always check if `playerRef.getReference()` is null before accessing the store, as the player may have disconnected. +{{< /callout >}} diff --git a/content/ui-systems/custom-pages.fr.md b/content/ui-systems/custom-pages.fr.md new file mode 100644 index 0000000..cf4ced0 --- /dev/null +++ b/content/ui-systems/custom-pages.fr.md @@ -0,0 +1,341 @@ +--- +title: Pages Personnalisées +type: docs +weight: 2 +--- + +Les pages personnalisées permettent aux plugins de créer des overlays UI interactifs qui peuvent recevoir des entrées utilisateur et afficher du contenu dynamique. + +**Package:** `com.hypixel.hytale.server.core.entity.entities.player.pages` + +## Durées de Vie des Pages + +Contrôle comment les pages peuvent être fermées : + +| Durée de Vie | Description | +|--------------|-------------| +| `CustomPageLifetime.CantClose` | La page ne peut pas être fermée par le joueur | +| `CustomPageLifetime.CanDismiss` | Le joueur peut fermer avec Échap | +| `CustomPageLifetime.CanDismissOrCloseThroughInteraction` | Peut fermer via Échap ou interaction | + +## Créer une Page Personnalisée + +Étendez `CustomUIPage` : + +```java +public class WelcomePage extends CustomUIPage { + + public WelcomePage(PlayerRef playerRef) { + super(playerRef, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Charger un template UI depuis les assets (extension .ui OBLIGATOIRE) + builder.append("Pages/WelcomeScreen.ui"); + + // Définir des valeurs dynamiques + builder.set("#player_name", playerRef.getUsername()); + builder.set("#server_name", "Mon Serveur"); + } +} +``` + +## UICommandBuilder + +Construire du contenu UI dynamiquement : + +### Ajouter du Contenu + +```java +// Ajouter un template UI (extension .ui OBLIGATOIRE) +builder.append("Pages/TemplatePath.ui"); + +// Ajouter à un sélecteur spécifique +builder.append("#container", "Pages/ChildTemplate.ui"); + +// Ajouter du YAML/JSON inline +builder.appendInline("#list", "{ type: text, content: 'Bonjour' }"); +``` + +### Définir des Valeurs + +```java +// Définir du texte +builder.set("#title", "Bienvenue !"); +builder.set("#description", Message.translation("my.key")); + +// Définir des nombres +builder.set("#score", 1500); +builder.set("#progress", 0.75f); + +// Définir un booléen +builder.set("#visible", true); + +// Mettre à null +builder.setNull("#optional_field"); +``` + +### Définir des Objets + +```java +// Définir un ItemStack +ItemStack item = ...; +builder.setObject("#item_display", item); + +// Définir une Zone UI +Area area = new Area(0, 0, 100, 50); +builder.setObject("#bounds", area); + +// Définir des tableaux/listes +builder.set("#items", itemStackArray); +builder.set("#options", stringList); +``` + +### Supprimer du Contenu + +```java +// Vider les enfants d'un élément +builder.clear("#container"); + +// Supprimer un élément entièrement +builder.remove("#element_id"); + +// Insérer avant un élément +builder.insertBefore("#existing", "Pages/NewElement.ui"); +``` + +## UIEventBuilder + +Lier des événements aux éléments UI avec `addEventBinding` : + +```java +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.ui.builder.EventData; + +@Override +public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("Pages/Menu.ui"); + + // Lier des événements de clic avec addEventBinding + events.addEventBinding(CustomUIEventBindingType.Activating, "#button_start", + EventData.of("Action", "start_game")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#button_settings", + EventData.of("Action", "open_settings")); +} +``` + +### Types de Liaison d'Événements + +| Type | Description | +|------|-------------| +| `Activating` | Clic/activation de bouton | +| `RightClicking` | Action clic droit | +| `DoubleClicking` | Action double-clic | +| `MouseEntered` | Entrée du survol souris | +| `MouseExited` | Sortie du survol souris | +| `ValueChanged` | Valeur d'entrée changée | +| `FocusGained` | Élément focus | +| `FocusLost` | Élément perd le focus | + +## Gérer les Événements + +Surchargez `handleDataEvent` pour traiter les interactions utilisateur. Le paramètre `rawData` contient du JSON avec les données de l'événement : + +```java +public class MenuPage extends CustomUIPage { + + public MenuPage(PlayerRef ref) { + super(ref, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("Pages/MainMenu.ui"); + + events.addEventBinding(CustomUIEventBindingType.Activating, "#play_button", + EventData.of("Action", "play")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#quit_button", + EventData.of("Action", "quit")); + } + + @Override + public void handleDataEvent(Ref ref, Store store, String rawData) { + // rawData contient du JSON comme : {"Action":"play"} + if (rawData.contains("\"Action\":\"play\"")) { + startGame(); + } else if (rawData.contains("\"Action\":\"quit\"")) { + close(); + } + } + + private void startGame() { + // Logique de jeu... + close(); // Fermer la page + } +} +``` + +{{< callout type="info" >}} +**Conseil :** Pour une gestion d'événements plus complexe, envisagez d'utiliser une bibliothèque de parsing JSON ou d'étendre `InteractiveCustomUIPage` avec une classe de données typée. +{{< /callout >}} + +## Ouvrir des Pages + +Utilisez `PageManager` pour ouvrir des pages : + +```java +Player player = ...; +PageManager pageManager = player.getPageManager(); + +// Ouvrir une page personnalisée +pageManager.openCustomPage( + player.getReference(), + player.getReference().getStore(), + new MenuPage(player.getPlayerRef()) +); +``` + +## Fermer des Pages + +```java +// Depuis l'intérieur de la page +protected void close() { + // Méthode héritée - ferme cette page +} + +// Depuis l'extérieur +pageManager.setPage(ref, store, Page.None); +``` + +## Mettre à Jour des Pages + +Mettre à jour le contenu sans reconstruire : + +```java +// Envoyer une mise à jour avec de nouvelles commandes +UICommandBuilder update = new UICommandBuilder(); +update.set("#score", newScore); +sendUpdate(update); + +// Reconstruction complète +rebuild(); + +// Vider et mettre à jour +sendUpdate(update, true); // clear=true +``` + +## Page avec Fenêtres + +Ouvrir des pages avec des fenêtres d'inventaire : + +```java +// Ouvrir une page avec des fenêtres +pageManager.openCustomPageWithWindows( + ref, store, + new CraftingPage(playerRef), + craftingWindow, + playerInventoryWindow +); +``` + +## Exemple de Page Interactive + +```java +public class ShopPage extends CustomUIPage { + + private int selectedItem = -1; + private final List items; + + public ShopPage(PlayerRef ref, List items) { + super(ref, CustomPageLifetime.CanDismiss); + this.items = items; + } + + @Override + public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + builder.append("Pages/Shop.ui"); + + // Peupler les items + for (int i = 0; i < items.size(); i++) { + ShopItem item = items.get(i); + builder.set("#item_" + i + "_name", item.getName()); + builder.set("#item_" + i + "_price", item.getPrice()); + events.addEventBinding(CustomUIEventBindingType.Activating, "#item_" + i, + EventData.of("Action", "select").append("Index", String.valueOf(i))); + } + + events.addEventBinding(CustomUIEventBindingType.Activating, "#buy_button", + EventData.of("Action", "buy")); + events.addEventBinding(CustomUIEventBindingType.Activating, "#close_button", + EventData.of("Action", "close")); + } + + @Override + public void handleDataEvent(Ref ref, Store store, String rawData) { + // Parser les données JSON de l'événement + if (rawData.contains("\"Action\":\"select\"")) { + // Extraire l'index du JSON + int indexStart = rawData.indexOf("\"Index\":\"") + 9; + int indexEnd = rawData.indexOf("\"", indexStart); + selectedItem = Integer.parseInt(rawData.substring(indexStart, indexEnd)); + updateSelection(); + } else if (rawData.contains("\"Action\":\"buy\"") && selectedItem >= 0) { + purchaseItem(selectedItem); + } else if (rawData.contains("\"Action\":\"close\"")) { + close(); + } + } + + private void updateSelection() { + UICommandBuilder update = new UICommandBuilder(); + update.set("#selected_index", selectedItem); + update.set("#selected_name", items.get(selectedItem).getName()); + sendUpdate(update); + } + + private void purchaseItem(int index) { + // Logique d'achat... + rebuild(); // Rafraîchir l'UI + } +} +``` + +## Pages Intégrées + +Types de pages disponibles : + +| Page | Description | +|------|-------------| +| `Page.None` | Pas de page ouverte (fermer la page courante) | +| `Page.Bench` | Interface d'établi/crafting | +| `Page.Inventory` | Inventaire du joueur | +| `Page.ToolsSettings` | Paramètres des outils | +| `Page.Map` | Carte du monde | +| `Page.MachinimaEditor` | Éditeur de machinima | +| `Page.ContentCreation` | Outils de création de contenu | +| `Page.Custom` | Page UI personnalisée | + +```java +// Ouvrir une page intégrée +pageManager.setPage(ref, store, Page.Inventory); +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Directives des Pages Personnalisées :** +- Utilisez `CanDismiss` sauf si la page doit être modale +- Gérez le callback `onDismiss` pour le nettoyage +- Utilisez `sendUpdate()` pour les petits changements, `rebuild()` pour les changements majeurs +- Stockez l'état de la page dans des champs d'instance +- Fermez correctement les pages en utilisant `close()` ou `setPage(Page.None)` +{{< /callout >}} + +{{< callout type="warning" >}} +**Important :** Vérifiez toujours si `playerRef.getReference()` est null avant d'accéder au store, car le joueur peut s'être déconnecté. +{{< /callout >}} diff --git a/content/ui-systems/hud-manager.en.md b/content/ui-systems/hud-manager.en.md new file mode 100644 index 0000000..4590c61 --- /dev/null +++ b/content/ui-systems/hud-manager.en.md @@ -0,0 +1,232 @@ +--- +title: HUD Manager +type: docs +weight: 1 +--- + +The `HudManager` controls which HUD components are visible to the player. + +**Package:** `com.hypixel.hytale.server.core.entity.entities.player.hud` + +## HUD Components + +All available HUD components: + +| Component | Description | +|-----------|-------------| +| `HudComponent.Hotbar` | Player's item hotbar | +| `HudComponent.StatusIcons` | Status effect icons | +| `HudComponent.Reticle` | Crosshair/targeting reticle | +| `HudComponent.Chat` | Chat window | +| `HudComponent.Requests` | Pending requests UI | +| `HudComponent.Notifications` | Alert notifications | +| `HudComponent.KillFeed` | Kill/death messages | +| `HudComponent.InputBindings` | Control hints | +| `HudComponent.PlayerList` | Tab player list | +| `HudComponent.EventTitle` | Large event titles | +| `HudComponent.Compass` | Direction compass | +| `HudComponent.ObjectivePanel` | Quest/objective tracker | +| `HudComponent.PortalPanel` | Portal interface | +| `HudComponent.BuilderToolsLegend` | Builder tool hints | +| `HudComponent.Speedometer` | Speed indicator | +| `HudComponent.UtilitySlotSelector` | Utility slot UI | +| `HudComponent.BlockVariantSelector` | Block variant picker | +| `HudComponent.BuilderToolsMaterialSlotSelector` | Builder material slots | +| `HudComponent.Stamina` | Stamina bar | +| `HudComponent.AmmoIndicator` | Ammunition counter | +| `HudComponent.Health` | Health bar | +| `HudComponent.Mana` | Mana bar | +| `HudComponent.Oxygen` | Oxygen/breath bar | +| `HudComponent.Sleep` | Sleep indicator | + +## Default Components + +These components are visible by default: + +```java +Set DEFAULT = Set.of( + HudComponent.UtilitySlotSelector, + HudComponent.BlockVariantSelector, + HudComponent.StatusIcons, + HudComponent.Hotbar, + HudComponent.Chat, + HudComponent.Notifications, + HudComponent.KillFeed, + HudComponent.InputBindings, + HudComponent.Reticle, + HudComponent.Compass, + HudComponent.Speedometer, + HudComponent.ObjectivePanel, + HudComponent.PortalPanel, + HudComponent.EventTitle, + HudComponent.Stamina, + HudComponent.AmmoIndicator, + HudComponent.Health, + HudComponent.Mana, + HudComponent.Oxygen, + HudComponent.BuilderToolsLegend, + HudComponent.Sleep +); +``` + +## Accessing the HUD Manager + +```java +Player player = ...; +HudManager hudManager = player.getHudManager(); +``` + +## Showing Components + +```java +// Show specific components (additive) +hudManager.showHudComponents(player.getPlayerRef(), + HudComponent.Health, + HudComponent.Stamina, + HudComponent.Hotbar +); + +// Show from a Set +Set components = Set.of( + HudComponent.Chat, + HudComponent.Notifications +); +hudManager.showHudComponents(player.getPlayerRef(), components); +``` + +## Hiding Components + +```java +// Hide specific components +hudManager.hideHudComponents(player.getPlayerRef(), + HudComponent.Compass, + HudComponent.Speedometer +); +``` + +## Setting Components + +Replace all visible components: + +```java +// Set exact visible components (replaces all) +hudManager.setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health +); + +// Clear all HUD components (empty) +hudManager.setVisibleHudComponents(player.getPlayerRef()); +``` + +## Querying Visible Components + +```java +Set visible = hudManager.getVisibleHudComponents(); + +if (visible.contains(HudComponent.Health)) { + // Health bar is visible +} +``` + +## Resetting the HUD + +```java +// Reset to default components and clear custom HUD +hudManager.resetHud(player.getPlayerRef()); + +// Reset entire UI state +hudManager.resetUserInterface(player.getPlayerRef()); +``` + +## Custom HUD Overlay + +For persistent overlays: + +```java +public class ScoreboardHud extends CustomUIHud { + + public ScoreboardHud(PlayerRef ref) { + super(ref); + } + + @Override + protected void build(UICommandBuilder builder) { + // Extension .ui is REQUIRED + builder.append("Hud/Scoreboard.ui"); + builder.set("#score", 1500); + builder.set("#rank", "Gold"); + } +} + +// Set custom HUD +hudManager.setCustomHud(player.getPlayerRef(), new ScoreboardHud(player.getPlayerRef())); + +// Remove custom HUD +hudManager.setCustomHud(player.getPlayerRef(), null); +``` + +## Practical Examples + +### Cinematic Mode + +Hide all UI for cutscenes: + +```java +public void enterCinematicMode(Player player) { + HudManager hud = player.getHudManager(); + + // Store current state if needed for restoration + Set previous = new HashSet<>(hud.getVisibleHudComponents()); + + // Clear all HUD + hud.setVisibleHudComponents(player.getPlayerRef()); +} + +public void exitCinematicMode(Player player, Set restore) { + player.getHudManager().setVisibleHudComponents(player.getPlayerRef(), restore); +} +``` + +### Minimal HUD Mode + +Show only essential components: + +```java +public void setMinimalHud(Player player) { + player.getHudManager().setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health, + HudComponent.Reticle + ); +} +``` + +### Builder Mode HUD + +Show builder-specific components: + +```java +public void enableBuilderHud(Player player) { + HudManager hud = player.getHudManager(); + hud.showHudComponents(player.getPlayerRef(), + HudComponent.BuilderToolsLegend, + HudComponent.BuilderToolsMaterialSlotSelector, + HudComponent.BlockVariantSelector + ); +} +``` + +## Best Practices + +{{< callout type="info" >}} +**HUD Guidelines:** +- Always use `PlayerRef` when calling HUD methods +- Store previous state before clearing if you need to restore later +- Consider game mode when deciding which components to show +- Use `resetHud()` to return to default state +{{< /callout >}} + +{{< callout type="warning" >}} +**Performance Note:** Avoid rapid toggling of HUD components as each change sends packets to the client. +{{< /callout >}} diff --git a/content/ui-systems/hud-manager.fr.md b/content/ui-systems/hud-manager.fr.md new file mode 100644 index 0000000..c6493eb --- /dev/null +++ b/content/ui-systems/hud-manager.fr.md @@ -0,0 +1,232 @@ +--- +title: Gestionnaire HUD +type: docs +weight: 1 +--- + +Le `HudManager` contrôle quels composants HUD sont visibles pour le joueur. + +**Package:** `com.hypixel.hytale.server.core.entity.entities.player.hud` + +## Composants HUD + +Tous les composants HUD disponibles : + +| Composant | Description | +|-----------|-------------| +| `HudComponent.Hotbar` | Barre d'objets du joueur | +| `HudComponent.StatusIcons` | Icônes d'effets de statut | +| `HudComponent.Reticle` | Réticule/viseur | +| `HudComponent.Chat` | Fenêtre de chat | +| `HudComponent.Requests` | UI des demandes en attente | +| `HudComponent.Notifications` | Notifications d'alerte | +| `HudComponent.KillFeed` | Messages de kill/mort | +| `HudComponent.InputBindings` | Indications de contrôle | +| `HudComponent.PlayerList` | Liste des joueurs (Tab) | +| `HudComponent.EventTitle` | Grands titres d'événements | +| `HudComponent.Compass` | Boussole de direction | +| `HudComponent.ObjectivePanel` | Suivi de quêtes/objectifs | +| `HudComponent.PortalPanel` | Interface de portail | +| `HudComponent.BuilderToolsLegend` | Indications d'outils de construction | +| `HudComponent.Speedometer` | Indicateur de vitesse | +| `HudComponent.UtilitySlotSelector` | UI des slots d'utilitaires | +| `HudComponent.BlockVariantSelector` | Sélecteur de variantes de blocs | +| `HudComponent.BuilderToolsMaterialSlotSelector` | Slots de matériaux du builder | +| `HudComponent.Stamina` | Barre d'endurance | +| `HudComponent.AmmoIndicator` | Compteur de munitions | +| `HudComponent.Health` | Barre de vie | +| `HudComponent.Mana` | Barre de mana | +| `HudComponent.Oxygen` | Barre d'oxygène/respiration | +| `HudComponent.Sleep` | Indicateur de sommeil | + +## Composants par Défaut + +Ces composants sont visibles par défaut : + +```java +Set DEFAULT = Set.of( + HudComponent.UtilitySlotSelector, + HudComponent.BlockVariantSelector, + HudComponent.StatusIcons, + HudComponent.Hotbar, + HudComponent.Chat, + HudComponent.Notifications, + HudComponent.KillFeed, + HudComponent.InputBindings, + HudComponent.Reticle, + HudComponent.Compass, + HudComponent.Speedometer, + HudComponent.ObjectivePanel, + HudComponent.PortalPanel, + HudComponent.EventTitle, + HudComponent.Stamina, + HudComponent.AmmoIndicator, + HudComponent.Health, + HudComponent.Mana, + HudComponent.Oxygen, + HudComponent.BuilderToolsLegend, + HudComponent.Sleep +); +``` + +## Accéder au Gestionnaire HUD + +```java +Player player = ...; +HudManager hudManager = player.getHudManager(); +``` + +## Afficher des Composants + +```java +// Afficher des composants spécifiques (additif) +hudManager.showHudComponents(player.getPlayerRef(), + HudComponent.Health, + HudComponent.Stamina, + HudComponent.Hotbar +); + +// Afficher depuis un Set +Set components = Set.of( + HudComponent.Chat, + HudComponent.Notifications +); +hudManager.showHudComponents(player.getPlayerRef(), components); +``` + +## Masquer des Composants + +```java +// Masquer des composants spécifiques +hudManager.hideHudComponents(player.getPlayerRef(), + HudComponent.Compass, + HudComponent.Speedometer +); +``` + +## Définir les Composants + +Remplacer tous les composants visibles : + +```java +// Définir exactement les composants visibles (remplace tout) +hudManager.setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health +); + +// Vider tous les composants HUD +hudManager.setVisibleHudComponents(player.getPlayerRef()); +``` + +## Requêter les Composants Visibles + +```java +Set visible = hudManager.getVisibleHudComponents(); + +if (visible.contains(HudComponent.Health)) { + // La barre de vie est visible +} +``` + +## Réinitialiser le HUD + +```java +// Réinitialiser aux composants par défaut et vider le HUD personnalisé +hudManager.resetHud(player.getPlayerRef()); + +// Réinitialiser l'état UI complet +hudManager.resetUserInterface(player.getPlayerRef()); +``` + +## HUD Personnalisé + +Pour les overlays persistants : + +```java +public class ScoreboardHud extends CustomUIHud { + + public ScoreboardHud(PlayerRef ref) { + super(ref); + } + + @Override + protected void build(UICommandBuilder builder) { + // L'extension .ui est OBLIGATOIRE + builder.append("Hud/Scoreboard.ui"); + builder.set("#score", 1500); + builder.set("#rank", "Or"); + } +} + +// Définir le HUD personnalisé +hudManager.setCustomHud(player.getPlayerRef(), new ScoreboardHud(player.getPlayerRef())); + +// Supprimer le HUD personnalisé +hudManager.setCustomHud(player.getPlayerRef(), null); +``` + +## Exemples Pratiques + +### Mode Cinématique + +Masquer toute l'UI pour les cinématiques : + +```java +public void enterCinematicMode(Player player) { + HudManager hud = player.getHudManager(); + + // Stocker l'état actuel si nécessaire pour restauration + Set previous = new HashSet<>(hud.getVisibleHudComponents()); + + // Vider tout le HUD + hud.setVisibleHudComponents(player.getPlayerRef()); +} + +public void exitCinematicMode(Player player, Set restore) { + player.getHudManager().setVisibleHudComponents(player.getPlayerRef(), restore); +} +``` + +### Mode HUD Minimal + +N'afficher que les composants essentiels : + +```java +public void setMinimalHud(Player player) { + player.getHudManager().setVisibleHudComponents(player.getPlayerRef(), + HudComponent.Hotbar, + HudComponent.Health, + HudComponent.Reticle + ); +} +``` + +### HUD Mode Construction + +Afficher les composants spécifiques au builder : + +```java +public void enableBuilderHud(Player player) { + HudManager hud = player.getHudManager(); + hud.showHudComponents(player.getPlayerRef(), + HudComponent.BuilderToolsLegend, + HudComponent.BuilderToolsMaterialSlotSelector, + HudComponent.BlockVariantSelector + ); +} +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Directives HUD :** +- Utilisez toujours `PlayerRef` lors de l'appel des méthodes HUD +- Stockez l'état précédent avant de vider si vous devez restaurer plus tard +- Considérez le mode de jeu lors du choix des composants à afficher +- Utilisez `resetHud()` pour revenir à l'état par défaut +{{< /callout >}} + +{{< callout type="warning" >}} +**Note de Performance :** Évitez le basculement rapide des composants HUD car chaque changement envoie des paquets au client. +{{< /callout >}} diff --git a/content/ui-systems/permissions/_index.en.md b/content/ui-systems/permissions/_index.en.md new file mode 100644 index 0000000..3c6c5e7 --- /dev/null +++ b/content/ui-systems/permissions/_index.en.md @@ -0,0 +1,349 @@ +--- +title: Permissions System +type: docs +weight: 12 +--- + +The Permissions system provides fine-grained access control for commands, editor features, and custom plugin functionality. + +**Package:** `com.hypixel.hytale.server.core.permissions` + +## Overview + +Hytale uses a permission system with: +- **User permissions** - Directly assigned to individual players +- **Group permissions** - Inherited by all members of a group +- **Permission providers** - Custom logic for dynamic permissions + +## Accessing the Permissions Module + +```java +import com.hypixel.hytale.server.core.permissions.PermissionsModule; + +// Use static get() method - NOT HytaleServer.get().getPermissionsModule() +PermissionsModule permissions = PermissionsModule.get(); +``` + +## Checking Permissions + +```java +UUID playerUuid = player.getPlayerRef().getUuid(); + +// Check if player has permission +boolean canEdit = permissions.hasPermission(playerUuid, "hytale.editor.asset"); + +// Check in command context +if (permissions.hasPermission(ctx.sender().getUuid(), "myplugin.admin")) { + // Admin-only logic +} +``` + +## User Permissions + +Manage permissions for individual players: + +```java +UUID playerUuid = player.getPlayerRef().getUuid(); + +// Add permissions +permissions.addUserPermission(playerUuid, Set.of( + "myplugin.feature.create", + "myplugin.feature.delete" +)); + +// Remove permissions +permissions.removeUserPermission(playerUuid, Set.of( + "myplugin.feature.delete" +)); +``` + +## Group Permissions + +Groups allow shared permissions across multiple players: + +### Managing Groups + +```java +// Add player to a group +permissions.addUserToGroup(playerUuid, "moderators"); + +// Remove player from a group +permissions.removeUserFromGroup(playerUuid, "moderators"); + +// Get all groups for a player +Set groups = permissions.getGroupsForUser(playerUuid); +``` + +### Group Permission Events + +Listen for group changes using specific event subclasses: + +```java +import com.hypixel.hytale.server.core.event.events.permissions.*; + +// Player ADDED to group +getEventRegistry().register(PlayerGroupEvent.Added.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + String group = event.getGroupName(); // Note: getGroupName(), NOT getGroup() + log("Player joined group: " + group); +}); + +// Player REMOVED from group +getEventRegistry().register(PlayerGroupEvent.Removed.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + String group = event.getGroupName(); + log("Player left group: " + group); +}); + +// Group permissions ADDED +getEventRegistry().register(GroupPermissionChangeEvent.Added.class, event -> { + String group = event.getGroupName(); + Set added = event.getAddedPermissions(); + log("Group " + group + " got permissions: " + added); +}); + +// Group permissions REMOVED +getEventRegistry().register(GroupPermissionChangeEvent.Removed.class, event -> { + String group = event.getGroupName(); + Set removed = event.getRemovedPermissions(); + log("Group " + group + " lost permissions: " + removed); +}); + +// Player permissions ADDED +getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsAdded.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + Set added = event.getAddedPermissions(); +}); + +// Player permissions REMOVED +getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsRemoved.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + Set removed = event.getRemovedPermissions(); +}); +``` + +## Built-in Permissions + +Hytale includes many built-in permissions: + +### Command Permissions + +| Permission | Description | +|------------|-------------| +| `hytale.command.*` | All command access | +| `hytale.command.gamemode` | Change game mode | +| `hytale.command.tp` | Teleportation | +| `hytale.command.give` | Give items | +| `hytale.command.time` | Time control | + +### Editor Permissions + +| Permission | Description | +|------------|-------------| +| `hytale.editor.asset` | Asset editing | +| `hytale.editor.packs.*` | Pack management | +| `hytale.editor.builderTools` | Builder tools access | +| `hytale.editor.brush.*` | Brush tools | +| `hytale.editor.prefab.*` | Prefab editing | +| `hytale.editor.selection.*` | Selection tools | +| `hytale.editor.history` | Undo/redo history | + +### Camera Permissions + +| Permission | Description | +|------------|-------------| +| `hytale.camera.flycam` | Free camera mode | + +## Custom Permission Provider + +Implement `PermissionProvider` for dynamic permission logic: + +{{< callout type="info" >}} +**Note:** `PermissionProvider` is an interface that provides sets of permissions, not a simple hasPermission check. The `PermissionsModule` checks these sets. +{{< /callout >}} + +```java +import com.hypixel.hytale.server.core.permissions.provider.PermissionProvider; + +public class VIPPermissionProvider implements PermissionProvider { + private final Map> vipPermissions = new ConcurrentHashMap<>(); + private final Map> vipGroups = new ConcurrentHashMap<>(); + + @Override + @Nonnull + public String getName() { + return "VIPPermissionProvider"; + } + + @Override + public Set getUserPermissions(@Nonnull UUID uuid) { + // Return the permissions for this user + // VIP users get extra permissions automatically + if (isVIP(uuid)) { + Set perms = new HashSet<>(vipPermissions.getOrDefault(uuid, Collections.emptySet())); + perms.add("vip.lounge"); + perms.add("vip.cosmetics"); + return perms; + } + return vipPermissions.getOrDefault(uuid, Collections.emptySet()); + } + + @Override + public void addUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + vipPermissions.computeIfAbsent(uuid, k -> new HashSet<>()).addAll(permissions); + } + + @Override + public void removeUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + Set userPerms = vipPermissions.get(uuid); + if (userPerms != null) { + userPerms.removeAll(permissions); + } + } + + @Override + public Set getGroupPermissions(@Nonnull String group) { + return Collections.emptySet(); + } + + @Override + public void addGroupPermissions(@Nonnull String group, @Nonnull Set permissions) {} + + @Override + public void removeGroupPermissions(@Nonnull String group, @Nonnull Set permissions) {} + + @Override + public Set getGroupsForUser(@Nonnull UUID uuid) { + return vipGroups.getOrDefault(uuid, Collections.emptySet()); + } + + @Override + public void addUserToGroup(@Nonnull UUID uuid, @Nonnull String group) { + vipGroups.computeIfAbsent(uuid, k -> new HashSet<>()).add(group); + } + + @Override + public void removeUserFromGroup(@Nonnull UUID uuid, @Nonnull String group) { + Set groups = vipGroups.get(uuid); + if (groups != null) { + groups.remove(group); + } + } + + private boolean isVIP(UUID uuid) { + // Your VIP checking logic + return vipDatabase.contains(uuid); + } +} +``` + +### Registering Custom Providers + +```java +@Override +public void start() { + PermissionsModule permissions = PermissionsModule.get(); + permissions.addProvider(new VIPPermissionProvider()); +} +``` + +## Permission Wildcards + +Permissions support wildcard matching: + +```java +// Grant all permissions under a namespace +permissions.addUserPermission(playerUuid, Set.of("myplugin.*")); + +// This now passes: +permissions.hasPermission(playerUuid, "myplugin.anything"); +permissions.hasPermission(playerUuid, "myplugin.feature.nested"); +``` + +## Command Permission Integration + +Commands automatically check permissions based on their ID: + +```java +public class AdminCommand extends AbstractCommand { + + public AdminCommand() { + super("admin", "myplugin.commands.admin.description"); + // Permission check is automatic based on command registration + } + + @Override + protected CompletableFuture execute(CommandContext ctx) { + // Only players with appropriate permission reach here + return null; + } +} +``` + +## Practical Examples + +### Role-Based Access + +```java +public class RoleManager { + private final PermissionsModule permissions; + + public RoleManager() { + this.permissions = PermissionsModule.get(); + } + + public void promoteToModerator(UUID playerUuid) { + permissions.addUserToGroup(playerUuid, "moderators"); + permissions.addUserPermission(playerUuid, Set.of( + "server.kick", + "server.mute", + "server.warn" + )); + } + + public void demoteFromModerator(UUID playerUuid) { + permissions.removeUserFromGroup(playerUuid, "moderators"); + permissions.removeUserPermission(playerUuid, Set.of( + "server.kick", + "server.mute", + "server.warn" + )); + } + + public boolean canModerate(UUID playerUuid) { + return permissions.getGroupsForUser(playerUuid).contains("moderators"); + } +} +``` + +### Feature Gating + +```java +public void useFeature(Player player, String featureName) { + UUID uuid = player.getPlayerRef().getUuid(); + PermissionsModule perms = PermissionsModule.get(); + + String permission = "myplugin.feature." + featureName; + if (!perms.hasPermission(uuid, permission)) { + player.sendMessage(Message.translation("error.no_permission")); + return; + } + + // Execute feature + executeFeature(player, featureName); +} +``` + +## Best Practices + +{{< callout type="info" >}} +**Permission Guidelines:** +- Use namespaced permissions (e.g., `myplugin.feature.name`) +- Prefer groups for role-based access over individual permissions +- Check permissions at entry points, not deep in logic +- Use wildcards sparingly and intentionally +{{< /callout >}} + +{{< callout type="warning" >}} +**Security Note:** Always validate permissions server-side. Never trust client-reported permission states. +{{< /callout >}} diff --git a/content/ui-systems/permissions/_index.fr.md b/content/ui-systems/permissions/_index.fr.md new file mode 100644 index 0000000..9d07d65 --- /dev/null +++ b/content/ui-systems/permissions/_index.fr.md @@ -0,0 +1,349 @@ +--- +title: Système de Permissions +type: docs +weight: 12 +--- + +Le système de Permissions fournit un contrôle d'accès granulaire pour les commandes, les fonctionnalités de l'éditeur et les fonctionnalités personnalisées des plugins. + +**Package:** `com.hypixel.hytale.server.core.permissions` + +## Vue d'Ensemble + +Hytale utilise un système de permissions avec : +- **Permissions utilisateur** - Directement assignées aux joueurs individuels +- **Permissions de groupe** - Héritées par tous les membres d'un groupe +- **Fournisseurs de permissions** - Logique personnalisée pour les permissions dynamiques + +## Accéder au Module de Permissions + +```java +import com.hypixel.hytale.server.core.permissions.PermissionsModule; + +// Utiliser la méthode statique get() - PAS HytaleServer.get().getPermissionsModule() +PermissionsModule permissions = PermissionsModule.get(); +``` + +## Vérifier les Permissions + +```java +UUID playerUuid = player.getPlayerRef().getUuid(); + +// Vérifier si le joueur a la permission +boolean canEdit = permissions.hasPermission(playerUuid, "hytale.editor.asset"); + +// Vérifier dans le contexte de commande +if (permissions.hasPermission(ctx.sender().getUuid(), "myplugin.admin")) { + // Logique réservée aux admins +} +``` + +## Permissions Utilisateur + +Gérer les permissions pour les joueurs individuels : + +```java +UUID playerUuid = player.getPlayerRef().getUuid(); + +// Ajouter des permissions +permissions.addUserPermission(playerUuid, Set.of( + "myplugin.feature.create", + "myplugin.feature.delete" +)); + +// Supprimer des permissions +permissions.removeUserPermission(playerUuid, Set.of( + "myplugin.feature.delete" +)); +``` + +## Permissions de Groupe + +Les groupes permettent de partager des permissions entre plusieurs joueurs : + +### Gérer les Groupes + +```java +// Ajouter un joueur à un groupe +permissions.addUserToGroup(playerUuid, "moderators"); + +// Retirer un joueur d'un groupe +permissions.removeUserFromGroup(playerUuid, "moderators"); + +// Obtenir tous les groupes d'un joueur +Set groups = permissions.getGroupsForUser(playerUuid); +``` + +### Événements de Permissions de Groupe + +Écouter les changements de groupes en utilisant des sous-classes spécifiques : + +```java +import com.hypixel.hytale.server.core.event.events.permissions.*; + +// Joueur AJOUTÉ à un groupe +getEventRegistry().register(PlayerGroupEvent.Added.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + String group = event.getGroupName(); // Note: getGroupName(), PAS getGroup() + log("Joueur a rejoint le groupe : " + group); +}); + +// Joueur RETIRÉ d'un groupe +getEventRegistry().register(PlayerGroupEvent.Removed.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + String group = event.getGroupName(); + log("Joueur a quitté le groupe : " + group); +}); + +// Permissions du groupe AJOUTÉES +getEventRegistry().register(GroupPermissionChangeEvent.Added.class, event -> { + String group = event.getGroupName(); + Set added = event.getAddedPermissions(); + log("Groupe " + group + " a reçu les permissions : " + added); +}); + +// Permissions du groupe RETIRÉES +getEventRegistry().register(GroupPermissionChangeEvent.Removed.class, event -> { + String group = event.getGroupName(); + Set removed = event.getRemovedPermissions(); + log("Groupe " + group + " a perdu les permissions : " + removed); +}); + +// Permissions du joueur AJOUTÉES +getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsAdded.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + Set added = event.getAddedPermissions(); +}); + +// Permissions du joueur RETIRÉES +getEventRegistry().register(PlayerPermissionChangeEvent.PermissionsRemoved.class, event -> { + UUID playerUuid = event.getPlayerUuid(); + Set removed = event.getRemovedPermissions(); +}); +``` + +## Permissions Intégrées + +Hytale inclut de nombreuses permissions intégrées : + +### Permissions de Commandes + +| Permission | Description | +|------------|-------------| +| `hytale.command.*` | Accès à toutes les commandes | +| `hytale.command.gamemode` | Changer de mode de jeu | +| `hytale.command.tp` | Téléportation | +| `hytale.command.give` | Donner des objets | +| `hytale.command.time` | Contrôle du temps | + +### Permissions de l'Éditeur + +| Permission | Description | +|------------|-------------| +| `hytale.editor.asset` | Édition des assets | +| `hytale.editor.packs.*` | Gestion des packs | +| `hytale.editor.builderTools` | Accès aux outils de construction | +| `hytale.editor.brush.*` | Outils de pinceau | +| `hytale.editor.prefab.*` | Édition des prefabs | +| `hytale.editor.selection.*` | Outils de sélection | +| `hytale.editor.history` | Historique annuler/rétablir | + +### Permissions de Caméra + +| Permission | Description | +|------------|-------------| +| `hytale.camera.flycam` | Mode caméra libre | + +## Fournisseur de Permission Personnalisé + +Implémentez `PermissionProvider` pour une logique de permission dynamique : + +{{< callout type="info" >}} +**Note :** `PermissionProvider` est une interface qui fournit des ensembles de permissions, pas une simple vérification hasPermission. Le `PermissionsModule` vérifie ces ensembles. +{{< /callout >}} + +```java +import com.hypixel.hytale.server.core.permissions.provider.PermissionProvider; + +public class VIPPermissionProvider implements PermissionProvider { + private final Map> vipPermissions = new ConcurrentHashMap<>(); + private final Map> vipGroups = new ConcurrentHashMap<>(); + + @Override + @Nonnull + public String getName() { + return "VIPPermissionProvider"; + } + + @Override + public Set getUserPermissions(@Nonnull UUID uuid) { + // Retourner les permissions pour cet utilisateur + // Les utilisateurs VIP reçoivent des permissions supplémentaires automatiquement + if (isVIP(uuid)) { + Set perms = new HashSet<>(vipPermissions.getOrDefault(uuid, Collections.emptySet())); + perms.add("vip.lounge"); + perms.add("vip.cosmetics"); + return perms; + } + return vipPermissions.getOrDefault(uuid, Collections.emptySet()); + } + + @Override + public void addUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + vipPermissions.computeIfAbsent(uuid, k -> new HashSet<>()).addAll(permissions); + } + + @Override + public void removeUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + Set userPerms = vipPermissions.get(uuid); + if (userPerms != null) { + userPerms.removeAll(permissions); + } + } + + @Override + public Set getGroupPermissions(@Nonnull String group) { + return Collections.emptySet(); + } + + @Override + public void addGroupPermissions(@Nonnull String group, @Nonnull Set permissions) {} + + @Override + public void removeGroupPermissions(@Nonnull String group, @Nonnull Set permissions) {} + + @Override + public Set getGroupsForUser(@Nonnull UUID uuid) { + return vipGroups.getOrDefault(uuid, Collections.emptySet()); + } + + @Override + public void addUserToGroup(@Nonnull UUID uuid, @Nonnull String group) { + vipGroups.computeIfAbsent(uuid, k -> new HashSet<>()).add(group); + } + + @Override + public void removeUserFromGroup(@Nonnull UUID uuid, @Nonnull String group) { + Set groups = vipGroups.get(uuid); + if (groups != null) { + groups.remove(group); + } + } + + private boolean isVIP(UUID uuid) { + // Votre logique de vérification VIP + return vipDatabase.contains(uuid); + } +} +``` + +### Enregistrer des Fournisseurs Personnalisés + +```java +@Override +public void start() { + PermissionsModule permissions = PermissionsModule.get(); + permissions.addProvider(new VIPPermissionProvider()); +} +``` + +## Wildcards de Permission + +Les permissions supportent les correspondances avec wildcard : + +```java +// Accorder toutes les permissions sous un namespace +permissions.addUserPermission(playerUuid, Set.of("myplugin.*")); + +// Ceci passe maintenant : +permissions.hasPermission(playerUuid, "myplugin.anything"); +permissions.hasPermission(playerUuid, "myplugin.feature.nested"); +``` + +## Intégration des Permissions de Commandes + +Les commandes vérifient automatiquement les permissions basées sur leur ID : + +```java +public class AdminCommand extends AbstractCommand { + + public AdminCommand() { + super("admin", "myplugin.commands.admin.description"); + // La vérification de permission est automatique basée sur l'enregistrement de la commande + } + + @Override + protected CompletableFuture execute(CommandContext ctx) { + // Seuls les joueurs avec la permission appropriée arrivent ici + return null; + } +} +``` + +## Exemples Pratiques + +### Accès Basé sur les Rôles + +```java +public class RoleManager { + private final PermissionsModule permissions; + + public RoleManager() { + this.permissions = PermissionsModule.get(); + } + + public void promoteToModerator(UUID playerUuid) { + permissions.addUserToGroup(playerUuid, "moderators"); + permissions.addUserPermission(playerUuid, Set.of( + "server.kick", + "server.mute", + "server.warn" + )); + } + + public void demoteFromModerator(UUID playerUuid) { + permissions.removeUserFromGroup(playerUuid, "moderators"); + permissions.removeUserPermission(playerUuid, Set.of( + "server.kick", + "server.mute", + "server.warn" + )); + } + + public boolean canModerate(UUID playerUuid) { + return permissions.getGroupsForUser(playerUuid).contains("moderators"); + } +} +``` + +### Verrouillage de Fonctionnalités + +```java +public void useFeature(Player player, String featureName) { + UUID uuid = player.getPlayerRef().getUuid(); + PermissionsModule perms = PermissionsModule.get(); + + String permission = "myplugin.feature." + featureName; + if (!perms.hasPermission(uuid, permission)) { + player.sendMessage(Message.translation("error.no_permission")); + return; + } + + // Exécuter la fonctionnalité + executeFeature(player, featureName); +} +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Directives des Permissions :** +- Utilisez des permissions avec namespace (ex: `myplugin.feature.name`) +- Préférez les groupes pour l'accès basé sur les rôles plutôt que les permissions individuelles +- Vérifiez les permissions aux points d'entrée, pas profondément dans la logique +- Utilisez les wildcards avec parcimonie et intentionnellement +{{< /callout >}} + +{{< callout type="warning" >}} +**Note de Sécurité :** Validez toujours les permissions côté serveur. Ne faites jamais confiance aux états de permission rapportés par le client. +{{< /callout >}} diff --git a/content/ui-systems/ui-files.en.md b/content/ui-systems/ui-files.en.md new file mode 100644 index 0000000..df1827c --- /dev/null +++ b/content/ui-systems/ui-files.en.md @@ -0,0 +1,161 @@ +--- +title: UI Files +type: docs +weight: 10 +--- + +UI files (`.ui`) are Hytale's declarative markup language for defining user interfaces. They describe the structure, layout, and styling of UI elements that can be loaded by plugins via `UICommandBuilder.append()`. + +## File Location + +UI files are typically stored in the Assets directory: + +``` +Assets/ + Common/UI/Custom/ # Server-side custom UI + Pages/ # Custom page templates + Hud/ # Custom HUD elements + Common.ui # Shared definitions + Sounds.ui # Sound definitions + Client/Interface/ # Client-side UI + InGame/Hud/ # HUD components + Common/ # Common components + Common.ui # Client shared definitions +``` + +## Basic Structure + +A `.ui` file consists of variable definitions and widget declarations: + +``` +// Import external definitions +$Common = "../Common.ui"; + +// Define local variables +@ButtonHeight = 44; +@TextColor = #96a9be; + +// Define a widget tree +Group #Container { + Anchor: (Width: 400, Height: 300); + Background: (Color: #000000(0.8)); + + Label #Title { + Text: "Hello World"; + Style: (FontSize: 24, TextColor: @TextColor); + } +} +``` + +## Loading UI Files from Plugins + +Use `UICommandBuilder.append()` to load a UI file: + +```java +@Override +public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Load the UI template (extension .ui is REQUIRED) + builder.append("Pages/MyPage.ui"); + + // Append to a specific element + builder.append("#ContentList", "Pages/ListItem.ui"); + + // Set dynamic values using selectors + builder.set("#Title", "Welcome, " + playerRef.getUsername()); +} +``` + +### Path Resolution + +The path is relative to `Common/UI/Custom/`. The `.ui` extension is **required**. + +| Path in `append()` | Resolved Location | +|-------------------|-------------------| +| `Pages/MyPage.ui` | `Common/UI/Custom/Pages/MyPage.ui` | +| `Common.ui` | `Common/UI/Custom/Common.ui` | + +### For Java Plugins (Mods) + +When creating a Java plugin with custom UI, place your `.ui` files in the resources folder: + +``` +your-plugin/ + src/main/ + java/ # Java source code + resources/ + Common/UI/Custom/ # UI files go here + Pages/ + MyPage.ui + MyButton.ui + Common.ui # Your shared definitions + plugin.json # Plugin manifest +``` + +Set `"IncludesAssetPack": true` in your `plugin.json` to enable asset loading: + +```json +{ + "Group": "MyGroup", + "Name": "MyPlugin", + "Version": "1.0.0", + "Main": "com.example.MyPlugin", + "IncludesAssetPack": true +} +``` + +{{< callout type="info" >}} +The server automatically looks for UI files in `Common/UI/Custom/` within your plugin's asset pack. This is why paths like `Pages/MyPage.ui` work without specifying the full path. +{{< /callout >}} + +## Key Concepts + +### Variables + +Variables store reusable values: + +``` +@Height = 50; // Number +@Color = #ff5500; // Color +@Style = (FontSize: 16, RenderBold: true); // Object +``` + +### Imports + +Import definitions from other files: + +``` +$Common = "Common.ui"; // Import file +$Common.@DefaultButtonStyle // Reference imported variable +``` + +### Widget IDs + +Use `#` to assign IDs that can be targeted from Java: + +``` +Label #PlayerName { + Text: "Unknown"; +} +``` + +```java +builder.set("#PlayerName", player.getUsername()); +``` + +### Localization + +Use `%` prefix for translation keys: + +``` +Label { + Text: %server.ui.welcome.title; +} +``` + +## Next Steps + +- [UI Syntax](ui-syntax) - Detailed syntax reference +- [UI Widgets](ui-widgets) - Available widget types +- [UI Properties](ui-properties) - Common properties +- [UI Styles](ui-styles) - Styling system diff --git a/content/ui-systems/ui-files.fr.md b/content/ui-systems/ui-files.fr.md new file mode 100644 index 0000000..3aefb1e --- /dev/null +++ b/content/ui-systems/ui-files.fr.md @@ -0,0 +1,161 @@ +--- +title: Fichiers UI +type: docs +weight: 10 +--- + +Les fichiers UI (`.ui`) sont le langage de balisage déclaratif de Hytale pour définir les interfaces utilisateur. Ils décrivent la structure, la disposition et le style des éléments UI qui peuvent être chargés par les plugins via `UICommandBuilder.append()`. + +## Emplacement des Fichiers + +Les fichiers UI sont généralement stockés dans le répertoire Assets : + +``` +Assets/ + Common/UI/Custom/ # UI personnalisée côté serveur + Pages/ # Templates de pages personnalisées + Hud/ # Éléments HUD personnalisés + Common.ui # Définitions partagées + Sounds.ui # Définitions de sons + Client/Interface/ # UI côté client + InGame/Hud/ # Composants HUD + Common/ # Composants communs + Common.ui # Définitions partagées client +``` + +## Structure de Base + +Un fichier `.ui` se compose de définitions de variables et de déclarations de widgets : + +``` +// Importer des définitions externes +$Common = "../Common.ui"; + +// Définir des variables locales +@ButtonHeight = 44; +@TextColor = #96a9be; + +// Définir un arbre de widgets +Group #Container { + Anchor: (Width: 400, Height: 300); + Background: (Color: #000000(0.8)); + + Label #Title { + Text: "Bonjour le monde"; + Style: (FontSize: 24, TextColor: @TextColor); + } +} +``` + +## Charger des Fichiers UI depuis les Plugins + +Utilisez `UICommandBuilder.append()` pour charger un fichier UI : + +```java +@Override +public void build(Ref ref, UICommandBuilder builder, + UIEventBuilder events, Store store) { + // Charger le template UI (extension .ui OBLIGATOIRE) + builder.append("Pages/MyPage.ui"); + + // Ajouter à un élément spécifique + builder.append("#ContentList", "Pages/ListItem.ui"); + + // Définir des valeurs dynamiques avec des sélecteurs + builder.set("#Title", "Bienvenue, " + playerRef.getUsername()); +} +``` + +### Résolution des Chemins + +Le chemin est relatif à `Common/UI/Custom/`. L'extension `.ui` est **obligatoire**. + +| Chemin dans `append()` | Emplacement Résolu | +|------------------------|-------------------| +| `Pages/MyPage.ui` | `Common/UI/Custom/Pages/MyPage.ui` | +| `Common.ui` | `Common/UI/Custom/Common.ui` | + +### Pour les Plugins Java (Mods) + +Lors de la création d'un plugin Java avec une UI personnalisée, placez vos fichiers `.ui` dans le dossier resources : + +``` +your-plugin/ + src/main/ + java/ # Code source Java + resources/ + Common/UI/Custom/ # Les fichiers UI vont ici + Pages/ + MyPage.ui + MyButton.ui + Common.ui # Vos définitions partagées + plugin.json # Manifeste du plugin +``` + +Définissez `"IncludesAssetPack": true` dans votre `plugin.json` pour activer le chargement des assets : + +```json +{ + "Group": "MyGroup", + "Name": "MyPlugin", + "Version": "1.0.0", + "Main": "com.example.MyPlugin", + "IncludesAssetPack": true +} +``` + +{{< callout type="info" >}} +Le serveur cherche automatiquement les fichiers UI dans `Common/UI/Custom/` au sein de l'asset pack de votre plugin. C'est pourquoi des chemins comme `Pages/MyPage.ui` fonctionnent sans spécifier le chemin complet. +{{< /callout >}} + +## Concepts Clés + +### Variables + +Les variables stockent des valeurs réutilisables : + +``` +@Height = 50; // Nombre +@Color = #ff5500; // Couleur +@Style = (FontSize: 16, RenderBold: true); // Objet +``` + +### Imports + +Importer des définitions depuis d'autres fichiers : + +``` +$Common = "Common.ui"; // Importer un fichier +$Common.@DefaultButtonStyle // Référencer une variable importée +``` + +### IDs de Widget + +Utilisez `#` pour assigner des IDs ciblables depuis Java : + +``` +Label #PlayerName { + Text: "Inconnu"; +} +``` + +```java +builder.set("#PlayerName", player.getUsername()); +``` + +### Localisation + +Utilisez le préfixe `%` pour les clés de traduction : + +``` +Label { + Text: %server.ui.welcome.title; +} +``` + +## Étapes Suivantes + +- [Syntaxe UI](ui-syntax) - Référence de syntaxe détaillée +- [Widgets UI](ui-widgets) - Types de widgets disponibles +- [Propriétés UI](ui-properties) - Propriétés communes +- [Styles UI](ui-styles) - Système de styles diff --git a/content/ui-systems/ui-properties.en.md b/content/ui-systems/ui-properties.en.md new file mode 100644 index 0000000..8f49d25 --- /dev/null +++ b/content/ui-systems/ui-properties.en.md @@ -0,0 +1,384 @@ +--- +title: UI Properties +type: docs +weight: 13 +--- + +This page documents common properties available on UI widgets. + +## Anchor (Positioning) + +The `Anchor` property controls widget position and size. + +### Position Properties + +``` +Anchor: ( + Left: 10, // Distance from left edge + Right: 10, // Distance from right edge + Top: 10, // Distance from top edge + Bottom: 10 // Distance from bottom edge +); +``` + +### Size Properties + +``` +Anchor: ( + Width: 200, // Fixed width + Height: 100 // Fixed height +); +``` + +### Shorthand Properties + +``` +Anchor: (Full: 10); // All sides = 10 +Anchor: (Horizontal: 20); // Left and Right = 20 +Anchor: (Vertical: 15); // Top and Bottom = 15 +``` + +### Combined Example + +``` +// Fixed size, positioned from top-left +Anchor: (Left: 50, Top: 100, Width: 300, Height: 200); + +// Fill parent with margins +Anchor: (Full: 16); + +// Anchored to bottom-right corner +Anchor: (Right: 20, Bottom: 20, Width: 100, Height: 40); + +// Full width, fixed height at bottom +Anchor: (Horizontal: 0, Bottom: 0, Height: 50); +``` + +## Background + +The `Background` property sets widget background appearance. + +### Solid Color + +``` +Background: (Color: #ff5500); +Background: (Color: #000000(0.5)); // With opacity +``` + +### Simple Texture + +``` +Background: "MyTexture.png"; +Background: (TexturePath: "MyTexture.png"); +``` + +### 9-Patch (Stretchable) Texture + +``` +// Uniform border +Background: (TexturePath: "Panel.png", Border: 20); + +// Separate horizontal/vertical borders +Background: ( + TexturePath: "Button.png", + HorizontalBorder: 80, + VerticalBorder: 12 +); +``` + +### Using PatchStyle + +``` +@MyBackground = PatchStyle( + TexturePath: "Panel.png", + Border: 16 +); + +Group { + Background: @MyBackground; +} +``` + +## Padding + +The `Padding` property sets inner spacing. + +``` +Padding: (Full: 16); // All sides +Padding: (Horizontal: 20); // Left and Right +Padding: (Vertical: 10); // Top and Bottom +Padding: (Left: 10, Right: 20); // Individual sides +Padding: (Top: 5, Bottom: 15, Horizontal: 10); +``` + +### Using Padding Constructor + +``` +@ContentPadding = Padding(Full: 16, Top: 8); + +Group { + Padding: @ContentPadding; +} +``` + +## LayoutMode + +The `LayoutMode` property controls how children are arranged. + +### Stacking Modes + +| Mode | Description | +|------|-------------| +| `Top` | Stack children from top to bottom | +| `Bottom` | Stack children from bottom to top | +| `Left` | Stack children from left to right | +| `Right` | Stack children from right to left | + +``` +Group { + LayoutMode: Top; // Vertical list, top-aligned + + Label { Text: "First"; Anchor: (Height: 30); } + Label { Text: "Second"; Anchor: (Height: 30); } + Label { Text: "Third"; Anchor: (Height: 30); } +} +``` + +### Centering Modes + +| Mode | Description | +|------|-------------| +| `Middle` | Center children vertically | +| `CenterMiddle` | Center children both horizontally and vertically | + +``` +Group { + LayoutMode: CenterMiddle; + + Label { Text: "Centered!"; } +} +``` + +### Scrolling Modes + +| Mode | Description | +|------|-------------| +| `TopScrolling` | Vertical scroll, content from top | +| `BottomScrolling` | Vertical scroll, content from bottom (chat-style) | + +``` +Group #ChatLog { + LayoutMode: BottomScrolling; + ScrollbarStyle: @DefaultScrollbarStyle; + AutoScrollDown: true; +} +``` + +### Wrapping Mode + +| Mode | Description | +|------|-------------| +| `LeftCenterWrap` | Flow left-to-right, wrap to next row | + +``` +Group #ItemGrid { + LayoutMode: LeftCenterWrap; + + // Items will wrap to new rows +} +``` + +## FlexWeight + +The `FlexWeight` property controls flexible sizing in layout. + +``` +Group { + LayoutMode: Left; + + Group { + Anchor: (Width: 100); // Fixed width + } + + Group { + FlexWeight: 1; // Takes remaining space + } + + Group { + FlexWeight: 2; // Takes 2x the space of FlexWeight: 1 + } +} +``` + +## Visible + +Controls widget visibility. + +``` +Group #Panel { + Visible: true; // or false, or @Variable +} + +Button #CloseButton { + Visible: @CloseButton; // Controlled by template parameter +} +``` + +## Text + +Sets text content for Label and TextButton widgets. + +``` +Label { + Text: "Static text"; +} + +Label { + Text: %server.ui.title; // Translation key +} + +Label #DynamicText { + Text: ""; // Set from Java +} +``` + +## ScrollbarStyle + +Configures scrollbar appearance for scrolling containers. + +``` +Group { + LayoutMode: TopScrolling; + ScrollbarStyle: ( + Spacing: 6, + Size: 6, + Background: (TexturePath: "Scrollbar.png", Border: 3), + Handle: (TexturePath: "ScrollbarHandle.png", Border: 3), + HoveredHandle: (TexturePath: "ScrollbarHandleHovered.png", Border: 3), + DraggedHandle: (TexturePath: "ScrollbarHandleDragged.png", Border: 3) + ); +} +``` + +### ScrollbarStyle Options + +| Property | Description | +|----------|-------------| +| `Spacing` | Gap between scrollbar and content | +| `Size` | Scrollbar width/height | +| `Background` | Track background | +| `Handle` | Normal handle appearance | +| `HoveredHandle` | Handle when hovered | +| `DraggedHandle` | Handle when dragging | +| `OnlyVisibleWhenHovered` | Hide when not interacting | + +## AutoScrollDown + +For BottomScrolling containers, automatically scroll to new content. + +``` +Group #ChatLog { + LayoutMode: BottomScrolling; + AutoScrollDown: true; +} +``` + +## Disabled + +Disables interaction with a widget. + +``` +Button #SubmitButton { + Disabled: false; +} + +ActionButton #Binding { + Disabled: true; +} +``` + +## TooltipText + +Displays tooltip on hover. + +``` +Button #InfoButton { + TooltipText: %ui.tooltip.help; +} +``` + +## TextTooltipStyle + +Configures tooltip appearance. + +``` +Button { + TooltipText: "Help text"; + TextTooltipStyle: ( + Background: (TexturePath: "TooltipBg.png", Border: 24), + MaxWidth: 400, + LabelStyle: (Wrap: true, FontSize: 16), + Padding: 24 + ); +} +``` + +## Common Property Combinations + +### Panel with Title and Content + +``` +Group #Panel { + Anchor: (Width: 400, Height: 300); + Background: (TexturePath: "Panel.png", Border: 20); + LayoutMode: Top; + Padding: (Full: 16); + + Label #Title { + Anchor: (Height: 40); + // ... + } + + Group #Content { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: @DefaultScrollbarStyle; + Padding: (Top: 8); + } +} +``` + +### Horizontal Button Row + +``` +Group #ButtonRow { + Anchor: (Height: 50); + LayoutMode: Right; + Padding: (Horizontal: 10); + + TextButton #Cancel { + Anchor: (Width: 100); + } + + Group { Anchor: (Width: 10); } // Spacer + + TextButton #Confirm { + Anchor: (Width: 100); + } +} +``` + +### Full-Screen Overlay + +``` +Group #Overlay { + Anchor: (Full: 0); + Background: (Color: #000000(0.7)); + LayoutMode: CenterMiddle; + + Group #Dialog { + Anchor: (Width: 500, Height: 300); + Background: (TexturePath: "Dialog.png", Border: 20); + } +} +``` diff --git a/content/ui-systems/ui-properties.fr.md b/content/ui-systems/ui-properties.fr.md new file mode 100644 index 0000000..e6acd9b --- /dev/null +++ b/content/ui-systems/ui-properties.fr.md @@ -0,0 +1,384 @@ +--- +title: Propriétés UI +type: docs +weight: 13 +--- + +Cette page documente les propriétés communes disponibles sur les widgets UI. + +## Anchor (Positionnement) + +La propriété `Anchor` contrôle la position et la taille du widget. + +### Propriétés de Position + +``` +Anchor: ( + Left: 10, // Distance depuis le bord gauche + Right: 10, // Distance depuis le bord droit + Top: 10, // Distance depuis le bord supérieur + Bottom: 10 // Distance depuis le bord inférieur +); +``` + +### Propriétés de Taille + +``` +Anchor: ( + Width: 200, // Largeur fixe + Height: 100 // Hauteur fixe +); +``` + +### Propriétés Raccourcies + +``` +Anchor: (Full: 10); // Tous les côtés = 10 +Anchor: (Horizontal: 20); // Gauche et Droite = 20 +Anchor: (Vertical: 15); // Haut et Bas = 15 +``` + +### Exemple Combiné + +``` +// Taille fixe, positionné depuis le coin supérieur gauche +Anchor: (Left: 50, Top: 100, Width: 300, Height: 200); + +// Remplit le parent avec des marges +Anchor: (Full: 16); + +// Ancré au coin inférieur droit +Anchor: (Right: 20, Bottom: 20, Width: 100, Height: 40); + +// Pleine largeur, hauteur fixe en bas +Anchor: (Horizontal: 0, Bottom: 0, Height: 50); +``` + +## Background + +La propriété `Background` définit l'apparence du fond du widget. + +### Couleur Unie + +``` +Background: (Color: #ff5500); +Background: (Color: #000000(0.5)); // Avec opacité +``` + +### Texture Simple + +``` +Background: "MyTexture.png"; +Background: (TexturePath: "MyTexture.png"); +``` + +### Texture 9-Patch (Étirable) + +``` +// Bordure uniforme +Background: (TexturePath: "Panel.png", Border: 20); + +// Bordures horizontales/verticales séparées +Background: ( + TexturePath: "Button.png", + HorizontalBorder: 80, + VerticalBorder: 12 +); +``` + +### Utiliser PatchStyle + +``` +@MyBackground = PatchStyle( + TexturePath: "Panel.png", + Border: 16 +); + +Group { + Background: @MyBackground; +} +``` + +## Padding + +La propriété `Padding` définit l'espacement intérieur. + +``` +Padding: (Full: 16); // Tous les côtés +Padding: (Horizontal: 20); // Gauche et Droite +Padding: (Vertical: 10); // Haut et Bas +Padding: (Left: 10, Right: 20); // Côtés individuels +Padding: (Top: 5, Bottom: 15, Horizontal: 10); +``` + +### Utiliser le Constructeur Padding + +``` +@ContentPadding = Padding(Full: 16, Top: 8); + +Group { + Padding: @ContentPadding; +} +``` + +## LayoutMode + +La propriété `LayoutMode` contrôle l'arrangement des enfants. + +### Modes d'Empilement + +| Mode | Description | +|------|-------------| +| `Top` | Empile les enfants de haut en bas | +| `Bottom` | Empile les enfants de bas en haut | +| `Left` | Empile les enfants de gauche à droite | +| `Right` | Empile les enfants de droite à gauche | + +``` +Group { + LayoutMode: Top; // Liste verticale, alignée en haut + + Label { Text: "Premier"; Anchor: (Height: 30); } + Label { Text: "Deuxième"; Anchor: (Height: 30); } + Label { Text: "Troisième"; Anchor: (Height: 30); } +} +``` + +### Modes de Centrage + +| Mode | Description | +|------|-------------| +| `Middle` | Centre les enfants verticalement | +| `CenterMiddle` | Centre les enfants horizontalement et verticalement | + +``` +Group { + LayoutMode: CenterMiddle; + + Label { Text: "Centré !"; } +} +``` + +### Modes avec Défilement + +| Mode | Description | +|------|-------------| +| `TopScrolling` | Défilement vertical, contenu depuis le haut | +| `BottomScrolling` | Défilement vertical, contenu depuis le bas (style chat) | + +``` +Group #ChatLog { + LayoutMode: BottomScrolling; + ScrollbarStyle: @DefaultScrollbarStyle; + AutoScrollDown: true; +} +``` + +### Mode Retour à la Ligne + +| Mode | Description | +|------|-------------| +| `LeftCenterWrap` | Flux gauche-droite, retour à la ligne suivante | + +``` +Group #ItemGrid { + LayoutMode: LeftCenterWrap; + + // Les items passeront à la ligne suivante +} +``` + +## FlexWeight + +La propriété `FlexWeight` contrôle le dimensionnement flexible dans la mise en page. + +``` +Group { + LayoutMode: Left; + + Group { + Anchor: (Width: 100); // Largeur fixe + } + + Group { + FlexWeight: 1; // Prend l'espace restant + } + + Group { + FlexWeight: 2; // Prend 2x l'espace de FlexWeight: 1 + } +} +``` + +## Visible + +Contrôle la visibilité du widget. + +``` +Group #Panel { + Visible: true; // ou false, ou @Variable +} + +Button #CloseButton { + Visible: @CloseButton; // Contrôlé par paramètre de template +} +``` + +## Text + +Définit le contenu texte pour les widgets Label et TextButton. + +``` +Label { + Text: "Texte statique"; +} + +Label { + Text: %server.ui.title; // Clé de traduction +} + +Label #DynamicText { + Text: ""; // Défini depuis Java +} +``` + +## ScrollbarStyle + +Configure l'apparence de la scrollbar pour les conteneurs défilants. + +``` +Group { + LayoutMode: TopScrolling; + ScrollbarStyle: ( + Spacing: 6, + Size: 6, + Background: (TexturePath: "Scrollbar.png", Border: 3), + Handle: (TexturePath: "ScrollbarHandle.png", Border: 3), + HoveredHandle: (TexturePath: "ScrollbarHandleHovered.png", Border: 3), + DraggedHandle: (TexturePath: "ScrollbarHandleDragged.png", Border: 3) + ); +} +``` + +### Options ScrollbarStyle + +| Propriété | Description | +|-----------|-------------| +| `Spacing` | Écart entre la scrollbar et le contenu | +| `Size` | Largeur/hauteur de la scrollbar | +| `Background` | Fond de la piste | +| `Handle` | Apparence normale de la poignée | +| `HoveredHandle` | Poignée au survol | +| `DraggedHandle` | Poignée pendant le glissement | +| `OnlyVisibleWhenHovered` | Masquer quand pas d'interaction | + +## AutoScrollDown + +Pour les conteneurs BottomScrolling, défile automatiquement vers le nouveau contenu. + +``` +Group #ChatLog { + LayoutMode: BottomScrolling; + AutoScrollDown: true; +} +``` + +## Disabled + +Désactive l'interaction avec un widget. + +``` +Button #SubmitButton { + Disabled: false; +} + +ActionButton #Binding { + Disabled: true; +} +``` + +## TooltipText + +Affiche une infobulle au survol. + +``` +Button #InfoButton { + TooltipText: %ui.tooltip.help; +} +``` + +## TextTooltipStyle + +Configure l'apparence de l'infobulle. + +``` +Button { + TooltipText: "Texte d'aide"; + TextTooltipStyle: ( + Background: (TexturePath: "TooltipBg.png", Border: 24), + MaxWidth: 400, + LabelStyle: (Wrap: true, FontSize: 16), + Padding: 24 + ); +} +``` + +## Combinaisons de Propriétés Courantes + +### Panneau avec Titre et Contenu + +``` +Group #Panel { + Anchor: (Width: 400, Height: 300); + Background: (TexturePath: "Panel.png", Border: 20); + LayoutMode: Top; + Padding: (Full: 16); + + Label #Title { + Anchor: (Height: 40); + // ... + } + + Group #Content { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: @DefaultScrollbarStyle; + Padding: (Top: 8); + } +} +``` + +### Rangée de Boutons Horizontale + +``` +Group #ButtonRow { + Anchor: (Height: 50); + LayoutMode: Right; + Padding: (Horizontal: 10); + + TextButton #Cancel { + Anchor: (Width: 100); + } + + Group { Anchor: (Width: 10); } // Espaceur + + TextButton #Confirm { + Anchor: (Width: 100); + } +} +``` + +### Overlay Plein Écran + +``` +Group #Overlay { + Anchor: (Full: 0); + Background: (Color: #000000(0.7)); + LayoutMode: CenterMiddle; + + Group #Dialog { + Anchor: (Width: 500, Height: 300); + Background: (TexturePath: "Dialog.png", Border: 20); + } +} +``` diff --git a/content/ui-systems/ui-styles.en.md b/content/ui-systems/ui-styles.en.md new file mode 100644 index 0000000..74cba15 --- /dev/null +++ b/content/ui-systems/ui-styles.en.md @@ -0,0 +1,367 @@ +--- +title: UI Styles +type: docs +weight: 14 +--- + +This page documents the styling system for Hytale UI widgets. + +## Style States + +Interactive widgets support multiple states: + +| State | When Applied | +|-------|--------------| +| `Default` | Normal state | +| `Hovered` | Mouse over widget | +| `Pressed` | Mouse button held | +| `Disabled` | Widget is disabled | + +``` +Style: ( + Default: (Background: "Button.png"), + Hovered: (Background: "ButtonHovered.png"), + Pressed: (Background: "ButtonPressed.png"), + Disabled: (Background: "ButtonDisabled.png") +); +``` + +## LabelStyle + +Text styling for Label widgets. + +``` +@MyLabelStyle = LabelStyle( + FontSize: 16, + FontName: "Default", // or "Secondary" + TextColor: #ffffff, + RenderBold: true, + RenderUppercase: false, + LetterSpacing: 0, + Wrap: false, + HorizontalAlignment: Center, // Start, Center, End + VerticalAlignment: Center, // Top, Center, Bottom + OutlineColor: #000000(0.5) +); + +Label { + Style: @MyLabelStyle; +} +``` + +### LabelStyle Properties + +| Property | Type | Description | +|----------|------|-------------| +| `FontSize` | number | Text size | +| `FontName` | string | Font family ("Default", "Secondary") | +| `TextColor` | color | Text color | +| `RenderBold` | bool | Bold text | +| `RenderUppercase` | bool | Force uppercase | +| `LetterSpacing` | number | Space between characters | +| `Wrap` | bool | Enable text wrapping | +| `HorizontalAlignment` | enum | Start, Center, End | +| `VerticalAlignment` | enum | Top, Center, Bottom | +| `OutlineColor` | color | Text outline color | +| `Alignment` | enum | Shorthand for both alignments (Center) | + +## ButtonStyle + +Styling for Button widgets (without text). + +``` +@MyButtonStyle = ButtonStyle( + Default: (Background: (TexturePath: "Button.png", Border: 12)), + Hovered: (Background: (TexturePath: "ButtonHovered.png", Border: 12)), + Pressed: (Background: (TexturePath: "ButtonPressed.png", Border: 12)), + Disabled: (Background: (TexturePath: "ButtonDisabled.png", Border: 12)), + Sounds: @ButtonSounds +); + +Button { + Style: @MyButtonStyle; +} +``` + +### Button State Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Background` | PatchStyle | Background appearance | + +## TextButtonStyle + +Styling for TextButton widgets. + +``` +@MyTextButtonStyle = TextButtonStyle( + Default: ( + Background: (TexturePath: "Button.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #bfcdd5, RenderBold: true) + ), + Hovered: ( + Background: (TexturePath: "ButtonHovered.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #ffffff, RenderBold: true) + ), + Pressed: ( + Background: (TexturePath: "ButtonPressed.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #aaaaaa, RenderBold: true) + ), + Disabled: ( + Background: (TexturePath: "ButtonDisabled.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #797b7c, RenderBold: true) + ), + Sounds: @ButtonSounds +); +``` + +### TextButton State Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Background` | PatchStyle | Background appearance | +| `LabelStyle` | LabelStyle | Text styling | + +## PatchStyle + +9-patch texture configuration for stretchable backgrounds. + +``` +@MyPatchStyle = PatchStyle( + TexturePath: "Panel.png", + Border: 16 // Uniform border +); + +@MyPatchStyle2 = PatchStyle( + TexturePath: "Button.png", + HorizontalBorder: 80, // Left/right border + VerticalBorder: 12 // Top/bottom border +); + +// With color tint +@ColoredPatch = PatchStyle( + Color: #ff0000(0.5) // Semi-transparent red +); +``` + +### PatchStyle Properties + +| Property | Type | Description | +|----------|------|-------------| +| `TexturePath` | string | Path to texture file | +| `Border` | number | Uniform 9-patch border | +| `HorizontalBorder` | number | Left/right 9-patch border | +| `VerticalBorder` | number | Top/bottom 9-patch border | +| `Color` | color | Solid color (instead of texture) | + +## InputFieldStyle + +Styling for text input fields. + +``` +@MyInputStyle = InputFieldStyle( + TextColor: #ffffff, + FontSize: 16 +); + +@MyPlaceholderStyle = InputFieldStyle( + TextColor: #6e7da1 +); + +TextField { + Style: @MyInputStyle; + PlaceholderStyle: @MyPlaceholderStyle; +} +``` + +## SliderStyle + +Styling for slider controls. + +``` +@MySliderStyle = SliderStyle( + Background: (TexturePath: "SliderBackground.png", Border: 2), + Handle: "SliderHandle.png", + HandleWidth: 16, + HandleHeight: 16, + Sounds: ( + Activate: (SoundPath: "SliderTick.ogg", Volume: 10), + MouseHover: (SoundPath: "Hover.ogg", Volume: 6) + ) +); +``` + +### SliderStyle Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Background` | PatchStyle | Track background | +| `Handle` | string | Handle texture path | +| `HandleWidth` | number | Handle width | +| `HandleHeight` | number | Handle height | +| `Sounds` | object | Sound effects | + +## CheckBoxStyle + +Styling for checkbox controls. + +``` +@MyCheckBoxStyle = CheckBoxStyle( + Unchecked: ( + DefaultBackground: (Color: #00000000), + HoveredBackground: (Color: #ffffff20), + PressedBackground: (Color: #ffffff10), + DisabledBackground: (Color: #424242), + ChangedSound: (SoundPath: "Untick.ogg", Volume: 6) + ), + Checked: ( + DefaultBackground: (TexturePath: "Checkmark.png"), + HoveredBackground: (TexturePath: "Checkmark.png"), + PressedBackground: (TexturePath: "Checkmark.png"), + ChangedSound: (SoundPath: "Tick.ogg", Volume: 6) + ) +); +``` + +## DropdownBoxStyle + +Styling for dropdown menus. + +``` +@MyDropdownStyle = DropdownBoxStyle( + DefaultBackground: (TexturePath: "Dropdown.png", Border: 16), + HoveredBackground: (TexturePath: "DropdownHovered.png", Border: 16), + PressedBackground: (TexturePath: "DropdownPressed.png", Border: 16), + DefaultArrowTexturePath: "DropdownCaret.png", + HoveredArrowTexturePath: "DropdownCaret.png", + PressedArrowTexturePath: "DropdownCaretPressed.png", + ArrowWidth: 13, + ArrowHeight: 18, + LabelStyle: (TextColor: #96a9be, FontSize: 13), + EntryLabelStyle: (TextColor: #b7cedd), + SelectedEntryLabelStyle: (TextColor: #b7cedd, RenderBold: true), + HorizontalPadding: 8, + PanelBackground: (TexturePath: "DropdownBox.png", Border: 16), + PanelPadding: 6, + PanelAlign: Right, // or Bottom + PanelOffset: 7, + EntryHeight: 31, + EntriesInViewport: 10, + HoveredEntryBackground: (Color: #0a0f17), + PressedEntryBackground: (Color: #0f1621), + Sounds: @DropdownBoxSounds, + EntrySounds: @ButtonSounds +); +``` + +## ScrollbarStyle + +Styling for scrollbars. + +``` +@MyScrollbarStyle = ScrollbarStyle( + Spacing: 6, + Size: 6, + Background: (TexturePath: "Scrollbar.png", Border: 3), + Handle: (TexturePath: "ScrollbarHandle.png", Border: 3), + HoveredHandle: (TexturePath: "ScrollbarHandleHovered.png", Border: 3), + DraggedHandle: (TexturePath: "ScrollbarHandleDragged.png", Border: 3), + OnlyVisibleWhenHovered: false +); +``` + +## TabNavigationStyle + +Styling for tab navigation. + +``` +@TabStyle = TabStyleState( + Background: "Tab.png", + Overlay: "TabOverlay.png", + IconAnchor: (Width: 44, Height: 44), + Anchor: (Width: 82, Height: 62) +); + +@MyTabsStyle = TabNavigationStyle( + TabStyle: ( + Default: @TabStyle, + Hovered: (...@TabStyle, Background: "TabHovered.png"), + Pressed: (...@TabStyle, Background: "TabPressed.png") + ), + SelectedTabStyle: ( + Default: ( + ...@TabStyle, + Overlay: "TabSelectedOverlay.png" + ) + ), + TabSounds: @TabSounds, + SeparatorAnchor: (Width: 5, Height: 34), + SeparatorBackground: "TabSeparator.png" +); +``` + +## TextTooltipStyle + +Styling for text tooltips. + +``` +@MyTooltipStyle = TextTooltipStyle( + Background: (TexturePath: "TooltipBackground.png", Border: 24), + MaxWidth: 400, + LabelStyle: (Wrap: true, FontSize: 16), + Padding: 24 +); +``` + +## Sound Definitions + +Sounds are defined as objects with audio properties. + +``` +@ButtonSounds = ( + Activate: ( + SoundPath: "Sounds/ButtonActivate.ogg", + Volume: 6, + MinPitch: -0.2, + MaxPitch: 0.2 + ), + MouseHover: ( + SoundPath: "Sounds/ButtonHover.ogg", + Volume: 6 + ) +); + +@DropdownBoxSounds = DropdownBoxSounds( + Activate: (SoundPath: "Tick.ogg", Volume: 6), + MouseHover: (SoundPath: "Hover.ogg", Volume: 6), + Close: (SoundPath: "Untick.ogg", Volume: 6) +); +``` + +### Sound Properties + +| Property | Type | Description | +|----------|------|-------------| +| `SoundPath` | string | Path to audio file | +| `Volume` | number | Volume level (can be negative) | +| `MinPitch` | number | Minimum pitch variation | +| `MaxPitch` | number | Maximum pitch variation | + +## Style Inheritance + +Use the spread operator to extend styles: + +``` +@BaseButtonStyle = TextButtonStyle( + Default: (Background: @DefaultBg, LabelStyle: @DefaultLabel), + Hovered: (Background: @HoveredBg, LabelStyle: @DefaultLabel), + Pressed: (Background: @PressedBg, LabelStyle: @DefaultLabel), + Sounds: @ButtonSounds +); + +@DestructiveButtonStyle = TextButtonStyle( + ...@BaseButtonStyle, + Default: (...@BaseButtonStyle.Default, Background: @DestructiveBg), + Sounds: @DestructiveSounds +); +``` diff --git a/content/ui-systems/ui-styles.fr.md b/content/ui-systems/ui-styles.fr.md new file mode 100644 index 0000000..a7d45fe --- /dev/null +++ b/content/ui-systems/ui-styles.fr.md @@ -0,0 +1,367 @@ +--- +title: Styles UI +type: docs +weight: 14 +--- + +Cette page documente le système de styles pour les widgets UI de Hytale. + +## États de Style + +Les widgets interactifs supportent plusieurs états : + +| État | Quand Appliqué | +|------|----------------| +| `Default` | État normal | +| `Hovered` | Souris sur le widget | +| `Pressed` | Bouton de souris maintenu | +| `Disabled` | Widget désactivé | + +``` +Style: ( + Default: (Background: "Button.png"), + Hovered: (Background: "ButtonHovered.png"), + Pressed: (Background: "ButtonPressed.png"), + Disabled: (Background: "ButtonDisabled.png") +); +``` + +## LabelStyle + +Style de texte pour les widgets Label. + +``` +@MyLabelStyle = LabelStyle( + FontSize: 16, + FontName: "Default", // ou "Secondary" + TextColor: #ffffff, + RenderBold: true, + RenderUppercase: false, + LetterSpacing: 0, + Wrap: false, + HorizontalAlignment: Center, // Start, Center, End + VerticalAlignment: Center, // Top, Center, Bottom + OutlineColor: #000000(0.5) +); + +Label { + Style: @MyLabelStyle; +} +``` + +### Propriétés LabelStyle + +| Propriété | Type | Description | +|-----------|------|-------------| +| `FontSize` | nombre | Taille du texte | +| `FontName` | chaîne | Famille de police ("Default", "Secondary") | +| `TextColor` | couleur | Couleur du texte | +| `RenderBold` | bool | Texte en gras | +| `RenderUppercase` | bool | Forcer les majuscules | +| `LetterSpacing` | nombre | Espace entre les caractères | +| `Wrap` | bool | Activer le retour à la ligne | +| `HorizontalAlignment` | enum | Start, Center, End | +| `VerticalAlignment` | enum | Top, Center, Bottom | +| `OutlineColor` | couleur | Couleur du contour du texte | +| `Alignment` | enum | Raccourci pour les deux alignements (Center) | + +## ButtonStyle + +Style pour les widgets Button (sans texte). + +``` +@MyButtonStyle = ButtonStyle( + Default: (Background: (TexturePath: "Button.png", Border: 12)), + Hovered: (Background: (TexturePath: "ButtonHovered.png", Border: 12)), + Pressed: (Background: (TexturePath: "ButtonPressed.png", Border: 12)), + Disabled: (Background: (TexturePath: "ButtonDisabled.png", Border: 12)), + Sounds: @ButtonSounds +); + +Button { + Style: @MyButtonStyle; +} +``` + +### Propriétés d'État Button + +| Propriété | Type | Description | +|-----------|------|-------------| +| `Background` | PatchStyle | Apparence du fond | + +## TextButtonStyle + +Style pour les widgets TextButton. + +``` +@MyTextButtonStyle = TextButtonStyle( + Default: ( + Background: (TexturePath: "Button.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #bfcdd5, RenderBold: true) + ), + Hovered: ( + Background: (TexturePath: "ButtonHovered.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #ffffff, RenderBold: true) + ), + Pressed: ( + Background: (TexturePath: "ButtonPressed.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #aaaaaa, RenderBold: true) + ), + Disabled: ( + Background: (TexturePath: "ButtonDisabled.png", Border: 12), + LabelStyle: (FontSize: 17, TextColor: #797b7c, RenderBold: true) + ), + Sounds: @ButtonSounds +); +``` + +### Propriétés d'État TextButton + +| Propriété | Type | Description | +|-----------|------|-------------| +| `Background` | PatchStyle | Apparence du fond | +| `LabelStyle` | LabelStyle | Style du texte | + +## PatchStyle + +Configuration de texture 9-patch pour les fonds étirables. + +``` +@MyPatchStyle = PatchStyle( + TexturePath: "Panel.png", + Border: 16 // Bordure uniforme +); + +@MyPatchStyle2 = PatchStyle( + TexturePath: "Button.png", + HorizontalBorder: 80, // Bordure gauche/droite + VerticalBorder: 12 // Bordure haut/bas +); + +// Avec teinte de couleur +@ColoredPatch = PatchStyle( + Color: #ff0000(0.5) // Rouge semi-transparent +); +``` + +### Propriétés PatchStyle + +| Propriété | Type | Description | +|-----------|------|-------------| +| `TexturePath` | chaîne | Chemin vers le fichier texture | +| `Border` | nombre | Bordure 9-patch uniforme | +| `HorizontalBorder` | nombre | Bordure 9-patch gauche/droite | +| `VerticalBorder` | nombre | Bordure 9-patch haut/bas | +| `Color` | couleur | Couleur unie (au lieu de texture) | + +## InputFieldStyle + +Style pour les champs de saisie texte. + +``` +@MyInputStyle = InputFieldStyle( + TextColor: #ffffff, + FontSize: 16 +); + +@MyPlaceholderStyle = InputFieldStyle( + TextColor: #6e7da1 +); + +TextField { + Style: @MyInputStyle; + PlaceholderStyle: @MyPlaceholderStyle; +} +``` + +## SliderStyle + +Style pour les contrôles curseur. + +``` +@MySliderStyle = SliderStyle( + Background: (TexturePath: "SliderBackground.png", Border: 2), + Handle: "SliderHandle.png", + HandleWidth: 16, + HandleHeight: 16, + Sounds: ( + Activate: (SoundPath: "SliderTick.ogg", Volume: 10), + MouseHover: (SoundPath: "Hover.ogg", Volume: 6) + ) +); +``` + +### Propriétés SliderStyle + +| Propriété | Type | Description | +|-----------|------|-------------| +| `Background` | PatchStyle | Fond de la piste | +| `Handle` | chaîne | Chemin texture de la poignée | +| `HandleWidth` | nombre | Largeur de la poignée | +| `HandleHeight` | nombre | Hauteur de la poignée | +| `Sounds` | objet | Effets sonores | + +## CheckBoxStyle + +Style pour les cases à cocher. + +``` +@MyCheckBoxStyle = CheckBoxStyle( + Unchecked: ( + DefaultBackground: (Color: #00000000), + HoveredBackground: (Color: #ffffff20), + PressedBackground: (Color: #ffffff10), + DisabledBackground: (Color: #424242), + ChangedSound: (SoundPath: "Untick.ogg", Volume: 6) + ), + Checked: ( + DefaultBackground: (TexturePath: "Checkmark.png"), + HoveredBackground: (TexturePath: "Checkmark.png"), + PressedBackground: (TexturePath: "Checkmark.png"), + ChangedSound: (SoundPath: "Tick.ogg", Volume: 6) + ) +); +``` + +## DropdownBoxStyle + +Style pour les menus déroulants. + +``` +@MyDropdownStyle = DropdownBoxStyle( + DefaultBackground: (TexturePath: "Dropdown.png", Border: 16), + HoveredBackground: (TexturePath: "DropdownHovered.png", Border: 16), + PressedBackground: (TexturePath: "DropdownPressed.png", Border: 16), + DefaultArrowTexturePath: "DropdownCaret.png", + HoveredArrowTexturePath: "DropdownCaret.png", + PressedArrowTexturePath: "DropdownCaretPressed.png", + ArrowWidth: 13, + ArrowHeight: 18, + LabelStyle: (TextColor: #96a9be, FontSize: 13), + EntryLabelStyle: (TextColor: #b7cedd), + SelectedEntryLabelStyle: (TextColor: #b7cedd, RenderBold: true), + HorizontalPadding: 8, + PanelBackground: (TexturePath: "DropdownBox.png", Border: 16), + PanelPadding: 6, + PanelAlign: Right, // ou Bottom + PanelOffset: 7, + EntryHeight: 31, + EntriesInViewport: 10, + HoveredEntryBackground: (Color: #0a0f17), + PressedEntryBackground: (Color: #0f1621), + Sounds: @DropdownBoxSounds, + EntrySounds: @ButtonSounds +); +``` + +## ScrollbarStyle + +Style pour les barres de défilement. + +``` +@MyScrollbarStyle = ScrollbarStyle( + Spacing: 6, + Size: 6, + Background: (TexturePath: "Scrollbar.png", Border: 3), + Handle: (TexturePath: "ScrollbarHandle.png", Border: 3), + HoveredHandle: (TexturePath: "ScrollbarHandleHovered.png", Border: 3), + DraggedHandle: (TexturePath: "ScrollbarHandleDragged.png", Border: 3), + OnlyVisibleWhenHovered: false +); +``` + +## TabNavigationStyle + +Style pour la navigation par onglets. + +``` +@TabStyle = TabStyleState( + Background: "Tab.png", + Overlay: "TabOverlay.png", + IconAnchor: (Width: 44, Height: 44), + Anchor: (Width: 82, Height: 62) +); + +@MyTabsStyle = TabNavigationStyle( + TabStyle: ( + Default: @TabStyle, + Hovered: (...@TabStyle, Background: "TabHovered.png"), + Pressed: (...@TabStyle, Background: "TabPressed.png") + ), + SelectedTabStyle: ( + Default: ( + ...@TabStyle, + Overlay: "TabSelectedOverlay.png" + ) + ), + TabSounds: @TabSounds, + SeparatorAnchor: (Width: 5, Height: 34), + SeparatorBackground: "TabSeparator.png" +); +``` + +## TextTooltipStyle + +Style pour les infobulles texte. + +``` +@MyTooltipStyle = TextTooltipStyle( + Background: (TexturePath: "TooltipBackground.png", Border: 24), + MaxWidth: 400, + LabelStyle: (Wrap: true, FontSize: 16), + Padding: 24 +); +``` + +## Définitions de Sons + +Les sons sont définis comme des objets avec des propriétés audio. + +``` +@ButtonSounds = ( + Activate: ( + SoundPath: "Sounds/ButtonActivate.ogg", + Volume: 6, + MinPitch: -0.2, + MaxPitch: 0.2 + ), + MouseHover: ( + SoundPath: "Sounds/ButtonHover.ogg", + Volume: 6 + ) +); + +@DropdownBoxSounds = DropdownBoxSounds( + Activate: (SoundPath: "Tick.ogg", Volume: 6), + MouseHover: (SoundPath: "Hover.ogg", Volume: 6), + Close: (SoundPath: "Untick.ogg", Volume: 6) +); +``` + +### Propriétés de Son + +| Propriété | Type | Description | +|-----------|------|-------------| +| `SoundPath` | chaîne | Chemin vers le fichier audio | +| `Volume` | nombre | Niveau de volume (peut être négatif) | +| `MinPitch` | nombre | Variation de hauteur minimale | +| `MaxPitch` | nombre | Variation de hauteur maximale | + +## Héritage de Styles + +Utilisez l'opérateur spread pour étendre les styles : + +``` +@BaseButtonStyle = TextButtonStyle( + Default: (Background: @DefaultBg, LabelStyle: @DefaultLabel), + Hovered: (Background: @HoveredBg, LabelStyle: @DefaultLabel), + Pressed: (Background: @PressedBg, LabelStyle: @DefaultLabel), + Sounds: @ButtonSounds +); + +@DestructiveButtonStyle = TextButtonStyle( + ...@BaseButtonStyle, + Default: (...@BaseButtonStyle.Default, Background: @DestructiveBg), + Sounds: @DestructiveSounds +); +``` diff --git a/content/ui-systems/ui-syntax.en.md b/content/ui-systems/ui-syntax.en.md new file mode 100644 index 0000000..93216c4 --- /dev/null +++ b/content/ui-systems/ui-syntax.en.md @@ -0,0 +1,331 @@ +--- +title: UI Syntax +type: docs +weight: 11 +--- + +This page covers the complete syntax of Hytale's UI markup language. + +## Comments + +``` +// Single line comment + +/* Multi-line + comment */ +``` + +## Variables + +### Local Variables + +Define variables with `@` prefix: + +``` +@MyNumber = 50; +@MyString = "Hello"; +@MyColor = #ff5500; +@MyObject = (Key: value, Other: 123); +``` + +### Variable Types + +| Type | Example | +|------|---------| +| Number | `@Size = 50;` | +| String | `@Text = "Hello";` | +| Color | `@Color = #ff5500;` | +| Object | `@Obj = (Width: 100, Height: 50);` | +| Array | `@List = [item1, item2, item3];` | +| Boolean | `@Visible = true;` | + +### Using Variables + +Reference variables with `@`: + +``` +Group { + Anchor: (Height: @MyNumber); + Background: (Color: @MyColor); +} +``` + +## Imports + +### Import Files + +Use `$` prefix to import external files: + +``` +$Common = "Common.ui"; +$Sounds = "../Sounds.ui"; +$ClientCommon = "../../Common.ui"; +``` + +### Reference Imported Variables + +Access imported variables with dot notation: + +``` +$Common.@DefaultButtonStyle +$Sounds.@ButtonsLight +``` + +### Use Imported Templates + +Instantiate templates from imports: + +``` +$Common.@TextButton { + @Text = "Click Me"; +} +``` + +## Colors + +### Hexadecimal + +``` +@Color1 = #ff5500; // RGB +@Color2 = #ff5500ff; // RGBA +``` + +### With Opacity + +``` +@Color3 = #ff5500(0.5); // 50% opacity +@Color4 = #000000(0.8); // 80% opacity +``` + +## Objects + +### Basic Object + +``` +@Style = ( + FontSize: 16, + TextColor: #ffffff, + RenderBold: true +); +``` + +### Nested Objects + +``` +@ButtonStyle = ( + Default: ( + Background: (TexturePath: "Button.png", Border: 12) + ), + Hovered: ( + Background: (TexturePath: "ButtonHovered.png", Border: 12) + ) +); +``` + +## Spread Operator + +Use `...` to extend/merge objects: + +``` +@BaseStyle = ( + FontSize: 16, + TextColor: #ffffff +); + +@ExtendedStyle = ( + ...@BaseStyle, // Inherit all properties from @BaseStyle + RenderBold: true // Add/override properties +); +``` + +Spread works in any object context: + +``` +Style: ( + ...$Common.@DefaultButtonStyle, + Sounds: ( + ...$Sounds.@ButtonsLight, + Activate: (Volume: 10) + ) +); +``` + +## Templates (Macros) + +### Define a Template + +Templates are reusable widget definitions with parameters: + +``` +@MyButton = TextButton { + @Text = ""; // Parameter with default + @Anchor = Anchor(); // Parameter with default + @Sounds = (); // Empty default + + Style: @DefaultTextButtonStyle; + Anchor: (...@Anchor, Height: 44); + Text: @Text; +}; +``` + +### Instantiate a Template + +``` +// Use with defaults +@MyButton {} + +// Override parameters +@MyButton { + @Text = "Submit"; + @Anchor = (Width: 200); +} + +// From import +$Common.@TextButton { + @Text = %ui.button.confirm; +} +``` + +## Widget Declarations + +### Basic Widget + +``` +WidgetType { + Property: value; +} +``` + +### Widget with ID + +IDs start with `#` and allow targeting from Java. IDs must be in **UpperCamelCase** format (no hyphens or underscores): + +``` +Label #PlayerName { + Text: "Unknown"; +} + +// Valid IDs +#MainPanel +#ConfirmButton +#PlayerHealthBar + +// Invalid IDs (will cause errors) +#main-panel // No hyphens allowed +#confirm_button // No underscores allowed +#playerhealthbar // Should be UpperCamelCase +``` + +### Nested Widgets + +``` +Group #Container { + Label #Title { + Text: "Header"; + } + + Group #Content { + Label { + Text: "Body text"; + } + } +} +``` + +### Anonymous Widgets + +Widgets without ID are anonymous: + +``` +Group { + Label { Text: "No ID"; } +} +``` + +## Selectors + +Selectors are strings starting with `#` used to target elements: + +```java +// In Java code +builder.set("#PlayerName", "Steve"); +builder.append("#Container", "ui/child_template"); +builder.clear("#Content"); +``` + +## Translation Keys + +Use `%` prefix for localized strings: + +``` +Label { + Text: %server.ui.shop.title; +} + +TextButton { + Text: %client.button.confirm; +} +``` + +## Typed Constructors + +Some complex types use function-style constructors: + +``` +// Style constructors +@Style = LabelStyle(FontSize: 16, TextColor: #fff); +@BtnStyle = ButtonStyle(Default: (...), Hovered: (...)); + +// Layout constructors +@Pad = Padding(Full: 10); +@Anch = Anchor(Width: 100, Height: 50); + +// Visual constructors +@Bg = PatchStyle(TexturePath: "bg.png", Border: 12); +@Scroll = ScrollbarStyle(Size: 6, Spacing: 8); +``` + +## Complete Example + +``` +$Common = "Common.ui"; +$Sounds = "Sounds.ui"; + +@PanelWidth = 400; +@PanelHeight = 300; +@TitleColor = #b4c8c9; + +@PanelTitle = Label { + @Text = ""; + + Style: ( + FontSize: 20, + TextColor: @TitleColor, + RenderBold: true, + HorizontalAlignment: Center + ); + Text: @Text; +}; + +Group #MainPanel { + Anchor: (Width: @PanelWidth, Height: @PanelHeight); + Background: (TexturePath: "Panel.png", Border: 20); + LayoutMode: Top; + Padding: (Full: 16); + + @PanelTitle { + @Text = %ui.panel.title; + } + + Group #Content { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $Common.@DefaultScrollbarStyle; + } + + $Common.@TextButton #ConfirmButton { + @Text = %ui.button.confirm; + @Sounds = $Sounds.@ButtonsLight; + } +} +``` diff --git a/content/ui-systems/ui-syntax.fr.md b/content/ui-systems/ui-syntax.fr.md new file mode 100644 index 0000000..567ca03 --- /dev/null +++ b/content/ui-systems/ui-syntax.fr.md @@ -0,0 +1,331 @@ +--- +title: Syntaxe UI +type: docs +weight: 11 +--- + +Cette page couvre la syntaxe complète du langage de balisage UI de Hytale. + +## Commentaires + +``` +// Commentaire sur une ligne + +/* Commentaire + multi-lignes */ +``` + +## Variables + +### Variables Locales + +Définissez les variables avec le préfixe `@` : + +``` +@MyNumber = 50; +@MyString = "Bonjour"; +@MyColor = #ff5500; +@MyObject = (Key: value, Other: 123); +``` + +### Types de Variables + +| Type | Exemple | +|------|---------| +| Nombre | `@Size = 50;` | +| Chaîne | `@Text = "Bonjour";` | +| Couleur | `@Color = #ff5500;` | +| Objet | `@Obj = (Width: 100, Height: 50);` | +| Tableau | `@List = [item1, item2, item3];` | +| Booléen | `@Visible = true;` | + +### Utiliser les Variables + +Référencez les variables avec `@` : + +``` +Group { + Anchor: (Height: @MyNumber); + Background: (Color: @MyColor); +} +``` + +## Imports + +### Importer des Fichiers + +Utilisez le préfixe `$` pour importer des fichiers externes : + +``` +$Common = "Common.ui"; +$Sounds = "../Sounds.ui"; +$ClientCommon = "../../Common.ui"; +``` + +### Référencer les Variables Importées + +Accédez aux variables importées avec la notation pointée : + +``` +$Common.@DefaultButtonStyle +$Sounds.@ButtonsLight +``` + +### Utiliser les Templates Importés + +Instanciez les templates depuis les imports : + +``` +$Common.@TextButton { + @Text = "Cliquez-moi"; +} +``` + +## Couleurs + +### Hexadécimal + +``` +@Color1 = #ff5500; // RGB +@Color2 = #ff5500ff; // RGBA +``` + +### Avec Opacité + +``` +@Color3 = #ff5500(0.5); // 50% d'opacité +@Color4 = #000000(0.8); // 80% d'opacité +``` + +## Objets + +### Objet Simple + +``` +@Style = ( + FontSize: 16, + TextColor: #ffffff, + RenderBold: true +); +``` + +### Objets Imbriqués + +``` +@ButtonStyle = ( + Default: ( + Background: (TexturePath: "Button.png", Border: 12) + ), + Hovered: ( + Background: (TexturePath: "ButtonHovered.png", Border: 12) + ) +); +``` + +## Opérateur Spread + +Utilisez `...` pour étendre/fusionner des objets : + +``` +@BaseStyle = ( + FontSize: 16, + TextColor: #ffffff +); + +@ExtendedStyle = ( + ...@BaseStyle, // Hérite de toutes les propriétés de @BaseStyle + RenderBold: true // Ajoute/remplace des propriétés +); +``` + +Le spread fonctionne dans tout contexte d'objet : + +``` +Style: ( + ...$Common.@DefaultButtonStyle, + Sounds: ( + ...$Sounds.@ButtonsLight, + Activate: (Volume: 10) + ) +); +``` + +## Templates (Macros) + +### Définir un Template + +Les templates sont des définitions de widgets réutilisables avec paramètres : + +``` +@MyButton = TextButton { + @Text = ""; // Paramètre avec défaut + @Anchor = Anchor(); // Paramètre avec défaut + @Sounds = (); // Défaut vide + + Style: @DefaultTextButtonStyle; + Anchor: (...@Anchor, Height: 44); + Text: @Text; +}; +``` + +### Instancier un Template + +``` +// Utiliser avec les défauts +@MyButton {} + +// Remplacer les paramètres +@MyButton { + @Text = "Valider"; + @Anchor = (Width: 200); +} + +// Depuis un import +$Common.@TextButton { + @Text = %ui.button.confirm; +} +``` + +## Déclarations de Widgets + +### Widget Simple + +``` +WidgetType { + Property: value; +} +``` + +### Widget avec ID + +Les IDs commencent par `#` et permettent le ciblage depuis Java. Les IDs doivent être en format **UpperCamelCase** (pas de tirets ni underscores) : + +``` +Label #PlayerName { + Text: "Inconnu"; +} + +// IDs valides +#MainPanel +#ConfirmButton +#PlayerHealthBar + +// IDs invalides (causeront des erreurs) +#main-panel // Pas de tirets +#confirm_button // Pas d'underscores +#playerhealthbar // Doit être en UpperCamelCase +``` + +### Widgets Imbriqués + +``` +Group #Container { + Label #Title { + Text: "En-tête"; + } + + Group #Content { + Label { + Text: "Corps du texte"; + } + } +} +``` + +### Widgets Anonymes + +Les widgets sans ID sont anonymes : + +``` +Group { + Label { Text: "Pas d'ID"; } +} +``` + +## Sélecteurs + +Les sélecteurs sont des chaînes commençant par `#` utilisées pour cibler des éléments : + +```java +// Dans le code Java +builder.set("#PlayerName", "Steve"); +builder.append("#Container", "ui/child_template"); +builder.clear("#Content"); +``` + +## Clés de Traduction + +Utilisez le préfixe `%` pour les chaînes localisées : + +``` +Label { + Text: %server.ui.shop.title; +} + +TextButton { + Text: %client.button.confirm; +} +``` + +## Constructeurs Typés + +Certains types complexes utilisent des constructeurs de style fonction : + +``` +// Constructeurs de styles +@Style = LabelStyle(FontSize: 16, TextColor: #fff); +@BtnStyle = ButtonStyle(Default: (...), Hovered: (...)); + +// Constructeurs de layout +@Pad = Padding(Full: 10); +@Anch = Anchor(Width: 100, Height: 50); + +// Constructeurs visuels +@Bg = PatchStyle(TexturePath: "bg.png", Border: 12); +@Scroll = ScrollbarStyle(Size: 6, Spacing: 8); +``` + +## Exemple Complet + +``` +$Common = "Common.ui"; +$Sounds = "Sounds.ui"; + +@PanelWidth = 400; +@PanelHeight = 300; +@TitleColor = #b4c8c9; + +@PanelTitle = Label { + @Text = ""; + + Style: ( + FontSize: 20, + TextColor: @TitleColor, + RenderBold: true, + HorizontalAlignment: Center + ); + Text: @Text; +}; + +Group #MainPanel { + Anchor: (Width: @PanelWidth, Height: @PanelHeight); + Background: (TexturePath: "Panel.png", Border: 20); + LayoutMode: Top; + Padding: (Full: 16); + + @PanelTitle { + @Text = %ui.panel.title; + } + + Group #Content { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $Common.@DefaultScrollbarStyle; + } + + $Common.@TextButton #ConfirmButton { + @Text = %ui.button.confirm; + @Sounds = $Sounds.@ButtonsLight; + } +} +``` diff --git a/content/ui-systems/ui-widgets.en.md b/content/ui-systems/ui-widgets.en.md new file mode 100644 index 0000000..48fb8af --- /dev/null +++ b/content/ui-systems/ui-widgets.en.md @@ -0,0 +1,393 @@ +--- +title: UI Widgets +type: docs +weight: 12 +--- + +This page documents all available widget types in Hytale's UI system. + +## Container Widgets + +### Group + +Basic container for organizing other widgets. + +``` +Group #Container { + Anchor: (Width: 400, Height: 300); + Background: (Color: #000000(0.5)); + LayoutMode: Top; + Padding: (Full: 16); + + // Child widgets... +} +``` + +**Key Properties:** +- `Anchor` - Position and size +- `Background` - Background color or texture +- `LayoutMode` - Child arrangement mode +- `Padding` - Inner spacing +- `Visible` - Show/hide +- `FlexWeight` - Flexible sizing weight + +## Text Widgets + +### Label + +Displays static or dynamic text. + +``` +Label #Title { + Text: "Hello World"; + Style: ( + FontSize: 24, + TextColor: #ffffff, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center + ); + Anchor: (Height: 40); +} +``` + +**Key Properties:** +- `Text` - Text content (string or `%translation.key`) +- `Style` - LabelStyle object +- `Anchor` - Position and size +- `Padding` - Text padding + +## Button Widgets + +### Button + +Clickable button without text label. + +``` +Button #CloseButton { + Anchor: (Width: 32, Height: 32); + Style: ( + Default: (Background: "CloseButton.png"), + Hovered: (Background: "CloseButtonHovered.png"), + Pressed: (Background: "CloseButtonPressed.png"), + Disabled: (Background: "CloseButtonDisabled.png"), + Sounds: @ButtonSounds + ); +} +``` + +### TextButton + +Button with text label. + +``` +TextButton #SubmitButton { + Text: %ui.button.submit; + Style: ( + Default: ( + Background: (TexturePath: "Button.png", Border: 12), + LabelStyle: (FontSize: 16, TextColor: #ffffff) + ), + Hovered: ( + Background: (TexturePath: "ButtonHovered.png", Border: 12), + LabelStyle: (FontSize: 16, TextColor: #ffffff) + ), + Pressed: ( + Background: (TexturePath: "ButtonPressed.png", Border: 12), + LabelStyle: (FontSize: 16, TextColor: #cccccc) + ), + Sounds: @ButtonSounds + ); + Anchor: (Height: 44); + Padding: (Horizontal: 24); +} +``` + +### BackButton + +Pre-styled back/dismiss button. + +``` +BackButton {} +``` + +### ActionButton + +Button that displays an action name with input binding. + +``` +ActionButton #BindingInventory { + Disabled: true; + LayoutMode: Right; + Alignment: Right; + ActionName: %client.hud.inputBindings.inventory; + Anchor: (Top: 15); +} +``` + +## Input Widgets + +### TextField + +Single-line text input. + +``` +TextField #NameInput { + Anchor: (Height: 38); + Style: @DefaultInputFieldStyle; + PlaceholderStyle: @DefaultInputFieldPlaceholderStyle; + Background: (TexturePath: "InputBox.png", Border: 16); + Padding: (Horizontal: 10); + PlaceholderText: %ui.placeholder.enterName; +} +``` + +### NumberField + +Numeric input field. + +``` +NumberField #QuantityInput { + Anchor: (Width: 100, Height: 38); + Style: @DefaultInputFieldStyle; + Background: (TexturePath: "InputBox.png", Border: 16); + Padding: (Horizontal: 10); + Min: 1; + Max: 100; +} +``` + +### CompactTextField + +Expandable search-style text field. + +``` +CompactTextField #SearchInput { + Anchor: (Height: 30); + CollapsedWidth: 34; + ExpandedWidth: 200; + PlaceholderText: %ui.search.placeholder; + Style: (FontSize: 16); + PlaceholderStyle: (TextColor: #3d5a85, RenderUppercase: true, FontSize: 14); + Padding: (Horizontal: 12, Left: 34); + Decoration: ( + Default: ( + Icon: (Texture: "SearchIcon.png", Width: 16, Height: 16, Offset: 9), + ClearButtonStyle: @ClearButtonStyle + ) + ); + ExpandSound: (SoundPath: "Sounds/Expand.ogg", Volume: 6); + CollapseSound: (SoundPath: "Sounds/Collapse.ogg", Volume: 5); +} +``` + +### Slider + +Horizontal slider control. + +``` +Slider #VolumeSlider { + Anchor: (Width: 200, Height: 16); + Style: ( + Background: (TexturePath: "SliderBackground.png", Border: 2), + Handle: "SliderHandle.png", + HandleWidth: 16, + HandleHeight: 16, + Sounds: ( + Activate: (SoundPath: "Tick.ogg", Volume: 10), + MouseHover: (SoundPath: "Hover.ogg", Volume: 6) + ) + ); + Min: 0; + Max: 100; + Value: 50; +} +``` + +### SliderNumberField + +Combined slider with numeric display. + +``` +SliderNumberField #BrightnessSlider { + Anchor: (Width: 270, Height: 5); + SliderStyle: @DefaultSliderStyle; + NumberFieldStyle: @DefaultInputFieldStyle; +} +``` + +### CheckBox + +Toggle checkbox. + +``` +CheckBox #EnableOption { + Anchor: (Width: 22, Height: 22); + Background: (TexturePath: "CheckBoxFrame.png", Border: 7); + Padding: (Full: 4); + Style: ( + Unchecked: ( + DefaultBackground: (Color: #00000000), + HoveredBackground: (Color: #00000000), + ChangedSound: (SoundPath: "Untick.ogg", Volume: 6) + ), + Checked: ( + DefaultBackground: (TexturePath: "Checkmark.png"), + HoveredBackground: (TexturePath: "Checkmark.png"), + ChangedSound: (SoundPath: "Tick.ogg", Volume: 6) + ) + ); + Value: false; +} +``` + +### DropdownBox + +Dropdown selection menu. + +``` +DropdownBox #LanguageSelector { + Anchor: (Width: 330, Height: 32); + Style: ( + DefaultBackground: (TexturePath: "Dropdown.png", Border: 16), + HoveredBackground: (TexturePath: "DropdownHovered.png", Border: 16), + PressedBackground: (TexturePath: "DropdownPressed.png", Border: 16), + DefaultArrowTexturePath: "DropdownCaret.png", + ArrowWidth: 13, + ArrowHeight: 18, + LabelStyle: (TextColor: #96a9be, FontSize: 13), + PanelBackground: (TexturePath: "DropdownBox.png", Border: 16), + EntryHeight: 31, + EntriesInViewport: 10, + Sounds: @DropdownBoxSounds + ); +} +``` + +## Display Widgets + +### Sprite + +Animated or static image. + +``` +Sprite #LoadingSpinner { + Anchor: (Width: 32, Height: 32); + TexturePath: "Spinner.png"; + Frame: (Width: 32, Height: 32, PerRow: 8, Count: 72); + FramesPerSecond: 30; +} +``` + +### AssetImage + +Display an asset image (item icon, etc.). + +``` +AssetImage #ItemIcon { + Anchor: (Width: 64, Height: 64); +} +``` + +### ProgressBar + +Progress indicator bar. + +``` +ProgressBar #HealthBar { + Anchor: (Width: 200, Height: 20); + BarTexturePath: "HealthBarFill.png"; + Alignment: Horizontal; + Direction: Start; + Value: 0.75; +} +``` + +**Properties:** +- `Alignment` - `Horizontal` or `Vertical` +- `Direction` - `Start` or `End` +- `Value` - 0.0 to 1.0 + +## Inventory Widgets + +### ItemGrid + +Grid display for inventory items. + +``` +ItemGrid #HotbarGrid { + SlotsPerRow: 9; + Style: ( + SlotBackground: "ItemSlot.png", + SlotSize: 64, + SlotSpacing: 4, + HoveredSlotBackground: "ItemSlotHovered.png", + SelectedSlotBackground: "ItemSlotSelected.png" + ); + Background: (TexturePath: "HotbarBackground.png", Border: 10); + Padding: 4; + RenderItemQualityBackground: true; + AreItemsDraggable: true; +} +``` + +## Navigation Widgets + +### TabNavigation + +Tab-based navigation. + +``` +TabNavigation #CategoryTabs { + Style: ( + TabStyle: ( + Default: (Background: "Tab.png", IconAnchor: (Width: 44, Height: 44)), + Hovered: (Background: "TabHovered.png"), + Pressed: (Background: "TabPressed.png") + ), + SelectedTabStyle: ( + Default: (Background: "TabSelected.png", Overlay: "TabSelectedOverlay.png") + ), + TabSounds: @TabSounds + ); +} +``` + +## Tooltip Properties + +Many widgets support tooltips: + +``` +TextButton #InfoButton { + TooltipText: %ui.tooltip.info; + TextTooltipStyle: ( + Background: (TexturePath: "TooltipBackground.png", Border: 24), + MaxWidth: 400, + LabelStyle: (Wrap: true, FontSize: 16), + Padding: 24 + ); +} +``` + +## Widget Summary Table + +| Widget | Purpose | +|--------|---------| +| `Group` | Container for layout | +| `Label` | Text display | +| `Button` | Clickable button | +| `TextButton` | Button with text | +| `BackButton` | Pre-styled back button | +| `ActionButton` | Action with keybind display | +| `TextField` | Text input | +| `NumberField` | Numeric input | +| `CompactTextField` | Expandable search field | +| `Slider` | Slider control | +| `SliderNumberField` | Slider with number display | +| `CheckBox` | Toggle checkbox | +| `DropdownBox` | Selection dropdown | +| `Sprite` | Image/animation | +| `AssetImage` | Asset icon display | +| `ProgressBar` | Progress indicator | +| `ItemGrid` | Inventory grid | +| `TabNavigation` | Tab navigation | diff --git a/content/ui-systems/ui-widgets.fr.md b/content/ui-systems/ui-widgets.fr.md new file mode 100644 index 0000000..8393527 --- /dev/null +++ b/content/ui-systems/ui-widgets.fr.md @@ -0,0 +1,393 @@ +--- +title: Widgets UI +type: docs +weight: 12 +--- + +Cette page documente tous les types de widgets disponibles dans le système UI de Hytale. + +## Widgets Conteneurs + +### Group + +Conteneur de base pour organiser d'autres widgets. + +``` +Group #Container { + Anchor: (Width: 400, Height: 300); + Background: (Color: #000000(0.5)); + LayoutMode: Top; + Padding: (Full: 16); + + // Widgets enfants... +} +``` + +**Propriétés Clés :** +- `Anchor` - Position et taille +- `Background` - Couleur ou texture de fond +- `LayoutMode` - Mode d'arrangement des enfants +- `Padding` - Espacement intérieur +- `Visible` - Afficher/masquer +- `FlexWeight` - Poids de dimensionnement flexible + +## Widgets Texte + +### Label + +Affiche du texte statique ou dynamique. + +``` +Label #Title { + Text: "Bonjour le monde"; + Style: ( + FontSize: 24, + TextColor: #ffffff, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center + ); + Anchor: (Height: 40); +} +``` + +**Propriétés Clés :** +- `Text` - Contenu texte (chaîne ou `%translation.key`) +- `Style` - Objet LabelStyle +- `Anchor` - Position et taille +- `Padding` - Marge du texte + +## Widgets Boutons + +### Button + +Bouton cliquable sans label texte. + +``` +Button #CloseButton { + Anchor: (Width: 32, Height: 32); + Style: ( + Default: (Background: "CloseButton.png"), + Hovered: (Background: "CloseButtonHovered.png"), + Pressed: (Background: "CloseButtonPressed.png"), + Disabled: (Background: "CloseButtonDisabled.png"), + Sounds: @ButtonSounds + ); +} +``` + +### TextButton + +Bouton avec label texte. + +``` +TextButton #SubmitButton { + Text: %ui.button.submit; + Style: ( + Default: ( + Background: (TexturePath: "Button.png", Border: 12), + LabelStyle: (FontSize: 16, TextColor: #ffffff) + ), + Hovered: ( + Background: (TexturePath: "ButtonHovered.png", Border: 12), + LabelStyle: (FontSize: 16, TextColor: #ffffff) + ), + Pressed: ( + Background: (TexturePath: "ButtonPressed.png", Border: 12), + LabelStyle: (FontSize: 16, TextColor: #cccccc) + ), + Sounds: @ButtonSounds + ); + Anchor: (Height: 44); + Padding: (Horizontal: 24); +} +``` + +### BackButton + +Bouton retour/fermer pré-stylisé. + +``` +BackButton {} +``` + +### ActionButton + +Bouton qui affiche un nom d'action avec sa touche associée. + +``` +ActionButton #BindingInventory { + Disabled: true; + LayoutMode: Right; + Alignment: Right; + ActionName: %client.hud.inputBindings.inventory; + Anchor: (Top: 15); +} +``` + +## Widgets de Saisie + +### TextField + +Champ de saisie texte mono-ligne. + +``` +TextField #NameInput { + Anchor: (Height: 38); + Style: @DefaultInputFieldStyle; + PlaceholderStyle: @DefaultInputFieldPlaceholderStyle; + Background: (TexturePath: "InputBox.png", Border: 16); + Padding: (Horizontal: 10); + PlaceholderText: %ui.placeholder.enterName; +} +``` + +### NumberField + +Champ de saisie numérique. + +``` +NumberField #QuantityInput { + Anchor: (Width: 100, Height: 38); + Style: @DefaultInputFieldStyle; + Background: (TexturePath: "InputBox.png", Border: 16); + Padding: (Horizontal: 10); + Min: 1; + Max: 100; +} +``` + +### CompactTextField + +Champ de texte extensible style recherche. + +``` +CompactTextField #SearchInput { + Anchor: (Height: 30); + CollapsedWidth: 34; + ExpandedWidth: 200; + PlaceholderText: %ui.search.placeholder; + Style: (FontSize: 16); + PlaceholderStyle: (TextColor: #3d5a85, RenderUppercase: true, FontSize: 14); + Padding: (Horizontal: 12, Left: 34); + Decoration: ( + Default: ( + Icon: (Texture: "SearchIcon.png", Width: 16, Height: 16, Offset: 9), + ClearButtonStyle: @ClearButtonStyle + ) + ); + ExpandSound: (SoundPath: "Sounds/Expand.ogg", Volume: 6); + CollapseSound: (SoundPath: "Sounds/Collapse.ogg", Volume: 5); +} +``` + +### Slider + +Contrôle curseur horizontal. + +``` +Slider #VolumeSlider { + Anchor: (Width: 200, Height: 16); + Style: ( + Background: (TexturePath: "SliderBackground.png", Border: 2), + Handle: "SliderHandle.png", + HandleWidth: 16, + HandleHeight: 16, + Sounds: ( + Activate: (SoundPath: "Tick.ogg", Volume: 10), + MouseHover: (SoundPath: "Hover.ogg", Volume: 6) + ) + ); + Min: 0; + Max: 100; + Value: 50; +} +``` + +### SliderNumberField + +Curseur combiné avec affichage numérique. + +``` +SliderNumberField #BrightnessSlider { + Anchor: (Width: 270, Height: 5); + SliderStyle: @DefaultSliderStyle; + NumberFieldStyle: @DefaultInputFieldStyle; +} +``` + +### CheckBox + +Case à cocher. + +``` +CheckBox #EnableOption { + Anchor: (Width: 22, Height: 22); + Background: (TexturePath: "CheckBoxFrame.png", Border: 7); + Padding: (Full: 4); + Style: ( + Unchecked: ( + DefaultBackground: (Color: #00000000), + HoveredBackground: (Color: #00000000), + ChangedSound: (SoundPath: "Untick.ogg", Volume: 6) + ), + Checked: ( + DefaultBackground: (TexturePath: "Checkmark.png"), + HoveredBackground: (TexturePath: "Checkmark.png"), + ChangedSound: (SoundPath: "Tick.ogg", Volume: 6) + ) + ); + Value: false; +} +``` + +### DropdownBox + +Menu de sélection déroulant. + +``` +DropdownBox #LanguageSelector { + Anchor: (Width: 330, Height: 32); + Style: ( + DefaultBackground: (TexturePath: "Dropdown.png", Border: 16), + HoveredBackground: (TexturePath: "DropdownHovered.png", Border: 16), + PressedBackground: (TexturePath: "DropdownPressed.png", Border: 16), + DefaultArrowTexturePath: "DropdownCaret.png", + ArrowWidth: 13, + ArrowHeight: 18, + LabelStyle: (TextColor: #96a9be, FontSize: 13), + PanelBackground: (TexturePath: "DropdownBox.png", Border: 16), + EntryHeight: 31, + EntriesInViewport: 10, + Sounds: @DropdownBoxSounds + ); +} +``` + +## Widgets d'Affichage + +### Sprite + +Image animée ou statique. + +``` +Sprite #LoadingSpinner { + Anchor: (Width: 32, Height: 32); + TexturePath: "Spinner.png"; + Frame: (Width: 32, Height: 32, PerRow: 8, Count: 72); + FramesPerSecond: 30; +} +``` + +### AssetImage + +Affiche une image d'asset (icône d'item, etc.). + +``` +AssetImage #ItemIcon { + Anchor: (Width: 64, Height: 64); +} +``` + +### ProgressBar + +Barre de progression. + +``` +ProgressBar #HealthBar { + Anchor: (Width: 200, Height: 20); + BarTexturePath: "HealthBarFill.png"; + Alignment: Horizontal; + Direction: Start; + Value: 0.75; +} +``` + +**Propriétés :** +- `Alignment` - `Horizontal` ou `Vertical` +- `Direction` - `Start` ou `End` +- `Value` - 0.0 à 1.0 + +## Widgets d'Inventaire + +### ItemGrid + +Grille d'affichage pour les items d'inventaire. + +``` +ItemGrid #HotbarGrid { + SlotsPerRow: 9; + Style: ( + SlotBackground: "ItemSlot.png", + SlotSize: 64, + SlotSpacing: 4, + HoveredSlotBackground: "ItemSlotHovered.png", + SelectedSlotBackground: "ItemSlotSelected.png" + ); + Background: (TexturePath: "HotbarBackground.png", Border: 10); + Padding: 4; + RenderItemQualityBackground: true; + AreItemsDraggable: true; +} +``` + +## Widgets de Navigation + +### TabNavigation + +Navigation par onglets. + +``` +TabNavigation #CategoryTabs { + Style: ( + TabStyle: ( + Default: (Background: "Tab.png", IconAnchor: (Width: 44, Height: 44)), + Hovered: (Background: "TabHovered.png"), + Pressed: (Background: "TabPressed.png") + ), + SelectedTabStyle: ( + Default: (Background: "TabSelected.png", Overlay: "TabSelectedOverlay.png") + ), + TabSounds: @TabSounds + ); +} +``` + +## Propriétés de Tooltip + +De nombreux widgets supportent les tooltips : + +``` +TextButton #InfoButton { + TooltipText: %ui.tooltip.info; + TextTooltipStyle: ( + Background: (TexturePath: "TooltipBackground.png", Border: 24), + MaxWidth: 400, + LabelStyle: (Wrap: true, FontSize: 16), + Padding: 24 + ); +} +``` + +## Tableau Récapitulatif des Widgets + +| Widget | Usage | +|--------|-------| +| `Group` | Conteneur pour la mise en page | +| `Label` | Affichage de texte | +| `Button` | Bouton cliquable | +| `TextButton` | Bouton avec texte | +| `BackButton` | Bouton retour pré-stylisé | +| `ActionButton` | Action avec affichage de touche | +| `TextField` | Saisie de texte | +| `NumberField` | Saisie numérique | +| `CompactTextField` | Champ de recherche extensible | +| `Slider` | Contrôle curseur | +| `SliderNumberField` | Curseur avec affichage numérique | +| `CheckBox` | Case à cocher | +| `DropdownBox` | Menu déroulant de sélection | +| `Sprite` | Image/animation | +| `AssetImage` | Affichage d'icône d'asset | +| `ProgressBar` | Indicateur de progression | +| `ItemGrid` | Grille d'inventaire | +| `TabNavigation` | Navigation par onglets | diff --git a/content/ui-systems/windows.en.md b/content/ui-systems/windows.en.md new file mode 100644 index 0000000..0379056 --- /dev/null +++ b/content/ui-systems/windows.en.md @@ -0,0 +1,174 @@ +--- +title: Windows +type: docs +weight: 3 +--- + +Windows are UI panels for displaying and managing inventory containers, crafting interfaces, and other item-based interactions. + +**Package:** `com.hypixel.hytale.server.core.entity.entities.player.windows` + +## Window Types + +| Type | Description | +|------|-------------| +| `WindowType.Container` | Standard inventory/container view | +| `WindowType.PocketCrafting` | Small crafting grid (2x2) | +| `WindowType.BasicCrafting` | Basic crafting table (3x3) | +| `WindowType.DiagramCrafting` | Diagram-based crafting | +| `WindowType.StructuralCrafting` | Structural/building crafting | +| `WindowType.Processing` | Processing/smelting interface | +| `WindowType.Memories` | Memories storage interface | + +## Window Manager + +Each player has a `WindowManager` that handles open windows: + +```java +Player player = ...; +WindowManager windowManager = player.getWindowManager(); +``` + +## Window Classes + +### ContainerWindow + +For standard container interfaces: + +```java +import com.hypixel.hytale.server.core.entity.entities.player.windows.ContainerWindow; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; + +// Open a container window - takes just the ItemContainer +ContainerWindow window = new ContainerWindow(itemContainer); +``` + +{{< callout type="info" >}} +`ContainerWindow` automatically uses `WindowType.Container`. The window ID is assigned internally. +{{< /callout >}} + +### BlockWindow + +`BlockWindow` is an abstract class for block-based containers. It's typically used through built-in interactions rather than instantiated directly. + +```java +// BlockWindow is abstract - used internally by block interactions +// Constructor: BlockWindow(WindowType, x, y, z, rotationIndex, BlockType) +``` + +### ItemStackContainerWindow + +For ItemStack-based containers: + +```java +import com.hypixel.hytale.server.core.entity.entities.player.windows.ItemStackContainerWindow; +import com.hypixel.hytale.server.core.inventory.container.ItemStackItemContainer; + +// Takes an ItemStackItemContainer +ItemStackContainerWindow window = new ItemStackContainerWindow(itemStackItemContainer); +``` + +## Opening Windows + +Use `PageManager.setPageWithWindows()` or `openCustomPageWithWindows()`: + +```java +Player player = ...; +PageManager pageManager = player.getPageManager(); +Ref ref = player.getReference(); +Store store = ref.getStore(); + +// Open page with inventory windows +ContainerWindow chestWindow = new ContainerWindow(chestContainer); + +boolean success = pageManager.setPageWithWindows( + ref, store, + Page.None, // or Page.Bench, Page.Inventory, etc. + true, // canCloseThroughInteraction + chestWindow +); +``` + +## With Custom Pages + +Combine windows with custom UI pages: + +```java +// Custom crafting UI with windows +CraftingPage craftingPage = new CraftingPage(player.getPlayerRef()); +ContainerWindow inputWindow = new ContainerWindow(1, WindowType.BasicCrafting, inputContainer); +ContainerWindow outputWindow = new ContainerWindow(2, WindowType.Container, outputContainer); + +pageManager.openCustomPageWithWindows( + ref, store, + craftingPage, + inputWindow, + outputWindow +); +``` + +## Window Events + +Windows interact with the inventory system. Handle inventory changes through the appropriate events and handlers. + +## Practical Example + +### Chest Container UI + +```java +public void openChest(Player player, ItemContainer chestContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + ContainerWindow window = new ContainerWindow(chestContainer); + + pageManager.setPageWithWindows( + ref, store, + Page.Bench, // Use Bench page for container UI + true, // Allow closing via interaction + window + ); +} +``` + +### Opening Multiple Windows + +```java +public void openCraftingUI(Player player, ItemContainer inputContainer, + ItemContainer outputContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + // Create multiple windows + ContainerWindow inputWindow = new ContainerWindow(inputContainer); + ContainerWindow outputWindow = new ContainerWindow(outputContainer); + + pageManager.setPageWithWindows( + ref, store, + Page.Bench, + true, + inputWindow, + outputWindow + ); +} +``` + +{{< callout type="info" >}} +**Note:** `BlockWindow` is abstract and used internally by block interactions. For custom crafting UIs, use `ContainerWindow` with your own containers. +{{< /callout >}} + +## Best Practices + +{{< callout type="info" >}} +**Window Guidelines:** +- Use unique window IDs for each open window +- Match `WindowType` to the intended interface +- Clean up containers when windows are closed +- Use `canCloseThroughInteraction` appropriately +{{< /callout >}} + +{{< callout type="warning" >}} +**Important:** Windows are tied to container state. Ensure containers persist for the duration the window is open. +{{< /callout >}} diff --git a/content/ui-systems/windows.fr.md b/content/ui-systems/windows.fr.md new file mode 100644 index 0000000..5577d7c --- /dev/null +++ b/content/ui-systems/windows.fr.md @@ -0,0 +1,174 @@ +--- +title: Fenêtres +type: docs +weight: 3 +--- + +Les fenêtres sont des panneaux UI pour afficher et gérer les conteneurs d'inventaire, les interfaces de crafting et autres interactions basées sur les objets. + +**Package:** `com.hypixel.hytale.server.core.entity.entities.player.windows` + +## Types de Fenêtres + +| Type | Description | +|------|-------------| +| `WindowType.Container` | Vue standard inventaire/conteneur | +| `WindowType.PocketCrafting` | Petite grille de crafting (2x2) | +| `WindowType.BasicCrafting` | Table de crafting basique (3x3) | +| `WindowType.DiagramCrafting` | Crafting basé sur des diagrammes | +| `WindowType.StructuralCrafting` | Crafting structurel/construction | +| `WindowType.Processing` | Interface de traitement/fonte | +| `WindowType.Memories` | Interface de stockage des Mémoires | + +## Gestionnaire de Fenêtres + +Chaque joueur a un `WindowManager` qui gère les fenêtres ouvertes : + +```java +Player player = ...; +WindowManager windowManager = player.getWindowManager(); +``` + +## Classes de Fenêtres + +### ContainerWindow + +Pour les interfaces de conteneur standard : + +```java +import com.hypixel.hytale.server.core.entity.entities.player.windows.ContainerWindow; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; + +// Ouvrir une fenêtre de conteneur - prend juste l'ItemContainer +ContainerWindow window = new ContainerWindow(itemContainer); +``` + +{{< callout type="info" >}} +`ContainerWindow` utilise automatiquement `WindowType.Container`. L'ID de fenêtre est assigné en interne. +{{< /callout >}} + +### BlockWindow + +`BlockWindow` est une classe abstraite pour les conteneurs liés aux blocs. Elle est typiquement utilisée via les interactions intégrées plutôt qu'instanciée directement. + +```java +// BlockWindow est abstraite - utilisée en interne par les interactions de blocs +// Constructeur: BlockWindow(WindowType, x, y, z, rotationIndex, BlockType) +``` + +### ItemStackContainerWindow + +Pour les conteneurs basés sur ItemStack : + +```java +import com.hypixel.hytale.server.core.entity.entities.player.windows.ItemStackContainerWindow; +import com.hypixel.hytale.server.core.inventory.container.ItemStackItemContainer; + +// Prend un ItemStackItemContainer +ItemStackContainerWindow window = new ItemStackContainerWindow(itemStackItemContainer); +``` + +## Ouvrir des Fenêtres + +Utilisez `PageManager.setPageWithWindows()` ou `openCustomPageWithWindows()` : + +```java +Player player = ...; +PageManager pageManager = player.getPageManager(); +Ref ref = player.getReference(); +Store store = ref.getStore(); + +// Ouvrir une page avec des fenêtres d'inventaire +ContainerWindow chestWindow = new ContainerWindow(chestContainer); + +boolean success = pageManager.setPageWithWindows( + ref, store, + Page.None, // ou Page.Bench, Page.Inventory, etc. + true, // canCloseThroughInteraction + chestWindow +); +``` + +## Avec des Pages Personnalisées + +Combiner les fenêtres avec des pages UI personnalisées : + +```java +// UI de crafting personnalisée avec fenêtres +CraftingPage craftingPage = new CraftingPage(player.getPlayerRef()); +ContainerWindow inputWindow = new ContainerWindow(inputContainer); +ContainerWindow outputWindow = new ContainerWindow(outputContainer); + +pageManager.openCustomPageWithWindows( + ref, store, + craftingPage, + inputWindow, + outputWindow +); +``` + +## Événements de Fenêtre + +Les fenêtres interagissent avec le système d'inventaire. Gérez les changements d'inventaire via les événements et handlers appropriés. + +## Exemple Pratique + +### UI de Conteneur Coffre + +```java +public void openChest(Player player, ItemContainer chestContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + ContainerWindow window = new ContainerWindow(chestContainer); + + pageManager.setPageWithWindows( + ref, store, + Page.Bench, // Utiliser Page.Bench pour l'UI de conteneur + true, // Permettre la fermeture via interaction + window + ); +} +``` + +### Ouvrir Plusieurs Fenêtres + +```java +public void openCraftingUI(Player player, ItemContainer inputContainer, + ItemContainer outputContainer) { + PageManager pageManager = player.getPageManager(); + Ref ref = player.getReference(); + Store store = ref.getStore(); + + // Créer plusieurs fenêtres + ContainerWindow inputWindow = new ContainerWindow(inputContainer); + ContainerWindow outputWindow = new ContainerWindow(outputContainer); + + pageManager.setPageWithWindows( + ref, store, + Page.Bench, + true, + inputWindow, + outputWindow + ); +} +``` + +{{< callout type="info" >}} +**Note :** `BlockWindow` est abstraite et utilisée en interne par les interactions de blocs. Pour des UI de crafting personnalisées, utilisez `ContainerWindow` avec vos propres conteneurs. +{{< /callout >}} + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Directives des Fenêtres :** +- Utilisez des IDs de fenêtre uniques pour chaque fenêtre ouverte +- Faites correspondre `WindowType` à l'interface prévue +- Nettoyez les conteneurs quand les fenêtres sont fermées +- Utilisez `canCloseThroughInteraction` de manière appropriée +{{< /callout >}} + +{{< callout type="warning" >}} +**Important :** Les fenêtres sont liées à l'état du conteneur. Assurez-vous que les conteneurs persistent pendant toute la durée d'ouverture de la fenêtre. +{{< /callout >}} diff --git a/content/world/_index.en.md b/content/world/_index.en.md new file mode 100644 index 0000000..6cbdf14 --- /dev/null +++ b/content/world/_index.en.md @@ -0,0 +1,35 @@ +--- +title: World +type: docs +weight: 4 +--- + +World management in Hytale covers universes, worlds, chunks, and blocks. + +{{< cards >}} + {{< card link="universe-and-worlds" title="Universe & Worlds" subtitle="World hierarchy and management" >}} + {{< card link="chunks" title="Chunks" subtitle="Chunk loading and storage" >}} + {{< card link="blocks" title="Blocks" subtitle="Block manipulation" >}} + {{< card link="player-refs" title="PlayerRef" subtitle="Cross-world player references" >}} + {{< card link="worldgen" title="World Generation" subtitle="Terrain, biomes, and structures" icon="globe" >}} + {{< card link="portals" title="Portals" subtitle="Inter-world travel and teleportation" icon="link" >}} + {{< card link="entities" title="Entities" subtitle="Players, creatures, and ECS components" icon="user" >}} + {{< card link="interactions" title="Interactions" subtitle="Block and entity interactions" icon="cursor-click" >}} +{{< /cards >}} + +## World Hierarchy + +``` +Universe (singleton) +└── World (multiple) + └── Chunk (many) + └── Block (many) +``` + +## Quick Access + +```java +Universe universe = Universe.get(); +World world = universe.getWorld("default"); +Collection allWorlds = universe.getWorlds().values(); +``` diff --git a/content/world/_index.fr.md b/content/world/_index.fr.md new file mode 100644 index 0000000..c5a3021 --- /dev/null +++ b/content/world/_index.fr.md @@ -0,0 +1,35 @@ +--- +title: Monde +type: docs +weight: 4 +--- + +La gestion du monde dans Hytale couvre les univers, mondes, chunks et blocs. + +{{< cards >}} + {{< card link="universe-and-worlds" title="Univers & Mondes" subtitle="Hiérarchie et gestion des mondes" >}} + {{< card link="chunks" title="Chunks" subtitle="Chargement et stockage des chunks" >}} + {{< card link="blocks" title="Blocs" subtitle="Manipulation des blocs" >}} + {{< card link="player-refs" title="PlayerRef" subtitle="Références joueur inter-mondes" >}} + {{< card link="worldgen" title="Génération de Monde" subtitle="Terrain, biomes et structures" icon="globe" >}} + {{< card link="portals" title="Portails" subtitle="Voyage inter-mondes et téléportation" icon="link" >}} + {{< card link="entities" title="Entités" subtitle="Joueurs, créatures et composants ECS" icon="user" >}} + {{< card link="interactions" title="Interactions" subtitle="Interactions blocs et entités" icon="cursor-click" >}} +{{< /cards >}} + +## Hiérarchie du Monde + +``` +Universe (singleton) +└── World (multiple) + └── Chunk (many) + └── Block (many) +``` + +## Accès Rapide + +```java +Universe universe = Universe.get(); +World world = universe.getWorld("default"); +Collection allWorlds = universe.getWorlds().values(); +``` diff --git a/content/world/blocks.en.md b/content/world/blocks.en.md new file mode 100644 index 0000000..8c0c0c5 --- /dev/null +++ b/content/world/blocks.en.md @@ -0,0 +1,329 @@ +--- +title: Blocks +type: docs +weight: 3 +--- + +Block manipulation is a core part of world modification in Hytale plugins. The `World` class provides block access through the `IChunkAccessorSync` interface, while `WorldChunk` implements the full `BlockAccessor` interface for rotation-aware placement. + +{{< callout type="warning" >}} +**Important:** `World` does NOT implement `BlockAccessor` directly. For `placeBlock()` with rotation, you must get the chunk first using `world.getChunk()`. +{{< /callout >}} + +## Getting Blocks + +```java +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.math.vector.Vector3i; + +World world = Universe.get().getWorld("default"); + +// Get block ID (int index) at position +int blockId = world.getBlock(x, y, z); +int blockId = world.getBlock(new Vector3i(x, y, z)); + +// Get BlockType at position +BlockType blockType = world.getBlockType(x, y, z); +BlockType blockType = world.getBlockType(new Vector3i(x, y, z)); + +// Check if block is empty +if (blockType == BlockType.EMPTY) { + // Air or empty space +} +``` + +## Setting Blocks + +The `setBlock` method takes a **String block type key**, not a BlockType object or int ID: + +```java +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; + +World world = player.getWorld(); + +// Set block by block type key (string ID) +world.setBlock(x, y, z, "stone"); +world.setBlock(x, y, z, "oak_planks", 0); // with settings + +// Using BlockType - get the ID string first +BlockType stone = BlockType.getAssetMap().getAsset("stone"); +if (stone != null) { + world.setBlock(x, y, z, stone.getId()); // Use getId() to get the string key +} + +// Block ID index (for internal use) +int stoneIndex = BlockType.getAssetMap().getIndex("stone"); +// Note: setBlock takes String, not int - use the key directly +``` + +## Block Placement with Rotation + +{{< callout type="info" >}} +**placeBlock() is on WorldChunk, not World!** You must get the chunk first to place blocks with rotation. +{{< /callout >}} + +```java +import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; +import com.hypixel.hytale.math.util.ChunkUtil; + +// Get the chunk containing this block position +WorldChunk chunk = world.getChunk(ChunkUtil.indexChunkFromBlock(x, z)); + +if (chunk != null) { + // Place block with rotation + // Rotation values: None, Ninety, OneEighty, TwoSeventy + chunk.placeBlock(x, y, z, "oak_log", + Rotation.Ninety, // yaw (90 degrees) + Rotation.None, // pitch + Rotation.None // roll + ); + + // Place with rotation tuple and options + chunk.placeBlock(x, y, z, "oak_log", + RotationTuple.of(Rotation.Ninety, Rotation.None, Rotation.None), + 0, // settings + true // validatePlacement + ); +} +``` + +### Rotation Values + +The `Rotation` enum has these values (NOT cardinal directions): + +| Rotation | Degrees | +|----------|---------| +| `Rotation.None` | 0° | +| `Rotation.Ninety` | 90° | +| `Rotation.OneEighty` | 180° | +| `Rotation.TwoSeventy` | 270° | + +## Breaking Blocks + +```java +// Break block at position (requires settings parameter) +world.breakBlock(x, y, z, 0); // 0 = default settings +``` + +## BlockType Assets + +BlockType assets define block properties: + +```java +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.protocol.BlockMaterial; + +// Get block type by ID +BlockType blockType = BlockType.getAssetMap().getAsset("stone"); + +if (blockType != null) { + // Get block ID string + String blockId = blockType.getId(); + + // Get material + BlockMaterial material = blockType.getMaterial(); +} + +// Get block ID index (for internal use) +int blockIndex = BlockType.getAssetMap().getIndex("stone"); + +// Get block from index +BlockType fromIndex = BlockType.getAssetMap().getAsset(blockIndex); +``` + +## Block Events + +### BreakBlockEvent + +Fired when a block is broken: + +```java +import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; +import com.hypixel.hytale.math.vector.Vector3i; +import java.util.logging.Level; + +getEventRegistry().register(BreakBlockEvent.class, event -> { + // Get break info + Vector3i position = event.getTargetBlock(); + BlockType blockType = event.getBlockType(); + + getLogger().at(Level.INFO).log("Block broken at " + position); + + // Cancel if protected + event.setCancelled(true); +}); +``` + +### PlaceBlockEvent + +Fired when a block is placed: + +```java +import com.hypixel.hytale.server.core.event.events.ecs.PlaceBlockEvent; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; + +getEventRegistry().register(PlaceBlockEvent.class, event -> { + // Get place info + Vector3i position = event.getTargetBlock(); + RotationTuple rotation = event.getRotation(); + + getLogger().at(Level.INFO).log("Block placed at " + position); + + // Cancel if not allowed + event.setCancelled(true); +}); +``` + +### DamageBlockEvent + +Fired when a block is damaged (before breaking): + +```java +import com.hypixel.hytale.server.core.event.events.ecs.DamageBlockEvent; + +getEventRegistry().register(DamageBlockEvent.class, event -> { + Vector3i position = event.getTargetBlock(); + BlockType blockType = event.getBlockType(); + + // Cancel to prevent damage + event.setCancelled(true); +}); +``` + +## Block Interaction State + +Blocks can have interaction states (like open/closed doors): + +```java +BlockType currentBlock = world.getBlockType(x, y, z); + +if (currentBlock != null) { + // Set block to a different state + world.setBlockInteractionState( + new Vector3i(x, y, z), + currentBlock, + "open" // state name + ); +} +``` + +## Working with Blocks + +### Check Area for Block Type + +```java +public boolean containsBlockType(World world, Vector3i min, Vector3i max, String blockTypeKey) { + // Note: Vector3i uses getX(), getY(), getZ() - NOT x(), y(), z() + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + BlockType block = world.getBlockType(x, y, z); + if (block != null && block.getId().equals(blockTypeKey)) { + return true; + } + } + } + } + return false; +} +``` + +### Fill Area + +```java +public void fillArea(World world, Vector3i min, Vector3i max, String blockTypeKey) { + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + world.setBlock(x, y, z, blockTypeKey); + } + } + } +} +``` + +### Replace Blocks + +```java +public void replaceBlocks(World world, Vector3i min, Vector3i max, + String fromBlock, String toBlock) { + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + BlockType current = world.getBlockType(x, y, z); + if (current != null && current.getId().equals(fromBlock)) { + world.setBlock(x, y, z, toBlock); + } + } + } + } +} +``` + +## Block Positions + +```java +import com.hypixel.hytale.math.vector.Vector3i; + +// Vector3i for integer block positions +Vector3i blockPos = new Vector3i(100, 64, 200); + +// Get individual coordinates - use getX(), getY(), getZ() +int x = blockPos.getX(); +int y = blockPos.getY(); +int z = blockPos.getZ(); + +// Adjacent positions +Vector3i above = new Vector3i(x, y + 1, z); +Vector3i below = new Vector3i(x, y - 1, z); +Vector3i north = new Vector3i(x, y, z - 1); +Vector3i south = new Vector3i(x, y, z + 1); +Vector3i east = new Vector3i(x + 1, y, z); +Vector3i west = new Vector3i(x - 1, y, z); +``` + +## Best Practices + +{{< callout type="info" >}} +**Block Manipulation Tips:** +- Block operations must run on the world's ticking thread +- Use `getBlockType()` for checking block types, `getBlock()` for raw IDs +- Check `BlockType.EMPTY` for air/empty blocks +- Events extend `CancellableEcsEvent` - use `setCancelled(true)` to prevent +- Consider chunk boundaries when processing large areas +- For `placeBlock()` with rotation, get the chunk first +{{< /callout >}} + +```java +// Safe block modification from async context +world.execute(() -> { + world.setBlock(x, y, z, "stone"); +}); +``` + +## API Reference + +### World (IChunkAccessorSync) Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getBlock(int, int, int)` | `int` | Get block ID at position | +| `getBlock(Vector3i)` | `int` | Get block ID at position | +| `getBlockType(int, int, int)` | `BlockType?` | Get BlockType at position | +| `getBlockType(Vector3i)` | `BlockType?` | Get BlockType at position | +| `setBlock(x, y, z, String)` | `void` | Set block by type key | +| `setBlock(x, y, z, String, int)` | `void` | Set block with settings | +| `breakBlock(x, y, z, int)` | `boolean` | Break block with settings | +| `setBlockInteractionState(...)` | `void` | Set block state | + +### WorldChunk (BlockAccessor) Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `placeBlock(x, y, z, String, Rotation, Rotation, Rotation)` | `boolean` | Place with rotation | +| `placeBlock(x, y, z, String, RotationTuple, int, boolean)` | `boolean` | Place with options | +| `testPlaceBlock(...)` | `boolean` | Test if placement valid | diff --git a/content/world/blocks.fr.md b/content/world/blocks.fr.md new file mode 100644 index 0000000..5db45ff --- /dev/null +++ b/content/world/blocks.fr.md @@ -0,0 +1,329 @@ +--- +title: Blocs +type: docs +weight: 3 +--- + +La manipulation des blocs est une partie centrale de la modification du monde dans les plugins Hytale. La classe `World` fournit l'accès aux blocs via l'interface `IChunkAccessorSync`, tandis que `WorldChunk` implémente l'interface complète `BlockAccessor` pour le placement avec rotation. + +{{< callout type="warning" >}} +**Important :** `World` n'implémente PAS `BlockAccessor` directement. Pour `placeBlock()` avec rotation, vous devez d'abord obtenir le chunk avec `world.getChunk()`. +{{< /callout >}} + +## Obtenir des Blocs + +```java +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.math.vector.Vector3i; + +World world = Universe.get().getWorld("default"); + +// Obtenir l'ID du bloc (index int) à une position +int blockId = world.getBlock(x, y, z); +int blockId = world.getBlock(new Vector3i(x, y, z)); + +// Obtenir le BlockType à une position +BlockType blockType = world.getBlockType(x, y, z); +BlockType blockType = world.getBlockType(new Vector3i(x, y, z)); + +// Vérifier si le bloc est vide +if (blockType == BlockType.EMPTY) { + // Air ou espace vide +} +``` + +## Définir des Blocs + +La méthode `setBlock` prend une **clé de type bloc String**, pas un objet BlockType ou un int ID : + +```java +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; + +World world = player.getWorld(); + +// Définir un bloc par clé de type (ID string) +world.setBlock(x, y, z, "stone"); +world.setBlock(x, y, z, "oak_planks", 0); // avec settings + +// Utiliser BlockType - obtenir d'abord la chaîne ID +BlockType stone = BlockType.getAssetMap().getAsset("stone"); +if (stone != null) { + world.setBlock(x, y, z, stone.getId()); // Utiliser getId() pour obtenir la clé string +} + +// Index d'ID de bloc (pour usage interne) +int stoneIndex = BlockType.getAssetMap().getIndex("stone"); +// Note : setBlock prend String, pas int - utilisez la clé directement +``` + +## Placement de Blocs avec Rotation + +{{< callout type="info" >}} +**placeBlock() est sur WorldChunk, pas World !** Vous devez d'abord obtenir le chunk pour placer des blocs avec rotation. +{{< /callout >}} + +```java +import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; +import com.hypixel.hytale.math.util.ChunkUtil; + +// Obtenir le chunk contenant cette position de bloc +WorldChunk chunk = world.getChunk(ChunkUtil.indexChunkFromBlock(x, z)); + +if (chunk != null) { + // Placer un bloc avec rotation + // Valeurs de Rotation : None, Ninety, OneEighty, TwoSeventy + chunk.placeBlock(x, y, z, "oak_log", + Rotation.Ninety, // yaw (90 degrés) + Rotation.None, // pitch + Rotation.None // roll + ); + + // Placer avec tuple de rotation et options + chunk.placeBlock(x, y, z, "oak_log", + RotationTuple.of(Rotation.Ninety, Rotation.None, Rotation.None), + 0, // settings + true // validatePlacement + ); +} +``` + +### Valeurs de Rotation + +L'enum `Rotation` a ces valeurs (PAS les directions cardinales) : + +| Rotation | Degrés | +|----------|--------| +| `Rotation.None` | 0° | +| `Rotation.Ninety` | 90° | +| `Rotation.OneEighty` | 180° | +| `Rotation.TwoSeventy` | 270° | + +## Casser des Blocs + +```java +// Casser un bloc à une position (nécessite le paramètre settings) +world.breakBlock(x, y, z, 0); // 0 = settings par défaut +``` + +## Assets BlockType + +Les assets BlockType définissent les propriétés des blocs : + +```java +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.protocol.BlockMaterial; + +// Obtenir le type de bloc par ID +BlockType blockType = BlockType.getAssetMap().getAsset("stone"); + +if (blockType != null) { + // Obtenir la chaîne ID du bloc + String blockId = blockType.getId(); + + // Obtenir le matériau + BlockMaterial material = blockType.getMaterial(); +} + +// Obtenir l'index d'ID du bloc (pour usage interne) +int blockIndex = BlockType.getAssetMap().getIndex("stone"); + +// Obtenir le bloc depuis l'index +BlockType fromIndex = BlockType.getAssetMap().getAsset(blockIndex); +``` + +## Événements de Blocs + +### BreakBlockEvent + +Déclenché quand un bloc est cassé : + +```java +import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; +import com.hypixel.hytale.math.vector.Vector3i; +import java.util.logging.Level; + +getEventRegistry().register(BreakBlockEvent.class, event -> { + // Obtenir les infos de destruction + Vector3i position = event.getTargetBlock(); + BlockType blockType = event.getBlockType(); + + getLogger().at(Level.INFO).log("Bloc cassé à " + position); + + // Annuler si protégé + event.setCancelled(true); +}); +``` + +### PlaceBlockEvent + +Déclenché quand un bloc est placé : + +```java +import com.hypixel.hytale.server.core.event.events.ecs.PlaceBlockEvent; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; + +getEventRegistry().register(PlaceBlockEvent.class, event -> { + // Obtenir les infos de placement + Vector3i position = event.getTargetBlock(); + RotationTuple rotation = event.getRotation(); + + getLogger().at(Level.INFO).log("Bloc placé à " + position); + + // Annuler si non autorisé + event.setCancelled(true); +}); +``` + +### DamageBlockEvent + +Déclenché quand un bloc est endommagé (avant d'être cassé) : + +```java +import com.hypixel.hytale.server.core.event.events.ecs.DamageBlockEvent; + +getEventRegistry().register(DamageBlockEvent.class, event -> { + Vector3i position = event.getTargetBlock(); + BlockType blockType = event.getBlockType(); + + // Annuler pour empêcher les dégâts + event.setCancelled(true); +}); +``` + +## État d'Interaction de Bloc + +Les blocs peuvent avoir des états d'interaction (comme portes ouvertes/fermées) : + +```java +BlockType currentBlock = world.getBlockType(x, y, z); + +if (currentBlock != null) { + // Définir le bloc à un état différent + world.setBlockInteractionState( + new Vector3i(x, y, z), + currentBlock, + "open" // nom de l'état + ); +} +``` + +## Travailler avec les Blocs + +### Vérifier une Zone pour un Type de Bloc + +```java +public boolean containsBlockType(World world, Vector3i min, Vector3i max, String blockTypeKey) { + // Note : Vector3i utilise getX(), getY(), getZ() - PAS x(), y(), z() + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + BlockType block = world.getBlockType(x, y, z); + if (block != null && block.getId().equals(blockTypeKey)) { + return true; + } + } + } + } + return false; +} +``` + +### Remplir une Zone + +```java +public void fillArea(World world, Vector3i min, Vector3i max, String blockTypeKey) { + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + world.setBlock(x, y, z, blockTypeKey); + } + } + } +} +``` + +### Remplacer des Blocs + +```java +public void replaceBlocks(World world, Vector3i min, Vector3i max, + String fromBlock, String toBlock) { + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + BlockType current = world.getBlockType(x, y, z); + if (current != null && current.getId().equals(fromBlock)) { + world.setBlock(x, y, z, toBlock); + } + } + } + } +} +``` + +## Positions de Blocs + +```java +import com.hypixel.hytale.math.vector.Vector3i; + +// Vector3i pour les positions de blocs entières +Vector3i blockPos = new Vector3i(100, 64, 200); + +// Obtenir les coordonnées individuelles - utiliser getX(), getY(), getZ() +int x = blockPos.getX(); +int y = blockPos.getY(); +int z = blockPos.getZ(); + +// Positions adjacentes +Vector3i above = new Vector3i(x, y + 1, z); +Vector3i below = new Vector3i(x, y - 1, z); +Vector3i north = new Vector3i(x, y, z - 1); +Vector3i south = new Vector3i(x, y, z + 1); +Vector3i east = new Vector3i(x + 1, y, z); +Vector3i west = new Vector3i(x - 1, y, z); +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Conseils pour la Manipulation de Blocs :** +- Les opérations sur les blocs doivent s'exécuter sur le thread de tick du monde +- Utilisez `getBlockType()` pour vérifier les types, `getBlock()` pour les IDs bruts +- Vérifiez `BlockType.EMPTY` pour les blocs air/vides +- Les événements étendent `CancellableEcsEvent` - utilisez `setCancelled(true)` pour empêcher +- Considérez les limites de chunks lors du traitement de grandes zones +- Pour `placeBlock()` avec rotation, obtenez d'abord le chunk +{{< /callout >}} + +```java +// Modification sécurisée de bloc depuis un contexte asynchrone +world.execute(() -> { + world.setBlock(x, y, z, "stone"); +}); +``` + +## Référence API + +### Méthodes World (IChunkAccessorSync) + +| Méthode | Retour | Description | +|--------|--------|-------------| +| `getBlock(int, int, int)` | `int` | Obtenir l'ID du bloc à la position | +| `getBlock(Vector3i)` | `int` | Obtenir l'ID du bloc à la position | +| `getBlockType(int, int, int)` | `BlockType?` | Obtenir le BlockType à la position | +| `getBlockType(Vector3i)` | `BlockType?` | Obtenir le BlockType à la position | +| `setBlock(x, y, z, String)` | `void` | Définir bloc par clé de type | +| `setBlock(x, y, z, String, int)` | `void` | Définir bloc avec settings | +| `breakBlock(x, y, z, int)` | `boolean` | Casser bloc avec settings | +| `setBlockInteractionState(...)` | `void` | Définir état du bloc | + +### Méthodes WorldChunk (BlockAccessor) + +| Méthode | Retour | Description | +|--------|--------|-------------| +| `placeBlock(x, y, z, String, Rotation, Rotation, Rotation)` | `boolean` | Placer avec rotation | +| `placeBlock(x, y, z, String, RotationTuple, int, boolean)` | `boolean` | Placer avec options | +| `testPlaceBlock(...)` | `boolean` | Tester si placement valide | diff --git a/content/world/chunks.en.md b/content/world/chunks.en.md new file mode 100644 index 0000000..e8146c2 --- /dev/null +++ b/content/world/chunks.en.md @@ -0,0 +1,172 @@ +--- +title: Chunks +type: docs +weight: 2 +--- + +Chunks are the fundamental unit of world storage and loading in Hytale. + +## Chunk Basics + +A chunk represents a fixed-size section of the world containing blocks: + +```java +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; + +World world = Universe.get().getWorld("default"); + +// Chunks are loaded automatically when needed +// Access blocks within a chunk through the World API +BlockType blockType = world.getBlockType(x, y, z); +``` + +## Chunk Coordinates + +Chunks use a separate coordinate system from block coordinates: + +```java +// Convert block coordinates to chunk coordinates +int chunkX = blockX >> 4; // Divide by 16 +int chunkZ = blockZ >> 4; + +// Convert chunk coordinates to block coordinates (corner) +int blockX = chunkX << 4; // Multiply by 16 +int blockZ = chunkZ << 4; +``` + +## ChunkStore + +The ChunkStore manages chunk persistence: + +```java +// ChunkStore handles saving and loading chunks +// This is typically managed internally by the server +``` + +## Chunk Loading + +Chunks are loaded dynamically based on player positions: + +```java +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Transform; +import java.util.logging.Level; + +getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + + // Get position from PlayerRef.getTransform() + // Note: Player.getPosition() does NOT exist! + Transform transform = playerRef.getTransform(); + if (transform != null) { + Vector3d pos = transform.getPosition(); + // Vector3d uses getX(), getZ() - NOT x(), z() + int chunkX = (int) pos.getX() >> 4; + int chunkZ = (int) pos.getZ() >> 4; + + getLogger().at(Level.INFO).log("Player at chunk: " + chunkX + ", " + chunkZ); + } +}); +``` + +## Working with Chunks + +### Iterating Blocks in a Chunk + +```java +// Iterate through all blocks in a chunk area +int chunkX = 0; +int chunkZ = 0; +int startX = chunkX << 4; +int startZ = chunkZ << 4; + +for (int x = startX; x < startX + 16; x++) { + for (int z = startZ; z < startZ + 16; z++) { + for (int y = 0; y < 256; y++) { + BlockType blockType = world.getBlockType(x, y, z); + // Process block... + } + } +} +``` + +### Chunk-based Operations + +```java +// Perform operations on a chunk basis for efficiency +public void processChunk(World world, int chunkX, int chunkZ) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + // Process relative to chunk origin + int worldX = baseX + x; + int worldZ = baseZ + z; + // ... + } + } +} +``` + +## Performance Considerations + +{{< callout type="warning" >}} +Avoid loading or processing many chunks simultaneously as this can impact server performance. Use async operations for heavy chunk processing. +{{< /callout >}} + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.math.vector.Vector3i; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +// Process chunks asynchronously +World world = player.getWorld(); +PlayerRef playerRef = player.getPlayerRef(); + +CompletableFuture.runAsync(() -> { + // Heavy chunk processing here + Map results = new HashMap<>(); + // ... process + + // Return to world thread to apply results + world.execute(() -> { + for (var entry : results.entrySet()) { + Vector3i pos = entry.getKey(); + // Vector3i uses getX(), getY(), getZ() - NOT x(), y(), z() + world.setBlock(pos.getX(), pos.getY(), pos.getZ(), entry.getValue()); + } + playerRef.sendMessage(Message.raw("Chunk processing complete!")); + }); +}); +``` + +## Chunk Management + +{{< callout type="info" >}} +**Note:** Hytale handles chunk loading and unloading internally. There are no direct chunk load/unload events exposed to plugins. Chunks are automatically managed based on player positions and view distance. +{{< /callout >}} + +```java +import com.hypixel.hytale.math.vector.Vector2i; + +// Calculate chunk coordinates from a player position +public Vector2i getChunkCoords(PlayerRef playerRef) { + Transform transform = playerRef.getTransform(); + if (transform == null) { + return new Vector2i(0, 0); + } + + Vector3d pos = transform.getPosition(); + // Vector3d uses getX(), getZ() - NOT x(), z() + int chunkX = (int) pos.getX() >> 4; + int chunkZ = (int) pos.getZ() >> 4; + return new Vector2i(chunkX, chunkZ); +} +``` diff --git a/content/world/chunks.fr.md b/content/world/chunks.fr.md new file mode 100644 index 0000000..0d0d46c --- /dev/null +++ b/content/world/chunks.fr.md @@ -0,0 +1,172 @@ +--- +title: Chunks +type: docs +weight: 2 +--- + +Les chunks sont l'unité fondamentale de stockage et de chargement du monde dans Hytale. + +## Bases des Chunks + +Un chunk représente une section de taille fixe du monde contenant des blocs : + +```java +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; + +World world = Universe.get().getWorld("default"); + +// Les chunks sont chargés automatiquement quand nécessaire +// Accédez aux blocs via l'API World +BlockType blockType = world.getBlockType(x, y, z); +``` + +## Coordonnées des Chunks + +Les chunks utilisent un système de coordonnées séparé des coordonnées de blocs : + +```java +// Convertir les coordonnées de bloc en coordonnées de chunk +int chunkX = blockX >> 4; // Diviser par 16 +int chunkZ = blockZ >> 4; + +// Convertir les coordonnées de chunk en coordonnées de bloc (coin) +int blockX = chunkX << 4; // Multiplier par 16 +int blockZ = chunkZ << 4; +``` + +## ChunkStore + +Le ChunkStore gère la persistance des chunks : + +```java +// ChunkStore gère la sauvegarde et le chargement des chunks +// Ceci est généralement géré en interne par le serveur +``` + +## Chargement des Chunks + +Les chunks sont chargés dynamiquement en fonction des positions des joueurs : + +```java +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Transform; +import java.util.logging.Level; + +getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + + // Obtenir la position depuis PlayerRef.getTransform() + // Note : Player.getPosition() n'existe PAS ! + Transform transform = playerRef.getTransform(); + if (transform != null) { + Vector3d pos = transform.getPosition(); + // Vector3d utilise getX(), getZ() - PAS x(), z() + int chunkX = (int) pos.getX() >> 4; + int chunkZ = (int) pos.getZ() >> 4; + + getLogger().at(Level.INFO).log("Joueur au chunk : " + chunkX + ", " + chunkZ); + } +}); +``` + +## Travailler avec les Chunks + +### Itérer les Blocs d'un Chunk + +```java +// Itérer tous les blocs dans une zone de chunk +int chunkX = 0; +int chunkZ = 0; +int startX = chunkX << 4; +int startZ = chunkZ << 4; + +for (int x = startX; x < startX + 16; x++) { + for (int z = startZ; z < startZ + 16; z++) { + for (int y = 0; y < 256; y++) { + BlockType blockType = world.getBlockType(x, y, z); + // Traiter le bloc... + } + } +} +``` + +### Opérations par Chunk + +```java +// Effectuer des opérations par chunk pour l'efficacité +public void processChunk(World world, int chunkX, int chunkZ) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + // Traiter relatif à l'origine du chunk + int worldX = baseX + x; + int worldZ = baseZ + z; + // ... + } + } +} +``` + +## Considérations de Performance + +{{< callout type="warning" >}} +Évitez de charger ou traiter beaucoup de chunks simultanément car cela peut impacter les performances du serveur. Utilisez des opérations asynchrones pour le traitement lourd de chunks. +{{< /callout >}} + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.math.vector.Vector3i; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +// Traiter les chunks de manière asynchrone +World world = player.getWorld(); +PlayerRef playerRef = player.getPlayerRef(); + +CompletableFuture.runAsync(() -> { + // Traitement lourd de chunks ici + Map results = new HashMap<>(); + // ... traiter + + // Retourner au thread du monde pour appliquer les résultats + world.execute(() -> { + for (var entry : results.entrySet()) { + Vector3i pos = entry.getKey(); + // Vector3i utilise getX(), getY(), getZ() - PAS x(), y(), z() + world.setBlock(pos.getX(), pos.getY(), pos.getZ(), entry.getValue()); + } + playerRef.sendMessage(Message.raw("Traitement des chunks terminé !")); + }); +}); +``` + +## Gestion des Chunks + +{{< callout type="info" >}} +**Note :** Hytale gère le chargement et déchargement des chunks en interne. Il n'y a pas d'événements de chargement/déchargement de chunks exposés aux plugins. Les chunks sont automatiquement gérés en fonction des positions des joueurs et de la distance de vue. +{{< /callout >}} + +```java +import com.hypixel.hytale.math.vector.Vector2i; + +// Calculer les coordonnées de chunk depuis une position de joueur +public Vector2i getChunkCoords(PlayerRef playerRef) { + Transform transform = playerRef.getTransform(); + if (transform == null) { + return new Vector2i(0, 0); + } + + Vector3d pos = transform.getPosition(); + // Vector3d utilise getX(), getZ() - PAS x(), z() + int chunkX = (int) pos.getX() >> 4; + int chunkZ = (int) pos.getZ() >> 4; + return new Vector2i(chunkX, chunkZ); +} +``` diff --git a/content/world/entities/_index.en.md b/content/world/entities/_index.en.md new file mode 100644 index 0000000..173ff99 --- /dev/null +++ b/content/world/entities/_index.en.md @@ -0,0 +1,53 @@ +--- +title: Entities +type: docs +weight: 7 +--- + +Entities are all dynamic objects in the game world - players, creatures, items, and more. Hytale uses an Entity Component System (ECS) architecture. + +{{< cards >}} + {{< card link="entity-hierarchy" title="Entity Hierarchy" subtitle="Entity → LivingEntity → Player" >}} + {{< card link="entity-components" title="Entity Components" subtitle="Transform, BoundingBox, and more" >}} + {{< card link="spawning-entities" title="Spawning Entities" subtitle="Create and spawn custom entities" >}} + {{< card link="player-api" title="Player API" subtitle="Player-specific methods and data" >}} + {{< card link="npc" title="NPC System" subtitle="AI, navigation, and behaviors" >}} + {{< card link="spawning" title="Spawning System" subtitle="Markers, beacons, and suppression" >}} + {{< card link="flocking-behavior" title="Flocking Behavior" subtitle="NPC group coordination" >}} + {{< card link="mounts" title="Mounts" subtitle="Riding entities and vehicles" >}} + {{< card link="inventory" title="Inventory" subtitle="Items, containers, and transactions" icon="archive" >}} +{{< /cards >}} + +## Entity Hierarchy + +``` +Entity +├── LivingEntity +│ ├── Player +│ └── Creature +└── BlockEntity +``` + +## Quick Example + +```java +getEventRegistry().register(PlayerConnectEvent.class, event -> { + Player player = event.getPlayer(); + + if (player != null) { + // Get position via TransformComponent + TransformComponent transform = player.getTransformComponent(); + Vector3d pos = transform.getPosition(); + + // Get world + World world = player.getWorld(); + + // Send message (requires Message object) + player.sendMessage(Message.raw("Welcome at " + pos.toString())); + } +}); +``` + +{{< callout type="info" >}} +`event.getPlayer()` may return null if the player hasn't fully loaded yet. Always check for null. For position access, `getTransformComponent()` is deprecated - prefer ECS component access in new code. +{{< /callout >}} diff --git a/content/world/entities/_index.fr.md b/content/world/entities/_index.fr.md new file mode 100644 index 0000000..cf46905 --- /dev/null +++ b/content/world/entities/_index.fr.md @@ -0,0 +1,53 @@ +--- +title: Entités +type: docs +weight: 7 +--- + +Les entités sont tous les objets dynamiques dans le monde du jeu - joueurs, créatures, items, et plus. Hytale utilise une architecture Entity Component System (ECS). + +{{< cards >}} + {{< card link="entity-hierarchy" title="Hiérarchie des Entités" subtitle="Entity → LivingEntity → Player" >}} + {{< card link="entity-components" title="Composants d'Entité" subtitle="Transform, BoundingBox, et plus" >}} + {{< card link="spawning-entities" title="Faire Apparaître des Entités" subtitle="Créer et spawner des entités personnalisées" >}} + {{< card link="player-api" title="API Joueur" subtitle="Méthodes et données spécifiques aux joueurs" >}} + {{< card link="npc" title="Système NPC" subtitle="IA, navigation, et comportements" >}} + {{< card link="spawning" title="Système de Spawning" subtitle="Marqueurs, beacons, et suppression" >}} + {{< card link="flocking-behavior" title="Comportement de Groupe" subtitle="Coordination de groupe NPC" >}} + {{< card link="mounts" title="Montures" subtitle="Chevaucher entites et vehicules" >}} + {{< card link="inventory" title="Inventaire" subtitle="Items, conteneurs et transactions" icon="archive" >}} +{{< /cards >}} + +## Hiérarchie des Entités + +``` +Entity +├── LivingEntity +│ ├── Player +│ └── Creature +└── BlockEntity +``` + +## Exemple Rapide + +```java +getEventRegistry().register(PlayerConnectEvent.class, event -> { + Player player = event.getPlayer(); + + if (player != null) { + // Obtenir la position via TransformComponent + TransformComponent transform = player.getTransformComponent(); + Vector3d pos = transform.getPosition(); + + // Obtenir le monde + World world = player.getWorld(); + + // Envoyer un message (nécessite un objet Message) + player.sendMessage(Message.raw("Bienvenue à " + pos.toString())); + } +}); +``` + +{{< callout type="info" >}} +`event.getPlayer()` peut retourner null si le joueur n'est pas encore complètement chargé. Vérifiez toujours la valeur null. Pour l'accès à la position, `getTransformComponent()` est déprécié - préférez l'accès aux composants ECS dans le nouveau code. +{{< /callout >}} diff --git a/content/world/entities/entity-components.en.md b/content/world/entities/entity-components.en.md new file mode 100644 index 0000000..dcd94d3 --- /dev/null +++ b/content/world/entities/entity-components.en.md @@ -0,0 +1,828 @@ +--- +title: Entity Components +type: docs +weight: 2 +--- + +Hytale uses an Entity Component System (ECS) where entities are composed of modular components. Each component adds specific functionality or data to an entity. + +## Component Categories + +{{< cards cols="3" >}} + {{< card link="#transform--position" title="Transform & Position" subtitle="Location, rotation, collision" >}} + {{< card link="#display--visual" title="Display & Visual" subtitle="Models, animations, effects" >}} + {{< card link="#behavior-markers" title="Behavior Markers" subtitle="Interaction flags" >}} + {{< card link="#physics" title="Physics" subtitle="Position data, collision" >}} + {{< card link="#audio" title="Audio" subtitle="Movement sounds" >}} + {{< card link="#custom-components" title="Custom Components" subtitle="Plugin-defined components" >}} +{{< /cards >}} + +--- + +## Component Access Pattern + +Before diving into specific components, understand how to access components in Hytale's ECS: + +```java +// Get component type from the component class +ComponentType transformType = + TransformComponent.getComponentType(); + +// Get Ref from PlayerRef or other source +Ref ref = playerRef.getReference(); +if (ref == null || !ref.isValid()) { + return; +} + +// Get Store from Ref, then access component via Store +Store store = ref.getStore(); +TransformComponent transform = store.getComponent(ref, transformType); + +// Check if entity has a component using Archetype +Archetype archetype = store.getArchetype(ref); +boolean hasComponent = archetype.contains(transformType); +``` + +{{< callout type="warning" >}} +**Important:** `Ref` does NOT have `getComponent()` or `hasComponent()` methods directly. You must get the `Store` first via `ref.getStore()`, then call `store.getComponent(ref, componentType)`. +{{< /callout >}} + +{{< callout type="info" >}} +All entity components implement `Component` and provide a static `getComponentType()` method to retrieve the `ComponentType` for component access. +{{< /callout >}} + +--- + +## Transform & Position + +### TransformComponent + +Core component for entity position, rotation, and chunk reference. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| position | `Vector3d` | World position (x, y, z) | +| rotation | `Vector3f` | Rotation angles (x, y, z) | +| chunkRef | `Ref` | Reference to containing chunk | +{{< /tab >}} +{{< tab >}} +- `getPosition()` - Returns world position as Vector3d +- `setPosition(Vector3d)` - Sets world position (uses `assign()`) +- `getRotation()` - Returns rotation angles +- `setRotation(Vector3f)` - Sets rotation angles (uses `assign()`) +- `teleportPosition(Vector3d)` - Teleports entity to position +- `getTransform()` - Returns Transform object combining position/rotation +- `getChunkRef()` - Returns chunk reference (`Ref`) +- `getChunk()` - **Deprecated** - Returns WorldChunk directly + +{{< callout type="warning" >}} +`getChunk()` is deprecated and marked for removal. Use `getChunkRef()` instead. +{{< /callout >}} +{{< /tab >}} +{{< tab >}} +```java +// Get store from ref +Store store = ref.getStore(); + +ComponentType transformType = + TransformComponent.getComponentType(); +TransformComponent transform = store.getComponent(ref, transformType); + +if (transform != null) { + // Get current position + Vector3d pos = transform.getPosition(); + getLogger().at(Level.INFO).log("Entity at: " + pos.x + ", " + pos.y + ", " + pos.z); + + // Get rotation + Vector3f rot = transform.getRotation(); + + // Get chunk reference (recommended) + Ref chunkRef = transform.getChunkRef(); +} +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +### BoundingBox + +Defines the collision bounds of an entity using a `Box` object. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| boundingBox | `Box` | The collision box with min/max corners | +| detailBoxes | `Map` | Named detail boxes for precise collision | +{{< /tab >}} +{{< tab >}} +- `getBoundingBox()` - Returns the `Box` object +- `setBoundingBox(Box)` - Sets the bounding box +- `getDetailBoxes()` - Returns map of detail boxes (can be null) +{{< /tab >}} +{{< tab >}} +```java +ComponentType boundsType = BoundingBox.getComponentType(); +BoundingBox bounds = store.getComponent(ref, boundsType); + +// Access the Box object +Box box = bounds.getBoundingBox(); + +// Box provides min/max as Vector3d +Vector3d min = box.getMin(); +Vector3d max = box.getMax(); + +// Box utility methods +double width = box.width(); // max.x - min.x +double height = box.height(); // max.y - min.y +double depth = box.depth(); // max.z - min.z + +// Center point +double centerX = box.middleX(); +double centerY = box.middleY(); +double centerZ = box.middleZ(); + +// Check intersection with another box +Box otherBox = otherBounds.getBoundingBox(); +if (box.isIntersecting(otherBox)) { + handleCollision(); +} + +// Modify box (mutates in place) +box.offset(1.0, 0, 0); // Move box by offset +box.scale(2.0f); // Scale box + +// Access detail boxes for precise collision +Map details = bounds.getDetailBoxes(); +if (details != null) { + for (Map.Entry entry : details.entrySet()) { + // Process named detail boxes + } +} +``` +{{< /tab >}} +{{< /tabs >}} + +#### The Box Class + +The `Box` class (`com.hypixel.hytale.math.shape.Box`) is a fundamental shape class: + +| Method | Description | +|--------|-------------| +| `width()` | Returns width (X axis) | +| `height()` | Returns height (Y axis) | +| `depth()` | Returns depth (Z axis) | +| `middleX/Y/Z()` | Returns center coordinates | +| `getVolume()` | Returns total volume | +| `isIntersecting(Box)` | Checks intersection with another box | +| `containsPosition(x,y,z)` | Checks if point is inside | +| `offset(x,y,z)` | Moves box by offset (mutates) | +| `scale(float)` | Scales box (mutates) | +| `union(Box)` | Expands to contain another box | +| `clone()` | Creates a copy | + +--- + +### CollisionResultComponent + +Stores collision detection results from the physics system. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| collisionResult | `CollisionResult` | Full collision computation result | +| collisionStartPosition | `Vector3d` | Position where collision started | +| collisionPositionOffset | `Vector3d` | Offset from collision | +| pendingCollisionCheck | `boolean` | Whether collision check is pending | +{{< /tab >}} +{{< tab >}} +- `getCollisionResult()` - Returns the `CollisionResult` object +- `isPendingCollisionCheck()` - Returns true if collision needs recomputation +{{< /tab >}} +{{< tab >}} +```java +ComponentType collisionType = + CollisionResultComponent.getComponentType(); +CollisionResultComponent collision = store.getComponent(ref,collisionType); + +// Get collision result object +CollisionResult result = collision.getCollisionResult(); + +// Check for block collisions +int blockCollisionCount = result.getBlockCollisionCount(); +if (blockCollisionCount > 0) { + BlockCollisionData firstCollision = result.getFirstBlockCollision(); + // Process collision +} + +// Check if sliding (on ground) +if (result.isSliding) { + // Entity is sliding on a surface + double slideStart = result.slideStart; + double slideEnd = result.slideEnd; +} + +// Check character collisions (if enabled) +int charCollisionCount = result.getCharacterCollisionCount(); +CharacterCollisionData charCollision = result.getFirstCharacterCollision(); + +// Access trigger blocks (collision triggers) +CollisionDataArray triggers = result.getTriggerBlocks(); +``` +{{< /tab >}} +{{< /tabs >}} + +{{< callout type="info" >}} +The `CollisionResult` class is complex and handles block collisions, character collisions, slides, and trigger blocks. It's primarily used internally by the collision system. +{{< /callout >}} + +--- + +### PositionDataComponent + +Tracks block type IDs at entity's position. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| insideBlockTypeId | `int` | Block type ID entity is inside | +| standingOnBlockTypeId | `int` | Block type ID entity is standing on | +{{< /tab >}} +{{< tab >}} +- `getInsideBlockTypeId()` - Returns block type ID entity is inside +- `setInsideBlockTypeId(int)` - Sets inside block type ID +- `getStandingOnBlockTypeId()` - Returns block type ID beneath entity +- `setStandingOnBlockTypeId(int)` - Sets standing-on block type ID +{{< /tab >}} +{{< tab >}} +```java +ComponentType posDataType = + PositionDataComponent.getComponentType(); +PositionDataComponent posData = store.getComponent(ref,posDataType); + +// Check what block entity is standing on +int standingOn = posData.getStandingOnBlockTypeId(); + +// Check what block entity is inside +int insideBlock = posData.getInsideBlockTypeId(); + +// Use block type registry to get actual BlockType +BlockType blockType = BlockType.getAssetMap().getAsset(standingOn); +if (blockType != null) { + getLogger().at(Level.INFO).log("Standing on: " + blockType.getId()); +} +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +## Display & Visual + +### DisplayNameComponent + +Controls the display name shown above an entity. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| displayName | `Message` | The displayed name as a Message object | +{{< /tab >}} +{{< tab >}} +- `getDisplayName()` - Returns the `Message` display name +{{< /tab >}} +{{< tab >}} +```java +ComponentType nameType = + DisplayNameComponent.getComponentType(); +DisplayNameComponent nameComp = store.getComponent(ref,nameType); + +// Get current display name +Message name = nameComp.getDisplayName(); + +// Message is the formatted text object used throughout Hytale +// Use Message.raw() or Message formatting methods to create new names +``` +{{< /tab >}} +{{< /tabs >}} + +{{< callout type="info" >}} +**Message Type:** Display names use Hytale's `Message` class, not plain strings. The `Message` class supports formatted text, colors, and localization. +{{< /callout >}} + +--- + +### ModelComponent + +Links an entity to its visual 3D model. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| model | `Model` | Reference to the model | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getModel()` - Returns the Model +- `setModel(Model)` - Sets the model +{{< /tab >}} +{{< tab >}} +```java +ComponentType modelType = ModelComponent.getComponentType(); +ModelComponent model = store.getComponent(ref,modelType); + +// Get current model +Model currentModel = model.getModel(); + +// Set new model +model.setModel(newModel); +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +### EntityScaleComponent + +Controls the scale/size of an entity. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| scale | `float` | Uniform scale factor (default: 1.0) | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getScale()` - Returns the scale factor (float) +- `setScale(float)` - Sets the uniform scale factor +{{< /tab >}} +{{< tab >}} +```java +ComponentType scaleType = + EntityScaleComponent.getComponentType(); +EntityScaleComponent scale = store.getComponent(ref,scaleType); + +// Get current scale +float currentScale = scale.getScale(); // Default: 1.0 + +// Make entity twice as large +scale.setScale(2.0f); + +// Make entity half size +scale.setScale(0.5f); + +// Giant boss spawn effect - track growth in tick handler +private final Map, Float> growingBosses = new ConcurrentHashMap<>(); + +public void startGrowBoss(Ref bossRef) { + EntityScaleComponent scale = store.getComponent(bossRef, scaleType); + scale.setScale(0.1f); // Start small + growingBosses.put(bossRef, 0.1f); +} + +// Call this from your tick handler (every 5 ticks) +public void onTick() { + growingBosses.entrySet().removeIf(entry -> { + Ref bossRef = entry.getKey(); + float currentScale = entry.getValue(); + + if (currentScale >= 1.0f) { + return true; // Remove from map + } + + EntityScaleComponent scale = store.getComponent(bossRef, scaleType); + if (scale != null) { + float newScale = currentScale + 0.1f; + scale.setScale(newScale); + entry.setValue(newScale); + } + return false; // Keep in map + }); +} +``` +{{< /tab >}} +{{< /tabs >}} + +{{< callout type="warning" >}} +**Uniform Scale Only:** Hytale's EntityScaleComponent uses a single `float` for uniform scaling on all axes, not per-axis Vector3f scaling. +{{< /callout >}} + +--- + +### ActiveAnimationComponent + +Manages currently playing animations per animation slot. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| activeAnimations | `String[]` | Animation names per slot | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getActiveAnimations()` - Returns array of animation names indexed by `AnimationSlot` +- `setPlayingAnimation(AnimationSlot, String)` - Sets animation for a specific slot +{{< /tab >}} +{{< tab >}} +```java +ComponentType animType = + ActiveAnimationComponent.getComponentType(); +ActiveAnimationComponent anim = store.getComponent(ref,animType); + +// Get all active animations +String[] animations = anim.getActiveAnimations(); + +// Set animation for specific slot +anim.setPlayingAnimation(AnimationSlot.Action, "attack_swing"); +anim.setPlayingAnimation(AnimationSlot.Movement, "walk"); + +// Clear animation on a slot +anim.setPlayingAnimation(AnimationSlot.Emote, null); +``` +{{< /tab >}} +{{< /tabs >}} + +#### AnimationSlot Enum + +Hytale uses animation slots to layer animations: + +| Slot | Value | Purpose | +|------|-------|---------| +| `Movement` | 0 | Locomotion (walk, run, jump) | +| `Status` | 1 | Status effects | +| `Action` | 2 | Actions (attack, use item) | +| `Face` | 3 | Facial expressions | +| `Emote` | 4 | Social emotes | + +--- + +### DynamicLight + +Emits dynamic light from an entity using a `ColorLight` structure. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| colorLight | `ColorLight` | Light data (radius, RGB) | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getColorLight()` - Returns the `ColorLight` object +- `setColorLight(ColorLight)` - Sets the light configuration +{{< /tab >}} +{{< tab >}} +```java +ComponentType lightType = DynamicLight.getComponentType(); +DynamicLight light = store.getComponent(ref,lightType); + +// Get current light +ColorLight currentLight = light.getColorLight(); + +// Create new light configuration +// ColorLight has public fields: radius, red, green, blue (all bytes) +ColorLight torchLight = new ColorLight(); +torchLight.radius = 8; // Light radius +torchLight.red = (byte)255; // Warm orange color +torchLight.green = (byte)200; +torchLight.blue = (byte)100; + +light.setColorLight(torchLight); + +// Or use constructor +ColorLight blueLight = new ColorLight( + (byte)12, // radius + (byte)50, // red + (byte)100, // green + (byte)255 // blue +); +light.setColorLight(blueLight); +``` +{{< /tab >}} +{{< /tabs >}} + +#### ColorLight Structure + +The `ColorLight` class (`com.hypixel.hytale.protocol.ColorLight`) has public fields: + +| Field | Type | Description | +|-------|------|-------------| +| `radius` | `byte` | Light radius | +| `red` | `byte` | Red component (0-255) | +| `green` | `byte` | Green component (0-255) | +| `blue` | `byte` | Blue component (0-255) | + +--- + +### HeadRotation + +Controls head rotation independently from body. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| rotation | `Vector3f` | Head rotation (x, y, z) | +{{< /tab >}} +{{< tab >}} +- `getRotation()` - Returns head rotation as Vector3f +- `setRotation(Vector3f)` - Sets head rotation (uses `assign()`) +- `getDirection()` - Returns look direction as Vector3d +- `getAxisDirection()` - Returns axis-aligned direction as Vector3i +- `teleportRotation(Vector3f)` - Teleports head rotation +{{< /tab >}} +{{< tab >}} +```java +ComponentType headType = HeadRotation.getComponentType(); +HeadRotation head = store.getComponent(ref,headType); + +// Get current head rotation +Vector3f rot = head.getRotation(); + +// Set head rotation (pitch, yaw, roll) +head.setRotation(new Vector3f(-30, 45, 0)); // Look up-right + +// Get look direction as normalized vector +Vector3d direction = head.getDirection(); + +// Get axis direction (discrete: -1, 0, or 1 per axis) +Vector3i axisDir = head.getAxisDirection(); + +// Teleport rotation (instant, no interpolation) +head.teleportRotation(new Vector3f(0, 180, 0)); // Face backward +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +## Behavior Markers + +Singleton components that act as flags modifying entity behavior. They use the singleton pattern with an `INSTANCE` field. + +### Interactable + +{{< badge "Singleton" >}} + +Marks an entity as interactable by players. + +```java +ComponentType type = Interactable.getComponentType(); + +// Check if entity has component using archetype +Store store = ref.getStore(); +Archetype archetype = store.getArchetype(ref); +if (archetype.contains(type)) { + // Entity is interactable +} + +// Component is singleton - use INSTANCE +Interactable component = Interactable.INSTANCE; +``` + +--- + +### Invulnerable + +{{< badge "Singleton" >}} + +Makes an entity immune to damage. + +```java +ComponentType type = Invulnerable.getComponentType(); + +// Check invulnerability in damage handler +getEventRegistry().register(EntityDamageEvent.class, event -> { + Ref targetRef = event.getTarget(); + if (targetRef != null && targetRef.isValid()) { + Store store = targetRef.getStore(); + Archetype archetype = store.getArchetype(targetRef); + if (archetype.contains(type)) { + event.setCancelled(true); + } + } +}); +``` + +--- + +### Intangible + +{{< badge "Singleton" >}} + +Makes an entity pass through other entities and blocks (no collision). + +```java +ComponentType type = Intangible.getComponentType(); + +// Ghost entity - no collision +Store store = ref.getStore(); +Archetype archetype = store.getArchetype(ref); +if (archetype.contains(type)) { + // Entity passes through obstacles +} +``` + +--- + +## Audio + +### MovementAudioComponent + +Handles movement-related sounds with a predicate system. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| shouldHearPredicate | `ShouldHearPredicate` | Predicate for who hears sounds | +| lastInsideBlockTypeId | `int` | Last block entity was inside | +| nextMoveInRepeat | `float` | Timer for movement sound repeat | +{{< /tab >}} +{{< tab >}} +- `getShouldHearPredicate(Ref)` - Returns predicate for sound hearing +- `getLastInsideBlockTypeId()` - Returns last inside block ID +- `setLastInsideBlockTypeId(int)` - Sets last inside block ID +- `canMoveInRepeat()` - Returns true if repeat timer is active +- `tickMoveInRepeat(float)` - Advances repeat timer, returns true if elapsed +- `setNextMoveInRepeat(float)` - Sets next repeat interval +{{< /tab >}} +{{< tab >}} +```java +ComponentType audioType = + MovementAudioComponent.getComponentType(); +MovementAudioComponent movementAudio = store.getComponent(ref,audioType); + +// Get predicate for who should hear movement sounds +MovementAudioComponent.ShouldHearPredicate predicate = + movementAudio.getShouldHearPredicate(entityRef); + +// The predicate returns true for entities that should hear the sound +// (excludes the owner by default) +boolean shouldHear = predicate.test(otherEntityRef); + +// Check move-in repeat timer +if (movementAudio.canMoveInRepeat()) { + float dt = 1.0f / 20.0f; // Delta time + if (movementAudio.tickMoveInRepeat(dt)) { + // Timer elapsed, play repeat sound + } +} + +// Disable repeat sounds +movementAudio.setNextMoveInRepeat(MovementAudioComponent.NO_REPEAT); +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +## Custom Components + +### Creating Custom Components + +Create custom components by implementing `Component`: + +```java +public class CustomDataComponent implements Component { + // Static component type reference (set during registration) + private static ComponentType TYPE; + + // Component data + private int level = 1; + private String faction = "neutral"; + + public static ComponentType getComponentType() { + return TYPE; + } + + public int getLevel() { return level; } + public void setLevel(int level) { this.level = level; } + + public String getFaction() { return faction; } + public void setFaction(String faction) { this.faction = faction; } + + @Override + @Nonnull + public Component clone() { + CustomDataComponent copy = new CustomDataComponent(); + copy.level = this.level; + copy.faction = this.faction; + return copy; + } +} +``` + +### Registering Components + +Register components during plugin start: + +```java +@Override +public void start() { + // Register with EntityStoreRegistry + ComponentType type = + getEntityStoreRegistry().register( + "custom_data", + CustomDataComponent.class, + CustomDataComponent::new + ); + + // Store type reference for static access + CustomDataComponent.TYPE = type; +} +``` + +### Using Custom Components + +```java +// Get component type +ComponentType type = + CustomDataComponent.getComponentType(); + +// Get store from ref +Store store = ref.getStore(); + +// Check if entity has component +Archetype archetype = store.getArchetype(ref); +if (archetype.contains(type)) { + CustomDataComponent data = store.getComponent(ref, type); + + if (data.getLevel() > 10) { + // High level entity logic + } +} +``` + +--- + +## Best Practices + +{{< callout type="info" >}} +**Component Guidelines:** +- Use `getComponentType()` static method to get the `ComponentType` for access +- Always check if a component exists using `hasComponent()` before accessing +- Register custom components in `start()`, after all plugins have completed setup +- Implement `clone()` for components that need copying +- Use singleton pattern (static `INSTANCE`) for marker components +{{< /callout >}} + +{{< callout type="warning" >}} +**Network Sync:** Many components have an `isNetworkOutdated` flag. This is managed automatically by the network system - avoid setting it manually unless you understand the networking layer. +{{< /callout >}} + +{{< callout type="error" >}} +**Thread Safety:** Component access should be done on the world's ticking thread. Use `world.isInThread()` to verify, or use the task system for cross-thread operations. +{{< /callout >}} + +--- + +## Component Type Reference + +| Component | Package | Type | Purpose | +|-----------|---------|------|---------| +| TransformComponent | entity.component | Data | Position, rotation, chunk | +| BoundingBox | entity.component | Data | Collision bounds | +| CollisionResultComponent | entity.component | Data | Collision results | +| PositionDataComponent | entity.component | Data | Block context | +| DisplayNameComponent | entity.component | Data | Visible name | +| ModelComponent | entity.component | Data | 3D model | +| EntityScaleComponent | entity.component | Data | Size scaling | +| ActiveAnimationComponent | entity.component | Data | Animations | +| DynamicLight | entity.component | Data | Light emission | +| HeadRotation | entity.component | Data | Head direction | +| MovementAudioComponent | entity.component | Data | Movement sounds | +| Interactable | entity.component | Singleton | Can be interacted | +| Invulnerable | entity.component | Singleton | Cannot be damaged | +| Intangible | entity.component | Singleton | No collision | diff --git a/content/world/entities/entity-components.fr.md b/content/world/entities/entity-components.fr.md new file mode 100644 index 0000000..dcd94d3 --- /dev/null +++ b/content/world/entities/entity-components.fr.md @@ -0,0 +1,828 @@ +--- +title: Entity Components +type: docs +weight: 2 +--- + +Hytale uses an Entity Component System (ECS) where entities are composed of modular components. Each component adds specific functionality or data to an entity. + +## Component Categories + +{{< cards cols="3" >}} + {{< card link="#transform--position" title="Transform & Position" subtitle="Location, rotation, collision" >}} + {{< card link="#display--visual" title="Display & Visual" subtitle="Models, animations, effects" >}} + {{< card link="#behavior-markers" title="Behavior Markers" subtitle="Interaction flags" >}} + {{< card link="#physics" title="Physics" subtitle="Position data, collision" >}} + {{< card link="#audio" title="Audio" subtitle="Movement sounds" >}} + {{< card link="#custom-components" title="Custom Components" subtitle="Plugin-defined components" >}} +{{< /cards >}} + +--- + +## Component Access Pattern + +Before diving into specific components, understand how to access components in Hytale's ECS: + +```java +// Get component type from the component class +ComponentType transformType = + TransformComponent.getComponentType(); + +// Get Ref from PlayerRef or other source +Ref ref = playerRef.getReference(); +if (ref == null || !ref.isValid()) { + return; +} + +// Get Store from Ref, then access component via Store +Store store = ref.getStore(); +TransformComponent transform = store.getComponent(ref, transformType); + +// Check if entity has a component using Archetype +Archetype archetype = store.getArchetype(ref); +boolean hasComponent = archetype.contains(transformType); +``` + +{{< callout type="warning" >}} +**Important:** `Ref` does NOT have `getComponent()` or `hasComponent()` methods directly. You must get the `Store` first via `ref.getStore()`, then call `store.getComponent(ref, componentType)`. +{{< /callout >}} + +{{< callout type="info" >}} +All entity components implement `Component` and provide a static `getComponentType()` method to retrieve the `ComponentType` for component access. +{{< /callout >}} + +--- + +## Transform & Position + +### TransformComponent + +Core component for entity position, rotation, and chunk reference. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| position | `Vector3d` | World position (x, y, z) | +| rotation | `Vector3f` | Rotation angles (x, y, z) | +| chunkRef | `Ref` | Reference to containing chunk | +{{< /tab >}} +{{< tab >}} +- `getPosition()` - Returns world position as Vector3d +- `setPosition(Vector3d)` - Sets world position (uses `assign()`) +- `getRotation()` - Returns rotation angles +- `setRotation(Vector3f)` - Sets rotation angles (uses `assign()`) +- `teleportPosition(Vector3d)` - Teleports entity to position +- `getTransform()` - Returns Transform object combining position/rotation +- `getChunkRef()` - Returns chunk reference (`Ref`) +- `getChunk()` - **Deprecated** - Returns WorldChunk directly + +{{< callout type="warning" >}} +`getChunk()` is deprecated and marked for removal. Use `getChunkRef()` instead. +{{< /callout >}} +{{< /tab >}} +{{< tab >}} +```java +// Get store from ref +Store store = ref.getStore(); + +ComponentType transformType = + TransformComponent.getComponentType(); +TransformComponent transform = store.getComponent(ref, transformType); + +if (transform != null) { + // Get current position + Vector3d pos = transform.getPosition(); + getLogger().at(Level.INFO).log("Entity at: " + pos.x + ", " + pos.y + ", " + pos.z); + + // Get rotation + Vector3f rot = transform.getRotation(); + + // Get chunk reference (recommended) + Ref chunkRef = transform.getChunkRef(); +} +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +### BoundingBox + +Defines the collision bounds of an entity using a `Box` object. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| boundingBox | `Box` | The collision box with min/max corners | +| detailBoxes | `Map` | Named detail boxes for precise collision | +{{< /tab >}} +{{< tab >}} +- `getBoundingBox()` - Returns the `Box` object +- `setBoundingBox(Box)` - Sets the bounding box +- `getDetailBoxes()` - Returns map of detail boxes (can be null) +{{< /tab >}} +{{< tab >}} +```java +ComponentType boundsType = BoundingBox.getComponentType(); +BoundingBox bounds = store.getComponent(ref, boundsType); + +// Access the Box object +Box box = bounds.getBoundingBox(); + +// Box provides min/max as Vector3d +Vector3d min = box.getMin(); +Vector3d max = box.getMax(); + +// Box utility methods +double width = box.width(); // max.x - min.x +double height = box.height(); // max.y - min.y +double depth = box.depth(); // max.z - min.z + +// Center point +double centerX = box.middleX(); +double centerY = box.middleY(); +double centerZ = box.middleZ(); + +// Check intersection with another box +Box otherBox = otherBounds.getBoundingBox(); +if (box.isIntersecting(otherBox)) { + handleCollision(); +} + +// Modify box (mutates in place) +box.offset(1.0, 0, 0); // Move box by offset +box.scale(2.0f); // Scale box + +// Access detail boxes for precise collision +Map details = bounds.getDetailBoxes(); +if (details != null) { + for (Map.Entry entry : details.entrySet()) { + // Process named detail boxes + } +} +``` +{{< /tab >}} +{{< /tabs >}} + +#### The Box Class + +The `Box` class (`com.hypixel.hytale.math.shape.Box`) is a fundamental shape class: + +| Method | Description | +|--------|-------------| +| `width()` | Returns width (X axis) | +| `height()` | Returns height (Y axis) | +| `depth()` | Returns depth (Z axis) | +| `middleX/Y/Z()` | Returns center coordinates | +| `getVolume()` | Returns total volume | +| `isIntersecting(Box)` | Checks intersection with another box | +| `containsPosition(x,y,z)` | Checks if point is inside | +| `offset(x,y,z)` | Moves box by offset (mutates) | +| `scale(float)` | Scales box (mutates) | +| `union(Box)` | Expands to contain another box | +| `clone()` | Creates a copy | + +--- + +### CollisionResultComponent + +Stores collision detection results from the physics system. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| collisionResult | `CollisionResult` | Full collision computation result | +| collisionStartPosition | `Vector3d` | Position where collision started | +| collisionPositionOffset | `Vector3d` | Offset from collision | +| pendingCollisionCheck | `boolean` | Whether collision check is pending | +{{< /tab >}} +{{< tab >}} +- `getCollisionResult()` - Returns the `CollisionResult` object +- `isPendingCollisionCheck()` - Returns true if collision needs recomputation +{{< /tab >}} +{{< tab >}} +```java +ComponentType collisionType = + CollisionResultComponent.getComponentType(); +CollisionResultComponent collision = store.getComponent(ref,collisionType); + +// Get collision result object +CollisionResult result = collision.getCollisionResult(); + +// Check for block collisions +int blockCollisionCount = result.getBlockCollisionCount(); +if (blockCollisionCount > 0) { + BlockCollisionData firstCollision = result.getFirstBlockCollision(); + // Process collision +} + +// Check if sliding (on ground) +if (result.isSliding) { + // Entity is sliding on a surface + double slideStart = result.slideStart; + double slideEnd = result.slideEnd; +} + +// Check character collisions (if enabled) +int charCollisionCount = result.getCharacterCollisionCount(); +CharacterCollisionData charCollision = result.getFirstCharacterCollision(); + +// Access trigger blocks (collision triggers) +CollisionDataArray triggers = result.getTriggerBlocks(); +``` +{{< /tab >}} +{{< /tabs >}} + +{{< callout type="info" >}} +The `CollisionResult` class is complex and handles block collisions, character collisions, slides, and trigger blocks. It's primarily used internally by the collision system. +{{< /callout >}} + +--- + +### PositionDataComponent + +Tracks block type IDs at entity's position. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| insideBlockTypeId | `int` | Block type ID entity is inside | +| standingOnBlockTypeId | `int` | Block type ID entity is standing on | +{{< /tab >}} +{{< tab >}} +- `getInsideBlockTypeId()` - Returns block type ID entity is inside +- `setInsideBlockTypeId(int)` - Sets inside block type ID +- `getStandingOnBlockTypeId()` - Returns block type ID beneath entity +- `setStandingOnBlockTypeId(int)` - Sets standing-on block type ID +{{< /tab >}} +{{< tab >}} +```java +ComponentType posDataType = + PositionDataComponent.getComponentType(); +PositionDataComponent posData = store.getComponent(ref,posDataType); + +// Check what block entity is standing on +int standingOn = posData.getStandingOnBlockTypeId(); + +// Check what block entity is inside +int insideBlock = posData.getInsideBlockTypeId(); + +// Use block type registry to get actual BlockType +BlockType blockType = BlockType.getAssetMap().getAsset(standingOn); +if (blockType != null) { + getLogger().at(Level.INFO).log("Standing on: " + blockType.getId()); +} +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +## Display & Visual + +### DisplayNameComponent + +Controls the display name shown above an entity. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| displayName | `Message` | The displayed name as a Message object | +{{< /tab >}} +{{< tab >}} +- `getDisplayName()` - Returns the `Message` display name +{{< /tab >}} +{{< tab >}} +```java +ComponentType nameType = + DisplayNameComponent.getComponentType(); +DisplayNameComponent nameComp = store.getComponent(ref,nameType); + +// Get current display name +Message name = nameComp.getDisplayName(); + +// Message is the formatted text object used throughout Hytale +// Use Message.raw() or Message formatting methods to create new names +``` +{{< /tab >}} +{{< /tabs >}} + +{{< callout type="info" >}} +**Message Type:** Display names use Hytale's `Message` class, not plain strings. The `Message` class supports formatted text, colors, and localization. +{{< /callout >}} + +--- + +### ModelComponent + +Links an entity to its visual 3D model. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| model | `Model` | Reference to the model | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getModel()` - Returns the Model +- `setModel(Model)` - Sets the model +{{< /tab >}} +{{< tab >}} +```java +ComponentType modelType = ModelComponent.getComponentType(); +ModelComponent model = store.getComponent(ref,modelType); + +// Get current model +Model currentModel = model.getModel(); + +// Set new model +model.setModel(newModel); +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +### EntityScaleComponent + +Controls the scale/size of an entity. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| scale | `float` | Uniform scale factor (default: 1.0) | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getScale()` - Returns the scale factor (float) +- `setScale(float)` - Sets the uniform scale factor +{{< /tab >}} +{{< tab >}} +```java +ComponentType scaleType = + EntityScaleComponent.getComponentType(); +EntityScaleComponent scale = store.getComponent(ref,scaleType); + +// Get current scale +float currentScale = scale.getScale(); // Default: 1.0 + +// Make entity twice as large +scale.setScale(2.0f); + +// Make entity half size +scale.setScale(0.5f); + +// Giant boss spawn effect - track growth in tick handler +private final Map, Float> growingBosses = new ConcurrentHashMap<>(); + +public void startGrowBoss(Ref bossRef) { + EntityScaleComponent scale = store.getComponent(bossRef, scaleType); + scale.setScale(0.1f); // Start small + growingBosses.put(bossRef, 0.1f); +} + +// Call this from your tick handler (every 5 ticks) +public void onTick() { + growingBosses.entrySet().removeIf(entry -> { + Ref bossRef = entry.getKey(); + float currentScale = entry.getValue(); + + if (currentScale >= 1.0f) { + return true; // Remove from map + } + + EntityScaleComponent scale = store.getComponent(bossRef, scaleType); + if (scale != null) { + float newScale = currentScale + 0.1f; + scale.setScale(newScale); + entry.setValue(newScale); + } + return false; // Keep in map + }); +} +``` +{{< /tab >}} +{{< /tabs >}} + +{{< callout type="warning" >}} +**Uniform Scale Only:** Hytale's EntityScaleComponent uses a single `float` for uniform scaling on all axes, not per-axis Vector3f scaling. +{{< /callout >}} + +--- + +### ActiveAnimationComponent + +Manages currently playing animations per animation slot. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| activeAnimations | `String[]` | Animation names per slot | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getActiveAnimations()` - Returns array of animation names indexed by `AnimationSlot` +- `setPlayingAnimation(AnimationSlot, String)` - Sets animation for a specific slot +{{< /tab >}} +{{< tab >}} +```java +ComponentType animType = + ActiveAnimationComponent.getComponentType(); +ActiveAnimationComponent anim = store.getComponent(ref,animType); + +// Get all active animations +String[] animations = anim.getActiveAnimations(); + +// Set animation for specific slot +anim.setPlayingAnimation(AnimationSlot.Action, "attack_swing"); +anim.setPlayingAnimation(AnimationSlot.Movement, "walk"); + +// Clear animation on a slot +anim.setPlayingAnimation(AnimationSlot.Emote, null); +``` +{{< /tab >}} +{{< /tabs >}} + +#### AnimationSlot Enum + +Hytale uses animation slots to layer animations: + +| Slot | Value | Purpose | +|------|-------|---------| +| `Movement` | 0 | Locomotion (walk, run, jump) | +| `Status` | 1 | Status effects | +| `Action` | 2 | Actions (attack, use item) | +| `Face` | 3 | Facial expressions | +| `Emote` | 4 | Social emotes | + +--- + +### DynamicLight + +Emits dynamic light from an entity using a `ColorLight` structure. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| colorLight | `ColorLight` | Light data (radius, RGB) | +| isNetworkOutdated | `boolean` | Network sync flag | +{{< /tab >}} +{{< tab >}} +- `getColorLight()` - Returns the `ColorLight` object +- `setColorLight(ColorLight)` - Sets the light configuration +{{< /tab >}} +{{< tab >}} +```java +ComponentType lightType = DynamicLight.getComponentType(); +DynamicLight light = store.getComponent(ref,lightType); + +// Get current light +ColorLight currentLight = light.getColorLight(); + +// Create new light configuration +// ColorLight has public fields: radius, red, green, blue (all bytes) +ColorLight torchLight = new ColorLight(); +torchLight.radius = 8; // Light radius +torchLight.red = (byte)255; // Warm orange color +torchLight.green = (byte)200; +torchLight.blue = (byte)100; + +light.setColorLight(torchLight); + +// Or use constructor +ColorLight blueLight = new ColorLight( + (byte)12, // radius + (byte)50, // red + (byte)100, // green + (byte)255 // blue +); +light.setColorLight(blueLight); +``` +{{< /tab >}} +{{< /tabs >}} + +#### ColorLight Structure + +The `ColorLight` class (`com.hypixel.hytale.protocol.ColorLight`) has public fields: + +| Field | Type | Description | +|-------|------|-------------| +| `radius` | `byte` | Light radius | +| `red` | `byte` | Red component (0-255) | +| `green` | `byte` | Green component (0-255) | +| `blue` | `byte` | Blue component (0-255) | + +--- + +### HeadRotation + +Controls head rotation independently from body. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| rotation | `Vector3f` | Head rotation (x, y, z) | +{{< /tab >}} +{{< tab >}} +- `getRotation()` - Returns head rotation as Vector3f +- `setRotation(Vector3f)` - Sets head rotation (uses `assign()`) +- `getDirection()` - Returns look direction as Vector3d +- `getAxisDirection()` - Returns axis-aligned direction as Vector3i +- `teleportRotation(Vector3f)` - Teleports head rotation +{{< /tab >}} +{{< tab >}} +```java +ComponentType headType = HeadRotation.getComponentType(); +HeadRotation head = store.getComponent(ref,headType); + +// Get current head rotation +Vector3f rot = head.getRotation(); + +// Set head rotation (pitch, yaw, roll) +head.setRotation(new Vector3f(-30, 45, 0)); // Look up-right + +// Get look direction as normalized vector +Vector3d direction = head.getDirection(); + +// Get axis direction (discrete: -1, 0, or 1 per axis) +Vector3i axisDir = head.getAxisDirection(); + +// Teleport rotation (instant, no interpolation) +head.teleportRotation(new Vector3f(0, 180, 0)); // Face backward +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +## Behavior Markers + +Singleton components that act as flags modifying entity behavior. They use the singleton pattern with an `INSTANCE` field. + +### Interactable + +{{< badge "Singleton" >}} + +Marks an entity as interactable by players. + +```java +ComponentType type = Interactable.getComponentType(); + +// Check if entity has component using archetype +Store store = ref.getStore(); +Archetype archetype = store.getArchetype(ref); +if (archetype.contains(type)) { + // Entity is interactable +} + +// Component is singleton - use INSTANCE +Interactable component = Interactable.INSTANCE; +``` + +--- + +### Invulnerable + +{{< badge "Singleton" >}} + +Makes an entity immune to damage. + +```java +ComponentType type = Invulnerable.getComponentType(); + +// Check invulnerability in damage handler +getEventRegistry().register(EntityDamageEvent.class, event -> { + Ref targetRef = event.getTarget(); + if (targetRef != null && targetRef.isValid()) { + Store store = targetRef.getStore(); + Archetype archetype = store.getArchetype(targetRef); + if (archetype.contains(type)) { + event.setCancelled(true); + } + } +}); +``` + +--- + +### Intangible + +{{< badge "Singleton" >}} + +Makes an entity pass through other entities and blocks (no collision). + +```java +ComponentType type = Intangible.getComponentType(); + +// Ghost entity - no collision +Store store = ref.getStore(); +Archetype archetype = store.getArchetype(ref); +if (archetype.contains(type)) { + // Entity passes through obstacles +} +``` + +--- + +## Audio + +### MovementAudioComponent + +Handles movement-related sounds with a predicate system. + +**Package:** `com.hypixel.hytale.server.core.modules.entity.component` + +{{< tabs items="Fields,Methods,Example" >}} +{{< tab >}} +| Field | Type | Description | +|-------|------|-------------| +| shouldHearPredicate | `ShouldHearPredicate` | Predicate for who hears sounds | +| lastInsideBlockTypeId | `int` | Last block entity was inside | +| nextMoveInRepeat | `float` | Timer for movement sound repeat | +{{< /tab >}} +{{< tab >}} +- `getShouldHearPredicate(Ref)` - Returns predicate for sound hearing +- `getLastInsideBlockTypeId()` - Returns last inside block ID +- `setLastInsideBlockTypeId(int)` - Sets last inside block ID +- `canMoveInRepeat()` - Returns true if repeat timer is active +- `tickMoveInRepeat(float)` - Advances repeat timer, returns true if elapsed +- `setNextMoveInRepeat(float)` - Sets next repeat interval +{{< /tab >}} +{{< tab >}} +```java +ComponentType audioType = + MovementAudioComponent.getComponentType(); +MovementAudioComponent movementAudio = store.getComponent(ref,audioType); + +// Get predicate for who should hear movement sounds +MovementAudioComponent.ShouldHearPredicate predicate = + movementAudio.getShouldHearPredicate(entityRef); + +// The predicate returns true for entities that should hear the sound +// (excludes the owner by default) +boolean shouldHear = predicate.test(otherEntityRef); + +// Check move-in repeat timer +if (movementAudio.canMoveInRepeat()) { + float dt = 1.0f / 20.0f; // Delta time + if (movementAudio.tickMoveInRepeat(dt)) { + // Timer elapsed, play repeat sound + } +} + +// Disable repeat sounds +movementAudio.setNextMoveInRepeat(MovementAudioComponent.NO_REPEAT); +``` +{{< /tab >}} +{{< /tabs >}} + +--- + +## Custom Components + +### Creating Custom Components + +Create custom components by implementing `Component`: + +```java +public class CustomDataComponent implements Component { + // Static component type reference (set during registration) + private static ComponentType TYPE; + + // Component data + private int level = 1; + private String faction = "neutral"; + + public static ComponentType getComponentType() { + return TYPE; + } + + public int getLevel() { return level; } + public void setLevel(int level) { this.level = level; } + + public String getFaction() { return faction; } + public void setFaction(String faction) { this.faction = faction; } + + @Override + @Nonnull + public Component clone() { + CustomDataComponent copy = new CustomDataComponent(); + copy.level = this.level; + copy.faction = this.faction; + return copy; + } +} +``` + +### Registering Components + +Register components during plugin start: + +```java +@Override +public void start() { + // Register with EntityStoreRegistry + ComponentType type = + getEntityStoreRegistry().register( + "custom_data", + CustomDataComponent.class, + CustomDataComponent::new + ); + + // Store type reference for static access + CustomDataComponent.TYPE = type; +} +``` + +### Using Custom Components + +```java +// Get component type +ComponentType type = + CustomDataComponent.getComponentType(); + +// Get store from ref +Store store = ref.getStore(); + +// Check if entity has component +Archetype archetype = store.getArchetype(ref); +if (archetype.contains(type)) { + CustomDataComponent data = store.getComponent(ref, type); + + if (data.getLevel() > 10) { + // High level entity logic + } +} +``` + +--- + +## Best Practices + +{{< callout type="info" >}} +**Component Guidelines:** +- Use `getComponentType()` static method to get the `ComponentType` for access +- Always check if a component exists using `hasComponent()` before accessing +- Register custom components in `start()`, after all plugins have completed setup +- Implement `clone()` for components that need copying +- Use singleton pattern (static `INSTANCE`) for marker components +{{< /callout >}} + +{{< callout type="warning" >}} +**Network Sync:** Many components have an `isNetworkOutdated` flag. This is managed automatically by the network system - avoid setting it manually unless you understand the networking layer. +{{< /callout >}} + +{{< callout type="error" >}} +**Thread Safety:** Component access should be done on the world's ticking thread. Use `world.isInThread()` to verify, or use the task system for cross-thread operations. +{{< /callout >}} + +--- + +## Component Type Reference + +| Component | Package | Type | Purpose | +|-----------|---------|------|---------| +| TransformComponent | entity.component | Data | Position, rotation, chunk | +| BoundingBox | entity.component | Data | Collision bounds | +| CollisionResultComponent | entity.component | Data | Collision results | +| PositionDataComponent | entity.component | Data | Block context | +| DisplayNameComponent | entity.component | Data | Visible name | +| ModelComponent | entity.component | Data | 3D model | +| EntityScaleComponent | entity.component | Data | Size scaling | +| ActiveAnimationComponent | entity.component | Data | Animations | +| DynamicLight | entity.component | Data | Light emission | +| HeadRotation | entity.component | Data | Head direction | +| MovementAudioComponent | entity.component | Data | Movement sounds | +| Interactable | entity.component | Singleton | Can be interacted | +| Invulnerable | entity.component | Singleton | Cannot be damaged | +| Intangible | entity.component | Singleton | No collision | diff --git a/content/world/entities/entity-hierarchy.en.md b/content/world/entities/entity-hierarchy.en.md new file mode 100644 index 0000000..44381b1 --- /dev/null +++ b/content/world/entities/entity-hierarchy.en.md @@ -0,0 +1,259 @@ +--- +title: Entity Hierarchy +type: docs +weight: 1 +--- + +Hytale uses an **Entity Component System (ECS)** architecture. Unlike traditional OOP where entities have methods directly on them, Hytale entities are composed of components stored in an `EntityStore`. + +{{< callout type="warning" >}} +**ECS Architecture:** Entity classes like `Player` and `LivingEntity` are themselves `Component` instances. Data is accessed through component lookup, not direct method calls on entities. +{{< /callout >}} + +## Class Hierarchy Overview + +{{< filetree/container >}} + {{< filetree/folder name="Entity (Component)" state="open" >}} + {{< filetree/folder name="LivingEntity" state="open" >}} + {{< filetree/file name="Player" >}} + {{< /filetree/folder >}} + {{< /filetree/folder >}} +{{< /filetree/container >}} + +--- + +## Entity (Base Class) + +**Package:** `com.hypixel.hytale.server.core.entity` + +The base class for all entities. `Entity` implements `Component` - it is stored as a component in the ECS system. + +### Available Methods + +| Method | Return Type | Description | Notes | +|--------|-------------|-------------|-------| +| `getWorld()` | `World` | The world this entity is in | | +| `getUuid()` | `UUID` | Unique identifier | Deprecated | +| `wasRemoved()` | `boolean` | Whether entity was removed | | +| `remove()` | `boolean` | Remove entity from world | | +| `getNetworkId()` | `int` | Network ID for this entity | Deprecated | +| `getTransformComponent()` | `TransformComponent` | Position/rotation data | Deprecated | +| `getReference()` | `Ref` | ECS reference | | + +```java +// Example: Accessing entity data +Entity entity = ...; // obtained from event or lookup + +if (!entity.wasRemoved()) { + World world = entity.getWorld(); + + // Position access (deprecated but works) + TransformComponent transform = entity.getTransformComponent(); + Vector3d position = transform.getPosition(); + + // ECS reference for component access + Ref ref = entity.getReference(); + if (ref != null && ref.isValid()) { + Store store = ref.getStore(); + // Access other components via store + } +} +``` + +--- + +## LivingEntity + +**Package:** `com.hypixel.hytale.server.core.entity` + +Extends Entity with inventory capabilities. Base class for entities that can hold items. + +### Available Methods + +| Method | Return Type | Description | +|--------|-------------|-------------| +| `getInventory()` | `Inventory` | Entity's inventory | +| `setInventory(Inventory)` | `Inventory` | Set inventory | +| `getCurrentFallDistance()` | `double` | Current fall distance | +| `getStatModifiersManager()` | `StatModifiersManager` | Stat modifiers | + +```java +LivingEntity living = (LivingEntity) entity; + +// Inventory access +Inventory inventory = living.getInventory(); + +// Fall distance tracking +double fallDistance = living.getCurrentFallDistance(); + +// Stat modifiers +StatModifiersManager stats = living.getStatModifiersManager(); +``` + +{{< callout type="info" >}} +Health, damage, and effects are managed through the ECS system, not directly on LivingEntity. See [Entity Components]({{< relref "entity-components" >}}) for component-based data access. +{{< /callout >}} + +--- + +## Player + +**Package:** `com.hypixel.hytale.server.core.entity.entities` + +Extends LivingEntity with player-specific features. + +### Available Methods + +| Method | Return Type | Description | +|--------|-------------|-------------| +| `sendMessage(Message)` | `void` | Send message to player | +| `hasPermission(String)` | `boolean` | Check permission | +| `hasPermission(String, boolean)` | `boolean` | Check with default | +| `getDisplayName()` | `String` | Player display name | +| `getGameMode()` | `GameMode` | Current game mode | +| `getInventory()` | `Inventory` | Player inventory | +| `getPlayerRef()` | `PlayerRef` | Thread-safe reference (deprecated) | +| `getWindowManager()` | `WindowManager` | Window management | +| `getPageManager()` | `PageManager` | Page management | +| `getHudManager()` | `HudManager` | HUD management | +| `getHotbarManager()` | `HotbarManager` | Hotbar management | + +```java +Player player = event.getPlayer(); + +if (player != null) { + // Send messages (requires Message object) + player.sendMessage(Message.raw("Welcome!")); + player.sendMessage(Message.translation("greeting.key") + .param("name", player.getDisplayName())); + + // Check permissions + if (player.hasPermission("admin.teleport")) { + // Has permission + } + + // Get display name + String name = player.getDisplayName(); + + // Get inventory + Inventory inventory = player.getInventory(); + + // Get position via TransformComponent (deprecated) + TransformComponent transform = player.getTransformComponent(); + Vector3d position = transform.getPosition(); +} +``` + +--- + +## PlayerRef (Thread-Safe Reference) + +**Package:** `com.hypixel.hytale.server.core.universe` + +`PlayerRef` provides thread-safe access to player data. Use it for async operations or storing references. + +### Available Methods + +| Method | Return Type | Description | +|--------|-------------|-------------| +| `getUsername()` | `String` | Player username | +| `getUuid()` | `UUID` | Player UUID | +| `getWorldUuid()` | `UUID` | Current world UUID | +| `sendMessage(Message)` | `void` | Send message | +| `getReference()` | `Ref` | ECS reference (may be null) | +| `getPacketHandler()` | `PacketHandler` | Network handler | + +```java +// Get PlayerRef from event +PlayerRef playerRef = event.getPlayerRef(); + +// Thread-safe access to player data +String username = playerRef.getUsername(); +UUID uuid = playerRef.getUuid(); + +// Send message (thread-safe) +playerRef.sendMessage(Message.raw("Hello, " + username + "!")); + +// Get ECS reference for component access +Ref ref = playerRef.getReference(); +if (ref != null && ref.isValid()) { + Store store = ref.getStore(); + + // Access Player component + Player player = store.getComponent(ref, Player.getComponentType()); + + // Access TransformComponent for position + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform != null) { + Vector3d position = transform.getPosition(); + } +} +``` + +--- + +## ECS Component Access Pattern + +The correct way to access entity data in Hytale's ECS: + +```java +getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + + // Get ECS reference + Ref ref = playerRef.getReference(); + if (ref == null || !ref.isValid()) { + return; + } + + // Get store for component access + Store store = ref.getStore(); + + // Access various components + Player player = store.getComponent(ref, Player.getComponentType()); + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + + if (player != null && transform != null) { + Vector3d position = transform.getPosition(); + player.sendMessage(Message.raw("You are at: " + position.toString())); + } +}); +``` + +--- + +## Type Checking & Casting + +Use pattern matching for safe type checking: + +```java +public void handleEntity(Entity entity) { + if (entity instanceof Player player) { + player.sendMessage(Message.raw("You're a player!")); + + } else if (entity instanceof LivingEntity living) { + Inventory inv = living.getInventory(); + } +} +``` + +--- + +## Best Practices + +{{< callout type="info" >}} +**Entity Handling Guidelines:** +- Always check `wasRemoved()` before using stored entity references +- Use `PlayerRef` for storing player references across ticks +- Access position via `TransformComponent` (deprecated methods work but ECS is preferred) +- Check `ref.isValid()` before accessing ECS components +{{< /callout >}} + +{{< callout type="warning" >}} +**Thread Safety:** Entity objects are not thread-safe. Use `PlayerRef` for async operations. Never store direct entity references in long-lived data structures without proper cleanup. +{{< /callout >}} + +{{< callout type="error" >}} +**Critical:** Never store `Entity` or `Player` references in static fields or maps without proper cleanup. Use UUIDs and look up entities when needed. +{{< /callout >}} + diff --git a/content/world/entities/entity-hierarchy.fr.md b/content/world/entities/entity-hierarchy.fr.md new file mode 100644 index 0000000..1d39de7 --- /dev/null +++ b/content/world/entities/entity-hierarchy.fr.md @@ -0,0 +1,259 @@ +--- +title: Hiérarchie des Entités +type: docs +weight: 1 +--- + +Hytale utilise une architecture **Entity Component System (ECS)**. Contrairement à la POO traditionnelle où les entités ont des méthodes directement sur elles, les entités Hytale sont composées de composants stockés dans un `EntityStore`. + +{{< callout type="warning" >}} +**Architecture ECS:** Les classes d'entités comme `Player` et `LivingEntity` sont elles-mêmes des instances de `Component`. Les données sont accessibles via la recherche de composants, pas via des appels de méthodes directs sur les entités. +{{< /callout >}} + +## Aperçu de la Hiérarchie de Classes + +{{< filetree/container >}} + {{< filetree/folder name="Entity (Component)" state="open" >}} + {{< filetree/folder name="LivingEntity" state="open" >}} + {{< filetree/file name="Player" >}} + {{< /filetree/folder >}} + {{< /filetree/folder >}} +{{< /filetree/container >}} + +--- + +## Entity (Classe de Base) + +**Package:** `com.hypixel.hytale.server.core.entity` + +La classe de base pour toutes les entités. `Entity` implémente `Component` - elle est stockée comme composant dans le système ECS. + +### Méthodes Disponibles + +| Méthode | Type de Retour | Description | Notes | +|---------|----------------|-------------|-------| +| `getWorld()` | `World` | Le monde où se trouve l'entité | | +| `getUuid()` | `UUID` | Identifiant unique | Dépréciée | +| `wasRemoved()` | `boolean` | Si l'entité a été supprimée | | +| `remove()` | `boolean` | Supprimer l'entité du monde | | +| `getNetworkId()` | `int` | ID réseau de l'entité | Dépréciée | +| `getTransformComponent()` | `TransformComponent` | Données de position/rotation | Dépréciée | +| `getReference()` | `Ref` | Référence ECS | | + +```java +// Exemple: Accéder aux données d'entité +Entity entity = ...; // obtenue depuis un événement ou une recherche + +if (!entity.wasRemoved()) { + World world = entity.getWorld(); + + // Accès à la position (dépréciée mais fonctionne) + TransformComponent transform = entity.getTransformComponent(); + Vector3d position = transform.getPosition(); + + // Référence ECS pour l'accès aux composants + Ref ref = entity.getReference(); + if (ref != null && ref.isValid()) { + Store store = ref.getStore(); + // Accéder à d'autres composants via le store + } +} +``` + +--- + +## LivingEntity + +**Package:** `com.hypixel.hytale.server.core.entity` + +Étend Entity avec des capacités d'inventaire. Classe de base pour les entités qui peuvent porter des objets. + +### Méthodes Disponibles + +| Méthode | Type de Retour | Description | +|---------|----------------|-------------| +| `getInventory()` | `Inventory` | Inventaire de l'entité | +| `setInventory(Inventory)` | `Inventory` | Définir l'inventaire | +| `getCurrentFallDistance()` | `double` | Distance de chute actuelle | +| `getStatModifiersManager()` | `StatModifiersManager` | Modificateurs de stats | + +```java +LivingEntity living = (LivingEntity) entity; + +// Accès à l'inventaire +Inventory inventory = living.getInventory(); + +// Suivi de la distance de chute +double fallDistance = living.getCurrentFallDistance(); + +// Modificateurs de stats +StatModifiersManager stats = living.getStatModifiersManager(); +``` + +{{< callout type="info" >}} +La santé, les dégâts et les effets sont gérés via le système ECS, pas directement sur LivingEntity. Voir [Composants d'Entité]({{< relref "entity-components" >}}) pour l'accès aux données basé sur les composants. +{{< /callout >}} + +--- + +## Player + +**Package:** `com.hypixel.hytale.server.core.entity.entities` + +Étend LivingEntity avec des fonctionnalités spécifiques aux joueurs. + +### Méthodes Disponibles + +| Méthode | Type de Retour | Description | +|---------|----------------|-------------| +| `sendMessage(Message)` | `void` | Envoyer un message au joueur | +| `hasPermission(String)` | `boolean` | Vérifier une permission | +| `hasPermission(String, boolean)` | `boolean` | Vérifier avec valeur par défaut | +| `getDisplayName()` | `String` | Nom d'affichage du joueur | +| `getGameMode()` | `GameMode` | Mode de jeu actuel | +| `getInventory()` | `Inventory` | Inventaire du joueur | +| `getPlayerRef()` | `PlayerRef` | Référence thread-safe (dépréciée) | +| `getWindowManager()` | `WindowManager` | Gestion des fenêtres | +| `getPageManager()` | `PageManager` | Gestion des pages | +| `getHudManager()` | `HudManager` | Gestion du HUD | +| `getHotbarManager()` | `HotbarManager` | Gestion de la barre d'action | + +```java +Player player = event.getPlayer(); + +if (player != null) { + // Envoyer des messages (nécessite un objet Message) + player.sendMessage(Message.raw("Bienvenue !")); + player.sendMessage(Message.translation("greeting.key") + .param("name", player.getDisplayName())); + + // Vérifier les permissions + if (player.hasPermission("admin.teleport")) { + // A la permission + } + + // Obtenir le nom d'affichage + String name = player.getDisplayName(); + + // Obtenir l'inventaire + Inventory inventory = player.getInventory(); + + // Obtenir la position via TransformComponent (dépréciée) + TransformComponent transform = player.getTransformComponent(); + Vector3d position = transform.getPosition(); +} +``` + +--- + +## PlayerRef (Référence Thread-Safe) + +**Package:** `com.hypixel.hytale.server.core.universe` + +`PlayerRef` fournit un accès thread-safe aux données du joueur. Utilisez-le pour les opérations async ou pour stocker des références. + +### Méthodes Disponibles + +| Méthode | Type de Retour | Description | +|---------|----------------|-------------| +| `getUsername()` | `String` | Nom d'utilisateur du joueur | +| `getUuid()` | `UUID` | UUID du joueur | +| `getWorldUuid()` | `UUID` | UUID du monde actuel | +| `sendMessage(Message)` | `void` | Envoyer un message | +| `getReference()` | `Ref` | Référence ECS (peut être null) | +| `getPacketHandler()` | `PacketHandler` | Gestionnaire réseau | + +```java +// Obtenir PlayerRef depuis l'événement +PlayerRef playerRef = event.getPlayerRef(); + +// Accès thread-safe aux données du joueur +String username = playerRef.getUsername(); +UUID uuid = playerRef.getUuid(); + +// Envoyer un message (thread-safe) +playerRef.sendMessage(Message.raw("Bonjour, " + username + " !")); + +// Obtenir la référence ECS pour l'accès aux composants +Ref ref = playerRef.getReference(); +if (ref != null && ref.isValid()) { + Store store = ref.getStore(); + + // Accéder au composant Player + Player player = store.getComponent(ref, Player.getComponentType()); + + // Accéder au TransformComponent pour la position + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform != null) { + Vector3d position = transform.getPosition(); + } +} +``` + +--- + +## Pattern d'Accès aux Composants ECS + +La bonne façon d'accéder aux données d'entité dans l'ECS de Hytale: + +```java +getEventRegistry().register(PlayerConnectEvent.class, event -> { + PlayerRef playerRef = event.getPlayerRef(); + + // Obtenir la référence ECS + Ref ref = playerRef.getReference(); + if (ref == null || !ref.isValid()) { + return; + } + + // Obtenir le store pour l'accès aux composants + Store store = ref.getStore(); + + // Accéder à divers composants + Player player = store.getComponent(ref, Player.getComponentType()); + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + + if (player != null && transform != null) { + Vector3d position = transform.getPosition(); + player.sendMessage(Message.raw("Vous êtes à : " + position.toString())); + } +}); +``` + +--- + +## Vérification de Type & Cast + +Utilisez le pattern matching pour une vérification de type sûre: + +```java +public void handleEntity(Entity entity) { + if (entity instanceof Player player) { + player.sendMessage(Message.raw("Vous êtes un joueur !")); + + } else if (entity instanceof LivingEntity living) { + Inventory inv = living.getInventory(); + } +} +``` + +--- + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Directives de Gestion des Entités:** +- Toujours vérifier `wasRemoved()` avant d'utiliser des références d'entités stockées +- Utiliser `PlayerRef` pour stocker des références de joueurs entre les ticks +- Accéder à la position via `TransformComponent` (les méthodes dépréciées fonctionnent mais l'ECS est préféré) +- Vérifier `ref.isValid()` avant d'accéder aux composants ECS +{{< /callout >}} + +{{< callout type="warning" >}} +**Sécurité des Threads:** Les objets Entity ne sont pas thread-safe. Utilisez `PlayerRef` pour les opérations async. Ne stockez jamais de références directes d'entités dans des structures de données à longue durée de vie sans nettoyage approprié. +{{< /callout >}} + +{{< callout type="error" >}} +**Critique:** Ne stockez jamais de références `Entity` ou `Player` dans des champs statiques ou des maps sans nettoyage approprié. Utilisez les UUID et recherchez les entités quand nécessaire. +{{< /callout >}} + diff --git a/content/world/entities/flocking-behavior.en.md b/content/world/entities/flocking-behavior.en.md new file mode 100644 index 0000000..6a95faa --- /dev/null +++ b/content/world/entities/flocking-behavior.en.md @@ -0,0 +1,302 @@ +--- +title: Flocking Behavior +type: docs +weight: 10 +--- + +The flock system enables NPCs to form and behave as coordinated groups. + +**Package:** `com.hypixel.hytale.server.flock` + +## Overview + +Flocks are entity-based groups that allow NPCs to: +- Move and act as a coordinated unit +- Share damage information across members +- Spawn in configurable group sizes +- Have designated leaders + +## Architecture + +``` +Flock System +├── Components +│ ├── Flock - Group entity component +│ ├── FlockMembership - Member reference +│ └── PersistentFlockData - Saved flock data +├── Assets +│ ├── FlockAsset - Flock configuration +│ ├── RangeSizeFlockAsset - Range-based size +│ └── WeightedSizeFlockAsset - Weighted size +├── Systems +│ ├── FlockSystems - Core flock logic +│ ├── FlockMembershipSystems - Member management +│ └── FlockDeathSystems - Death handling +└── NPC Components + ├── BodyMotionFlock - Movement behavior + ├── ActionFlockJoin/Leave - Join/leave actions + └── SensorFlockLeader - Leader detection +``` + +## Flock Component + +The flock entity holds shared group state: + +```java +public class Flock implements Component { + // Shared damage data for combat coordination + private DamageData currentDamageData; + private DamageData nextDamageData; + + // Leader-specific damage data + private DamageData currentLeaderDamageData; + private DamageData nextLeaderDamageData; + + // Persistent group configuration + private PersistentFlockData flockData; + + // Removal status + public enum FlockRemovedStatus { + NOT_REMOVED, + DISSOLVED, + UNLOADED + } +} +``` + +## FlockMembership Component + +Each NPC member has a membership component: + +```java +public class FlockMembership implements Component { + // Reference to the flock entity + private Ref flockRef; +} +``` + +## Creating Flocks + +### Spawn with Flock + +```java +// Spawn an NPC with its flock +Ref flockRef = FlockPlugin.trySpawnFlock( + npcRef, // Initial NPC reference + npc, // NPC component + store, // Entity store + roleIndex, // NPC role index + position, // Spawn position + rotation, // Spawn rotation + flockDefinition, // FlockAsset config (determines size) + postSpawnCallback // Called for each spawned member +); +``` + +### Create Empty Flock + +```java +// Create flock entity for manual member management +Ref flockRef = FlockPlugin.createFlock(store, role); + +// Or with explicit configuration +Ref flockRef = FlockPlugin.createFlock( + store, + flockAsset, // FlockAsset configuration + allowedRoles // Roles that can join +); +``` + +### Join Existing Flock + +```java +// Add NPC to existing flock +FlockMembershipSystems.join(npcRef, flockRef, store); +``` + +## Flock Assets + +### FlockAsset + +Base configuration for flocks: + +```java +public class FlockAsset { + // Asset identifier + private String id; + + // Pick group size for spawning + public abstract int pickFlockSize(); +} +``` + +### RangeSizeFlockAsset + +Flock with random size in range: + +```json +{ + "Type": "RangeSize", + "Id": "wolf_pack", + "MinSize": 3, + "MaxSize": 8 +} +``` + +### WeightedSizeFlockAsset + +Flock with weighted random size: + +```json +{ + "Type": "WeightedSize", + "Id": "deer_herd", + "Sizes": [ + { "Size": 2, "Weight": 1 }, + { "Size": 4, "Weight": 2 }, + { "Size": 6, "Weight": 1 } + ] +} +``` + +## NPC Core Components + +### BodyMotionFlock + +Controls flocking movement behavior: + +```json +{ + "Type": "Flock", + "SeparationWeight": 1.5, + "AlignmentWeight": 1.0, + "CohesionWeight": 1.0 +} +``` + +### ActionFlockJoin + +Join a flock: + +```json +{ + "Type": "JoinFlock", + "FlockId": "wolf_pack" +} +``` + +### ActionFlockLeave + +Leave current flock: + +```json +{ + "Type": "LeaveFlock" +} +``` + +### SensorFlockLeader + +Detect flock leader: + +```json +{ + "Type": "FlockLeader", + "Output": "leader_ref" +} +``` + +### EntityFilterFlock + +Filter entities by flock membership: + +```json +{ + "Type": "Flock", + "IncludeSelf": false, + "OnlyMembers": true +} +``` + +## Damage Sharing + +Flocks share combat information: + +```java +// Get damage data for the flock +Flock flock = store.getComponent(flockRef, Flock.getComponentType()); +DamageData damageData = flock.getDamageData(); + +// Track kills for the flock +flock.onTargetKilled(componentAccessor, targetRef); +``` + +### Double Buffering + +Damage data uses double buffering to avoid race conditions: + +```java +// Called each tick +flock.swapDamageDataBuffers(); +// currentDamageData contains last tick's data +// nextDamageData accumulates current tick's data +``` + +## Conditions + +### FlockSizeCondition + +Check flock size in decision making: + +```json +{ + "Type": "FlockSize", + "Min": 2, + "Max": 10 +} +``` + +## Plugin Access + +```java +FlockPlugin flockPlugin = FlockPlugin.get(); + +// Component types +ComponentType flockType = + flockPlugin.getFlockComponentType(); +ComponentType membershipType = + flockPlugin.getFlockMembershipComponentType(); +ComponentType dataType = + flockPlugin.getPersistentFlockDataComponentType(); +``` + +## Utility Methods + +```java +// Check if entity is in a flock +boolean isMember = FlockPlugin.isFlockMember(npcRef, store); + +// Get flock reference from entity +Ref flockRef = FlockPlugin.getFlockReference(npcRef, store); + +// Get flock component from entity +Flock flock = FlockPlugin.getFlock(store, npcRef); +``` + +## Flock Spawning Behavior + +When spawning a flock: +1. Initial NPC is created +2. Flock entity is created with membership +3. Additional members spawn at same location +4. Members spread with slight random offset +5. Each member joins the flock + +```java +// Members spawn with random offset +memberTransform.getPosition().assign( + x + RandomExtra.randomRange(-0.5, 0.5), + offsetY, + z + RandomExtra.randomRange(-0.5, 0.5) +); +``` diff --git a/content/world/entities/flocking-behavior.fr.md b/content/world/entities/flocking-behavior.fr.md new file mode 100644 index 0000000..29c3f53 --- /dev/null +++ b/content/world/entities/flocking-behavior.fr.md @@ -0,0 +1,302 @@ +--- +title: Comportement de Groupe +type: docs +weight: 10 +--- + +Le systeme de groupe (flock) permet aux NPCs de former et se comporter comme des unites coordonnees. + +**Package:** `com.hypixel.hytale.server.flock` + +## Apercu + +Les groupes sont des ensembles bases sur des entites qui permettent aux NPCs de: +- Se deplacer et agir comme une unite coordonnee +- Partager les informations de degats entre membres +- Apparaitre en tailles de groupe configurables +- Avoir des leaders designes + +## Architecture + +``` +Systeme Groupe +├── Composants +│ ├── Flock - Composant entite groupe +│ ├── FlockMembership - Reference membre +│ └── PersistentFlockData - Donnees groupe sauvegardees +├── Assets +│ ├── FlockAsset - Configuration groupe +│ ├── RangeSizeFlockAsset - Taille par plage +│ └── WeightedSizeFlockAsset - Taille ponderee +├── Systemes +│ ├── FlockSystems - Logique groupe principale +│ ├── FlockMembershipSystems - Gestion membres +│ └── FlockDeathSystems - Gestion mort +└── Composants NPC + ├── BodyMotionFlock - Comportement mouvement + ├── ActionFlockJoin/Leave - Actions joindre/quitter + └── SensorFlockLeader - Detection leader +``` + +## Composant Flock + +L'entite groupe contient l'etat partage: + +```java +public class Flock implements Component { + // Donnees degats partagees pour coordination combat + private DamageData currentDamageData; + private DamageData nextDamageData; + + // Donnees degats specifiques leader + private DamageData currentLeaderDamageData; + private DamageData nextLeaderDamageData; + + // Configuration groupe persistante + private PersistentFlockData flockData; + + // Statut de suppression + public enum FlockRemovedStatus { + NOT_REMOVED, + DISSOLVED, + UNLOADED + } +} +``` + +## Composant FlockMembership + +Chaque NPC membre a un composant d'appartenance: + +```java +public class FlockMembership implements Component { + // Reference vers l'entite groupe + private Ref flockRef; +} +``` + +## Creation de Groupes + +### Spawn avec Groupe + +```java +// Faire apparaitre un NPC avec son groupe +Ref flockRef = FlockPlugin.trySpawnFlock( + npcRef, // Reference NPC initial + npc, // Composant NPC + store, // Store entite + roleIndex, // Index role NPC + position, // Position spawn + rotation, // Rotation spawn + flockDefinition, // Config FlockAsset (determine taille) + postSpawnCallback // Appele pour chaque membre spawne +); +``` + +### Creer Groupe Vide + +```java +// Creer entite groupe pour gestion manuelle des membres +Ref flockRef = FlockPlugin.createFlock(store, role); + +// Ou avec configuration explicite +Ref flockRef = FlockPlugin.createFlock( + store, + flockAsset, // Configuration FlockAsset + allowedRoles // Roles pouvant rejoindre +); +``` + +### Rejoindre Groupe Existant + +```java +// Ajouter NPC a groupe existant +FlockMembershipSystems.join(npcRef, flockRef, store); +``` + +## Assets de Groupe + +### FlockAsset + +Configuration de base pour groupes: + +```java +public class FlockAsset { + // Identifiant asset + private String id; + + // Choisir taille groupe pour spawn + public abstract int pickFlockSize(); +} +``` + +### RangeSizeFlockAsset + +Groupe avec taille aleatoire dans plage: + +```json +{ + "Type": "RangeSize", + "Id": "wolf_pack", + "MinSize": 3, + "MaxSize": 8 +} +``` + +### WeightedSizeFlockAsset + +Groupe avec taille aleatoire ponderee: + +```json +{ + "Type": "WeightedSize", + "Id": "deer_herd", + "Sizes": [ + { "Size": 2, "Weight": 1 }, + { "Size": 4, "Weight": 2 }, + { "Size": 6, "Weight": 1 } + ] +} +``` + +## Composants Core NPC + +### BodyMotionFlock + +Controle comportement mouvement de groupe: + +```json +{ + "Type": "Flock", + "SeparationWeight": 1.5, + "AlignmentWeight": 1.0, + "CohesionWeight": 1.0 +} +``` + +### ActionFlockJoin + +Rejoindre un groupe: + +```json +{ + "Type": "JoinFlock", + "FlockId": "wolf_pack" +} +``` + +### ActionFlockLeave + +Quitter groupe actuel: + +```json +{ + "Type": "LeaveFlock" +} +``` + +### SensorFlockLeader + +Detecter leader du groupe: + +```json +{ + "Type": "FlockLeader", + "Output": "leader_ref" +} +``` + +### EntityFilterFlock + +Filtrer entites par appartenance groupe: + +```json +{ + "Type": "Flock", + "IncludeSelf": false, + "OnlyMembers": true +} +``` + +## Partage de Degats + +Les groupes partagent informations de combat: + +```java +// Obtenir donnees degats du groupe +Flock flock = store.getComponent(flockRef, Flock.getComponentType()); +DamageData damageData = flock.getDamageData(); + +// Suivre kills pour le groupe +flock.onTargetKilled(componentAccessor, targetRef); +``` + +### Double Buffering + +Les donnees de degats utilisent double buffering pour eviter conditions de course: + +```java +// Appele chaque tick +flock.swapDamageDataBuffers(); +// currentDamageData contient donnees du tick precedent +// nextDamageData accumule donnees du tick actuel +``` + +## Conditions + +### FlockSizeCondition + +Verifier taille groupe dans prise de decision: + +```json +{ + "Type": "FlockSize", + "Min": 2, + "Max": 10 +} +``` + +## Acces au Plugin + +```java +FlockPlugin flockPlugin = FlockPlugin.get(); + +// Types de composants +ComponentType flockType = + flockPlugin.getFlockComponentType(); +ComponentType membershipType = + flockPlugin.getFlockMembershipComponentType(); +ComponentType dataType = + flockPlugin.getPersistentFlockDataComponentType(); +``` + +## Methodes Utilitaires + +```java +// Verifier si entite dans un groupe +boolean isMember = FlockPlugin.isFlockMember(npcRef, store); + +// Obtenir reference groupe depuis entite +Ref flockRef = FlockPlugin.getFlockReference(npcRef, store); + +// Obtenir composant groupe depuis entite +Flock flock = FlockPlugin.getFlock(store, npcRef); +``` + +## Comportement de Spawn des Groupes + +Lors du spawn d'un groupe: +1. NPC initial est cree +2. Entite groupe est creee avec appartenance +3. Membres additionnels spawn au meme emplacement +4. Membres se dispersent avec leger offset aleatoire +5. Chaque membre rejoint le groupe + +```java +// Membres spawn avec offset aleatoire +memberTransform.getPosition().assign( + x + RandomExtra.randomRange(-0.5, 0.5), + offsetY, + z + RandomExtra.randomRange(-0.5, 0.5) +); +``` diff --git a/content/world/entities/inventory/_index.en.md b/content/world/entities/inventory/_index.en.md new file mode 100644 index 0000000..7cac8c3 --- /dev/null +++ b/content/world/entities/inventory/_index.en.md @@ -0,0 +1,57 @@ +--- +title: Inventory +type: docs +weight: 7 +--- + +The inventory system in Hytale handles items, containers, and player inventories. + +{{< cards >}} + {{< card link="itemstacks" title="ItemStacks" subtitle="Working with immutable item stacks" >}} + {{< card link="containers" title="Containers" subtitle="Inventory and container types" >}} + {{< card link="transactions" title="Transactions" subtitle="Safe item modifications" >}} +{{< /cards >}} + +## Overview + +The inventory system follows an immutable pattern: +- **ItemStack** - Immutable representation of items +- **Inventory** - Player's personal inventory +- **ItemContainer** - Generic container for items +- **Transactions** - Safe way to modify items + +## Quick Start + +```java +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import java.util.logging.Level; + +// Get player inventory +Player player = ...; +Inventory inv = player.getInventory(); + +// Get item in hand (via Inventory, not directly on Player) +ItemStack hand = inv.getItemInHand(); + +// Check if item exists +if (hand != null && !hand.isEmpty()) { + // Use getItemId() - Item.getName() doesn't exist + getLogger().at(Level.INFO).log("Holding: " + hand.getItemId()); +} +``` + +## Key Concepts + +{{< callout type="info" >}} +ItemStack is **immutable**. Methods like `withQuantity()` return a new ItemStack rather than modifying the original. +{{< /callout >}} + +```java +// Immutable pattern +ItemStack original = new ItemStack("iron_sword", 1); +ItemStack modified = original.withQuantity(5); // New instance + +// original still has quantity of 1 +// modified has quantity of 5 +``` diff --git a/content/world/entities/inventory/_index.fr.md b/content/world/entities/inventory/_index.fr.md new file mode 100644 index 0000000..9dd32fa --- /dev/null +++ b/content/world/entities/inventory/_index.fr.md @@ -0,0 +1,57 @@ +--- +title: Inventaire +type: docs +weight: 7 +--- + +Le système d'inventaire dans Hytale gère les objets, conteneurs et inventaires des joueurs. + +{{< cards >}} + {{< card link="itemstacks" title="ItemStacks" subtitle="Travailler avec les piles d'objets immuables" >}} + {{< card link="containers" title="Conteneurs" subtitle="Types d'inventaires et conteneurs" >}} + {{< card link="transactions" title="Transactions" subtitle="Modifications sûres des objets" >}} +{{< /cards >}} + +## Aperçu + +Le système d'inventaire suit un pattern immuable : +- **ItemStack** - Représentation immuable des objets +- **Inventory** - Inventaire personnel du joueur +- **ItemContainer** - Conteneur générique pour les objets +- **Transactions** - Moyen sûr de modifier les objets + +## Démarrage Rapide + +```java +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import java.util.logging.Level; + +// Obtenir l'inventaire du joueur +Player player = ...; +Inventory inv = player.getInventory(); + +// Obtenir l'objet en main (via Inventory, pas directement sur Player) +ItemStack hand = inv.getItemInHand(); + +// Vérifier si l'objet existe +if (hand != null && !hand.isEmpty()) { + // Utiliser getItemId() - Item.getName() n'existe pas + getLogger().at(Level.INFO).log("Tient : " + hand.getItemId()); +} +``` + +## Concepts Clés + +{{< callout type="info" >}} +ItemStack est **immuable**. Les méthodes comme `withQuantity()` retournent un nouveau ItemStack plutôt que de modifier l'original. +{{< /callout >}} + +```java +// Pattern immuable +ItemStack original = new ItemStack("iron_sword", 1); +ItemStack modified = original.withQuantity(5); // Nouvelle instance + +// original a toujours une quantité de 1 +// modified a une quantité de 5 +``` diff --git a/content/world/entities/inventory/containers.en.md b/content/world/entities/inventory/containers.en.md new file mode 100644 index 0000000..bcf6e4b --- /dev/null +++ b/content/world/entities/inventory/containers.en.md @@ -0,0 +1,668 @@ +--- +title: Containers +type: docs +weight: 2 +--- + +Containers hold and manage collections of ItemStacks in Hytale. The inventory system uses `ItemContainer` as the base abstraction with `Inventory` as the player's specialized multi-section container. + +## Inventory Structure + +Player inventories are composed of multiple `ItemContainer` sections: + +```java +Inventory inv = player.getInventory(); + +// Get individual sections +ItemContainer storage = inv.getStorage(); // Main storage (36 slots default) +ItemContainer hotbar = inv.getHotbar(); // Hotbar (9 slots default) +ItemContainer armor = inv.getArmor(); // Armor slots +ItemContainer utility = inv.getUtility(); // Utility items (4 slots default) +ItemContainer backpack = inv.getBackpack(); // Backpack (expandable) + +// Combined containers for operations across multiple sections +CombinedItemContainer hotbarFirst = inv.getCombinedHotbarFirst(); +CombinedItemContainer storageFirst = inv.getCombinedStorageFirst(); +``` + +### Section IDs + +Each section has a constant ID for operations: + +| Section | ID | Constant | +|---------|----|----| +| Hotbar | -1 | `Inventory.HOTBAR_SECTION_ID` | +| Storage | -2 | `Inventory.STORAGE_SECTION_ID` | +| Armor | -3 | `Inventory.ARMOR_SECTION_ID` | +| Utility | -5 | `Inventory.UTILITY_SECTION_ID` | +| Tools | -8 | `Inventory.TOOLS_SECTION_ID` (deprecated) | +| Backpack | -9 | `Inventory.BACKPACK_SECTION_ID` | + +## ItemContainer Basics + +### Reading Items + +```java +ItemContainer container = inv.getStorage(); + +// Get capacity (NOT getSize!) +short capacity = container.getCapacity(); + +// Get item at slot (slot is SHORT type) +ItemStack item = container.getItemStack((short) 0); + +// Check if empty +boolean empty = container.isEmpty(); + +// Safe item access +ItemStack stack = container.getItemStack((short) slot); +if (!ItemStack.isEmpty(stack)) { + String itemId = stack.getItemId(); + int quantity = stack.getQuantity(); +} +``` + +### Setting Items + +```java +ItemContainer container = inv.getHotbar(); + +// Set item at slot - returns a Transaction +ItemStackSlotTransaction transaction = container.setItemStackForSlot( + (short) 0, + new ItemStack("iron_sword", 1) +); + +// Check if operation succeeded +if (transaction.succeeded()) { + // Item was set successfully +} + +// Clear a slot +container.setItemStackForSlot((short) 0, null); +``` + +### Adding Items + +```java +ItemContainer container = inv.getStorage(); +ItemStack toAdd = new ItemStack("wood_plank", 64); + +// Add to first available slot (stacks with existing items first) +ItemStackTransaction transaction = container.addItemStack(toAdd); + +// Check for remaining items that couldn't fit +ItemStack remainder = transaction.getRemainder(); +if (!ItemStack.isEmpty(remainder)) { + // Some items couldn't fit + int leftover = remainder.getQuantity(); +} + +// Add to specific slot +ItemStackSlotTransaction slotTransaction = container.addItemStackToSlot( + (short) 5, + toAdd +); + +// Check if container can fit items before adding +if (container.canAddItemStack(toAdd)) { + container.addItemStack(toAdd); +} +``` + +### Removing Items + +```java +// Remove from specific slot +SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + +// Remove specific quantity from slot +ItemStackSlotTransaction removeTransaction = container.removeItemStackFromSlot( + (short) 0, + 10 // quantity to remove +); + +// Remove specific item type from anywhere in container +ItemStack toRemove = new ItemStack("iron_ore", 5); +ItemStackTransaction itemTransaction = container.removeItemStack(toRemove); + +// Check if items can be removed before removing +if (container.canRemoveItemStack(toRemove)) { + container.removeItemStack(toRemove); +} +``` + +## Active Slots + +Players have active slots for hotbar and utility items: + +```java +Inventory inv = player.getInventory(); + +// Get active hotbar slot (0-8) +byte activeHotbar = inv.getActiveHotbarSlot(); + +// Set active hotbar slot +inv.setActiveHotbarSlot((byte) 3); + +// Get item currently in hand +ItemStack handItem = inv.getItemInHand(); + +// Get/set active utility slot +byte activeUtility = inv.getActiveUtilitySlot(); +inv.setActiveUtilitySlot((byte) 1); + +// Get utility item +ItemStack utilityItem = inv.getUtilityItem(); +``` + +## Moving Items + +### Between Slots + +```java +Inventory inv = player.getInventory(); +ItemContainer storage = inv.getStorage(); +ItemContainer hotbar = inv.getHotbar(); + +// Move item from one slot to another in same container +MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, // from slot + 64, // quantity + storage, // to container + (short) 5 // to slot +); + +// Move between different containers +storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 +); + +// Use Inventory's moveItem for section-based moves +inv.moveItem( + Inventory.STORAGE_SECTION_ID, // from section + 5, // from slot + 10, // quantity + Inventory.HOTBAR_SECTION_ID, // to section + 0 // to slot +); +``` + +### Smart Moving + +```java +// Smart move considers item type for optimal placement +inv.smartMoveItem( + Inventory.STORAGE_SECTION_ID, + 0, // slot + 64, // quantity + SmartMoveType.EquipOrMergeStack // tries to equip armor or merge stacks +); +``` + +## Iterating Containers + +```java +ItemContainer container = inv.getStorage(); + +// Iterate all non-empty slots +container.forEach((slot, itemStack) -> { + getLogger().at(Level.INFO).log("Slot " + slot + ": " + + itemStack.getItemId() + " x" + itemStack.getQuantity()); +}); + +// Count items matching a condition +int swordCount = container.countItemStacks( + stack -> stack.getItemId().contains("sword") +); + +// Check if container has stackable items +ItemStack testStack = new ItemStack("stone", 1); +boolean hasStackable = container.containsItemStacksStackableWith(testStack); +``` + +## Checking Contents + +```java +ItemContainer container = inv.getStorage(); + +// Check if inventory contains specific item type and quantity +public boolean hasItems(ItemContainer container, String itemId, int amount) { + int count = container.countItemStacks( + stack -> stack.getItemId().equals(itemId) + ); + return count >= amount; +} + +// Find first slot with specific item +public short findSlotWithItem(ItemContainer container, String itemId) { + for (short i = 0; i < container.getCapacity(); i++) { + ItemStack stack = container.getItemStack(i); + if (!ItemStack.isEmpty(stack) && stack.getItemId().equals(itemId)) { + return i; + } + } + return -1; // Not found +} + +// Count empty slots +public int countEmptySlots(ItemContainer container) { + int empty = 0; + for (short i = 0; i < container.getCapacity(); i++) { + if (ItemStack.isEmpty(container.getItemStack(i))) { + empty++; + } + } + return empty; +} +``` + +## Clearing Containers + +```java +ItemContainer container = inv.getStorage(); + +// Clear entire container +ClearTransaction transaction = container.clear(); + +// Drop all items (returns list of dropped items) +List droppedItems = container.dropAllItemStacks(); + +// Remove all items (returns list) +List removedItems = container.removeAllItemStacks(); + +// Clear entire player inventory +inv.clear(); + +// Drop all from player inventory +List allDropped = inv.dropAllItemStacks(); +``` + +## Sorting + +Available `SortType` values: `NAME`, `TYPE`, `RARITY` + +```java +ItemContainer container = inv.getStorage(); + +// Sort items +container.sortItems(SortType.NAME); + +// Sort via Inventory (also saves sort preference) +inv.sortStorage(SortType.NAME); +inv.setSortType(SortType.RARITY); // Or TYPE +``` + +## Container Events + +```java +// Register for container change events +container.registerChangeEvent(event -> { + ItemContainer changedContainer = event.container(); + Transaction transaction = event.transaction(); + + getLogger().at(Level.INFO).log("Container changed!"); +}); + +// With priority +container.registerChangeEvent(EventPriority.EARLY, event -> { + // Handle early +}); +``` + +## Transaction System + +All modification operations return Transaction objects: + +```java +// Transactions track success and changes +ItemStackTransaction transaction = container.addItemStack(itemStack); + +if (transaction.succeeded()) { + ItemStack remainder = transaction.getRemainder(); + // ... +} + +// Slot transactions include slot info +ItemStackSlotTransaction slotTransaction = container.setItemStackForSlot( + (short) 0, + itemStack +); + +if (slotTransaction.succeeded()) { + short slot = slotTransaction.getSlot(); + ItemStack before = slotTransaction.getSlotBefore(); + ItemStack after = slotTransaction.getSlotAfter(); +} +``` + +## Operation Parameters + +Most container operations support optional parameters: + +```java +// Default values +DEFAULT_ADD_ALL_OR_NOTHING = false; // Partial adds allowed +DEFAULT_REMOVE_ALL_OR_NOTHING = true; // Only full removals +DEFAULT_FULL_STACKS = false; // Can split stacks +DEFAULT_EXACT_AMOUNT = true; // Exact quantities only +DEFAULT_FILTER = true; // Apply slot filters + +// Add with parameters +container.addItemStack(itemStack, allOrNothing, fullStacks, filter); + +// allOrNothing: if true, fails entirely if not all items fit +// fullStacks: if true, only fills empty slots (no stacking) +// filter: if true, respects slot filters +``` + +## Material and Resource Removal + +Containers support removing items by material type or resource type (used for crafting): + +{{< callout type="info" >}} +**MaterialQuantity constructor:** `MaterialQuantity(itemId, resourceTypeId, tag, quantity, metadata)` + +At least one of `itemId`, `resourceTypeId`, or `tag` must be non-null. +{{< /callout >}} + +```java +// Remove by material using itemId +MaterialQuantity materialByItem = new MaterialQuantity("iron_ingot", null, null, 5, null); +if (container.canRemoveMaterial(materialByItem)) { + MaterialTransaction transaction = container.removeMaterial(materialByItem); +} + +// Remove by material using resourceTypeId +MaterialQuantity materialByResource = new MaterialQuantity(null, "iron", null, 5, null); +if (container.canRemoveMaterial(materialByResource)) { + MaterialTransaction transaction = container.removeMaterial(materialByResource); +} + +// Remove by resource type (simpler constructor) +ResourceQuantity resource = new ResourceQuantity("wood", 10); +if (container.canRemoveResource(resource)) { + ResourceTransaction transaction = container.removeResource(resource); +} + +// Remove by tag index +if (container.canRemoveTag(tagIndex, quantity)) { + TagTransaction transaction = container.removeTag(tagIndex, quantity); +} + +// Bulk removal +List materials = List.of( + new MaterialQuantity("iron_ingot", null, null, 2, null), + new MaterialQuantity(null, null, "Wood", 5, null) // Using tag +); +if (container.canRemoveMaterials(materials)) { + container.removeMaterials(materials); +} +``` + +## Bulk Operations + +Add or remove multiple items at once: + +```java +// Add multiple items +List items = List.of( + new ItemStack("iron_ore", 10), + new ItemStack("gold_ore", 5) +); + +if (container.canAddItemStacks(items)) { + ListTransaction transaction = container.addItemStacks(items); +} + +// Remove multiple items +if (container.canRemoveItemStacks(items)) { + container.removeItemStacks(items); +} + +// Add items in order (preserves slot positions) +container.addItemStacksOrdered(items); +container.addItemStacksOrdered((short) 5, items); // Starting at slot 5 +``` + +## Advanced Move Operations + +```java +// Move all items to another container +ListTransaction> result = + storage.moveAllItemStacksTo(hotbar, backpack); + +// Move all items matching a condition +storage.moveAllItemStacksTo( + item -> item.getItemId().contains("ore"), + hotbar +); + +// Quick stack: only moves items that can stack with existing items +storage.quickStackTo(hotbar); + +// Combine small stacks into one slot +container.combineItemStacksIntoSlot(targetContainer, (short) 0); + +// Swap items between containers +storage.swapItems( + (short) 0, // source position + hotbar, // target container + (short) 0, // destination position + (short) 5 // number of slots to swap +); +``` + +## Replace Operations + +```java +// Replace item in slot if it matches expected +ItemStackSlotTransaction transaction = container.replaceItemStackInSlot( + (short) 0, + expectedItem, // must match current item to proceed + newItem +); + +// Replace all items using a function +container.replaceAll((slot, existing) -> { + if (existing.getItemId().equals("old_item")) { + return new ItemStack("new_item", existing.getQuantity()); + } + return existing; +}); +``` + +## Slot Filters + +Control which items can go in which slots: + +```java +// Set global filter for the container +container.setGlobalFilter(FilterType.WHITELIST); + +// Set filter for specific slot +container.setSlotFilter( + FilterActionType.ADD, // Filter on add operations + (short) 0, // Slot + slotFilter // Filter implementation +); +``` + +## Best Practices + +{{< callout type="info" >}} +**Container Tips:** +- Slot indices are `short`, not `int` - cast appropriately +- Always check `ItemStack.isEmpty(stack)` - handles both null and empty +- Use `getQuantity()` not `getCount()` +- Check transaction success with `succeeded()` +- Use combined containers for cross-section operations +- Player inventory changes trigger `LivingEntityInventoryChangeEvent` +{{< /callout >}} + +```java +// Safe pattern for working with containers +public void safeAddItem(ItemContainer container, ItemStack item) { + if (ItemStack.isEmpty(item)) { + return; + } + + if (!container.canAddItemStack(item)) { + // Handle full container + return; + } + + ItemStackTransaction transaction = container.addItemStack(item); + if (!transaction.succeeded()) { + // Handle failure + } +} +``` + +## ItemContainer API Reference + +### Core Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getCapacity()` | `short` | Total number of slots | +| `getItemStack(short)` | `ItemStack?` | Get item at slot | +| `isEmpty()` | `boolean` | True if no items | +| `clone()` | `ItemContainer` | Clone the container | +| `clear()` | `ClearTransaction` | Remove all items | + +### Add Operations + +| Method | Returns | Description | +|--------|---------|-------------| +| `canAddItemStack(ItemStack)` | `boolean` | Check if can add | +| `canAddItemStack(ItemStack, fullStacks, filter)` | `boolean` | Check with options | +| `addItemStack(ItemStack)` | `ItemStackTransaction` | Add to first available | +| `addItemStack(ItemStack, allOrNothing, fullStacks, filter)` | `ItemStackTransaction` | Add with options | +| `canAddItemStackToSlot(short, ItemStack, allOrNothing, filter)` | `boolean` | Check if can add to slot | +| `addItemStackToSlot(short, ItemStack)` | `ItemStackSlotTransaction` | Add to specific slot | +| `addItemStackToSlot(short, ItemStack, allOrNothing, filter)` | `ItemStackSlotTransaction` | Add to slot with options | +| `canAddItemStacks(List)` | `boolean` | Check if can add multiple | +| `addItemStacks(List)` | `ListTransaction` | Add multiple items | +| `addItemStacksOrdered(List)` | `ListTransaction` | Add in order | +| `addItemStacksOrdered(short offset, List)` | `ListTransaction` | Add in order from offset | + +### Set Operations + +| Method | Returns | Description | +|--------|---------|-------------| +| `setItemStackForSlot(short, ItemStack)` | `ItemStackSlotTransaction` | Set item at slot | +| `setItemStackForSlot(short, ItemStack, filter)` | `ItemStackSlotTransaction` | Set with filter option | +| `replaceItemStackInSlot(short, ItemStack expected, ItemStack new)` | `ItemStackSlotTransaction` | Replace if matches | +| `replaceAll(SlotReplacementFunction)` | `ListTransaction` | Replace all items | + +### Remove Operations + +| Method | Returns | Description | +|--------|---------|-------------| +| `removeItemStackFromSlot(short)` | `SlotTransaction` | Remove entire slot | +| `removeItemStackFromSlot(short, filter)` | `SlotTransaction` | Remove with filter | +| `removeItemStackFromSlot(short, quantity)` | `ItemStackSlotTransaction` | Remove quantity | +| `removeItemStackFromSlot(short, ItemStack, quantity)` | `ItemStackSlotTransaction` | Remove matching item | +| `canRemoveItemStack(ItemStack)` | `boolean` | Check if can remove | +| `removeItemStack(ItemStack)` | `ItemStackTransaction` | Remove item type | +| `canRemoveItemStacks(List)` | `boolean` | Check if can remove multiple | +| `removeItemStacks(List)` | `ListTransaction` | Remove multiple items | +| `removeAllItemStacks()` | `List` | Remove and return all | +| `dropAllItemStacks()` | `List` | Drop all (respects cantDrop) | +| `dropAllItemStacks(filter)` | `List` | Drop with filter option | + +### Material/Resource/Tag Removal + +| Method | Returns | Description | +|--------|---------|-------------| +| `canRemoveMaterial(MaterialQuantity)` | `boolean` | Check material removal | +| `removeMaterial(MaterialQuantity)` | `MaterialTransaction` | Remove by material | +| `removeMaterialFromSlot(short, MaterialQuantity)` | `MaterialSlotTransaction` | Remove material from slot | +| `canRemoveMaterials(List)` | `boolean` | Check multiple materials | +| `removeMaterials(List)` | `ListTransaction` | Remove multiple materials | +| `canRemoveResource(ResourceQuantity)` | `boolean` | Check resource removal | +| `removeResource(ResourceQuantity)` | `ResourceTransaction` | Remove by resource | +| `removeResourceFromSlot(short, ResourceQuantity)` | `ResourceSlotTransaction` | Remove resource from slot | +| `canRemoveResources(List)` | `boolean` | Check multiple resources | +| `removeResources(List)` | `ListTransaction` | Remove multiple resources | +| `canRemoveTag(tagIndex, quantity)` | `boolean` | Check tag removal | +| `removeTag(tagIndex, quantity)` | `TagTransaction` | Remove by tag | +| `removeTagFromSlot(short, tagIndex, quantity)` | `TagSlotTransaction` | Remove tag from slot | + +### Move Operations + +| Method | Returns | Description | +|--------|---------|-------------| +| `moveItemStackFromSlot(short, ItemContainer)` | `MoveTransaction` | Move slot to container | +| `moveItemStackFromSlot(short, quantity, ItemContainer)` | `MoveTransaction` | Move quantity | +| `moveItemStackFromSlot(short, ItemContainer...)` | `ListTransaction` | Move to multiple containers | +| `moveItemStackFromSlotToSlot(short, quantity, ItemContainer, short)` | `MoveTransaction` | Move to specific slot | +| `moveAllItemStacksTo(ItemContainer...)` | `ListTransaction` | Move all items | +| `moveAllItemStacksTo(Predicate, ItemContainer...)` | `ListTransaction` | Move matching items | +| `quickStackTo(ItemContainer...)` | `ListTransaction` | Move stackable items only | +| `combineItemStacksIntoSlot(ItemContainer, short)` | `ListTransaction` | Combine stacks into slot | +| `swapItems(short srcPos, ItemContainer, short destPos, short length)` | `ListTransaction` | Swap item ranges | + +### Utility Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `forEach(ShortObjectConsumer)` | `void` | Iterate non-empty slots | +| `forEachWithMeta(consumer, meta)` | `void` | Iterate with metadata | +| `countItemStacks(Predicate)` | `int` | Count matching items (total quantity) | +| `containsItemStacksStackableWith(ItemStack)` | `boolean` | Check for stackable items | +| `sortItems(SortType)` | `ListTransaction` | Sort container | +| `registerChangeEvent(Consumer)` | `EventRegistration` | Listen for changes | +| `registerChangeEvent(EventPriority, Consumer)` | `EventRegistration` | Listen with priority | +| `setGlobalFilter(FilterType)` | `void` | Set container filter | +| `setSlotFilter(FilterActionType, short, SlotFilter)` | `void` | Set slot filter | +| `containsContainer(ItemContainer)` | `boolean` | Check if contains container | + +### Static Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `copy(from, to, remainder)` | `T` | Copy items between containers | +| `ensureContainerCapacity(container, capacity, supplier, remainder)` | `T` | Ensure container has capacity | +| `getNewContainer(capacity, supplier)` | `ItemContainer` | Create or get empty | +| `getMatchingResourceType(Item, resourceId)` | `ItemResourceType?` | Find resource type for item | +| `validateQuantity(int)` | `void` | Throws if < 0 | +| `validateSlotIndex(short, capacity)` | `void` | Throws if out of bounds | + +### Static Constants + +| Field | Type | Description | +|-------|------|-------------| +| `CODEC` | `CodecMapCodec` | Serialization codec | +| `DEFAULT_ADD_ALL_OR_NOTHING` | `boolean` | false | +| `DEFAULT_REMOVE_ALL_OR_NOTHING` | `boolean` | true | +| `DEFAULT_FULL_STACKS` | `boolean` | false | +| `DEFAULT_EXACT_AMOUNT` | `boolean` | true | +| `DEFAULT_FILTER` | `boolean` | true | + +### Nested Classes + +| Class | Description | +|-------|-------------| +| `ItemContainerChangeEvent` | Event record with `container()` and `transaction()` | + +## Inventory API Reference + +| Method | Returns | Description | +|--------|---------|-------------| +| `getStorage()` | `ItemContainer` | Main storage section | +| `getHotbar()` | `ItemContainer` | Hotbar section | +| `getArmor()` | `ItemContainer` | Armor section | +| `getUtility()` | `ItemContainer` | Utility section | +| `getBackpack()` | `ItemContainer` | Backpack section | +| `getSectionById(int)` | `ItemContainer?` | Get section by ID | +| `getItemInHand()` | `ItemStack?` | Currently held item | +| `getActiveHotbarSlot()` | `byte` | Active hotbar slot | +| `setActiveHotbarSlot(byte)` | `void` | Set active hotbar | +| `getCombinedHotbarFirst()` | `CombinedItemContainer` | Hotbar+Storage combined | +| `moveItem(...)` | `void` | Move between sections | +| `clear()` | `void` | Clear all sections | diff --git a/content/world/entities/inventory/containers.fr.md b/content/world/entities/inventory/containers.fr.md new file mode 100644 index 0000000..6e10bce --- /dev/null +++ b/content/world/entities/inventory/containers.fr.md @@ -0,0 +1,668 @@ +--- +title: Conteneurs +type: docs +weight: 2 +--- + +Les conteneurs gèrent des collections d'ItemStacks dans Hytale. Le système d'inventaire utilise `ItemContainer` comme abstraction de base avec `Inventory` comme conteneur multi-sections spécialisé du joueur. + +## Structure de l'Inventaire + +Les inventaires des joueurs sont composés de plusieurs sections `ItemContainer` : + +```java +Inventory inv = player.getInventory(); + +// Obtenir les sections individuelles +ItemContainer storage = inv.getStorage(); // Stockage principal (36 slots par défaut) +ItemContainer hotbar = inv.getHotbar(); // Hotbar (9 slots par défaut) +ItemContainer armor = inv.getArmor(); // Emplacements d'armure +ItemContainer utility = inv.getUtility(); // Objets utilitaires (4 slots par défaut) +ItemContainer backpack = inv.getBackpack(); // Sac à dos (extensible) + +// Conteneurs combinés pour opérations multi-sections +CombinedItemContainer hotbarFirst = inv.getCombinedHotbarFirst(); +CombinedItemContainer storageFirst = inv.getCombinedStorageFirst(); +``` + +### IDs de Section + +Chaque section a un ID constant pour les opérations : + +| Section | ID | Constante | +|---------|----|----| +| Hotbar | -1 | `Inventory.HOTBAR_SECTION_ID` | +| Stockage | -2 | `Inventory.STORAGE_SECTION_ID` | +| Armure | -3 | `Inventory.ARMOR_SECTION_ID` | +| Utilitaire | -5 | `Inventory.UTILITY_SECTION_ID` | +| Outils | -8 | `Inventory.TOOLS_SECTION_ID` (déprécié) | +| Sac à dos | -9 | `Inventory.BACKPACK_SECTION_ID` | + +## Bases d'ItemContainer + +### Lire les Objets + +```java +ItemContainer container = inv.getStorage(); + +// Obtenir la capacité (PAS getSize!) +short capacity = container.getCapacity(); + +// Obtenir l'objet au slot (slot est de type SHORT) +ItemStack item = container.getItemStack((short) 0); + +// Vérifier si vide +boolean empty = container.isEmpty(); + +// Accès sécurisé aux objets +ItemStack stack = container.getItemStack((short) slot); +if (!ItemStack.isEmpty(stack)) { + String itemId = stack.getItemId(); + int quantity = stack.getQuantity(); +} +``` + +### Définir des Objets + +```java +ItemContainer container = inv.getHotbar(); + +// Définir l'objet au slot - retourne une Transaction +ItemStackSlotTransaction transaction = container.setItemStackForSlot( + (short) 0, + new ItemStack("iron_sword", 1) +); + +// Vérifier si l'opération a réussi +if (transaction.succeeded()) { + // L'objet a été défini avec succès +} + +// Vider un slot +container.setItemStackForSlot((short) 0, null); +``` + +### Ajouter des Objets + +```java +ItemContainer container = inv.getStorage(); +ItemStack toAdd = new ItemStack("wood_plank", 64); + +// Ajouter au premier slot disponible (empile avec les objets existants d'abord) +ItemStackTransaction transaction = container.addItemStack(toAdd); + +// Vérifier les objets restants qui n'ont pas pu rentrer +ItemStack remainder = transaction.getRemainder(); +if (!ItemStack.isEmpty(remainder)) { + // Certains objets n'ont pas pu rentrer + int leftover = remainder.getQuantity(); +} + +// Ajouter à un slot spécifique +ItemStackSlotTransaction slotTransaction = container.addItemStackToSlot( + (short) 5, + toAdd +); + +// Vérifier si le conteneur peut accueillir les objets avant d'ajouter +if (container.canAddItemStack(toAdd)) { + container.addItemStack(toAdd); +} +``` + +### Retirer des Objets + +```java +// Retirer d'un slot spécifique +SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + +// Retirer une quantité spécifique du slot +ItemStackSlotTransaction removeTransaction = container.removeItemStackFromSlot( + (short) 0, + 10 // quantité à retirer +); + +// Retirer un type d'objet spécifique de n'importe où dans le conteneur +ItemStack toRemove = new ItemStack("iron_ore", 5); +ItemStackTransaction itemTransaction = container.removeItemStack(toRemove); + +// Vérifier si les objets peuvent être retirés avant de retirer +if (container.canRemoveItemStack(toRemove)) { + container.removeItemStack(toRemove); +} +``` + +## Slots Actifs + +Les joueurs ont des slots actifs pour la hotbar et les objets utilitaires : + +```java +Inventory inv = player.getInventory(); + +// Obtenir le slot actif de la hotbar (0-8) +byte activeHotbar = inv.getActiveHotbarSlot(); + +// Définir le slot actif de la hotbar +inv.setActiveHotbarSlot((byte) 3); + +// Obtenir l'objet actuellement en main +ItemStack handItem = inv.getItemInHand(); + +// Obtenir/définir le slot utilitaire actif +byte activeUtility = inv.getActiveUtilitySlot(); +inv.setActiveUtilitySlot((byte) 1); + +// Obtenir l'objet utilitaire +ItemStack utilityItem = inv.getUtilityItem(); +``` + +## Déplacer des Objets + +### Entre les Slots + +```java +Inventory inv = player.getInventory(); +ItemContainer storage = inv.getStorage(); +ItemContainer hotbar = inv.getHotbar(); + +// Déplacer un objet d'un slot à un autre dans le même conteneur +MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, // depuis slot + 64, // quantité + storage, // vers conteneur + (short) 5 // vers slot +); + +// Déplacer entre différents conteneurs +storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 +); + +// Utiliser moveItem de l'Inventory pour les déplacements par section +inv.moveItem( + Inventory.STORAGE_SECTION_ID, // depuis section + 5, // depuis slot + 10, // quantité + Inventory.HOTBAR_SECTION_ID, // vers section + 0 // vers slot +); +``` + +### Déplacement Intelligent + +```java +// Le déplacement intelligent considère le type d'objet pour un placement optimal +inv.smartMoveItem( + Inventory.STORAGE_SECTION_ID, + 0, // slot + 64, // quantité + SmartMoveType.EquipOrMergeStack // tente d'équiper l'armure ou fusionner les piles +); +``` + +## Itérer les Conteneurs + +```java +ItemContainer container = inv.getStorage(); + +// Itérer tous les slots non vides +container.forEach((slot, itemStack) -> { + getLogger().at(Level.INFO).log("Slot " + slot + ": " + + itemStack.getItemId() + " x" + itemStack.getQuantity()); +}); + +// Compter les objets correspondant à une condition +int swordCount = container.countItemStacks( + stack -> stack.getItemId().contains("sword") +); + +// Vérifier si le conteneur a des objets empilables +ItemStack testStack = new ItemStack("stone", 1); +boolean hasStackable = container.containsItemStacksStackableWith(testStack); +``` + +## Vérifier le Contenu + +```java +ItemContainer container = inv.getStorage(); + +// Vérifier si l'inventaire contient un type et quantité d'objet spécifiques +public boolean hasItems(ItemContainer container, String itemId, int amount) { + int count = container.countItemStacks( + stack -> stack.getItemId().equals(itemId) + ); + return count >= amount; +} + +// Trouver le premier slot avec un objet spécifique +public short findSlotWithItem(ItemContainer container, String itemId) { + for (short i = 0; i < container.getCapacity(); i++) { + ItemStack stack = container.getItemStack(i); + if (!ItemStack.isEmpty(stack) && stack.getItemId().equals(itemId)) { + return i; + } + } + return -1; // Non trouvé +} + +// Compter les slots vides +public int countEmptySlots(ItemContainer container) { + int empty = 0; + for (short i = 0; i < container.getCapacity(); i++) { + if (ItemStack.isEmpty(container.getItemStack(i))) { + empty++; + } + } + return empty; +} +``` + +## Vider les Conteneurs + +```java +ItemContainer container = inv.getStorage(); + +// Vider tout le conteneur +ClearTransaction transaction = container.clear(); + +// Lâcher tous les objets (retourne la liste des objets lâchés) +List droppedItems = container.dropAllItemStacks(); + +// Retirer tous les objets (retourne la liste) +List removedItems = container.removeAllItemStacks(); + +// Vider tout l'inventaire du joueur +inv.clear(); + +// Lâcher tout de l'inventaire du joueur +List allDropped = inv.dropAllItemStacks(); +``` + +## Tri + +Valeurs `SortType` disponibles : `NAME`, `TYPE`, `RARITY` + +```java +ItemContainer container = inv.getStorage(); + +// Trier les objets +container.sortItems(SortType.NAME); + +// Trier via Inventory (sauvegarde aussi la préférence de tri) +inv.sortStorage(SortType.NAME); +inv.setSortType(SortType.RARITY); // Ou TYPE +``` + +## Événements de Conteneur + +```java +// S'enregistrer pour les événements de changement de conteneur +container.registerChangeEvent(event -> { + ItemContainer changedContainer = event.container(); + Transaction transaction = event.transaction(); + + getLogger().at(Level.INFO).log("Conteneur modifié !"); +}); + +// Avec priorité +container.registerChangeEvent(EventPriority.EARLY, event -> { + // Gérer tôt +}); +``` + +## Système de Transactions + +Toutes les opérations de modification retournent des objets Transaction : + +```java +// Les transactions suivent le succès et les changements +ItemStackTransaction transaction = container.addItemStack(itemStack); + +if (transaction.succeeded()) { + ItemStack remainder = transaction.getRemainder(); + // ... +} + +// Les transactions de slot incluent les infos du slot +ItemStackSlotTransaction slotTransaction = container.setItemStackForSlot( + (short) 0, + itemStack +); + +if (slotTransaction.succeeded()) { + short slot = slotTransaction.getSlot(); + ItemStack before = slotTransaction.getSlotBefore(); + ItemStack after = slotTransaction.getSlotAfter(); +} +``` + +## Paramètres d'Opération + +La plupart des opérations de conteneur supportent des paramètres optionnels : + +```java +// Valeurs par défaut +DEFAULT_ADD_ALL_OR_NOTHING = false; // Ajouts partiels autorisés +DEFAULT_REMOVE_ALL_OR_NOTHING = true; // Uniquement retraits complets +DEFAULT_FULL_STACKS = false; // Peut diviser les piles +DEFAULT_EXACT_AMOUNT = true; // Quantités exactes uniquement +DEFAULT_FILTER = true; // Applique les filtres de slot + +// Ajouter avec paramètres +container.addItemStack(itemStack, allOrNothing, fullStacks, filter); + +// allOrNothing: si true, échoue entièrement si tous les objets ne rentrent pas +// fullStacks: si true, remplit uniquement les slots vides (pas d'empilement) +// filter: si true, respecte les filtres de slot +``` + +## Retrait par Matériau et Ressource + +Les conteneurs supportent le retrait d'objets par type de matériau ou de ressource (utilisé pour le craft) : + +{{< callout type="info" >}} +**Constructeur MaterialQuantity :** `MaterialQuantity(itemId, resourceTypeId, tag, quantity, metadata)` + +Au moins un parmi `itemId`, `resourceTypeId`, ou `tag` doit être non-null. +{{< /callout >}} + +```java +// Retirer par matériau en utilisant itemId +MaterialQuantity materialByItem = new MaterialQuantity("iron_ingot", null, null, 5, null); +if (container.canRemoveMaterial(materialByItem)) { + MaterialTransaction transaction = container.removeMaterial(materialByItem); +} + +// Retirer par matériau en utilisant resourceTypeId +MaterialQuantity materialByResource = new MaterialQuantity(null, "iron", null, 5, null); +if (container.canRemoveMaterial(materialByResource)) { + MaterialTransaction transaction = container.removeMaterial(materialByResource); +} + +// Retirer par type de ressource (constructeur plus simple) +ResourceQuantity resource = new ResourceQuantity("wood", 10); +if (container.canRemoveResource(resource)) { + ResourceTransaction transaction = container.removeResource(resource); +} + +// Retirer par index de tag +if (container.canRemoveTag(tagIndex, quantity)) { + TagTransaction transaction = container.removeTag(tagIndex, quantity); +} + +// Retrait en masse +List materials = List.of( + new MaterialQuantity("iron_ingot", null, null, 2, null), + new MaterialQuantity(null, null, "Wood", 5, null) // Avec tag +); +if (container.canRemoveMaterials(materials)) { + container.removeMaterials(materials); +} +``` + +## Opérations en Masse + +Ajouter ou retirer plusieurs objets à la fois : + +```java +// Ajouter plusieurs objets +List items = List.of( + new ItemStack("iron_ore", 10), + new ItemStack("gold_ore", 5) +); + +if (container.canAddItemStacks(items)) { + ListTransaction transaction = container.addItemStacks(items); +} + +// Retirer plusieurs objets +if (container.canRemoveItemStacks(items)) { + container.removeItemStacks(items); +} + +// Ajouter les objets dans l'ordre (préserve les positions de slot) +container.addItemStacksOrdered(items); +container.addItemStacksOrdered((short) 5, items); // À partir du slot 5 +``` + +## Opérations de Déplacement Avancées + +```java +// Déplacer tous les objets vers un autre conteneur +ListTransaction> result = + storage.moveAllItemStacksTo(hotbar, backpack); + +// Déplacer tous les objets correspondant à une condition +storage.moveAllItemStacksTo( + item -> item.getItemId().contains("ore"), + hotbar +); + +// Quick stack : déplace uniquement les objets qui peuvent s'empiler avec des objets existants +storage.quickStackTo(hotbar); + +// Combiner les petites piles dans un slot +container.combineItemStacksIntoSlot(targetContainer, (short) 0); + +// Échanger des objets entre conteneurs +storage.swapItems( + (short) 0, // position source + hotbar, // conteneur cible + (short) 0, // position destination + (short) 5 // nombre de slots à échanger +); +``` + +## Opérations de Remplacement + +```java +// Remplacer l'objet dans le slot s'il correspond à l'attendu +ItemStackSlotTransaction transaction = container.replaceItemStackInSlot( + (short) 0, + expectedItem, // doit correspondre à l'objet actuel pour continuer + newItem +); + +// Remplacer tous les objets avec une fonction +container.replaceAll((slot, existing) -> { + if (existing.getItemId().equals("old_item")) { + return new ItemStack("new_item", existing.getQuantity()); + } + return existing; +}); +``` + +## Filtres de Slot + +Contrôler quels objets peuvent aller dans quels slots : + +```java +// Définir un filtre global pour le conteneur +container.setGlobalFilter(FilterType.WHITELIST); + +// Définir un filtre pour un slot spécifique +container.setSlotFilter( + FilterActionType.ADD, // Filtre sur les opérations d'ajout + (short) 0, // Slot + slotFilter // Implémentation du filtre +); +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Conseils pour les Conteneurs :** +- Les indices de slot sont `short`, pas `int` - castez correctement +- Vérifiez toujours `ItemStack.isEmpty(stack)` - gère null et vide +- Utilisez `getQuantity()` pas `getCount()` +- Vérifiez le succès de la transaction avec `succeeded()` +- Utilisez les conteneurs combinés pour les opérations multi-sections +- Les changements d'inventaire du joueur déclenchent `LivingEntityInventoryChangeEvent` +{{< /callout >}} + +```java +// Pattern sécurisé pour travailler avec les conteneurs +public void safeAddItem(ItemContainer container, ItemStack item) { + if (ItemStack.isEmpty(item)) { + return; + } + + if (!container.canAddItemStack(item)) { + // Gérer le conteneur plein + return; + } + + ItemStackTransaction transaction = container.addItemStack(item); + if (!transaction.succeeded()) { + // Gérer l'échec + } +} +``` + +## Référence API ItemContainer + +### Méthodes Principales + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getCapacity()` | `short` | Nombre total de slots | +| `getItemStack(short)` | `ItemStack?` | Obtenir l'objet au slot | +| `isEmpty()` | `boolean` | True si aucun objet | +| `clone()` | `ItemContainer` | Cloner le conteneur | +| `clear()` | `ClearTransaction` | Retirer tous les objets | + +### Opérations d'Ajout + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `canAddItemStack(ItemStack)` | `boolean` | Vérifier si peut ajouter | +| `canAddItemStack(ItemStack, fullStacks, filter)` | `boolean` | Vérifier avec options | +| `addItemStack(ItemStack)` | `ItemStackTransaction` | Ajouter au premier disponible | +| `addItemStack(ItemStack, allOrNothing, fullStacks, filter)` | `ItemStackTransaction` | Ajouter avec options | +| `canAddItemStackToSlot(short, ItemStack, allOrNothing, filter)` | `boolean` | Vérifier si peut ajouter au slot | +| `addItemStackToSlot(short, ItemStack)` | `ItemStackSlotTransaction` | Ajouter à un slot spécifique | +| `addItemStackToSlot(short, ItemStack, allOrNothing, filter)` | `ItemStackSlotTransaction` | Ajouter au slot avec options | +| `canAddItemStacks(List)` | `boolean` | Vérifier si peut ajouter plusieurs | +| `addItemStacks(List)` | `ListTransaction` | Ajouter plusieurs objets | +| `addItemStacksOrdered(List)` | `ListTransaction` | Ajouter dans l'ordre | +| `addItemStacksOrdered(short offset, List)` | `ListTransaction` | Ajouter dans l'ordre depuis offset | + +### Opérations de Définition + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `setItemStackForSlot(short, ItemStack)` | `ItemStackSlotTransaction` | Définir l'objet au slot | +| `setItemStackForSlot(short, ItemStack, filter)` | `ItemStackSlotTransaction` | Définir avec option filtre | +| `replaceItemStackInSlot(short, ItemStack attendu, ItemStack nouveau)` | `ItemStackSlotTransaction` | Remplacer si correspondance | +| `replaceAll(SlotReplacementFunction)` | `ListTransaction` | Remplacer tous les objets | + +### Opérations de Retrait + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `removeItemStackFromSlot(short)` | `SlotTransaction` | Retirer tout le slot | +| `removeItemStackFromSlot(short, filter)` | `SlotTransaction` | Retirer avec filtre | +| `removeItemStackFromSlot(short, quantity)` | `ItemStackSlotTransaction` | Retirer une quantité | +| `removeItemStackFromSlot(short, ItemStack, quantity)` | `ItemStackSlotTransaction` | Retirer objet correspondant | +| `canRemoveItemStack(ItemStack)` | `boolean` | Vérifier si peut retirer | +| `removeItemStack(ItemStack)` | `ItemStackTransaction` | Retirer type d'objet | +| `canRemoveItemStacks(List)` | `boolean` | Vérifier si peut retirer plusieurs | +| `removeItemStacks(List)` | `ListTransaction` | Retirer plusieurs objets | +| `removeAllItemStacks()` | `List` | Retirer et retourner tous | +| `dropAllItemStacks()` | `List` | Lâcher tous (respecte cantDrop) | +| `dropAllItemStacks(filter)` | `List` | Lâcher avec option filtre | + +### Retrait Matériau/Ressource/Tag + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `canRemoveMaterial(MaterialQuantity)` | `boolean` | Vérifier retrait matériau | +| `removeMaterial(MaterialQuantity)` | `MaterialTransaction` | Retirer par matériau | +| `removeMaterialFromSlot(short, MaterialQuantity)` | `MaterialSlotTransaction` | Retirer matériau du slot | +| `canRemoveMaterials(List)` | `boolean` | Vérifier plusieurs matériaux | +| `removeMaterials(List)` | `ListTransaction` | Retirer plusieurs matériaux | +| `canRemoveResource(ResourceQuantity)` | `boolean` | Vérifier retrait ressource | +| `removeResource(ResourceQuantity)` | `ResourceTransaction` | Retirer par ressource | +| `removeResourceFromSlot(short, ResourceQuantity)` | `ResourceSlotTransaction` | Retirer ressource du slot | +| `canRemoveResources(List)` | `boolean` | Vérifier plusieurs ressources | +| `removeResources(List)` | `ListTransaction` | Retirer plusieurs ressources | +| `canRemoveTag(tagIndex, quantity)` | `boolean` | Vérifier retrait tag | +| `removeTag(tagIndex, quantity)` | `TagTransaction` | Retirer par tag | +| `removeTagFromSlot(short, tagIndex, quantity)` | `TagSlotTransaction` | Retirer tag du slot | + +### Opérations de Déplacement + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `moveItemStackFromSlot(short, ItemContainer)` | `MoveTransaction` | Déplacer slot vers conteneur | +| `moveItemStackFromSlot(short, quantity, ItemContainer)` | `MoveTransaction` | Déplacer quantité | +| `moveItemStackFromSlot(short, ItemContainer...)` | `ListTransaction` | Déplacer vers plusieurs conteneurs | +| `moveItemStackFromSlotToSlot(short, quantity, ItemContainer, short)` | `MoveTransaction` | Déplacer vers slot spécifique | +| `moveAllItemStacksTo(ItemContainer...)` | `ListTransaction` | Déplacer tous les objets | +| `moveAllItemStacksTo(Predicate, ItemContainer...)` | `ListTransaction` | Déplacer objets correspondants | +| `quickStackTo(ItemContainer...)` | `ListTransaction` | Déplacer uniquement empilables | +| `combineItemStacksIntoSlot(ItemContainer, short)` | `ListTransaction` | Combiner piles dans slot | +| `swapItems(short srcPos, ItemContainer, short destPos, short length)` | `ListTransaction` | Échanger plages d'objets | + +### Méthodes Utilitaires + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `forEach(ShortObjectConsumer)` | `void` | Itérer slots non vides | +| `forEachWithMeta(consumer, meta)` | `void` | Itérer avec métadonnées | +| `countItemStacks(Predicate)` | `int` | Compter objets correspondants (quantité totale) | +| `containsItemStacksStackableWith(ItemStack)` | `boolean` | Vérifier objets empilables | +| `sortItems(SortType)` | `ListTransaction` | Trier conteneur | +| `registerChangeEvent(Consumer)` | `EventRegistration` | Écouter les changements | +| `registerChangeEvent(EventPriority, Consumer)` | `EventRegistration` | Écouter avec priorité | +| `setGlobalFilter(FilterType)` | `void` | Définir filtre conteneur | +| `setSlotFilter(FilterActionType, short, SlotFilter)` | `void` | Définir filtre slot | +| `containsContainer(ItemContainer)` | `boolean` | Vérifier si contient conteneur | + +### Méthodes Statiques + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `copy(from, to, remainder)` | `T` | Copier objets entre conteneurs | +| `ensureContainerCapacity(container, capacity, supplier, remainder)` | `T` | S'assurer de la capacité | +| `getNewContainer(capacity, supplier)` | `ItemContainer` | Créer ou obtenir vide | +| `getMatchingResourceType(Item, resourceId)` | `ItemResourceType?` | Trouver type ressource pour objet | +| `validateQuantity(int)` | `void` | Lance exception si < 0 | +| `validateSlotIndex(short, capacity)` | `void` | Lance exception si hors limites | + +### Constantes Statiques + +| Champ | Type | Description | +|-------|------|-------------| +| `CODEC` | `CodecMapCodec` | Codec de sérialisation | +| `DEFAULT_ADD_ALL_OR_NOTHING` | `boolean` | false | +| `DEFAULT_REMOVE_ALL_OR_NOTHING` | `boolean` | true | +| `DEFAULT_FULL_STACKS` | `boolean` | false | +| `DEFAULT_EXACT_AMOUNT` | `boolean` | true | +| `DEFAULT_FILTER` | `boolean` | true | + +### Classes Imbriquées + +| Classe | Description | +|-------|-------------| +| `ItemContainerChangeEvent` | Record événement avec `container()` et `transaction()` | + +## Référence API Inventory + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getStorage()` | `ItemContainer` | Section de stockage principal | +| `getHotbar()` | `ItemContainer` | Section hotbar | +| `getArmor()` | `ItemContainer` | Section armure | +| `getUtility()` | `ItemContainer` | Section utilitaire | +| `getBackpack()` | `ItemContainer` | Section sac à dos | +| `getSectionById(int)` | `ItemContainer?` | Obtenir section par ID | +| `getItemInHand()` | `ItemStack?` | Objet actuellement tenu | +| `getActiveHotbarSlot()` | `byte` | Slot actif de la hotbar | +| `setActiveHotbarSlot(byte)` | `void` | Définir hotbar active | +| `getCombinedHotbarFirst()` | `CombinedItemContainer` | Hotbar+Stockage combinés | +| `moveItem(...)` | `void` | Déplacer entre sections | +| `clear()` | `void` | Vider toutes les sections | diff --git a/content/world/entities/inventory/itemstacks.en.md b/content/world/entities/inventory/itemstacks.en.md new file mode 100644 index 0000000..49a4f42 --- /dev/null +++ b/content/world/entities/inventory/itemstacks.en.md @@ -0,0 +1,354 @@ +--- +title: ItemStacks +type: docs +weight: 1 +--- + +ItemStack represents a stack of items in Hytale with quantity, durability, and metadata. + +## Creating ItemStacks + +ItemStacks are created using constructors with an item ID: + +```java +// Create by item ID +ItemStack sword = new ItemStack("iron_sword"); + +// Create with quantity +ItemStack materials = new ItemStack("wood_plank", 64); + +// Create with quantity and metadata +BsonDocument metadata = new BsonDocument(); +ItemStack customItem = new ItemStack("iron_sword", 1, metadata); + +// Create with full parameters (durability) +ItemStack damagedSword = new ItemStack("iron_sword", 1, 50.0, 100.0, null); +// Parameters: itemId, quantity, durability, maxDurability, metadata +``` + +## ItemStack Properties + +```java +ItemStack stack = new ItemStack("iron_sword", 1); + +// Get the item ID +String itemId = stack.getItemId(); // "iron_sword" + +// Get the Item asset +Item item = stack.getItem(); + +// Get quantity (NOT getCount!) +int quantity = stack.getQuantity(); + +// Check if empty +boolean empty = stack.isEmpty(); + +// Check validity +boolean valid = stack.isValid(); + +// Durability +double durability = stack.getDurability(); +double maxDurability = stack.getMaxDurability(); +boolean unbreakable = stack.isUnbreakable(); // true if maxDurability <= 0 +boolean broken = stack.isBroken(); // true if durability == 0 +``` + +## Modifying ItemStacks + +ItemStack uses a `with*` pattern that returns NEW instances: + +```java +ItemStack stack = new ItemStack("iron_sword", 1); + +// Change quantity - returns NEW ItemStack or null if quantity is 0 +ItemStack moreItems = stack.withQuantity(32); + +// Change durability +ItemStack damaged = stack.withDurability(50.0); + +// Increase durability +ItemStack repaired = stack.withIncreasedDurability(25.0); + +// Restore full durability +ItemStack fullyRepaired = stack.withRestoredDurability(100.0); + +// Change max durability +ItemStack stronger = stack.withMaxDurability(200.0); + +// Change state (for items with states) +ItemStack newState = stack.withState("activated"); + +// Add/modify metadata +ItemStack withMeta = stack.withMetadata(metadataDocument); + +// Add specific metadata value +ItemStack tagged = stack.withMetadata("CustomKey", Codec.STRING, "CustomValue"); +``` + +{{< callout type="warning" >}} +**Important:** `withQuantity(0)` returns `null`, not an empty ItemStack. Always check for null when decreasing quantity! +{{< /callout >}} + +## The EMPTY Constant + +Use `ItemStack.EMPTY` for empty stacks: + +```java +// Static empty instance (singleton) +ItemStack empty = ItemStack.EMPTY; + +// Check for empty +if (stack.isEmpty()) { + // Stack is empty +} + +// Static helper method +if (ItemStack.isEmpty(stack)) { + // Handles null and empty stacks +} +``` + +## Comparing ItemStacks + +```java +ItemStack a = new ItemStack("iron_sword", 1); +ItemStack b = new ItemStack("iron_sword", 5); +ItemStack c = new ItemStack("diamond_sword", 1); + +// Check if stackable (same itemId, durability, maxDurability, AND metadata) +// Note: Different quantities can stack, but durability values must match exactly +boolean canStack = a.isStackableWith(b); + +// Check equivalent type (same itemId and metadata, ignores durability values) +boolean sameType = a.isEquivalentType(b); + +// Check same item type only (just itemId comparison) +boolean sameItem = ItemStack.isSameItemType(a, c); // false + +// Static helpers (handle nulls safely) +ItemStack.isStackableWith(a, b); +ItemStack.isEquivalentType(a, b); +``` + +## Working with Metadata + +ItemStack supports BSON metadata for custom data: + +```java +// Create metadata codec +KeyedCodec OWNER_KEY = new KeyedCodec<>("Owner", Codec.STRING); + +// Add metadata +ItemStack withOwner = stack.withMetadata(OWNER_KEY, "PlayerName"); + +// Read metadata +String owner = stack.getFromMetadataOrNull(OWNER_KEY); + +// Read with key and codec +Integer level = stack.getFromMetadataOrNull("Level", Codec.INTEGER); + +// Read with default from BuilderCodec +MyData data = stack.getFromMetadataOrDefault("Data", MyData.CODEC); +``` + +## Block Items + +Check if an item can be placed as a block: + +```java +ItemStack stack = new ItemStack("stone", 1); + +// Get associated block key (null if not a block item) +String blockKey = stack.getBlockKey(); +if (blockKey != null) { + // This item can be placed as a block +} + +// Check via Item asset +Item item = stack.getItem(); +if (item.hasBlockType()) { + String blockId = item.getBlockId(); +} +``` + +## Common Patterns + +### Consuming Items + +```java +public ItemStack consumeOne(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int newQuantity = stack.getQuantity() - 1; + + // withQuantity returns null if quantity is 0 + return stack.withQuantity(newQuantity); +} + +// Usage with container +public void useItem(ItemContainer container, short slot) { + ItemStack current = container.getItemStack(slot); + ItemStack remaining = consumeOne(current); + + // remaining may be null if stack was depleted + container.setItemStackForSlot(slot, remaining); +} +``` + +### Checking Item Type + +```java +public boolean isHoldingSword(ItemStack hand) { + if (hand == null || hand.isEmpty()) { + return false; + } + + // Check item type by ID + return hand.getItemId().contains("sword"); + + // Or check via Item asset + // return hand.getItem().getCategory().equals("weapon"); +} +``` + +### Splitting Stacks + +```java +public ItemStack[] splitStack(ItemStack stack, int splitAmount) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int currentQuantity = stack.getQuantity(); + if (splitAmount >= currentQuantity) { + return new ItemStack[] { stack, null }; + } + + // Create two stacks + ItemStack remaining = stack.withQuantity(currentQuantity - splitAmount); + ItemStack split = stack.withQuantity(splitAmount); + + return new ItemStack[] { remaining, split }; +} +``` + +### Merging Stacks + +```java +public ItemStack[] mergeStacks(ItemStack target, ItemStack source) { + if (!target.isStackableWith(source)) { + return new ItemStack[] { target, source }; // Can't merge + } + + int maxStack = target.getItem().getMaxStack(); + int totalQuantity = target.getQuantity() + source.getQuantity(); + + if (totalQuantity <= maxStack) { + // Full merge + return new ItemStack[] { + target.withQuantity(totalQuantity), + null + }; + } + + // Partial merge + return new ItemStack[] { + target.withQuantity(maxStack), + source.withQuantity(totalQuantity - maxStack) + }; +} +``` + +## Best Practices + +{{< callout type="info" >}} +**Remember:** +- Use `getQuantity()` not `getCount()` - Hytale uses "quantity" +- `withQuantity(0)` returns `null` - check for this! +- Use `ItemStack.isEmpty(stack)` to handle both null and empty +- ItemStacks are mostly immutable - `with*` methods return new instances +- Use `isStackableWith()` before attempting to merge stacks +- `ResourceType.Id` must not be null when creating items via codecs +{{< /callout >}} + +```java +// Good: Handle null from withQuantity +ItemStack result = stack.withQuantity(newQuantity); +if (result == null) { + // Stack depleted, handle appropriately +} + +// Good: Safe empty check +if (ItemStack.isEmpty(stack)) { + // Handles both null and empty ItemStack.EMPTY +} + +// Bad: Ignoring the returned value +stack.withQuantity(10); // Returns new stack, original unchanged! +``` + +## ItemStack API Reference + +### Instance Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getItemId()` | `String` | The item type identifier | +| `getItem()` | `Item` | The Item asset (returns `Item.UNKNOWN` if not found) | +| `getQuantity()` | `int` | Stack size | +| `getDurability()` | `double` | Current durability | +| `getMaxDurability()` | `double` | Maximum durability | +| `isEmpty()` | `boolean` | True if itemId equals "Empty" | +| `isUnbreakable()` | `boolean` | True if maxDurability <= 0 | +| `isBroken()` | `boolean` | True if NOT unbreakable AND durability == 0 | +| `isValid()` | `boolean` | True if empty OR item asset exists | +| `isStackableWith(ItemStack)` | `boolean` | Same itemId, durability, maxDurability, metadata | +| `isEquivalentType(ItemStack)` | `boolean` | Same itemId and metadata (ignores durability) | +| `getBlockKey()` | `String?` | Block ID if item is placeable, null otherwise | +| `getOverrideDroppedItemAnimation()` | `boolean` | Animation override flag | +| `getMetadata()` | `BsonDocument?` | **Deprecated** - Returns cloned metadata | +| `getFromMetadataOrNull(KeyedCodec)` | `T?` | Get typed metadata value | +| `getFromMetadataOrNull(String, Codec)` | `T?` | Get typed metadata by key | +| `getFromMetadataOrDefault(String, BuilderCodec)` | `T` | Get metadata with default | + +### Modifier Methods (return new ItemStack) + +| Method | Returns | Description | +|--------|---------|-------------| +| `withQuantity(int)` | `ItemStack?` | **Returns null if quantity is 0** | +| `withDurability(double)` | `ItemStack` | Clamped to [0, maxDurability] | +| `withMaxDurability(double)` | `ItemStack` | Also clamps current durability | +| `withIncreasedDurability(double)` | `ItemStack` | Add to current durability | +| `withRestoredDurability(double)` | `ItemStack` | Set both durability and max | +| `withState(String)` | `ItemStack` | Change item state | +| `withMetadata(BsonDocument)` | `ItemStack` | Replace all metadata | +| `withMetadata(KeyedCodec, T)` | `ItemStack` | Set typed metadata | +| `withMetadata(String, Codec, T)` | `ItemStack` | Set metadata by key | +| `withMetadata(String, BsonValue)` | `ItemStack` | Set raw BSON value | +| `setOverrideDroppedItemAnimation(boolean)` | `void` | **Mutates in place** | + +### Static Fields + +| Field | Type | Description | +|-------|------|-------------| +| `EMPTY` | `ItemStack` | Singleton empty stack (itemId = "Empty") | +| `EMPTY_ARRAY` | `ItemStack[]` | Empty array constant | +| `CODEC` | `BuilderCodec` | Serialization codec | + +### Static Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `isEmpty(ItemStack)` | `boolean` | Null-safe empty check | +| `isStackableWith(ItemStack, ItemStack)` | `boolean` | Null-safe stackable check | +| `isEquivalentType(ItemStack, ItemStack)` | `boolean` | Null-safe type check | +| `isSameItemType(ItemStack, ItemStack)` | `boolean` | Compare itemId only | +| `fromPacket(ItemQuantity)` | `ItemStack?` | Create from network packet | + +### Nested Classes + +| Class | Description | +|-------|-------------| +| `ItemStack.Metadata` | Contains `BLOCK_STATE` constant for block state metadata key | diff --git a/content/world/entities/inventory/itemstacks.fr.md b/content/world/entities/inventory/itemstacks.fr.md new file mode 100644 index 0000000..d86c088 --- /dev/null +++ b/content/world/entities/inventory/itemstacks.fr.md @@ -0,0 +1,354 @@ +--- +title: ItemStacks +type: docs +weight: 1 +--- + +ItemStack représente une pile d'objets dans Hytale avec quantité, durabilité et métadonnées. + +## Créer des ItemStacks + +Les ItemStacks sont créés en utilisant des constructeurs avec un ID d'objet : + +```java +// Créer par ID d'objet +ItemStack sword = new ItemStack("iron_sword"); + +// Créer avec quantité +ItemStack materials = new ItemStack("wood_plank", 64); + +// Créer avec quantité et métadonnées +BsonDocument metadata = new BsonDocument(); +ItemStack customItem = new ItemStack("iron_sword", 1, metadata); + +// Créer avec tous les paramètres (durabilité) +ItemStack damagedSword = new ItemStack("iron_sword", 1, 50.0, 100.0, null); +// Paramètres: itemId, quantity, durability, maxDurability, metadata +``` + +## Propriétés d'ItemStack + +```java +ItemStack stack = new ItemStack("iron_sword", 1); + +// Obtenir l'ID de l'objet +String itemId = stack.getItemId(); // "iron_sword" + +// Obtenir l'asset Item +Item item = stack.getItem(); + +// Obtenir la quantité (PAS getCount!) +int quantity = stack.getQuantity(); + +// Vérifier si vide +boolean empty = stack.isEmpty(); + +// Vérifier la validité +boolean valid = stack.isValid(); + +// Durabilité +double durability = stack.getDurability(); +double maxDurability = stack.getMaxDurability(); +boolean unbreakable = stack.isUnbreakable(); // true si maxDurability <= 0 +boolean broken = stack.isBroken(); // true si durability == 0 +``` + +## Modifier les ItemStacks + +ItemStack utilise un pattern `with*` qui retourne de NOUVELLES instances : + +```java +ItemStack stack = new ItemStack("iron_sword", 1); + +// Changer la quantité - retourne un NOUVEL ItemStack ou null si quantité est 0 +ItemStack moreItems = stack.withQuantity(32); + +// Changer la durabilité +ItemStack damaged = stack.withDurability(50.0); + +// Augmenter la durabilité +ItemStack repaired = stack.withIncreasedDurability(25.0); + +// Restaurer la durabilité complète +ItemStack fullyRepaired = stack.withRestoredDurability(100.0); + +// Changer la durabilité max +ItemStack stronger = stack.withMaxDurability(200.0); + +// Changer l'état (pour les objets avec états) +ItemStack newState = stack.withState("activated"); + +// Ajouter/modifier les métadonnées +ItemStack withMeta = stack.withMetadata(metadataDocument); + +// Ajouter une valeur de métadonnée spécifique +ItemStack tagged = stack.withMetadata("CustomKey", Codec.STRING, "CustomValue"); +``` + +{{< callout type="warning" >}} +**Important :** `withQuantity(0)` retourne `null`, pas un ItemStack vide. Vérifiez toujours null quand vous diminuez la quantité ! +{{< /callout >}} + +## La Constante EMPTY + +Utilisez `ItemStack.EMPTY` pour les piles vides : + +```java +// Instance vide statique (singleton) +ItemStack empty = ItemStack.EMPTY; + +// Vérifier si vide +if (stack.isEmpty()) { + // La pile est vide +} + +// Méthode helper statique +if (ItemStack.isEmpty(stack)) { + // Gère les piles null et vides +} +``` + +## Comparer les ItemStacks + +```java +ItemStack a = new ItemStack("iron_sword", 1); +ItemStack b = new ItemStack("iron_sword", 5); +ItemStack c = new ItemStack("diamond_sword", 1); + +// Vérifier si empilables (même itemId, durability, maxDurability ET metadata) +// Note: Des quantités différentes peuvent s'empiler, mais les durabilités doivent correspondre exactement +boolean canStack = a.isStackableWith(b); + +// Vérifier type équivalent (même itemId et metadata, ignore les durabilités) +boolean sameType = a.isEquivalentType(b); + +// Vérifier même type d'objet seulement (juste itemId) +boolean sameItem = ItemStack.isSameItemType(a, c); // false + +// Helpers statiques (gèrent les nulls en sécurité) +ItemStack.isStackableWith(a, b); +ItemStack.isEquivalentType(a, b); +``` + +## Travailler avec les Métadonnées + +ItemStack supporte les métadonnées BSON pour les données personnalisées : + +```java +// Créer un codec de métadonnée +KeyedCodec OWNER_KEY = new KeyedCodec<>("Owner", Codec.STRING); + +// Ajouter des métadonnées +ItemStack withOwner = stack.withMetadata(OWNER_KEY, "PlayerName"); + +// Lire les métadonnées +String owner = stack.getFromMetadataOrNull(OWNER_KEY); + +// Lire avec clé et codec +Integer level = stack.getFromMetadataOrNull("Level", Codec.INTEGER); + +// Lire avec défaut depuis BuilderCodec +MyData data = stack.getFromMetadataOrDefault("Data", MyData.CODEC); +``` + +## Objets de Bloc + +Vérifier si un objet peut être placé comme bloc : + +```java +ItemStack stack = new ItemStack("stone", 1); + +// Obtenir la clé de bloc associée (null si pas un objet de bloc) +String blockKey = stack.getBlockKey(); +if (blockKey != null) { + // Cet objet peut être placé comme bloc +} + +// Vérifier via l'asset Item +Item item = stack.getItem(); +if (item.hasBlockType()) { + String blockId = item.getBlockId(); +} +``` + +## Patterns Courants + +### Consommer des Objets + +```java +public ItemStack consumeOne(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int newQuantity = stack.getQuantity() - 1; + + // withQuantity retourne null si quantité est 0 + return stack.withQuantity(newQuantity); +} + +// Utilisation avec conteneur +public void useItem(ItemContainer container, short slot) { + ItemStack current = container.getItemStack(slot); + ItemStack remaining = consumeOne(current); + + // remaining peut être null si la pile est épuisée + container.setItemStackForSlot(slot, remaining); +} +``` + +### Vérifier le Type d'Objet + +```java +public boolean isHoldingSword(ItemStack hand) { + if (hand == null || hand.isEmpty()) { + return false; + } + + // Vérifier le type d'objet par ID + return hand.getItemId().contains("sword"); + + // Ou vérifier via l'asset Item + // return hand.getItem().getCategory().equals("weapon"); +} +``` + +### Diviser des Piles + +```java +public ItemStack[] splitStack(ItemStack stack, int splitAmount) { + if (stack == null || stack.isEmpty()) { + return null; + } + + int currentQuantity = stack.getQuantity(); + if (splitAmount >= currentQuantity) { + return new ItemStack[] { stack, null }; + } + + // Créer deux piles + ItemStack remaining = stack.withQuantity(currentQuantity - splitAmount); + ItemStack split = stack.withQuantity(splitAmount); + + return new ItemStack[] { remaining, split }; +} +``` + +### Fusionner des Piles + +```java +public ItemStack[] mergeStacks(ItemStack target, ItemStack source) { + if (!target.isStackableWith(source)) { + return new ItemStack[] { target, source }; // Ne peut pas fusionner + } + + int maxStack = target.getItem().getMaxStack(); + int totalQuantity = target.getQuantity() + source.getQuantity(); + + if (totalQuantity <= maxStack) { + // Fusion complète + return new ItemStack[] { + target.withQuantity(totalQuantity), + null + }; + } + + // Fusion partielle + return new ItemStack[] { + target.withQuantity(maxStack), + source.withQuantity(totalQuantity - maxStack) + }; +} +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Rappelez-vous :** +- Utilisez `getQuantity()` pas `getCount()` - Hytale utilise "quantity" +- `withQuantity(0)` retourne `null` - vérifiez cela ! +- Utilisez `ItemStack.isEmpty(stack)` pour gérer null et vide +- Les ItemStacks sont principalement immuables - les méthodes `with*` retournent de nouvelles instances +- Utilisez `isStackableWith()` avant de tenter de fusionner des piles +- `ResourceType.Id` ne doit pas être null lors de la création d'objets via codecs +{{< /callout >}} + +```java +// Bon : Gérer null de withQuantity +ItemStack result = stack.withQuantity(newQuantity); +if (result == null) { + // Pile épuisée, gérer appropriément +} + +// Bon : Vérification vide sécurisée +if (ItemStack.isEmpty(stack)) { + // Gère à la fois null et ItemStack.EMPTY vide +} + +// Mauvais : Ignorer la valeur retournée +stack.withQuantity(10); // Retourne nouvelle pile, original inchangé ! +``` + +## Référence API ItemStack + +### Méthodes d'Instance + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getItemId()` | `String` | L'identifiant du type d'objet | +| `getItem()` | `Item` | L'asset Item (retourne `Item.UNKNOWN` si non trouvé) | +| `getQuantity()` | `int` | Taille de la pile | +| `getDurability()` | `double` | Durabilité actuelle | +| `getMaxDurability()` | `double` | Durabilité maximum | +| `isEmpty()` | `boolean` | True si itemId égale "Empty" | +| `isUnbreakable()` | `boolean` | True si maxDurability <= 0 | +| `isBroken()` | `boolean` | True si PAS incassable ET durability == 0 | +| `isValid()` | `boolean` | True si vide OU l'asset existe | +| `isStackableWith(ItemStack)` | `boolean` | Même itemId, durability, maxDurability, metadata | +| `isEquivalentType(ItemStack)` | `boolean` | Même itemId et metadata (ignore durabilité) | +| `getBlockKey()` | `String?` | ID bloc si plaçable, null sinon | +| `getOverrideDroppedItemAnimation()` | `boolean` | Flag d'override animation | +| `getMetadata()` | `BsonDocument?` | **Déprécié** - Retourne metadata clonée | +| `getFromMetadataOrNull(KeyedCodec)` | `T?` | Obtenir valeur metadata typée | +| `getFromMetadataOrNull(String, Codec)` | `T?` | Obtenir metadata par clé | +| `getFromMetadataOrDefault(String, BuilderCodec)` | `T` | Obtenir metadata avec défaut | + +### Méthodes de Modification (retournent nouveau ItemStack) + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `withQuantity(int)` | `ItemStack?` | **Retourne null si quantité est 0** | +| `withDurability(double)` | `ItemStack` | Borné à [0, maxDurability] | +| `withMaxDurability(double)` | `ItemStack` | Borne aussi la durabilité actuelle | +| `withIncreasedDurability(double)` | `ItemStack` | Ajoute à la durabilité | +| `withRestoredDurability(double)` | `ItemStack` | Définit durabilité et max | +| `withState(String)` | `ItemStack` | Change l'état de l'objet | +| `withMetadata(BsonDocument)` | `ItemStack` | Remplace toutes les metadata | +| `withMetadata(KeyedCodec, T)` | `ItemStack` | Définit metadata typée | +| `withMetadata(String, Codec, T)` | `ItemStack` | Définit metadata par clé | +| `withMetadata(String, BsonValue)` | `ItemStack` | Définit valeur BSON brute | +| `setOverrideDroppedItemAnimation(boolean)` | `void` | **Mute en place** | + +### Champs Statiques + +| Champ | Type | Description | +|-------|------|-------------| +| `EMPTY` | `ItemStack` | Pile vide singleton (itemId = "Empty") | +| `EMPTY_ARRAY` | `ItemStack[]` | Constante tableau vide | +| `CODEC` | `BuilderCodec` | Codec de sérialisation | + +### Méthodes Statiques + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `isEmpty(ItemStack)` | `boolean` | Vérification vide null-safe | +| `isStackableWith(ItemStack, ItemStack)` | `boolean` | Vérification empilable null-safe | +| `isEquivalentType(ItemStack, ItemStack)` | `boolean` | Vérification type null-safe | +| `isSameItemType(ItemStack, ItemStack)` | `boolean` | Compare itemId seulement | +| `fromPacket(ItemQuantity)` | `ItemStack?` | Créer depuis paquet réseau | + +### Classes Imbriquées + +| Classe | Description | +|-------|-------------| +| `ItemStack.Metadata` | Contient constante `BLOCK_STATE` pour clé metadata état bloc | diff --git a/content/world/entities/inventory/transactions.en.md b/content/world/entities/inventory/transactions.en.md new file mode 100644 index 0000000..426f364 --- /dev/null +++ b/content/world/entities/inventory/transactions.en.md @@ -0,0 +1,430 @@ +--- +title: Transactions +type: docs +weight: 3 +--- + +Transactions track the results of inventory operations in Hytale. Every modification to an `ItemContainer` returns a Transaction object that describes what happened. + +## Transaction Interface + +All transactions implement the base `Transaction` interface: + +```java +public interface Transaction { + // Did the operation succeed? + boolean succeeded(); + + // Was a specific slot modified? + boolean wasSlotModified(short slot); +} +``` + +## Transaction Types + +### SlotTransaction + +Tracks changes to a single slot: + +```java +SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + +if (transaction.succeeded()) { + // What was in the slot before + ItemStack before = transaction.getSlotBefore(); + + // What is in the slot now + ItemStack after = transaction.getSlotAfter(); + + // The item that was removed/output + ItemStack output = transaction.getOutput(); + + // Which slot was affected + short slot = transaction.getSlot(); + + // What type of action (ADD, REMOVE, REPLACE) + ActionType action = transaction.getAction(); +} +``` + +### ItemStackSlotTransaction + +Extended slot transaction with additional details: + +```java +ItemStackSlotTransaction transaction = container.setItemStackForSlot( + (short) 0, + new ItemStack("iron_sword", 1) +); + +if (transaction.succeeded()) { + short slot = transaction.getSlot(); + ItemStack before = transaction.getSlotBefore(); + ItemStack after = transaction.getSlotAfter(); + + // Check options used + boolean wasFiltered = transaction.isFilter(); + boolean wasAllOrNothing = transaction.isAllOrNothing(); +} +``` + +### ItemStackTransaction + +Tracks operations that may affect multiple slots: + +```java +ItemStack toAdd = new ItemStack("stone", 128); +ItemStackTransaction transaction = container.addItemStack(toAdd); + +if (transaction.succeeded()) { + // Items that couldn't fit (null if all fit) + ItemStack remainder = transaction.getRemainder(); + + // Original item we tried to add + ItemStack query = transaction.getQuery(); + + // List of all slot transactions that occurred + List slotTransactions = transaction.getSlotTransactions(); + + for (ItemStackSlotTransaction slotTx : slotTransactions) { + getLogger().at(Level.INFO).log("Modified slot " + slotTx.getSlot()); + } +} +``` + +### MoveTransaction + +Tracks moving items between containers: + +```java +MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 +); + +if (transaction.succeeded()) { + // Transaction for removing from source + SlotTransaction removeTransaction = transaction.getRemoveTransaction(); + + // Transaction for adding to destination + SlotTransaction addTransaction = transaction.getAddTransaction(); + + // The destination container + ItemContainer destination = transaction.getOtherContainer(); + + // Direction of the move + MoveType moveType = transaction.getMoveType(); +} +``` + +### ListTransaction + +Wraps multiple transactions: + +```java +List items = List.of( + new ItemStack("stone", 64), + new ItemStack("wood", 64) +); + +ListTransaction transaction = container.addItemStacks(items); + +if (transaction.succeeded()) { + List results = transaction.getList(); + + for (ItemStackTransaction result : results) { + if (result.succeeded()) { + ItemStack remainder = result.getRemainder(); + // ... + } + } +} +``` + +## ActionType + +Operations are categorized by action type: + +```java +public enum ActionType { + SET, // Items set (add=true, remove=false, destroy=true) + ADD, // Items added to slot (add=true, remove=false, destroy=false) + REMOVE, // Items removed from slot (add=false, remove=true, destroy=false) + REPLACE // Slot contents replaced (add=true, remove=true, destroy=false) +} + +// Check action characteristics +if (action.isAdd()) { /* operation adds items */ } +if (action.isRemove()) { /* operation removes items */ } +if (action.isDestroy()) { /* operation destroys slot contents */ } +``` + +## MoveType + +Direction of move operations: + +```java +public enum MoveType { + MOVE_TO_SELF, // Items being moved TO this container + MOVE_FROM_SELF // Items being moved FROM this container +} +``` + +## Common Patterns + +### Check Before Modify + +```java +// Safe pattern: verify first, then execute +public boolean safeTransfer(ItemContainer from, ItemContainer to, String itemId, int amount) { + ItemStack toRemove = new ItemStack(itemId, amount); + + // Check both operations can succeed + if (!from.canRemoveItemStack(toRemove)) { + return false; // Not enough items + } + + if (!to.canAddItemStack(toRemove)) { + return false; // No space + } + + // Execute removal + ItemStackTransaction removeResult = from.removeItemStack(toRemove); + if (!removeResult.succeeded()) { + return false; + } + + // Execute addition + ItemStack removed = removeResult.getQuery(); + ItemStackTransaction addResult = to.addItemStack(removed); + + // Handle any remainder + ItemStack remainder = addResult.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Put remainder back + from.addItemStack(remainder); + } + + return true; +} +``` + +### Handling Remainders + +```java +public void giveItemSafe(Player player, String itemId, int quantity) { + ItemContainer storage = player.getInventory().getStorage(); + ItemStack item = new ItemStack(itemId, quantity); + + ItemStackTransaction result = storage.addItemStack(item); + + if (!result.succeeded()) { + player.sendMessage(Message.raw("Inventory full!")); + return; + } + + ItemStack remainder = result.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + player.sendMessage(Message.raw( + "Only received " + (quantity - remainder.getQuantity()) + " items, " + + "inventory full!" + )); + } +} +``` + +### Atomic Operations + +```java +// allOrNothing=true ensures partial operations don't happen +public boolean buyItem(Player player, String itemId, int price, int quantity) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + + ItemStack currency = new ItemStack("gold_coin", price); + ItemStack item = new ItemStack(itemId, quantity); + + // Check both operations can succeed fully + if (!storage.canRemoveItemStack(currency)) { + player.sendMessage(Message.raw("Not enough gold!")); + return false; + } + + if (!storage.canAddItemStack(item)) { + player.sendMessage(Message.raw("Inventory full!")); + return false; + } + + // Remove currency with allOrNothing=true + ItemStackTransaction removeResult = storage.removeItemStack(currency, true, true); + if (!removeResult.succeeded()) { + return false; + } + + // Add item + ItemStackTransaction addResult = storage.addItemStack(item, true, false, true); + if (!addResult.succeeded()) { + // Rollback: return the currency + storage.addItemStack(currency); + return false; + } + + return true; +} +``` + +### Tracking Changes + +```java +// Use wasSlotModified to check specific slots +public void onContainerChange(ItemContainer.ItemContainerChangeEvent event) { + Transaction transaction = event.transaction(); + + // Check if hotbar slot 0 was affected + if (transaction.wasSlotModified((short) 0)) { + getLogger().at(Level.INFO).log("First slot was modified!"); + } + + // Check all hotbar slots + for (short i = 0; i < 9; i++) { + if (transaction.wasSlotModified(i)) { + getLogger().at(Level.INFO).log("Hotbar slot " + i + " modified"); + } + } +} +``` + +## Transaction Options + +Many operations accept optional parameters: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `allOrNothing` | If true, operation fails if not all items can be processed | `false` | +| `fullStacks` | If true, only add to empty slots (not partial stacks) | `false` | +| `exactAmount` | If true, must remove exact quantity requested | `true` | +| `filter` | If true, respect slot filters | `true` | + +```java +// Default behavior +container.addItemStack(item); + +// With options +container.addItemStack(item, true, false, true); // allOrNothing, fullStacks, filter +container.removeItemStack(item, true, true); // allOrNothing, filter +``` + +## Best Practices + +{{< callout type="info" >}} +**Transaction Tips:** +- Always check `succeeded()` before accessing results +- Handle remainders when adding items +- Use `canAddItemStack()`/`canRemoveItemStack()` for pre-validation +- Use `allOrNothing=true` for critical operations +- Check `wasSlotModified()` to track specific slot changes +{{< /callout >}} + +```java +// Good: Check success and handle remainder +ItemStackTransaction tx = container.addItemStack(item); +if (tx.succeeded()) { + ItemStack remainder = tx.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Handle leftover items + } +} + +// Bad: Assume success +container.addItemStack(item); // Might fail silently! +``` + +## Transaction API Reference + +### Transaction Interface + +| Method | Returns | Description | +|--------|---------|-------------| +| `succeeded()` | `boolean` | True if operation succeeded | +| `wasSlotModified(short)` | `boolean` | True if specific slot was modified | + +### SlotTransaction + +| Method | Returns | Description | +|--------|---------|-------------| +| `getSlot()` | `short` | The affected slot index | +| `getAction()` | `ActionType` | Type of action performed | +| `getSlotBefore()` | `ItemStack?` | Contents before operation | +| `getSlotAfter()` | `ItemStack?` | Contents after operation | +| `getOutput()` | `ItemStack?` | Items removed/output | +| `isAllOrNothing()` | `boolean` | allOrNothing parameter used | +| `isExactAmount()` | `boolean` | exactAmount parameter used | +| `isFilter()` | `boolean` | filter parameter used | + +| Static | Type | Description | +|--------|------|-------------| +| `FAILED_ADD` | `SlotTransaction` | Pre-built failed add transaction | + +### ItemStackSlotTransaction (extends SlotTransaction) + +| Method | Returns | Description | +|--------|---------|-------------| +| `isAddToExistingSlot()` | `boolean` | True if added to existing stack | +| `getQuery()` | `ItemStack?` | Original item requested | +| `getRemainder()` | `ItemStack?` | Items that couldn't fit | + +### ItemStackTransaction + +| Method | Returns | Description | +|--------|---------|-------------| +| `getAction()` | `ActionType?` | Type of action performed | +| `getQuery()` | `ItemStack?` | Original item requested | +| `getRemainder()` | `ItemStack?` | Items that couldn't fit | +| `isAllOrNothing()` | `boolean` | allOrNothing parameter used | +| `isFilter()` | `boolean` | filter parameter used | +| `getSlotTransactions()` | `List` | All slot transactions | + +| Static | Type | Description | +|--------|------|-------------| +| `FAILED_ADD` | `ItemStackTransaction` | Pre-built failed add transaction | + +### MoveTransaction + +| Method | Returns | Description | +|--------|---------|-------------| +| `getRemoveTransaction()` | `SlotTransaction` | Transaction for removal | +| `getMoveType()` | `MoveType` | Direction of move | +| `getOtherContainer()` | `ItemContainer` | The other container involved | +| `getAddTransaction()` | `T` | Transaction for addition | +| `toInverted(ItemContainer)` | `MoveTransaction` | Create inverted view for other container | + +### ListTransaction + +| Method | Returns | Description | +|--------|---------|-------------| +| `getList()` | `List` | All transactions in list | +| `size()` | `int` | Number of transactions | + +| Static | Type | Description | +|--------|------|-------------| +| `EMPTY_SUCCESSFUL_TRANSACTION` | `ListTransaction` | Pre-built empty success | +| `EMPTY_FAILED_TRANSACTION` | `ListTransaction` | Pre-built empty failure | +| `getEmptyTransaction(boolean)` | `ListTransaction` | Get empty success/failure | + +### ActionType Enum + +| Value | isAdd | isRemove | isDestroy | Description | +|-------|-------|----------|-----------|-------------| +| `SET` | true | false | true | Set slot contents | +| `ADD` | true | false | false | Add items to slot | +| `REMOVE` | false | true | false | Remove items from slot | +| `REPLACE` | true | true | false | Replace slot contents | + +### MoveType Enum + +| Value | Description | +|-------|-------------| +| `MOVE_TO_SELF` | Items moving to this container | +| `MOVE_FROM_SELF` | Items moving from this container | diff --git a/content/world/entities/inventory/transactions.fr.md b/content/world/entities/inventory/transactions.fr.md new file mode 100644 index 0000000..fdc0c02 --- /dev/null +++ b/content/world/entities/inventory/transactions.fr.md @@ -0,0 +1,430 @@ +--- +title: Transactions +type: docs +weight: 3 +--- + +Les transactions suivent les résultats des opérations d'inventaire dans Hytale. Chaque modification d'un `ItemContainer` retourne un objet Transaction qui décrit ce qui s'est passé. + +## Interface Transaction + +Toutes les transactions implémentent l'interface de base `Transaction` : + +```java +public interface Transaction { + // L'opération a-t-elle réussi ? + boolean succeeded(); + + // Un slot spécifique a-t-il été modifié ? + boolean wasSlotModified(short slot); +} +``` + +## Types de Transactions + +### SlotTransaction + +Suit les changements d'un seul slot : + +```java +SlotTransaction transaction = container.removeItemStackFromSlot((short) 0); + +if (transaction.succeeded()) { + // Ce qui était dans le slot avant + ItemStack before = transaction.getSlotBefore(); + + // Ce qui est dans le slot maintenant + ItemStack after = transaction.getSlotAfter(); + + // L'objet qui a été retiré/sorti + ItemStack output = transaction.getOutput(); + + // Quel slot a été affecté + short slot = transaction.getSlot(); + + // Quel type d'action (ADD, REMOVE, REPLACE) + ActionType action = transaction.getAction(); +} +``` + +### ItemStackSlotTransaction + +Transaction de slot étendue avec des détails supplémentaires : + +```java +ItemStackSlotTransaction transaction = container.setItemStackForSlot( + (short) 0, + new ItemStack("iron_sword", 1) +); + +if (transaction.succeeded()) { + short slot = transaction.getSlot(); + ItemStack before = transaction.getSlotBefore(); + ItemStack after = transaction.getSlotAfter(); + + // Vérifier les options utilisées + boolean wasFiltered = transaction.isFilter(); + boolean wasAllOrNothing = transaction.isAllOrNothing(); +} +``` + +### ItemStackTransaction + +Suit les opérations qui peuvent affecter plusieurs slots : + +```java +ItemStack toAdd = new ItemStack("stone", 128); +ItemStackTransaction transaction = container.addItemStack(toAdd); + +if (transaction.succeeded()) { + // Objets qui n'ont pas pu rentrer (null si tous ont rentré) + ItemStack remainder = transaction.getRemainder(); + + // Objet original qu'on a essayé d'ajouter + ItemStack query = transaction.getQuery(); + + // Liste de toutes les transactions de slot qui ont eu lieu + List slotTransactions = transaction.getSlotTransactions(); + + for (ItemStackSlotTransaction slotTx : slotTransactions) { + getLogger().at(Level.INFO).log("Slot modifié " + slotTx.getSlot()); + } +} +``` + +### MoveTransaction + +Suit le déplacement d'objets entre conteneurs : + +```java +MoveTransaction transaction = storage.moveItemStackFromSlotToSlot( + (short) 0, + 32, + hotbar, + (short) 0 +); + +if (transaction.succeeded()) { + // Transaction pour le retrait de la source + SlotTransaction removeTransaction = transaction.getRemoveTransaction(); + + // Transaction pour l'ajout à la destination + SlotTransaction addTransaction = transaction.getAddTransaction(); + + // Le conteneur de destination + ItemContainer destination = transaction.getOtherContainer(); + + // Direction du déplacement + MoveType moveType = transaction.getMoveType(); +} +``` + +### ListTransaction + +Encapsule plusieurs transactions : + +```java +List items = List.of( + new ItemStack("stone", 64), + new ItemStack("wood", 64) +); + +ListTransaction transaction = container.addItemStacks(items); + +if (transaction.succeeded()) { + List results = transaction.getList(); + + for (ItemStackTransaction result : results) { + if (result.succeeded()) { + ItemStack remainder = result.getRemainder(); + // ... + } + } +} +``` + +## ActionType + +Les opérations sont catégorisées par type d'action : + +```java +public enum ActionType { + SET, // Objets définis (add=true, remove=false, destroy=true) + ADD, // Objets ajoutés au slot (add=true, remove=false, destroy=false) + REMOVE, // Objets retirés du slot (add=false, remove=true, destroy=false) + REPLACE // Contenu du slot remplacé (add=true, remove=true, destroy=false) +} + +// Vérifier les caractéristiques de l'action +if (action.isAdd()) { /* opération ajoute des objets */ } +if (action.isRemove()) { /* opération retire des objets */ } +if (action.isDestroy()) { /* opération détruit le contenu du slot */ } +``` + +## MoveType + +Direction des opérations de déplacement : + +```java +public enum MoveType { + MOVE_TO_SELF, // Objets déplacés VERS ce conteneur + MOVE_FROM_SELF // Objets déplacés DEPUIS ce conteneur +} +``` + +## Patterns Courants + +### Vérifier Avant de Modifier + +```java +// Pattern sûr : vérifier d'abord, puis exécuter +public boolean safeTransfer(ItemContainer from, ItemContainer to, String itemId, int amount) { + ItemStack toRemove = new ItemStack(itemId, amount); + + // Vérifier que les deux opérations peuvent réussir + if (!from.canRemoveItemStack(toRemove)) { + return false; // Pas assez d'objets + } + + if (!to.canAddItemStack(toRemove)) { + return false; // Pas de place + } + + // Exécuter le retrait + ItemStackTransaction removeResult = from.removeItemStack(toRemove); + if (!removeResult.succeeded()) { + return false; + } + + // Exécuter l'ajout + ItemStack removed = removeResult.getQuery(); + ItemStackTransaction addResult = to.addItemStack(removed); + + // Gérer les restes éventuels + ItemStack remainder = addResult.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Remettre les restes + from.addItemStack(remainder); + } + + return true; +} +``` + +### Gérer les Restes + +```java +public void giveItemSafe(Player player, String itemId, int quantity) { + ItemContainer storage = player.getInventory().getStorage(); + ItemStack item = new ItemStack(itemId, quantity); + + ItemStackTransaction result = storage.addItemStack(item); + + if (!result.succeeded()) { + player.sendMessage(Message.raw("Inventaire plein !")); + return; + } + + ItemStack remainder = result.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + player.sendMessage(Message.raw( + "Seulement reçu " + (quantity - remainder.getQuantity()) + " objets, " + + "inventaire plein !" + )); + } +} +``` + +### Opérations Atomiques + +```java +// allOrNothing=true garantit que les opérations partielles ne se produisent pas +public boolean buyItem(Player player, String itemId, int price, int quantity) { + Inventory inv = player.getInventory(); + ItemContainer storage = inv.getStorage(); + + ItemStack currency = new ItemStack("gold_coin", price); + ItemStack item = new ItemStack(itemId, quantity); + + // Vérifier que les deux opérations peuvent réussir complètement + if (!storage.canRemoveItemStack(currency)) { + player.sendMessage(Message.raw("Pas assez d'or !")); + return false; + } + + if (!storage.canAddItemStack(item)) { + player.sendMessage(Message.raw("Inventaire plein !")); + return false; + } + + // Retirer la monnaie avec allOrNothing=true + ItemStackTransaction removeResult = storage.removeItemStack(currency, true, true); + if (!removeResult.succeeded()) { + return false; + } + + // Ajouter l'objet + ItemStackTransaction addResult = storage.addItemStack(item, true, false, true); + if (!addResult.succeeded()) { + // Annuler : rendre la monnaie + storage.addItemStack(currency); + return false; + } + + return true; +} +``` + +### Suivre les Changements + +```java +// Utiliser wasSlotModified pour vérifier des slots spécifiques +public void onContainerChange(ItemContainer.ItemContainerChangeEvent event) { + Transaction transaction = event.transaction(); + + // Vérifier si le slot 0 de la hotbar a été affecté + if (transaction.wasSlotModified((short) 0)) { + getLogger().at(Level.INFO).log("Premier slot modifié !"); + } + + // Vérifier tous les slots de la hotbar + for (short i = 0; i < 9; i++) { + if (transaction.wasSlotModified(i)) { + getLogger().at(Level.INFO).log("Slot hotbar " + i + " modifié"); + } + } +} +``` + +## Options de Transaction + +De nombreuses opérations acceptent des paramètres optionnels : + +| Paramètre | Description | Par défaut | +|-----------|-------------|---------| +| `allOrNothing` | Si vrai, l'opération échoue si tous les objets ne peuvent pas être traités | `false` | +| `fullStacks` | Si vrai, ajoute seulement aux slots vides (pas aux piles partielles) | `false` | +| `exactAmount` | Si vrai, doit retirer exactement la quantité demandée | `true` | +| `filter` | Si vrai, respecte les filtres de slot | `true` | + +```java +// Comportement par défaut +container.addItemStack(item); + +// Avec options +container.addItemStack(item, true, false, true); // allOrNothing, fullStacks, filter +container.removeItemStack(item, true, true); // allOrNothing, filter +``` + +## Bonnes Pratiques + +{{< callout type="info" >}} +**Conseils pour les Transactions :** +- Toujours vérifier `succeeded()` avant d'accéder aux résultats +- Gérer les restes lors de l'ajout d'objets +- Utiliser `canAddItemStack()`/`canRemoveItemStack()` pour la pré-validation +- Utiliser `allOrNothing=true` pour les opérations critiques +- Vérifier `wasSlotModified()` pour suivre les changements de slots spécifiques +{{< /callout >}} + +```java +// Bon : Vérifier le succès et gérer les restes +ItemStackTransaction tx = container.addItemStack(item); +if (tx.succeeded()) { + ItemStack remainder = tx.getRemainder(); + if (!ItemStack.isEmpty(remainder)) { + // Gérer les objets restants + } +} + +// Mauvais : Supposer le succès +container.addItemStack(item); // Pourrait échouer silencieusement ! +``` + +## Référence API Transaction + +### Interface Transaction + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `succeeded()` | `boolean` | True si l'opération a réussi | +| `wasSlotModified(short)` | `boolean` | True si le slot spécifique a été modifié | + +### SlotTransaction + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getSlot()` | `short` | Index du slot affecté | +| `getAction()` | `ActionType` | Type d'action effectuée | +| `getSlotBefore()` | `ItemStack?` | Contenu avant l'opération | +| `getSlotAfter()` | `ItemStack?` | Contenu après l'opération | +| `getOutput()` | `ItemStack?` | Objets retirés/sortis | +| `isAllOrNothing()` | `boolean` | Paramètre allOrNothing utilisé | +| `isExactAmount()` | `boolean` | Paramètre exactAmount utilisé | +| `isFilter()` | `boolean` | Paramètre filter utilisé | + +| Statique | Type | Description | +|----------|------|-------------| +| `FAILED_ADD` | `SlotTransaction` | Transaction d'ajout échouée pré-construite | + +### ItemStackSlotTransaction (étend SlotTransaction) + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `isAddToExistingSlot()` | `boolean` | True si ajouté à une pile existante | +| `getQuery()` | `ItemStack?` | Objet original demandé | +| `getRemainder()` | `ItemStack?` | Objets qui n'ont pas pu rentrer | + +### ItemStackTransaction + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getAction()` | `ActionType?` | Type d'action effectuée | +| `getQuery()` | `ItemStack?` | Objet original demandé | +| `getRemainder()` | `ItemStack?` | Objets qui n'ont pas pu rentrer | +| `isAllOrNothing()` | `boolean` | Paramètre allOrNothing utilisé | +| `isFilter()` | `boolean` | Paramètre filter utilisé | +| `getSlotTransactions()` | `List` | Toutes les transactions de slot | + +| Statique | Type | Description | +|----------|------|-------------| +| `FAILED_ADD` | `ItemStackTransaction` | Transaction d'ajout échouée pré-construite | + +### MoveTransaction + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getRemoveTransaction()` | `SlotTransaction` | Transaction de retrait | +| `getMoveType()` | `MoveType` | Direction du déplacement | +| `getOtherContainer()` | `ItemContainer` | L'autre conteneur impliqué | +| `getAddTransaction()` | `T` | Transaction d'ajout | +| `toInverted(ItemContainer)` | `MoveTransaction` | Créer vue inversée pour l'autre conteneur | + +### ListTransaction + +| Méthode | Retour | Description | +|--------|---------|-------------| +| `getList()` | `List` | Toutes les transactions de la liste | +| `size()` | `int` | Nombre de transactions | + +| Statique | Type | Description | +|----------|------|-------------| +| `EMPTY_SUCCESSFUL_TRANSACTION` | `ListTransaction` | Succès vide pré-construit | +| `EMPTY_FAILED_TRANSACTION` | `ListTransaction` | Échec vide pré-construit | +| `getEmptyTransaction(boolean)` | `ListTransaction` | Obtenir succès/échec vide | + +### Enum ActionType + +| Valeur | isAdd | isRemove | isDestroy | Description | +|--------|-------|----------|-----------|-------------| +| `SET` | true | false | true | Définir le contenu du slot | +| `ADD` | true | false | false | Ajouter des objets au slot | +| `REMOVE` | false | true | false | Retirer des objets du slot | +| `REPLACE` | true | true | false | Remplacer le contenu du slot | + +### Enum MoveType + +| Valeur | Description | +|--------|-------------| +| `MOVE_TO_SELF` | Objets déplacés vers ce conteneur | +| `MOVE_FROM_SELF` | Objets déplacés depuis ce conteneur | diff --git a/content/world/entities/mounts.en.md b/content/world/entities/mounts.en.md new file mode 100644 index 0000000..295cfb0 --- /dev/null +++ b/content/world/entities/mounts.en.md @@ -0,0 +1,291 @@ +--- +title: Mounts +type: docs +weight: 11 +--- + +The mount system allows entities to ride other entities or sit on blocks. + +**Package:** `com.hypixel.hytale.builtin.mounts` + +## Overview + +The mount system supports: +- Riding NPC entities +- Sitting on block-based seats +- Minecart vehicle mechanics +- Mount-specific movement controls + +## Architecture + +``` +Mount System +├── Components +│ ├── NPCMountComponent - Mount NPC configuration +│ ├── MountedComponent - "I am riding something" +│ ├── MountedByComponent - "Something rides me" +│ ├── BlockMountComponent - Block seat +│ └── MinecartComponent - Minecart vehicle +├── Systems +│ ├── MountSystems - Core mount logic +│ └── NPCMountSystems - NPC-specific logic +├── Interactions +│ ├── MountInteraction - Mount NPC +│ ├── SeatingInteraction - Sit on block +│ └── SpawnMinecartInteraction - Create minecart +└── Commands + ├── MountCommand - Force mount + ├── MountCheckCommand - Check status + └── DismountCommand - Force dismount +``` + +## Components + +### NPCMountComponent + +Attached to NPCs that can be mounted: + +```java +public class NPCMountComponent implements Component { + // Player currently riding (if any) + private PlayerRef ownerPlayerRef; + + // Original NPC role before mounting + private int originalRoleIndex; + + // Mount configuration + // Movement speeds, abilities, etc. +} +``` + +### MountedComponent + +Attached to entities that are riding something: + +```java +public class MountedComponent implements Component { + // Reference to what we're riding + private Ref mountRef; +} +``` + +### MountedByComponent + +Attached to entities being ridden: + +```java +public class MountedByComponent implements Component { + // Reference to the rider + private Ref riderRef; +} +``` + +### BlockMountComponent + +Chunk component for block-based seating: + +```java +public class BlockMountComponent implements Component { + // Block position and configuration + // Player currently seated +} +``` + +### MinecartComponent + +Vehicle component for minecarts: + +```java +public class MinecartComponent implements Component { + // Rail movement configuration + // Speed, acceleration, etc. +} +``` + +## Mounting NPCs + +### Mount Interaction + +Players mount NPCs via interaction: + +```java +// Registered as "Mount" interaction type +Interaction.CODEC.register("Mount", + MountInteraction.class, + MountInteraction.CODEC); +``` + +### Mounting Process + +1. Player interacts with mountable NPC +2. `NPCMountComponent` is added/configured +3. `MountedComponent` added to player +4. `MountedByComponent` added to NPC +5. NPC role changes to "mounted" behavior +6. Player movement settings updated + +### Dismounting + +Dismounting can happen: +- Player manually dismounts +- Player disconnects +- Mount or player dies +- Command forces dismount + +```java +// Dismount NPC +MountPlugin.dismountNpc(store, mountEntityId); + +// Reset player movement settings +MountPlugin.resetOriginalPlayerMovementSettings(playerRef, store); +``` + +## Block Seating + +### Seating Interaction + +Players sit on blocks via interaction: + +```java +// Registered as "Seating" interaction type +Interaction.CODEC.register("Seating", + SeatingInteraction.class, + SeatingInteraction.CODEC); +``` + +### Block Mount API + +```java +BlockMountAPI api = BlockMountAPI.get(); + +// Check if block is a seat +boolean isSeat = api.isSeat(world, blockPos); + +// Get seated player +PlayerRef seated = api.getSeatedPlayer(world, blockPos); +``` + +## Minecarts + +### Spawn Minecart Interaction + +Create minecarts via interaction: + +```java +// Registered as "SpawnMinecart" interaction type +Interaction.CODEC.register("SpawnMinecart", + SpawnMinecartInteraction.class, + SpawnMinecartInteraction.CODEC); +``` + +### Minecart Behavior + +- Follows rail tracks +- Configurable speed and acceleration +- Can carry players and items +- Collision with other minecarts + +## NPC Core Component + +NPCs can use mount actions: + +```java +// Registered core component type +NPCPlugin.get().registerCoreComponentType("Mount", BuilderActionMount::new); +``` + +```json +{ + "Type": "Mount", + "MountTarget": "player", + "Duration": 10.0 +} +``` + +## Commands + +### /mount + +Force mount a player on an NPC: + +``` +/mount +``` + +### /mountcheck + +Check mount status: + +``` +/mountcheck +``` + +### /dismount + +Force dismount a player: + +``` +/dismount +``` + +## Plugin Access + +```java +MountPlugin mounts = MountPlugin.getInstance(); + +// Component types +ComponentType npcMountType = + mounts.getMountComponentType(); +ComponentType mountedType = + mounts.getMountedComponentType(); +ComponentType mountedByType = + mounts.getMountedByComponentType(); +ComponentType minecartType = + mounts.getMinecartComponentType(); +ComponentType blockMountType = + mounts.getBlockMountComponentType(); +``` + +## Systems + +### Mount Tracking + +```java +// Update mount position tracking +MountSystems.TrackerUpdate + +// Remove mount tracking on entity removal +MountSystems.TrackerRemove +``` + +### Death Handling + +```java +// Dismount player when they die +NPCMountSystems.DismountOnPlayerDeath + +// Dismount when mount dies +NPCMountSystems.DismountOnMountDeath +``` + +### Movement + +```java +// Handle player mount input +MountSystems.HandleMountInput + +// Teleport mounted entity with mount +MountSystems.TeleportMountedEntity +``` + +## Events + +### Player Disconnect + +When player disconnects while mounted: + +```java +// Automatically dismount on disconnect +getEventRegistry().register(PlayerDisconnectEvent.class, + MountPlugin::onPlayerDisconnect); +``` diff --git a/content/world/entities/mounts.fr.md b/content/world/entities/mounts.fr.md new file mode 100644 index 0000000..8f4a7ac --- /dev/null +++ b/content/world/entities/mounts.fr.md @@ -0,0 +1,291 @@ +--- +title: Montures +type: docs +weight: 11 +--- + +Le systeme de montures permet aux entites de chevaucher d'autres entites ou s'asseoir sur des blocs. + +**Package:** `com.hypixel.hytale.builtin.mounts` + +## Apercu + +Le systeme de montures supporte: +- Chevaucher des entites NPC +- S'asseoir sur des sieges bases sur blocs +- Mecaniques de vehicules minecart +- Controles de mouvement specifiques aux montures + +## Architecture + +``` +Systeme Montures +├── Composants +│ ├── NPCMountComponent - Config monture NPC +│ ├── MountedComponent - "Je chevauche quelque chose" +│ ├── MountedByComponent - "Quelque chose me chevauche" +│ ├── BlockMountComponent - Siege bloc +│ └── MinecartComponent - Vehicule minecart +├── Systemes +│ ├── MountSystems - Logique monture principale +│ └── NPCMountSystems - Logique specifique NPC +├── Interactions +│ ├── MountInteraction - Monter NPC +│ ├── SeatingInteraction - S'asseoir sur bloc +│ └── SpawnMinecartInteraction - Creer minecart +└── Commandes + ├── MountCommand - Forcer montage + ├── MountCheckCommand - Verifier statut + └── DismountCommand - Forcer demontage +``` + +## Composants + +### NPCMountComponent + +Attache aux NPCs qui peuvent etre montes: + +```java +public class NPCMountComponent implements Component { + // Joueur chevauchant actuellement (si present) + private PlayerRef ownerPlayerRef; + + // Role NPC original avant montage + private int originalRoleIndex; + + // Configuration monture + // Vitesses mouvement, capacites, etc. +} +``` + +### MountedComponent + +Attache aux entites qui chevauchent quelque chose: + +```java +public class MountedComponent implements Component { + // Reference vers ce qu'on chevauche + private Ref mountRef; +} +``` + +### MountedByComponent + +Attache aux entites etant chevauchees: + +```java +public class MountedByComponent implements Component { + // Reference vers le cavalier + private Ref riderRef; +} +``` + +### BlockMountComponent + +Composant chunk pour sieges bases sur blocs: + +```java +public class BlockMountComponent implements Component { + // Position bloc et configuration + // Joueur actuellement assis +} +``` + +### MinecartComponent + +Composant vehicule pour minecarts: + +```java +public class MinecartComponent implements Component { + // Configuration mouvement rails + // Vitesse, acceleration, etc. +} +``` + +## Monter des NPCs + +### Interaction Monture + +Les joueurs montent les NPCs via interaction: + +```java +// Enregistre comme type interaction "Mount" +Interaction.CODEC.register("Mount", + MountInteraction.class, + MountInteraction.CODEC); +``` + +### Processus de Montage + +1. Joueur interagit avec NPC montable +2. `NPCMountComponent` est ajoute/configure +3. `MountedComponent` ajoute au joueur +4. `MountedByComponent` ajoute au NPC +5. Role NPC change vers comportement "monte" +6. Parametres mouvement joueur mis a jour + +### Demontage + +Le demontage peut survenir: +- Joueur demonte manuellement +- Joueur se deconnecte +- Monture ou joueur meurt +- Commande force demontage + +```java +// Demonter NPC +MountPlugin.dismountNpc(store, mountEntityId); + +// Reinitialiser parametres mouvement joueur +MountPlugin.resetOriginalPlayerMovementSettings(playerRef, store); +``` + +## Sieges de Blocs + +### Interaction Siege + +Les joueurs s'assoient sur blocs via interaction: + +```java +// Enregistre comme type interaction "Seating" +Interaction.CODEC.register("Seating", + SeatingInteraction.class, + SeatingInteraction.CODEC); +``` + +### API Monture Bloc + +```java +BlockMountAPI api = BlockMountAPI.get(); + +// Verifier si bloc est un siege +boolean isSeat = api.isSeat(world, blockPos); + +// Obtenir joueur assis +PlayerRef seated = api.getSeatedPlayer(world, blockPos); +``` + +## Minecarts + +### Interaction Spawn Minecart + +Creer minecarts via interaction: + +```java +// Enregistre comme type interaction "SpawnMinecart" +Interaction.CODEC.register("SpawnMinecart", + SpawnMinecartInteraction.class, + SpawnMinecartInteraction.CODEC); +``` + +### Comportement Minecart + +- Suit les rails +- Vitesse et acceleration configurables +- Peut transporter joueurs et items +- Collision avec autres minecarts + +## Composant Core NPC + +Les NPCs peuvent utiliser actions de monture: + +```java +// Type composant core enregistre +NPCPlugin.get().registerCoreComponentType("Mount", BuilderActionMount::new); +``` + +```json +{ + "Type": "Mount", + "MountTarget": "player", + "Duration": 10.0 +} +``` + +## Commandes + +### /mount + +Forcer un joueur a monter un NPC: + +``` +/mount +``` + +### /mountcheck + +Verifier statut monture: + +``` +/mountcheck +``` + +### /dismount + +Forcer demontage d'un joueur: + +``` +/dismount +``` + +## Acces au Plugin + +```java +MountPlugin mounts = MountPlugin.getInstance(); + +// Types de composants +ComponentType npcMountType = + mounts.getMountComponentType(); +ComponentType mountedType = + mounts.getMountedComponentType(); +ComponentType mountedByType = + mounts.getMountedByComponentType(); +ComponentType minecartType = + mounts.getMinecartComponentType(); +ComponentType blockMountType = + mounts.getBlockMountComponentType(); +``` + +## Systemes + +### Suivi Monture + +```java +// Mettre a jour suivi position monture +MountSystems.TrackerUpdate + +// Supprimer suivi monture a suppression entite +MountSystems.TrackerRemove +``` + +### Gestion Mort + +```java +// Demonter joueur quand il meurt +NPCMountSystems.DismountOnPlayerDeath + +// Demonter quand monture meurt +NPCMountSystems.DismountOnMountDeath +``` + +### Mouvement + +```java +// Gerer input joueur sur monture +MountSystems.HandleMountInput + +// Teleporter entite montee avec monture +MountSystems.TeleportMountedEntity +``` + +## Evenements + +### Deconnexion Joueur + +Quand joueur se deconnecte en montant: + +```java +// Demonter automatiquement a deconnexion +getEventRegistry().register(PlayerDisconnectEvent.class, + MountPlugin::onPlayerDisconnect); +``` diff --git a/content/world/entities/npc/_index.en.md b/content/world/entities/npc/_index.en.md new file mode 100644 index 0000000..07d2cb8 --- /dev/null +++ b/content/world/entities/npc/_index.en.md @@ -0,0 +1,86 @@ +--- +title: NPC System +type: docs +weight: 10 +--- + +The NPC (Non-Player Character) system provides a complete framework for creating intelligent game characters with autonomous behavior, decision-making, and navigation capabilities. + +**Package:** `com.hypixel.hytale.server.npc` + +{{< cards >}} + {{< card link="npc-basics" title="NPC Basics" subtitle="Creating and configuring NPCs" >}} + {{< card link="npc-components" title="NPC Components" subtitle="Core components and data" >}} + {{< card link="npc-ai" title="NPC AI" subtitle="Blackboard, decisions, and sensors" >}} + {{< card link="npc-movement" title="NPC Movement" subtitle="Navigation and pathfinding" >}} + {{< card link="npc-commands" title="NPC Commands" subtitle="Admin and debug commands" >}} +{{< /cards >}} + +## Architecture Overview + +The NPC system is built on several interconnected subsystems: + +``` +NPCEntity +├── Blackboard (shared state/memory) +├── Role (behavioral template) +│ ├── Instructions (high-level goals) +│ ├── Sensors (perception) +│ └── Actions (behaviors) +├── DecisionMaker (AI logic) +│ ├── Evaluators (condition checking) +│ └── Options (action selection) +└── Movement + ├── MotionController (movement execution) + ├── PathFollower (path tracking) + └── NavigationGraph (A* pathfinding) +``` + +## Quick Example + +```java +// Register NPC-related event +getEventRegistry().register(NPCSpawnEvent.class, event -> { + NPCEntity npc = event.getNPC(); + + // Access blackboard for state + Blackboard blackboard = npc.getBlackboard(); + + // Get current role + Role role = npc.getRole(); + + // Check if NPC has target + if (blackboard.hasTarget()) { + Entity target = blackboard.getTarget(); + } +}); +``` + +## Key Classes + +| Class | Description | +|-------|-------------| +| `NPCEntity` | Base class for all NPCs | +| `NPCPlugin` | Plugin entry point for NPC system | +| `Blackboard` | Shared state container for NPC data | +| `Role` | Defines NPC behavior template | +| `DecisionMaker` | AI decision logic | +| `MotionController` | Movement execution | +| `PathFollower` | Path tracking and following | + +## Subpackages + +| Package | Files | Description | +|---------|-------|-------------| +| `corecomponents/` | 327 | Core ECS components for NPCs | +| `asset/` | 152 | NPC asset configuration | +| `util/` | 50 | Utility classes | +| `blackboard/` | 30 | State management | +| `movement/` | 27 | Movement behaviors | +| `systems/` | 25 | ECS systems | +| `commands/` | 23 | Admin commands | +| `decisionmaker/` | 22 | AI decision logic | +| `sensorinfo/` | 20 | Perception system | +| `role/` | 17 | Role definitions | +| `instructions/` | 14 | High-level behaviors | +| `navigation/` | 12 | Pathfinding | diff --git a/content/world/entities/npc/_index.fr.md b/content/world/entities/npc/_index.fr.md new file mode 100644 index 0000000..6190702 --- /dev/null +++ b/content/world/entities/npc/_index.fr.md @@ -0,0 +1,86 @@ +--- +title: Système NPC +type: docs +weight: 10 +--- + +Le système NPC (Non-Player Character) fournit un framework complet pour créer des personnages de jeu intelligents avec un comportement autonome, une prise de décision et des capacités de navigation. + +**Package:** `com.hypixel.hytale.server.npc` + +{{< cards >}} + {{< card link="npc-basics" title="Bases NPC" subtitle="Création et configuration des NPCs" >}} + {{< card link="npc-components" title="Composants NPC" subtitle="Composants de base et données" >}} + {{< card link="npc-ai" title="IA NPC" subtitle="Blackboard, décisions et capteurs" >}} + {{< card link="npc-movement" title="Mouvement NPC" subtitle="Navigation et pathfinding" >}} + {{< card link="npc-commands" title="Commandes NPC" subtitle="Commandes admin et debug" >}} +{{< /cards >}} + +## Vue d'Ensemble de l'Architecture + +Le système NPC est construit sur plusieurs sous-systèmes interconnectés : + +``` +NPCEntity +├── Blackboard (état/mémoire partagé) +├── Role (template comportemental) +│ ├── Instructions (objectifs haut niveau) +│ ├── Sensors (perception) +│ └── Actions (comportements) +├── DecisionMaker (logique IA) +│ ├── Evaluators (vérification conditions) +│ └── Options (sélection actions) +└── Movement + ├── MotionController (exécution mouvement) + ├── PathFollower (suivi de chemin) + └── NavigationGraph (pathfinding A*) +``` + +## Exemple Rapide + +```java +// Enregistrer un événement lié aux NPCs +getEventRegistry().register(NPCSpawnEvent.class, event -> { + NPCEntity npc = event.getNPC(); + + // Accéder au blackboard pour l'état + Blackboard blackboard = npc.getBlackboard(); + + // Obtenir le rôle actuel + Role role = npc.getRole(); + + // Vérifier si le NPC a une cible + if (blackboard.hasTarget()) { + Entity target = blackboard.getTarget(); + } +}); +``` + +## Classes Principales + +| Classe | Description | +|--------|-------------| +| `NPCEntity` | Classe de base pour tous les NPCs | +| `NPCPlugin` | Point d'entrée plugin pour le système NPC | +| `Blackboard` | Conteneur d'état partagé pour les données NPC | +| `Role` | Définit le template de comportement NPC | +| `DecisionMaker` | Logique de décision IA | +| `MotionController` | Exécution des mouvements | +| `PathFollower` | Suivi et parcours de chemin | + +## Sous-packages + +| Package | Fichiers | Description | +|---------|----------|-------------| +| `corecomponents/` | 327 | Composants ECS de base pour NPCs | +| `asset/` | 152 | Configuration des assets NPC | +| `util/` | 50 | Classes utilitaires | +| `blackboard/` | 30 | Gestion d'état | +| `movement/` | 27 | Comportements de mouvement | +| `systems/` | 25 | Systèmes ECS | +| `commands/` | 23 | Commandes admin | +| `decisionmaker/` | 22 | Logique de décision IA | +| `sensorinfo/` | 20 | Système de perception | +| `role/` | 17 | Définitions de rôles | +| `instructions/` | 14 | Comportements haut niveau | +| `navigation/` | 12 | Pathfinding | diff --git a/content/world/entities/npc/npc-ai.en.md b/content/world/entities/npc/npc-ai.en.md new file mode 100644 index 0000000..879a5c6 --- /dev/null +++ b/content/world/entities/npc/npc-ai.en.md @@ -0,0 +1,373 @@ +--- +title: NPC AI +type: docs +weight: 3 +--- + +The NPC AI system provides intelligent decision-making through blackboards, decision makers, sensors, and instructions. + +**Packages:** +- `com.hypixel.hytale.server.npc.blackboard` +- `com.hypixel.hytale.server.npc.decisionmaker` +- `com.hypixel.hytale.server.npc.sensorinfo` +- `com.hypixel.hytale.server.npc.instructions` + +## Blackboard System + +The Blackboard is a shared memory space where NPC components communicate through key-value pairs. + +### Blackboard Class + +```java +public class Blackboard { + private Map, Object> data; + + // Store value + public void set(BlackboardKey key, T value); + + // Retrieve value + public T get(BlackboardKey key); + public T getOrDefault(BlackboardKey key, T defaultValue); + + // Check existence + public boolean has(BlackboardKey key); + + // Remove value + public void remove(BlackboardKey key); + + // Clear all + public void clear(); +} +``` + +### BlackboardKey + +Type-safe keys for blackboard access: + +```java +// Predefined keys +public class BlackboardKeys { + public static final BlackboardKey TARGET = + new BlackboardKey<>("target", Entity.class); + public static final BlackboardKey HOME_POSITION = + new BlackboardKey<>("home_position", Vector3d.class); + public static final BlackboardKey ALERT_LEVEL = + new BlackboardKey<>("alert_level", Float.class); + public static final BlackboardKey IN_COMBAT = + new BlackboardKey<>("in_combat", Boolean.class); +} + +// Custom keys +BlackboardKey CUSTOM_KEY = new BlackboardKey<>("custom_data", String.class); +``` + +### Using the Blackboard + +```java +NPCEntity npc = // get NPC +Blackboard bb = npc.getBlackboard(); + +// Set target +bb.set(BlackboardKeys.TARGET, targetEntity); + +// Get home position +Vector3d home = bb.getOrDefault(BlackboardKeys.HOME_POSITION, npc.getPosition()); + +// Check combat status +if (bb.getOrDefault(BlackboardKeys.IN_COMBAT, false)) { + // Handle combat +} +``` + +## Decision Maker System + +The Decision Maker evaluates options and selects the best action for the NPC to take. + +### DecisionMaker Interface + +```java +public interface DecisionMaker { + // Evaluate and select best option + Option evaluate(NPCEntity npc, Blackboard blackboard); + + // Get all available options + List