This commit is contained in:
2026-05-26 23:18:17 -04:00
commit b2a780064d
18 changed files with 906 additions and 0 deletions
@@ -0,0 +1,79 @@
package net.kewwbec.hub;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
import com.hypixel.hytale.server.core.event.events.player.RemovedPlayerFromWorldEvent;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.universe.world.events.AllWorldsLoadedEvent;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.hub.config.HubConfig;
import net.kewwbec.hub.config.HubConfigLoader;
import net.kewwbec.hub.listener.HubJoinListener;
import net.kewwbec.hub.listener.HubMessageListener;
import net.kewwbec.hub.protection.CancelDamageBlockSystem;
import net.kewwbec.hub.protection.CancelPlaceBlockSystem;
import net.kewwbec.hub.protection.CancelBreakBlockSystem;
import net.kewwbec.hub.protection.HubWorldFilter;
import net.kewwbec.hub.service.HubWorldService;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.logging.Level;
public final class HubPlugin extends JavaPlugin {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private HubConfig config;
private HubWorldFilter filter;
private HubWorldService worldService;
public HubPlugin(@Nonnull JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
try {
this.config = HubConfigLoader.loadOrCreate(getDataDirectory());
} catch (IOException e) {
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(e)).log("Failed to load Hub config");
this.config = new HubConfig();
}
this.filter = new HubWorldFilter(config);
LOGGER.at(Level.INFO).log("Hub setup complete");
}
@Override
protected void start() {
this.worldService = new HubWorldService(config);
HubJoinListener joinListener = new HubJoinListener(config, filter);
HubMessageListener messageListener = new HubMessageListener(config, filter);
getEventRegistry().registerGlobal(AllWorldsLoadedEvent.class, e -> worldService.applyToConfiguredWorlds());
worldService.applyToConfiguredWorlds();
getEventRegistry().registerGlobal(PlayerReadyEvent.class, joinListener::onReady);
getEventRegistry().registerGlobal(AddPlayerToWorldEvent.class, messageListener::onAdd);
getEventRegistry().registerGlobal(RemovedPlayerFromWorldEvent.class, messageListener::onRemove);
if (config.protection.disable_block_break) {
getEntityStoreRegistry().registerSystem(new CancelBreakBlockSystem(filter));
}
if (config.protection.disable_block_place) {
getEntityStoreRegistry().registerSystem(new CancelPlaceBlockSystem(filter));
}
if (config.protection.disable_block_damage) {
getEntityStoreRegistry().registerSystem(new CancelDamageBlockSystem(filter));
}
LOGGER.at(Level.INFO).log("Hub started");
}
@Override
protected void shutdown() {
LOGGER.at(Level.INFO).log("Hub shutdown complete");
}
}
@@ -0,0 +1,62 @@
package net.kewwbec.hub.config;
import java.util.ArrayList;
import java.util.List;
public final class HubConfig {
public WorldsSection worlds = new WorldsSection();
public ProtectionSection protection = new ProtectionSection();
public MessagesSection messages = new MessagesSection();
public SpawnSection spawn = new SpawnSection();
public static final class WorldsSection {
/**
* World names this plugin treats as hubs. Empty = apply to every world this server hosts.
*/
public List<String> hub_worlds = new ArrayList<>();
}
public static final class ProtectionSection {
/** Cancel BreakBlockEvent in hub worlds. */
public boolean disable_block_break = true;
/** Cancel PlaceBlockEvent in hub worlds. */
public boolean disable_block_place = true;
/** Cancel DamageBlockEvent in hub worlds (mining progress). */
public boolean disable_block_damage = true;
/** WorldConfig.isPvpEnabled = false on hub worlds. */
public boolean disable_pvp = true;
/** WorldConfig.isFallDamageEnabled = false on hub worlds. */
public boolean disable_fall_damage = true;
/**
* Master toggle for writing the WorldConfig flags (PvP, fall damage). Set false to manage
* them yourself in Hytale's world config and have Hub leave them alone.
*/
public boolean apply_world_config = true;
}
public static final class MessagesSection {
/**
* Private welcome shown only to the joining player. {player} is substituted.
* Empty/null disables.
*/
public String welcome = "Welcome to the hub, {player}!";
/**
* Replaces Hytale's default join broadcast on hub worlds. {player} is substituted.
* Empty/null suppresses the join broadcast entirely.
*/
public String join_announcement = "{player} joined the hub.";
/**
* Replaces Hytale's default leave broadcast on hub worlds. {player} is substituted.
* Empty/null suppresses the leave broadcast entirely.
*/
public String leave_announcement = "";
}
public static final class SpawnSection {
/**
* If true, teleport every connecting player to the world's spawn point.
*/
public boolean teleport_on_join = true;
}
}
@@ -0,0 +1,37 @@
package net.kewwbec.hub.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public final class HubConfigLoader {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private HubConfigLoader() {
}
@Nonnull
public static HubConfig loadOrCreate(@Nonnull Path dataDirectory) throws IOException {
Path file = dataDirectory.resolve("config.json");
if (!Files.exists(file)) {
Files.createDirectories(dataDirectory);
HubConfig fresh = new HubConfig();
Files.writeString(file, GSON.toJson(fresh), StandardCharsets.UTF_8);
return fresh;
}
String json = Files.readString(file, StandardCharsets.UTF_8);
HubConfig parsed = GSON.fromJson(json, HubConfig.class);
if (parsed == null) parsed = new HubConfig();
if (parsed.worlds == null) parsed.worlds = new HubConfig.WorldsSection();
if (parsed.protection == null) parsed.protection = new HubConfig.ProtectionSection();
if (parsed.messages == null) parsed.messages = new HubConfig.MessagesSection();
if (parsed.spawn == null) parsed.spawn = new HubConfig.SpawnSection();
return parsed;
}
}
@@ -0,0 +1,73 @@
package net.kewwbec.hub.listener;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.math.vector.Transform;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.hub.config.HubConfig;
import net.kewwbec.hub.protection.HubWorldFilter;
import javax.annotation.Nonnull;
import java.awt.Color;
import java.util.logging.Level;
/**
* On PlayerReadyEvent in a hub world: teleport to spawn and send a private welcome.
*
* Join/leave broadcasts are handled by HubMessageListener (which replaces Hytale's built-in
* join/leave messages on AddPlayerToWorldEvent / RemovedPlayerFromWorldEvent).
*/
public final class HubJoinListener {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private final HubConfig config;
private final HubWorldFilter filter;
public HubJoinListener(@Nonnull HubConfig config, @Nonnull HubWorldFilter filter) {
this.config = config;
this.filter = filter;
}
public void onReady(@Nonnull PlayerReadyEvent event) {
Ref<EntityStore> ref = event.getPlayerRef();
if (ref == null || !ref.isValid()) return;
Store<EntityStore> store = ref.getStore();
World world = store.getExternalData().getWorld();
if (world == null || !filter.isHub(world)) return;
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef == null) return;
if (config.spawn.teleport_on_join) {
try {
WorldConfig wcfg = world.getWorldConfig();
ISpawnProvider sp = (wcfg == null) ? null : wcfg.getSpawnProvider();
if (sp != null) {
Transform spawn = sp.getSpawnPoint(world, playerRef.getUuid());
if (spawn != null) {
store.putComponent(ref, Teleport.getComponentType(),
Teleport.createForPlayer(world, spawn));
}
}
} catch (RuntimeException e) {
LOGGER.at(Level.WARNING).log("Hub: teleport to spawn failed for %s: %s", playerRef.getUsername(), e.getMessage());
}
}
if (config.messages.welcome != null && !config.messages.welcome.isBlank()) {
String welcome = config.messages.welcome.replace("{player}", playerRef.getUsername());
try {
playerRef.sendMessage(Message.raw(welcome).color(Color.YELLOW));
} catch (RuntimeException ignored) {}
}
}
}
@@ -0,0 +1,64 @@
package net.kewwbec.hub.listener;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
import com.hypixel.hytale.server.core.event.events.player.RemovedPlayerFromWorldEvent;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.hub.config.HubConfig;
import net.kewwbec.hub.protection.HubWorldFilter;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Replaces (or suppresses) Hytale's default join/leave broadcasts on hub worlds.
*
* AddPlayerToWorldEvent / RemovedPlayerFromWorldEvent are fired by Hytale's player-add/remove
* systems with a default joinMessage / leaveMessage. The systems check shouldBroadcastJoinMessage
* (or leave) after dispatch and broadcast accordingly. We rewrite the message text here, or
* suppress entirely if the configured format is blank.
*/
public final class HubMessageListener {
private final HubConfig config;
private final HubWorldFilter filter;
public HubMessageListener(@Nonnull HubConfig config, @Nonnull HubWorldFilter filter) {
this.config = config;
this.filter = filter;
}
public void onAdd(@Nonnull AddPlayerToWorldEvent event) {
if (!filter.isHub(event.getWorld())) return;
String username = resolveUsername(event.getHolder());
String template = config.messages.join_announcement;
if (template == null || template.isBlank()) {
event.setBroadcastJoinMessage(false);
return;
}
event.setJoinMessage(Message.raw(template.replace("{player}", username)));
event.setBroadcastJoinMessage(true);
}
public void onRemove(@Nonnull RemovedPlayerFromWorldEvent event) {
if (!filter.isHub(event.getWorld())) return;
String username = resolveUsername(event.getHolder());
String template = config.messages.leave_announcement;
if (template == null || template.isBlank()) {
event.setBroadcastLeaveMessage(false);
return;
}
event.setLeaveMessage(Message.raw(template.replace("{player}", username)));
event.setBroadcastLeaveMessage(true);
}
@Nonnull
private static String resolveUsername(@Nullable Holder<EntityStore> holder) {
if (holder == null) return "?";
PlayerRef ref = holder.getComponent(PlayerRef.getComponentType());
return (ref == null) ? "?" : ref.getUsername();
}
}
@@ -0,0 +1,47 @@
package net.kewwbec.hub.protection;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.EntityEventSystem;
import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
/**
* ECS system that cancels BreakBlockEvent inside hub worlds.
*
* BreakBlockEvent extends CancellableEcsEvent and is dispatched per-entity
* (the breaker) via entityStore.invoke(ref, event). Hytale's BlockHarvestUtils
* checks event.isCancelled() and aborts the actual break if true.
*/
public final class CancelBreakBlockSystem extends EntityEventSystem<EntityStore, BreakBlockEvent> {
private final HubWorldFilter filter;
public CancelBreakBlockSystem(@Nonnull HubWorldFilter filter) {
super(BreakBlockEvent.class);
this.filter = filter;
}
@Override
@Nonnull
public Query<EntityStore> getQuery() {
return Query.any();
}
@Override
public void handle(int chunkSlot,
@Nonnull ArchetypeChunk<EntityStore> chunk,
@Nonnull Store<EntityStore> store,
@Nonnull CommandBuffer<EntityStore> commandBuffer,
@Nonnull BreakBlockEvent event) {
if (event.isCancelled()) return;
World world = store.getExternalData().getWorld();
if (world == null || !filter.isHub(world)) return;
event.setCancelled(true);
}
}
@@ -0,0 +1,40 @@
package net.kewwbec.hub.protection;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.EntityEventSystem;
import com.hypixel.hytale.server.core.event.events.ecs.DamageBlockEvent;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
public final class CancelDamageBlockSystem extends EntityEventSystem<EntityStore, DamageBlockEvent> {
private final HubWorldFilter filter;
public CancelDamageBlockSystem(@Nonnull HubWorldFilter filter) {
super(DamageBlockEvent.class);
this.filter = filter;
}
@Override
@Nonnull
public Query<EntityStore> getQuery() {
return Query.any();
}
@Override
public void handle(int chunkSlot,
@Nonnull ArchetypeChunk<EntityStore> chunk,
@Nonnull Store<EntityStore> store,
@Nonnull CommandBuffer<EntityStore> commandBuffer,
@Nonnull DamageBlockEvent event) {
if (event.isCancelled()) return;
World world = store.getExternalData().getWorld();
if (world == null || !filter.isHub(world)) return;
event.setCancelled(true);
}
}
@@ -0,0 +1,40 @@
package net.kewwbec.hub.protection;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.EntityEventSystem;
import com.hypixel.hytale.server.core.event.events.ecs.PlaceBlockEvent;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
public final class CancelPlaceBlockSystem extends EntityEventSystem<EntityStore, PlaceBlockEvent> {
private final HubWorldFilter filter;
public CancelPlaceBlockSystem(@Nonnull HubWorldFilter filter) {
super(PlaceBlockEvent.class);
this.filter = filter;
}
@Override
@Nonnull
public Query<EntityStore> getQuery() {
return Query.any();
}
@Override
public void handle(int chunkSlot,
@Nonnull ArchetypeChunk<EntityStore> chunk,
@Nonnull Store<EntityStore> store,
@Nonnull CommandBuffer<EntityStore> commandBuffer,
@Nonnull PlaceBlockEvent event) {
if (event.isCancelled()) return;
World world = store.getExternalData().getWorld();
if (world == null || !filter.isHub(world)) return;
event.setCancelled(true);
}
}
@@ -0,0 +1,31 @@
package net.kewwbec.hub.protection;
import com.hypixel.hytale.server.core.universe.world.World;
import net.kewwbec.hub.config.HubConfig;
import javax.annotation.Nonnull;
import java.util.List;
/**
* Decides whether a given world should have hub behavior applied.
*
* If {@link HubConfig.WorldsSection#hub_worlds} is empty, every world is a hub world.
* Otherwise only worlds whose name matches (case-insensitive) are hubs.
*/
public final class HubWorldFilter {
private final List<String> configured;
public HubWorldFilter(@Nonnull HubConfig config) {
this.configured = config.worlds.hub_worlds;
}
public boolean isHub(@Nonnull World world) {
if (configured == null || configured.isEmpty()) return true;
String name = world.getName();
for (String hw : configured) {
if (hw != null && hw.equalsIgnoreCase(name)) return true;
}
return false;
}
}
@@ -0,0 +1,94 @@
package net.kewwbec.hub.service;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import net.kewwbec.hub.config.HubConfig;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
/**
* Applies hub-protection flags to configured worlds at startup. Reads
* {@link HubConfig.ProtectionSection} and pushes settings into each world's WorldConfig.
*
* Only writes settings whose values differ; calls markChanged() once if anything changed.
*/
public final class HubWorldService {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private final HubConfig config;
public HubWorldService(@Nonnull HubConfig config) {
this.config = config;
}
public void applyToConfiguredWorlds() {
if (!config.protection.apply_world_config) {
LOGGER.at(Level.INFO).log("Hub: apply_world_config=false, skipping world-flag enforcement");
return;
}
Universe universe = Universe.get();
if (universe == null) {
LOGGER.at(Level.WARNING).log("Hub: Universe.get() is null at apply time");
return;
}
Collection<World> targets = resolveWorlds(universe);
if (targets.isEmpty()) {
LOGGER.at(Level.WARNING).log("Hub: no hub worlds resolved (configured: %s)", config.worlds.hub_worlds);
return;
}
for (World w : targets) {
applyToWorld(w);
}
}
@Nonnull
private Collection<World> resolveWorlds(@Nonnull Universe universe) {
Set<World> out = new HashSet<>();
List<String> configured = config.worlds.hub_worlds;
if (configured == null || configured.isEmpty()) {
out.addAll(universe.getWorlds().values());
return out;
}
for (String name : configured) {
World w = universe.getWorld(name);
if (w == null) {
LOGGER.at(Level.WARNING).log("Hub: configured world '%s' not loaded", name);
continue;
}
out.add(w);
}
return out;
}
private void applyToWorld(@Nonnull World world) {
WorldConfig cfg = world.getWorldConfig();
if (cfg == null) return;
boolean changed = false;
if (config.protection.disable_pvp && cfg.isPvpEnabled()) {
cfg.setPvpEnabled(false);
changed = true;
}
if (config.protection.disable_fall_damage && cfg.isFallDamageEnabled()) {
cfg.setFallDamageEnabled(false);
changed = true;
}
if (changed) {
cfg.markChanged();
LOGGER.at(Level.INFO).log("Hub: applied protection flags to world '%s'", world.getName());
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"Group": "kewwbec",
"Name": "Hub",
"Version": "0.1.0",
"Description": "Lobby/hub behavior: protection, welcome message, spawn-on-join",
"IncludesAssetPack": false,
"Authors": [
{"Name": "kewwbec"}
],
"ServerVersion": "*",
"Dependencies": {
"kewwbec:NetworkCore": "*"
},
"OptionalDependencies": {},
"DisabledByDefault": false,
"Main": "net.kewwbec.hub.HubPlugin"
}