Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions src/main/java/com/easyclaims/EasyClaims.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,41 @@ public void refreshWorldMap(String worldName) {
}
}

/**
* Toggles claim overlay visibility on the world map.
* Controls rendering via config flag - does NOT swap providers to preserve BetterMap settings.
*
* @param show true to show claims, false to hide
*/
public void setClaimMapVisibility(boolean show) {
config.setShowClaimsOnMap(show);

// Refresh all world maps to apply the visibility change
// The EasyClaimsWorldMapProvider checks showClaimsOnMap at render time
for (Map.Entry<String, World> entry : WORLDS.entrySet()) {
String worldName = entry.getKey();
World world = entry.getValue();

if (world == null || world.getWorldConfig().isDeleteOnRemove()) {
continue;
}

try {
refreshWorldMap(worldName);
getLogger().atInfo().log("[Map] Refreshed map for world: %s (claims %s)",
worldName, show ? "visible" : "hidden");
} catch (Exception e) {
getLogger().atSevere().withCause(e).log("[Map] Error refreshing map for world: %s", worldName);
}
}
}

/**
* Refreshes specific chunks on the world map.
* More efficient than refreshing the entire map when only a few chunks changed.
*
* When text grouping is enabled, uses a larger refresh radius to ensure
* all chunks in potential groups are updated when claims change.
*
* @param worldName The world name
* @param chunkX The chunk X coordinate
Expand All @@ -206,10 +238,17 @@ public void refreshWorldMapChunk(String worldName, int chunkX, int chunkZ) {
}

try {
// Create a set with this chunk and its neighbors (for border updates)
// Determine refresh radius based on grouping settings
// When grouping is enabled, we need to refresh all chunks that could be
// in the same group, since adding/removing a chunk affects group membership.
// The grouping algorithm uses maxExpand=8, so use that radius.
// When grouping is disabled, only need to refresh immediate neighbors for borders.
int refreshRadius = config.isMapTextGrouping() ? 8 : 1;

// Create a set with this chunk and surrounding chunks
LongSet chunksToRefresh = new LongOpenHashSet();
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
for (int dx = -refreshRadius; dx <= refreshRadius; dx++) {
for (int dz = -refreshRadius; dz <= refreshRadius; dz++) {
chunksToRefresh.add(ChunkUtil.indexChunk(chunkX + dx, chunkZ + dz));
}
}
Expand All @@ -226,7 +265,8 @@ public void refreshWorldMapChunk(String worldName, int chunkX, int chunkZ) {
}
}

getLogger().atFine().log("[Map] Refreshed chunk %d,%d in world %s", chunkX, chunkZ, worldName);
getLogger().atFine().log("[Map] Refreshed chunk %d,%d (radius %d) in world %s",
chunkX, chunkZ, refreshRadius, worldName);
} catch (Exception e) {
getLogger().atWarning().withCause(e).log("[Map] Error refreshing chunk %d,%d", chunkX, chunkZ);
}
Expand Down
184 changes: 184 additions & 0 deletions src/main/java/com/easyclaims/EasyClaimsAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,188 @@ public static String getClaimDisplayName(String worldName, int chunkX, int chunk
// Fall back to owner name
return getOwnerName(worldName, chunkX, chunkZ);
}

/**
* Checks if claim overlays should be shown on the map.
* This is a global setting that affects all players.
*/
public static boolean shouldShowClaimsOnMap() {
if (pluginConfig == null) return true; // Default to showing claims
return pluginConfig.isShowClaimsOnMap();
}

/**
* Gets the effective text scale for map rendering.
* @param imageWidth The width of the map image
* @param imageHeight The height of the map image
* @return The scale factor (0 = no text, 1-3 = text size)
*/
public static int getMapTextScale(int imageWidth, int imageHeight) {
if (pluginConfig == null) {
// Default AUTO behavior - match PluginConfig.MapTextScale.AUTO
// Keep text small - scale 1 is 7px tall, good for most tile sizes
int minDimension = Math.min(imageWidth, imageHeight);
if (minDimension < 12) return 0; // Too small for any text
if (minDimension < 128) return 1; // Scale 1 (7px) for 12-127px tiles
if (minDimension < 384) return 2; // Scale 2 (14px) for 128-383px tiles
return 3; // Scale 3 (21px) for 384px+ tiles
}
return pluginConfig.getMapTextScale().getEffectiveScale(imageWidth, imageHeight);
}

/**
* Gets the effective text mode for map rendering.
* @param imageWidth The width of the map image
* @param imageHeight The height of the map image
* @return The text mode (never AUTO - resolved to actual mode)
*/
public static PluginConfig.MapTextMode getMapTextMode(int imageWidth, int imageHeight) {
if (pluginConfig == null) {
// Default AUTO behavior
int minDimension = Math.min(imageWidth, imageHeight);
return minDimension >= 64 ? PluginConfig.MapTextMode.OVERFLOW : PluginConfig.MapTextMode.FIT;
}
return pluginConfig.getMapTextMode().getEffectiveMode(imageWidth, imageHeight);
}

/**
* Whether text grouping (spanning across tiles) is enabled.
* When disabled, each tile renders text independently.
*/
public static boolean isMapTextGroupingEnabled() {
if (pluginConfig == null) return false; // Default off
return pluginConfig.isMapTextGrouping();
}

/**
* Gets the claim group info for text spanning.
* Finds the maximal RECTANGLE of same-owner claims that contains this chunk.
*
* Algorithm: Iteratively expand outward from current chunk until stable.
* Then verify all chunks in the rectangle compute the SAME rectangle.
* This prevents issues with L-shaped or irregular claims where different
* chunks would compute overlapping but different rectangles.
*
* @param worldName The world name
* @param chunkX The chunk X coordinate
* @param chunkZ The chunk Z coordinate
* @return int[4] = {groupWidth, groupHeight, posX, posZ} or null if not claimed
* posX/posZ are 0-indexed position of this chunk within the rectangle
*/
public static int[] getClaimGroupInfo(String worldName, int chunkX, int chunkZ) {
UUID owner = getClaimOwner(worldName, chunkX, chunkZ);
if (owner == null) return null;

int maxExpand = 8; // Allow larger groups

// Compute the rectangle from this chunk's perspective
int[] rect = computeRectangle(worldName, chunkX, chunkZ, owner, maxExpand);
int minX = rect[0], maxX = rect[1], minZ = rect[2], maxZ = rect[3];

int groupWidth = maxX - minX + 1;
int groupHeight = maxZ - minZ + 1;

// For single chunks, no verification needed
if (groupWidth == 1 && groupHeight == 1) {
return new int[]{1, 1, 0, 0};
}

// Verify all chunks in the rectangle compute the SAME rectangle
// This prevents issues with L-shaped or irregular claims
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
int[] otherRect = computeRectangle(worldName, x, z, owner, maxExpand);
if (otherRect[0] != minX || otherRect[1] != maxX ||
otherRect[2] != minZ || otherRect[3] != maxZ) {
// Inconsistent rectangles - this chunk is at a junction
// Fall back to single-chunk rendering
return new int[]{1, 1, 0, 0};
}
}
}

int posX = chunkX - minX;
int posZ = chunkZ - minZ;

return new int[]{groupWidth, groupHeight, posX, posZ};
}

/**
* Computes the maximal rectangle containing (chunkX, chunkZ) where all chunks
* are owned by the given owner.
*
* @return int[4] = {minX, maxX, minZ, maxZ}
*/
private static int[] computeRectangle(String worldName, int chunkX, int chunkZ, UUID owner, int maxExpand) {
int minX = chunkX, maxX = chunkX;
int minZ = chunkZ, maxZ = chunkZ;

// Iteratively expand until no changes
boolean changed = true;
int iterations = 0;
int maxIterations = maxExpand * 4; // Safety limit

while (changed && iterations < maxIterations) {
changed = false;
iterations++;

// Try expand left - check if full column is owned
if (minX > chunkX - maxExpand) {
if (isColumnOwnedBy(worldName, minX - 1, minZ, maxZ, owner)) {
minX--;
changed = true;
}
}

// Try expand right
if (maxX < chunkX + maxExpand) {
if (isColumnOwnedBy(worldName, maxX + 1, minZ, maxZ, owner)) {
maxX++;
changed = true;
}
}

// Try expand up
if (minZ > chunkZ - maxExpand) {
if (isRowOwnedBy(worldName, minX, maxX, minZ - 1, owner)) {
minZ--;
changed = true;
}
}

// Try expand down
if (maxZ < chunkZ + maxExpand) {
if (isRowOwnedBy(worldName, minX, maxX, maxZ + 1, owner)) {
maxZ++;
changed = true;
}
}
}

return new int[]{minX, maxX, minZ, maxZ};
}

/**
* Checks if an entire column (fixed X, range of Z) is owned by the given owner.
*/
private static boolean isColumnOwnedBy(String worldName, int x, int minZ, int maxZ, UUID owner) {
for (int z = minZ; z <= maxZ; z++) {
if (!owner.equals(getClaimOwner(worldName, x, z))) {
return false;
}
}
return true;
}

/**
* Checks if an entire row (range of X, fixed Z) is owned by the given owner.
*/
private static boolean isRowOwnedBy(String worldName, int minX, int maxX, int z, UUID owner) {
for (int x = minX; x <= maxX; x++) {
if (!owner.equals(getClaimOwner(worldName, x, z))) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ public AdminSetSubcommand(EasyClaims plugin) {
addSubCommand(new SetMaxSubcommand(plugin));
addSubCommand(new SetBufferSubcommand(plugin));
addSubCommand(new SetAllowPvpToggleSubcommand(plugin));
addSubCommand(new SetShowClaimsOnMapSubcommand(plugin));
addSubCommand(new SetMapTextScaleSubcommand(plugin));
addSubCommand(new SetMapTextModeSubcommand(plugin));
addSubCommand(new SetMapTextGroupingSubcommand(plugin));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.easyclaims.commands.subcommands.admin.set;

import java.awt.Color;

import javax.annotation.Nonnull;

import com.easyclaims.EasyClaims;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message;
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;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
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;

/**
* Admin command to toggle text grouping on the world map.
* When enabled, text spans across multiple adjacent claim tiles.
* When disabled (default), each tile renders text independently.
*/
public class SetMapTextGroupingSubcommand extends AbstractPlayerCommand {
private final EasyClaims plugin;
private final RequiredArg<String> valueArg;

public SetMapTextGroupingSubcommand(EasyClaims plugin) {
super("maptextgrouping", "Toggle text spanning across claim tiles (experimental)");
this.plugin = plugin;
this.valueArg = withRequiredArg("enabled", "1/0, true/false, on/off", ArgTypes.STRING);
addAliases("textgrouping", "grouptext");
requirePermission("easyclaims.admin");
}

@Override
protected void execute(@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> playerRef,
@Nonnull PlayerRef playerData,
@Nonnull World world) {
String input = valueArg.get(ctx).toLowerCase().trim();

// Parse various boolean representations
Boolean value = parseBoolean(input);
if (value == null) {
playerData.sendMessage(Message.raw("Invalid value: " + input + ". Use: 1/0, true/false, on/off").color(Color.RED));
return;
}

// Check if value is already set
boolean currentValue = plugin.getPluginConfig().isMapTextGrouping();
if (value == currentValue) {
playerData.sendMessage(Message.raw("Text grouping is already " + (value ? "enabled" : "disabled") + ".").color(Color.YELLOW));
return;
}

plugin.getPluginConfig().setMapTextGrouping(value);

if (value) {
playerData.sendMessage(Message.raw("Text grouping enabled (experimental). Text will span across adjacent claim tiles.").color(new Color(85, 255, 85)));
} else {
playerData.sendMessage(Message.raw("Text grouping disabled. Each tile renders text independently.").color(new Color(85, 255, 85)));
}

playerData.sendMessage(Message.raw("Refreshing map...").color(Color.GRAY));

// Refresh all world maps to apply the change
for (String worldName : EasyClaims.WORLDS.keySet()) {
plugin.refreshWorldMap(worldName);
}
}

/**
* Parses various boolean representations.
* @return true, false, or null if invalid
*/
private Boolean parseBoolean(String input) {
return switch (input) {
case "1", "true", "on", "yes", "enable", "enabled" -> true;
case "0", "false", "off", "no", "disable", "disabled" -> false;
default -> null;
};
}
}
Loading