This commit is contained in:
2026-05-26 23:18:17 -04:00
commit b2a780064d
18 changed files with 906 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
# Overview
## Architecture
```
HubPlugin (entry)
+-- HubWorldFilter decides if a world is a hub world (config-driven)
+-- HubWorldService applies world-level WorldConfig flags (PvP, fall damage)
+-- HubJoinListener PlayerReadyEvent: teleport to spawn + private welcome
+-- HubMessageListener replaces/suppresses Hytale's built-in join/leave broadcasts
+-- CancelBreakBlockSystem ECS: cancels BreakBlockEvent in hub worlds
+-- CancelPlaceBlockSystem ECS: cancels PlaceBlockEvent in hub worlds
+-- CancelDamageBlockSystem ECS: cancels DamageBlockEvent in hub worlds
```
## What runs when
### Boot-time, in `start()`
1. Build `HubWorldFilter` from config (which worlds are hubs).
2. Register `AllWorldsLoadedEvent` listener -> `HubWorldService.applyToConfiguredWorlds()`. Also call it once now (covers reload after worlds already loaded).
3. Register `PlayerReadyEvent` -> `HubJoinListener.onReady(...)`.
4. Register `AddPlayerToWorldEvent` / `RemovedPlayerFromWorldEvent` -> `HubMessageListener` (replaces Hytale's default join/leave broadcast text, or suppresses entirely).
5. Register the three `EntityEventSystem` subclasses (one per protected event) with `getEntityStoreRegistry()`. Each is gated on its own `protection.disable_*` toggle.
`HubWorldService` walks every configured hub world and sets:
- `isPvpEnabled = false` (if `protection.disable_pvp`)
- `isFallDamageEnabled = false` (if `protection.disable_fall_damage`)
Only the flags that actually differ from current state are written. `markChanged()` is called once if anything changed.
### Per-player, on `PlayerReadyEvent`
For each player who's just finished joining a hub world (i.e. their world is in `worlds.hub_worlds`, or that list is empty meaning "every world on this server is a hub"):
1. Look up the world's spawn point via `WorldConfig.getSpawnProvider().getSpawnPoint(world, uuid)` and add a `Teleport` component to the player's entity.
2. Send the welcome message privately to the joining player.
The join/leave announcements are NOT sent here. They are emitted by `HubMessageListener` riding on `AddPlayerToWorldEvent` / `RemovedPlayerFromWorldEvent`, which are the same events Hytale's own broadcast uses, so we get exactly one message instead of two.
## Why ECS systems instead of GameMode.Adventure
In Hytale, `GameMode` only has two values: `Adventure` and `Creative`. `Adventure` is what Minecraft players would call "Survival" - the player can still break and place blocks. It is NOT a block-protection mode. So we cancel `BreakBlockEvent`, `PlaceBlockEvent`, and `DamageBlockEvent` directly via ECS systems that filter on world. This actually prevents the action and is the same approach the server's own gating uses.
## Why intercept `AddPlayerToWorldEvent` for messages
Hytale's player-add system fires `AddPlayerToWorldEvent` with a default `joinMessage`, then broadcasts it after the listeners run (only if `shouldBroadcastJoinMessage()` returned true). We rewrite the message text (or call `setBroadcastJoinMessage(false)` to suppress entirely if the configured format is blank). Same pattern for `RemovedPlayerFromWorldEvent`. This way the hub's announcement is the one and only line players see; we never run alongside Hytale's default.
## Why `PlayerReadyEvent` for spawn-teleport
`PlayerConnectEvent` fires earlier in the flow, before the player is fully in the world. Teleporting at that point is racy. `PlayerReadyEvent` fires when the player is actually playable.
## File map
| File | Responsibility |
|---|---|
| [HubPlugin.java](../src/main/java/net/kewwbec/hub/HubPlugin.java) | Entry. Lifecycle, wiring. |
| [config/HubConfig.java](../src/main/java/net/kewwbec/hub/config/HubConfig.java) | Config record. |
| [config/HubConfigLoader.java](../src/main/java/net/kewwbec/hub/config/HubConfigLoader.java) | JSON loader. |
| [protection/HubWorldFilter.java](../src/main/java/net/kewwbec/hub/protection/HubWorldFilter.java) | Is-this-world-a-hub predicate. |
| [protection/CancelBreakBlockSystem.java](../src/main/java/net/kewwbec/hub/protection/CancelBreakBlockSystem.java) | ECS: cancel BreakBlockEvent in hubs. |
| [protection/CancelPlaceBlockSystem.java](../src/main/java/net/kewwbec/hub/protection/CancelPlaceBlockSystem.java) | ECS: cancel PlaceBlockEvent in hubs. |
| [protection/CancelDamageBlockSystem.java](../src/main/java/net/kewwbec/hub/protection/CancelDamageBlockSystem.java) | ECS: cancel DamageBlockEvent in hubs. |
| [service/HubWorldService.java](../src/main/java/net/kewwbec/hub/service/HubWorldService.java) | Applies world-level flags (PvP, fall damage). |
| [listener/HubJoinListener.java](../src/main/java/net/kewwbec/hub/listener/HubJoinListener.java) | PlayerReadyEvent: spawn-teleport + private welcome. |
| [listener/HubMessageListener.java](../src/main/java/net/kewwbec/hub/listener/HubMessageListener.java) | Replaces/suppresses Hytale's built-in join/leave broadcast. |
+83
View File
@@ -0,0 +1,83 @@
# Configuration
## Location
`<plugin data dir>/config.json`. Written with defaults on first boot.
## Full config
```json
{
"worlds": {
"hub_worlds": []
},
"protection": {
"disable_block_break": true,
"disable_block_place": true,
"disable_block_damage": true,
"disable_pvp": true,
"disable_fall_damage": true,
"apply_world_config": true
},
"messages": {
"welcome": "Welcome to the hub, {player}!",
"join_announcement": "{player} joined the hub.",
"leave_announcement": ""
},
"spawn": {
"teleport_on_join": true
}
}
```
## Fields
### worlds
| Field | Default | Meaning |
|---|---|---|
| `hub_worlds` | `[]` | World names this plugin treats as hubs. **Empty = every world this server hosts is treated as a hub.** Most hub servers run one world, so empty is fine. If you have multiple worlds and only some are hubs, list the hub names here. |
### protection
| Field | Default | Meaning |
|---|---|---|
| `disable_block_break` | `true` | Register an ECS system that cancels `BreakBlockEvent` in hub worlds. Players cannot mine blocks. |
| `disable_block_place` | `true` | Register an ECS system that cancels `PlaceBlockEvent` in hub worlds. Players cannot place blocks. |
| `disable_block_damage` | `true` | Register an ECS system that cancels `DamageBlockEvent` in hub worlds. Blocks cannot be damaged (the partial-damage step before a break). |
| `disable_pvp` | `true` | Apply `WorldConfig.setPvpEnabled(false)` on the hub world. Players can't damage each other. |
| `disable_fall_damage` | `true` | Apply `WorldConfig.setFallDamageEnabled(false)`. Players don't take fall damage. |
| `apply_world_config` | `true` | Master toggle for the world-flag application (PvP + fall damage). Set false if you want to manage these flags yourself in Hytale's world config and have Hub leave them alone. Does NOT affect the ECS block protections - toggle those individually via the `disable_block_*` flags. |
> Note: Hytale's `GameMode` only has `Adventure` and `Creative`. `Adventure` is the equivalent of Minecraft's *Survival* (players can still break/place). There is no gamemode that prevents block actions, which is why hub protection uses ECS event cancellation instead.
### messages
| Field | Default | Meaning |
|---|---|---|
| `welcome` | `"Welcome to the hub, {player}!"` | Sent privately to the joining player on `PlayerReadyEvent`. `{player}` is replaced with their username. Empty/null disables. |
| `join_announcement` | `"{player} joined the hub."` | **Replaces** Hytale's built-in join broadcast on `AddPlayerToWorldEvent`. `{player}` is replaced. Empty/null suppresses the broadcast entirely (no replacement and no default). |
| `leave_announcement` | `""` (empty) | **Replaces** Hytale's built-in leave broadcast on `RemovedPlayerFromWorldEvent`. `{player}` is replaced. Default is empty so no leave message is shown - staff plugin handles network-wide leave announcements. Set to e.g. `"{player} left the hub."` if you want a per-server leave line. |
### spawn
| Field | Default | Meaning |
|---|---|---|
| `teleport_on_join` | `true` | On `PlayerReadyEvent`, teleport the player to the world's spawn point. The spawn point comes from `WorldConfig.getSpawnProvider().getSpawnPoint(...)` - same source Hytale uses for new players. |
## Recommended setups
**Single-world dedicated hub server (most common):**
- Defaults are fine. `hub_worlds: []` means "everything on this server is a hub".
**Multi-world server where only one world is a hub:**
- `hub_worlds: ["lobby"]` (or whatever your hub world is named).
- Other worlds are unaffected by protection / spawn-on-join / messages.
**Hub world managed entirely outside this plugin:**
- `protection.apply_world_config: false` to leave the world's PvP/fall flags alone.
- `protection.disable_block_*: false` to leave block events alone.
- Hub will still do welcome message + spawn-on-join + join/leave message replacement.
**Allow building in the hub (e.g. for staff):**
- Hytale's permission system would need a per-player override; this plugin cancels at the world level, not per-player. If you want admins-can-build behavior, that needs a small change to the protection systems to check a permission node before cancelling.
+54
View File
@@ -0,0 +1,54 @@
# Limits and Gotchas
What Hub v1 protects against, and what it doesn't.
## Protected
- **Block break**: `CancelBreakBlockSystem` cancels `BreakBlockEvent` for any breaker entity inside a hub world.
- **Block place**: `CancelPlaceBlockSystem` cancels `PlaceBlockEvent` likewise.
- **Block damage** (the partial-damage ticks before a break): `CancelDamageBlockSystem` cancels `DamageBlockEvent`.
- **PvP**: `WorldConfig.isPvpEnabled = false`. Players can't damage other players in the hub world.
- **Fall damage**: `WorldConfig.isFallDamageEnabled = false`.
- **Spawn location**: every joining player is teleported to the spawn point. They can't end up somewhere weird from a previous session.
- **Duplicate join/leave broadcasts**: Hub replaces (or suppresses) Hytale's built-in `AddPlayerToWorldEvent.joinMessage` and `RemovedPlayerFromWorldEvent.leaveMessage` rather than emitting alongside them.
## NOT protected against in v1
If you find a specific destructive action that's still possible in the hub, file it - the fix is usually a new ECS system following the same pattern as the existing three (extend `EntityEventSystem<EntityStore, SomeEvent>`, `Query.any()`, cancel if world is hub).
- **Mob damage**: hostile NPCs can still hit players. If your hub has no hostile mobs (typical), this doesn't matter.
- **Item drops**: Players might be able to drop items via `DropItemEvent`.
- **Item pickups**: Same for `InteractivelyPickupItemEvent`.
- **Inventory edits**: `InventoryChangeEvent` not intercepted.
- **Crafting**: `PlayerCraftEvent` / `CraftRecipeEvent` not intercepted.
- **Explosions**: `ExplosionEvent` not intercepted (creeper-equivalents could still damage terrain).
## Why ECS systems and not GameMode
In some Minecraft-style servers `GameMode.Adventure` prevents block break/place. Hytale's `GameMode` enum only has `Adventure` and `Creative`, and `Adventure` is the equivalent of Survival - the player can still freely break and place. There is no gamemode that blocks block actions. So Hub uses ECS event cancellation at the world level, which is what actually works.
## Per-server, not network
Hub doesn't know about other Hub servers. Each Hub server protects its own world(s). If you have multiple lobbies, install Hub on each. Config can differ per server.
## Timing of world flag application
`AllWorldsLoadedEvent` fires after Hytale finishes loading worlds at boot. Hub's `applyToConfiguredWorlds()` runs then. There's a brief window during boot (before `AllWorldsLoadedEvent`) where the world's flags are whatever was saved on disk. Connecting players in that window get the saved-disk flags. In practice nobody connects during this window because the server isn't accepting players yet.
If you do a hot plugin reload mid-run (someone removes and re-adds the plugin without restarting the server), we also call `applyToConfiguredWorlds()` in `start()` directly to catch already-loaded worlds.
## ECS system registration is start()-time only
Registering an `EntityEventSystem` after the server has booted requires a registry refresh that we don't trigger here. If you toggle a `disable_block_*` flag at runtime via config reload, the change won't take effect until the next server start. Restart for protection-flag changes to apply.
## Join/leave message timing
The welcome message and spawn teleport fire on `PlayerReadyEvent` - by definition the player is "ready" and the world is loaded for them. Should be visible in chat as they appear in-world.
The join/leave broadcasts ride on `AddPlayerToWorldEvent` / `RemovedPlayerFromWorldEvent` and are sent by Hytale itself (we just rewrite the text). They behave exactly like Hytale's defaults - same broadcast scope, same timing - except worded the way you configured.
If `messages.join_announcement` or `leave_announcement` is empty/blank, `setBroadcast*Message(false)` is called, which fully suppresses Hytale's default (no replacement, no original).
## Plugin install order
Hub declares no dependencies in its manifest in v1 (it's standalone). If you want network-wide hub messages later, add `Dependencies: {kewwbec:NetworkCore: "*"}` and pull the MessageBus from `NetworkCore.getInstance()` in `start()`.
+5
View File
@@ -0,0 +1,5 @@
# Hub Docs
1. [01_Overview.md](01_Overview.md) - architecture and what each piece does
2. [02_Configuration.md](02_Configuration.md) - config.json fields
3. [03_Limits_and_Gotchas.md](03_Limits_and_Gotchas.md) - what we don't (yet) protect against, and why