-
Notifications
You must be signed in to change notification settings - Fork 2
EcsEventHelper
EcsEventHelper provides simplified access to ECS-based events in Hytale. These events require Entity Component System (ECS) registration, which EcsEventHelper handles automatically for you.
What it does:
- Creates and registers ECS systems automatically
- Provides simple callbacks for block breaking, placing, and damage tracking
- Filters out false positives (e.g., "Empty" blocks during placement)
- Extracts block type, item information, and mining progress from events
When to use:
- Detecting when players break or place blocks
- Tracking mining progress and tool effectiveness
- Building protection systems or region management
- Tracking block modifications
- Creating custom building or mining mechanics
Note: Crafting detection is available through EventHelper.onCraftRecipe() instead of ECS events.
AddPlayerToWorldEvent callback, not in your plugin's setup() method.
Detects when a player breaks a block.
Callback Parameters (Basic):
-
Vector3i position- The exact position of the broken block -
String blockTypeId- The block type ID (e.g., "Soil_Dirt", "Rock_Stone")
Callback Parameters (With Player Entity):
-
Vector3i position- The exact position of the broken block -
String blockTypeId- The block type ID (e.g., "Soil_Dirt", "Rock_Stone") -
Entity playerEntity- The player entity who broke the block
Features:
- Automatically filters out "Empty" blocks (prevents false positives during block placement)
- Provides block type ID for identifying what was broken
- Fires for all block breaking actions
- Optional player entity parameter for accessing player-specific data
Example (Basic):
EcsEventHelper.onBlockBreak(world, (position, blockTypeId) -> {
getLogger().at(Level.INFO).log("Block broken: " + blockTypeId + " at " + position);
// Example: Track mining statistics
if (blockTypeId.contains("Ore_")) {
// Player mined an ore block
incrementMiningStats(blockTypeId);
}
});Example (With Player Entity):
EcsEventHelper.onBlockBreak(world, (position, blockTypeId, playerEntity) -> {
if (playerEntity != null) {
String playerName = playerEntity.getLegacyDisplayName();
getLogger().at(Level.INFO).log(playerName + " broke " + blockTypeId + " at " + position);
// Example: Drain stamina when breaking blocks
StatsHelper.addStat(playerEntity, "Stamina", -2.0f);
// Example: Check player permissions
if (!hasPermission(playerEntity, "build.break")) {
// Restore the block
BlockHelper.setBlockByName(world, position, blockTypeId);
}
}
});Detects when a player places a block.
Callback Parameters (Basic):
-
Vector3i position- The exact position where the block was placed -
String itemId- The item ID being placed from the player's hand
Callback Parameters (With Player Entity):
-
Vector3i position- The exact position where the block was placed -
String itemId- The item ID being placed from the player's hand -
Entity playerEntity- The player entity who placed the block
Features:
- Provides the item ID being placed
- Provides exact block position
- Fires for all block placements
- Optional player entity parameter for accessing player-specific data
Example (Basic):
EcsEventHelper.onBlockPlace(world, (position, itemId) -> {
getLogger().at(Level.INFO).log("Block placed: " + itemId + " at " + position);
// Example: Track building statistics
incrementBlocksPlaced(itemId);
});Example (With Player Entity):
EcsEventHelper.onBlockPlace(world, (position, itemId, playerEntity) -> {
if (playerEntity != null) {
String playerName = playerEntity.getLegacyDisplayName();
getLogger().at(Level.INFO).log(playerName + " placed " + itemId + " at " + position);
// Example: Reward player for building
if (itemId.contains("Wood")) {
StatsHelper.addStat(playerEntity, "Mana", 1.0f);
}
// Example: Check build permissions
if (!canBuildHere(playerEntity, position)) {
BlockHelper.setBlock(world, position, 0); // Remove the block
}
}
});Detects when a player damages a block (mining progress tracking).
Callback Parameters (Basic):
-
Vector3i position- The exact position of the block being damaged -
String blockTypeId- The block type ID (e.g., "Rock_Stone") -
float currentDamage- The current accumulated damage on the block (0.0 to 1.0+) -
float damage- The amount of damage being applied this tick -
String itemInHand- The item ID in the player's hand (null if empty/hand)
Callback Parameters (With Player Entity):
-
Vector3i position- The exact position of the block being damaged -
String blockTypeId- The block type ID (e.g., "Rock_Stone") -
float currentDamage- The current accumulated damage on the block (0.0 to 1.0+) -
float damage- The amount of damage being applied this tick -
String itemInHand- The item ID in the player's hand (null if empty/hand) -
Entity playerEntity- The player entity damaging the block
Features:
- Fires continuously while a player is mining/damaging a block
- Provides real-time mining progress information
- Shows what tool is being used
- Tracks damage accumulation
- Optional player entity parameter for accessing player-specific data
Example (Basic):
EcsEventHelper.onBlockDamage(world, (position, blockTypeId, currentDamage, damage, itemInHand) -> {
String tool = itemInHand != null ? itemInHand : "Hand";
getLogger().at(Level.INFO).log("Mining " + blockTypeId + " at " + position);
getLogger().at(Level.INFO).log(" Progress: " + String.format("%.1f%%", currentDamage * 100));
getLogger().at(Level.INFO).log(" Tool: " + tool);
// Example: Warn when block is almost broken
if (currentDamage >= 0.9f) {
WorldHelper.log(world, "Block almost broken!");
}
// Example: Track mining speed with different tools
if ("Pickaxe_Diamond".equals(itemInHand)) {
// Player is using diamond pickaxe - fast mining!
}
});Example (With Player Entity):
EcsEventHelper.onBlockDamage(world, (position, blockTypeId, currentDamage, damage, itemInHand, playerEntity) -> {
if (playerEntity != null) {
String playerName = playerEntity.getLegacyDisplayName();
String tool = itemInHand != null ? itemInHand : "Hand";
getLogger().at(Level.INFO).log(playerName + " mining " + blockTypeId +
" - " + String.format("%.1f%%", currentDamage * 100) + " with " + tool);
// Example: Drain stamina while mining
StatsHelper.addStat(playerEntity, "Stamina", -0.5f);
// Example: Apply mining fatigue if low on stamina
float stamina = StatsHelper.getStamina(playerEntity);
if (stamina < 10.0f) {
// Slow down mining by reducing damage
// (Note: This is just for demonstration, actual implementation would vary)
}
// Example: Grant XP for mining
if (currentDamage >= 1.0f) {
grantMiningXP(playerEntity, blockTypeId);
}
}
});Use Cases:
- Mining progress bars/indicators
- Tool effectiveness tracking
- Mining speed analysis
- Custom mining mechanics
- Block hardness testing
- Mining fatigue effects
- Protected block warnings
NEW! Advanced version that provides read/write access to block health for implementing mining speed multipliers, efficiency enchantments, and custom mining mechanics.
Callback Parameter:
-
BlockDamageContext context- Context object with read/write access to block health
BlockDamageContext Methods:
Read Methods:
-
getPosition()- Block position -
getBlockTypeId()- Block type ID -
getGatherType()- Block gather type (e.g., "Rocks", "Woods", "Soils", "SoftBlocks", etc.) -
getCurrentDamage()- Current damage from event -
getDamage()- Damage amount this tick -
getItemInHand()- Item player is holding (null if empty) -
getPlayerEntity()- Player entity -
getWorld()- World instance -
getBlockHealth()- Current block health (0.0 = destroyed, 1.0 = full health)
Write Methods:
-
setBlockHealth(float)- Set health directly (0.0 to 1.0) -
applyDamage(float)- Apply additional damage to the block -
repairBlock(float)- Repair the block by a certain amount -
setMiningSpeedMultiplier(float)- Multiply mining speed (2.0 = 2x faster, 0.5 = 2x slower)
Example - Efficiency Enchantment (Gather Type Filtering):
EcsEventHelper.onBlockDamage(world, (context) -> {
String gatherType = context.getGatherType();
String tool = context.getItemInHand();
// Pickaxe is 2x faster on ALL rocks (not just specific block IDs!)
if ("Tool_Pickaxe_Crude".equals(tool) && "Rocks".equals(gatherType)) {
context.setMiningSpeedMultiplier(2.0f);
}
// Axe is 2x faster on ALL woods
if ("Tool_Axe_Crude".equals(tool) && "Woods".equals(gatherType)) {
context.setMiningSpeedMultiplier(2.0f);
}
// Shovel is 2x faster on ALL soils
if ("Tool_Shovel_Crude".equals(tool) && "Soils".equals(gatherType)) {
context.setMiningSpeedMultiplier(2.0f);
}
// VIP player gets 3x mining speed on all blocks
String playerName = EntityHelper.getName(context.getPlayerEntity());
if ("VIPPlayer".equals(playerName)) {
context.setMiningSpeedMultiplier(3.0f);
}
});Available Gather Types:
-
"Rocks"- Stone, granite, slate, etc. -
"Woods"- Trees, logs, wooden blocks -
"Soils"- Dirt, grass, sand, etc. -
"SoftBlocks"- Easily breakable blocks -
"Benches"- Crafting benches and workstations -
"VolcanicRocks"- Volcanic stone types
Example - Custom Mining Mechanics:
EcsEventHelper.onBlockDamage(world, (context) -> {
// Make Rock_Slate require diamond pickaxe
if ("Rock_Slate".equals(context.getBlockTypeId())) {
if (!"Tool_Pickaxe_Iron".equals(context.getItemInHand())) {
// Wrong tool - repair the block (cancel damage)
context.repairBlock(context.getDamage());
if (context.getPlayerEntity() != null) {
PlayerHelper.sendMessage(context.getPlayerEntity(),
"You need a iron pickaxe to mine Slate!");
}
}
}
// Apply extra damage when player has strength buff
if (hasStrengthBuff(context.getPlayerEntity())) {
context.applyDamage(context.getDamage() * 0.5f); // 50% extra damage
}
});Example - Direct Health Manipulation:
EcsEventHelper.onBlockDamage(world, (context) -> {
// Instant-break glass blocks
if (context.getBlockTypeId().contains("Glass")) {
context.setBlockHealth(0.0f); // Instantly destroy
}
// Make bedrock indestructible
if ("Rock_Bedrock".equals(context.getBlockTypeId())) {
context.setBlockHealth(1.0f); // Always full health
}
});Use Cases:
- Efficiency enchantments (like Minecraft)
- Tool-specific mining speeds
- VIP/permission-based mining boosts
- Custom block hardness requirements
- Strength/weakness buffs affecting mining
- Instant-break mechanics for certain blocks
- Indestructible blocks
- Mining fatigue effects
- Progressive mining difficulty
Detects when a player interacts with a block (right-click or F key).
Callback Parameters (Basic):
-
Vector3i position- The exact position of the block being interacted with -
String blockTypeId- The block type ID (e.g., "Chest_Wood", "Door_Wood")
Callback Parameters (With Player Entity):
-
Vector3i position- The exact position of the block being interacted with -
String blockTypeId- The block type ID (e.g., "Chest_Wood", "Door_Wood") -
Entity playerEntity- The player entity who interacted with the block
Features:
- Fires when a player right-clicks or presses F on a block
- Provides exact block position and type
- Works with all interactable blocks (chests, doors, buttons, etc.)
- Optional player entity parameter for accessing player-specific data
Example (Basic):
EcsEventHelper.onBlockInteract(world, (position, blockTypeId) -> {
getLogger().at(Level.INFO).log("Block interacted: " + blockTypeId + " at " + position);
// Example: Track container usage
if (blockTypeId.contains("Chest")) {
incrementChestOpenCount();
}
});Example (With Player Entity):
EcsEventHelper.onBlockInteract(world, (position, blockTypeId, playerEntity) -> {
if (playerEntity != null) {
String playerName = EntityHelper.getName(playerEntity);
getLogger().at(Level.INFO).log(playerName + " interacted with " + blockTypeId + " at " + position);
// Example: Check permissions for container access
if (blockTypeId.contains("Chest")) {
if (!PlayerHelper.hasPermission(playerEntity, "container.access")) {
PlayerHelper.sendMessage(playerEntity, "You don't have permission to open chests!");
return;
}
}
// Example: Track player interactions
if (blockTypeId.contains("Door")) {
PlayerHelper.sendMessage(playerEntity, "Door opened!");
}
// Example: Custom container registration
if (ContainerHelper.isContainerType(blockTypeId)) {
ContainerHelper.onContainerChange(world, position, (transaction) -> {
getLogger().at(Level.INFO).log(playerName + " modified container: " + transaction.getAction());
});
}
}
});Use Cases:
- Container access tracking
- Permission-based block interactions
- Custom door/button mechanics
- Player activity monitoring
- Container registration on interaction
- Block usage statistics
- Interactive block rewards
Detects when a player discovers a new zone on the map.
Callback Parameters (Basic):
-
WorldMapTracker.ZoneDiscoveryInfo discoveryInfo- Complete zone discovery information
Callback Parameters (With Player Entity):
-
WorldMapTracker.ZoneDiscoveryInfo discoveryInfo- Complete zone discovery information -
Entity playerEntity- The player entity who discovered the zone
ZoneDiscoveryInfo Fields:
-
zoneName()- The name of the discovered zone -
regionName()- The region the zone belongs to -
display()- Whether to display the discovery notification -
discoverySoundEventId()- Sound event ID to play (can be null) -
icon()- Zone icon identifier (can be null) -
major()- Whether this is a major zone discovery -
duration()- Display duration in seconds -
fadeInDuration()- Fade in animation duration -
fadeOutDuration()- Fade out animation duration
Features:
- Fires when a player enters a new zone for the first time
- Provides complete zone metadata
- Distinguishes between major and minor zones
- Includes display and sound settings
- Optional player entity parameter for accessing player-specific data
Example (Basic):
EcsEventHelper.onZoneDiscovery(world, (discoveryInfo) -> {
String zoneName = discoveryInfo.zoneName();
String region = discoveryInfo.regionName();
getLogger().at(Level.INFO).log("Player discovered: " + zoneName + " in " + region);
// Example: Track exploration progress
if (discoveryInfo.major()) {
// Major zone discovered - award achievement
WorldHelper.log(world, "Major discovery: " + zoneName + "!");
}
});Example (With Player Entity):
EcsEventHelper.onZoneDiscovery(world, (discoveryInfo, playerEntity) -> {
if (playerEntity != null) {
String playerName = EntityHelper.getName(playerEntity);
String zoneName = discoveryInfo.zoneName();
String region = discoveryInfo.regionName();
getLogger().at(Level.INFO).log(playerName + " discovered: " + zoneName + " in " + region);
// Example: Send personalized message
PlayerHelper.sendMessage(playerEntity, "You discovered " + zoneName + "!");
// Example: Custom rewards for specific zones
if ("Emerald_Grove".equals(zoneName)) {
InventoryHelper.giveItem(playerEntity, "Gem_Emerald", 5);
PlayerHelper.sendMessage(playerEntity, "You received 5 Emeralds for discovering Emerald Grove!");
}
// Example: Grant exploration XP
if (discoveryInfo.major()) {
StatsHelper.addStat(playerEntity, "Mana", 10.0f);
PlayerHelper.sendMessage(playerEntity, "Major discovery! +10 Mana");
}
}
});Use Cases:
- Exploration tracking and statistics
- Achievement systems for map discovery
- Custom zone discovery rewards
- Quest progression based on exploration
- Zone-specific welcome messages
- Map completion tracking
- Region unlock systems
- Player-specific discovery notifications
@Override
protected void setup() {
getLogger().at(Level.INFO).log("Setting up plugin...");
// Register simple global events first
EventHelper.onPlayerChat(this, (username, message) -> {
getLogger().at(Level.INFO).log("[Chat] " + username + ": " + message);
});
EventHelper.onItemDrop(this, (itemId, quantity) -> {
getLogger().at(Level.INFO).log("[Drop] " + quantity + "x " + itemId);
});
// Register ECS events when world is available
this.getEventRegistry().registerGlobal(AddPlayerToWorldEvent.class, (event) -> {
World world = event.getWorld();
getLogger().at(Level.INFO).log("World available, registering ECS events...");
// Now we can register ECS events
EcsEventHelper.onBlockBreak(world, (position, blockTypeId) -> {
getLogger().at(Level.INFO).log("[Break] " + blockTypeId + " at " + position);
});
EcsEventHelper.onBlockPlace(world, (position, itemId) -> {
getLogger().at(Level.INFO).log("[Place] " + itemId + " at " + position);
});
EcsEventHelper.onBlockDamage(world, (position, blockTypeId, currentDamage, damage, itemInHand) -> {
String tool = itemInHand != null ? itemInHand : "Hand";
getLogger().at(Level.INFO).log("[Damage] " + blockTypeId + " - " +
String.format("%.1f%%", currentDamage * 100) + " with " + tool);
});
EcsEventHelper.onZoneDiscovery(world, (discoveryInfo) -> {
getLogger().at(Level.INFO).log("[Discovery] " + discoveryInfo.zoneName() +
" in " + discoveryInfo.regionName());
});
});
}EcsEventHelper.onBlockBreak(world, (position, blockTypeId) -> {
if (isInProtectedRegion(position)) {
// Restore the block that was broken
BlockHelper.setBlockByName(world, position, blockTypeId);
WorldHelper.broadcastMessage(world, Message.raw("Cannot break blocks in protected area!"));
}
});
private boolean isInProtectedRegion(Vector3i position) {
// Check if position is in spawn protection (0,0 to 100,100)
return position.getX() >= 0 && position.getX() <= 100 &&
position.getZ() >= 0 && position.getZ() <= 100;
}EcsEventHelper.onBlockPlace(world, (position, itemId) -> {
if (position.getY() > 100) {
// Remove the placed block
BlockHelper.setBlock(world, position, 0); // 0 = air
WorldHelper.broadcastMessage(world, Message.raw("Cannot build above Y=100!"));
}
});// Track what blocks players mine
private Map<String, Integer> miningStats = new HashMap<>();
EcsEventHelper.onBlockBreak(world, (position, blockTypeId) -> {
// Increment counter for this block type
miningStats.put(blockTypeId, miningStats.getOrDefault(blockTypeId, 0) + 1);
// Special handling for ores
if (blockTypeId.contains("Ore_Diamond")) {
WorldHelper.broadcastMessage(world, Message.raw("A player found diamond ore!"));
}
// Log stats every 100 blocks
int totalMined = miningStats.values().stream().mapToInt(Integer::intValue).sum();
if (totalMined % 100 == 0) {
getLogger().at(Level.INFO).log("Total blocks mined: " + totalMined);
getLogger().at(Level.INFO).log("Most mined: " + getMostMinedBlock());
}
});
private String getMostMinedBlock() {
return miningStats.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("None");
}// Track blocks placed by players
private Map<String, Integer> buildingStats = new HashMap<>();
EcsEventHelper.onBlockPlace(world, (position, itemId) -> {
// Get player who placed the block (you'd need to track this)
String playerName = getCurrentPlayer(); // Implement this
// Increment counter
String key = playerName + ":" + itemId;
buildingStats.put(key, buildingStats.getOrDefault(key, 0) + 1);
// Check for milestones
int totalPlaced = buildingStats.values().stream().mapToInt(Integer::intValue).sum();
if (totalPlaced == 1000) {
WorldHelper.broadcastMessage(world, Message.raw("1000 blocks placed in the building contest!"));
}
});EcsEventHelper.onBlockBreak(world, (position, blockTypeId) -> {
// Custom drops for specific blocks
if (blockTypeId.equals("Rock_Stone")) {
// 10% chance to drop extra cobblestone
if (Math.random() < 0.1) {
// Spawn extra item at the block position
spawnItem(world, position, "Rock_Stone_Cobble", 1);
}
}
if (blockTypeId.contains("Ore_")) {
// Double ore drops on weekends
if (isWeekend()) {
String oreType = blockTypeId.replace("Ore_", "Ingot_");
spawnItem(world, position, oreType, 2);
}
}
});When you call EcsEventHelper.onBlockBreak() or onBlockPlace(), the helper:
- Creates a custom
EntityEventSystemfor the event type - Registers the system with
EntityStore.REGISTRY - Sets up the query to target player entities using
PlayerRef.getComponentType() - Configures dependencies using
RootDependency.first() - Wraps your callback with error handling
This all happens automatically - you just provide the callback!
When a player places a block, Hytale internally:
- Fires a
BreakBlockEventfor the "Empty" block at that position - Then fires a
PlaceBlockEventfor the actual block
The onBlockBreak() method filters out "Empty" blocks to prevent false positives. If you need to detect when air blocks are explicitly broken (which is rare), you'll need to use the raw BreakBlockEvent directly.
ECS events are efficient because they:
- Only fire for entities matching the query (players in this case)
- Run on the world's main thread (thread-safe)
- Are managed by Hytale's optimized ECS system
However, avoid expensive operations in your callbacks. If you need to do heavy processing, use WorldHelper.executeOnWorldThread() to defer it.
Detects when a player changes game mode (Creative, Adventure, etc.).
Callback Parameters:
-
Entity playerEntity- The player entity changing game mode -
GameMode newGameMode- The new game mode being switched to
Features:
- Cancellable event - Can prevent game mode changes by cancelling the event
- Works with both manual mode changes and
PlayerHelper.setGameMode() - Fires for all game mode transitions
- Provides player entity for permission checks or logging
Example:
EcsEventHelper.onGameModeChange(world, (playerEntity, newGameMode) -> {
String playerName = EntityHelper.getName(playerEntity);
getLogger().at(Level.INFO).log(playerName + " changed to " + newGameMode + " mode");
// Example: Log creative mode usage for moderation
if (newGameMode == GameMode.valueOf("Creative")) {
logCreativeModeUsage(playerName);
}
// Example: Apply mode-specific effects
if (newGameMode == GameMode.valueOf("Adventure")) {
PlayerHelper.sendMessage(playerEntity, "Welcome to Adventure mode!");
}
});Use Cases:
- Log game mode changes for moderation
- Apply mode-specific effects or permissions
- Prevent mode switching in certain areas
- Track creative mode usage
- Custom mode change handlers
Detects when the moon phase changes in the world.
Callback Parameters:
-
int newMoonPhase- The new moon phase (typically 0-7)
Features:
- Tracks lunar cycle progression
- Fires once per moon phase change
- Useful for time-based mechanics
Example:
EcsEventHelper.onMoonPhaseChange(world, (moonPhase) -> {
getLogger().at(Level.INFO).log("Moon phase changed to: " + moonPhase);
// Example: Full moon werewolf transformation
if (moonPhase == 0) { // Full moon
world.getPlayers().forEach(player -> {
if (hasWerewolfCurse(player)) {
transformToWerewolf(player);
PlayerHelper.sendMessage(player, "The full moon rises... you transform!");
}
});
}
// Example: Moon-dependent mob spawning
if (moonPhase >= 6) { // New moon or near-new moon
increaseMobSpawnRate(world, 1.5f);
}
});Use Cases:
- Werewolf/vampire mechanics triggered by full moon
- Lunar calendar events
- Time-based mob spawning
- Moon-dependent crafting recipes
- Celestial event systems
- Tide mechanics
| Feature | EventHelper | EcsEventHelper |
|---|---|---|
| Registration | Plugin setup | After World available |
| Event Types | Global events | ECS events |
| Examples | Chat, items, player join/leave | Block break/place |
| Complexity | Simple | Handles ECS complexity |
| When to use | Most events | Block-related events |
- EventHelper - For simple global events
- BlockHelper - For block manipulation
- WorldHelper - For world operations
Solution: Make sure you're registering them in the AddPlayerToWorldEvent callback, not in setup():
// ❌ WRONG - Don't do this
@Override
protected void setup() {
EcsEventHelper.onBlockBreak(world, ...); // world is null here!
}
// ✅ CORRECT - Do this
@Override
protected void setup() {
this.getEventRegistry().registerGlobal(AddPlayerToWorldEvent.class, (event) -> {
World world = event.getWorld();
EcsEventHelper.onBlockBreak(world, ...); // Now world is available!
});
}Solution: This is already handled! The onBlockBreak() method filters out "Empty" blocks automatically. If you're still seeing issues, make sure you're using the latest version of HytaleDevLib.
Note: ECS event callbacks in HytaleDevLib don't support cancellation directly. However, you can:
- Restore broken blocks using
BlockHelper.setBlockByName() - Remove placed blocks using
BlockHelper.setBlock(world, position, 0) - This achieves the same result as cancellation