Improve WordCoords
All checks were successful
/ build (push) Successful in 7m30s

Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
This commit is contained in:
Minecon724 2025-05-16 10:22:01 +02:00
commit 2c835a4eab
Signed by untrusted user who does not match committer: m724
GPG key ID: A02E6E67AB961189
8 changed files with 2222 additions and 163 deletions

View file

@ -0,0 +1,45 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.wordcoords;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.codec.DecoderWordsToCoords;
import eu.m724.tweaks.module.wordcoords.codec.EncoderCoordsToWords;
import java.util.NoSuchElementException;
public class WordCoordsCodec {
private final EncoderCoordsToWords encoder;
private final DecoderWordsToCoords decoder;
public WordCoordsCodec(WordList wordList) {
this.encoder = new EncoderCoordsToWords(wordList);
this.decoder = new DecoderWordsToCoords(wordList);
DebugLogger.fine("Words: %d (%d bits/w)", wordList.getWordCount(), wordList.getBitsPerWord());
}
/**
* Encodes coords to words
* @param x The X coordinate
* @param z The Z coordinate
* @return The words
*/
public String[] encodeWords(int x, int z) {
return encoder.encodeWords(x, z);
}
/**
* Decodes words to coords
* @param words The words
* @return The X,Z coordinates
* @throws NoSuchElementException if one or more words are invalid
*/
public int[] decodeCoords(String[] words) throws NoSuchElementException {
return decoder.decodeCoords(words);
}
}

View file

@ -1,36 +0,0 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.wordcoords;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.converter.Decoder;
import eu.m724.tweaks.module.wordcoords.converter.Encoder;
import java.util.NoSuchElementException;
public class WordCoordsConverter {
private final Encoder encoder;
private final Decoder decoder;
public WordCoordsConverter(WordList wordList) {
this.encoder = new Encoder(wordList);
this.decoder = new Decoder(wordList);
DebugLogger.fine("Words: %d (%d bits)", wordList.getWordCount(), wordList.getBitsPerWord());
DebugLogger.fine("Bits per word: %d", wordList.getBitsPerWord());
}
public String[] encode(int x, int z) {
return encoder.encode(x, z);
}
public int[] decode(String[] words) throws NoSuchElementException {
return decoder.decode(words);
}
}

View file

@ -6,6 +6,7 @@
package eu.m724.tweaks.module.wordcoords; package eu.m724.tweaks.module.wordcoords;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.Language; import eu.m724.tweaks.Language;
import eu.m724.tweaks.module.TweaksModule; import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
@ -25,35 +26,64 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.io.InputStream;
import java.util.List; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
public class WordCoordsModule extends TweaksModule implements CommandExecutor, Listener { public class WordCoordsModule extends TweaksModule implements CommandExecutor, Listener {
private WordList wordList; private static final int MAX_RADIUS = 30_000_000;
private WordCoordsConverter converter;
private WordCoordsCodec converter;
@Override @Override
protected void onInit() { protected void onInit() {
Path wordListFile = getPlugin().getDataFolder().toPath().resolve("storage/wordlist.txt");
if (Files.notExists(wordListFile)) {
try { try {
this.wordList = WordList.fromFile(getPlugin().getDataFolder().toPath().resolve("storage/wordlist.txt")); saveDefaultWordList(wordListFile);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException("Failed to save default word list", e);
}
} }
this.converter = new WordCoordsConverter(wordList); WordList wordList;
try {
wordList = WordList.fromFile(wordListFile);
} catch (IOException e) {
throw new RuntimeException("Failed to load word list", e);
}
this.converter = new WordCoordsCodec(wordList);
registerCommand("wordcoords", this); registerCommand("wordcoords", this);
registerEvents(this); registerEvents(this);
} }
private void saveDefaultWordList(Path wordListFile) throws IOException {
try (InputStream is = getPlugin().getResource("wordlist.txt")) {
assert is != null;
Files.copy(is, wordListFile);
}
}
@Override @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
int x = 0, z = 0; int x = 0, z = 0;
String[] words = new String[0];
boolean encode = false; // means encode pos to words boolean encode = false; // means encode pos to words
args = String.join(" ", args)
.replaceAll("[^\\p{L}\\p{N}\\s]", " ")
.trim()
.split(" +");
if (args.length == 1 && args[0].isEmpty())
args = new String[0]; // empty split fix
DebugLogger.fine("Args: %s %d", String.join(", ", args), args.length);
if (args.length == 0) { if (args.length == 0) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Language.getString("wordCoordsPlayerOnly")); sender.sendMessage(Language.getString("wordCoordsPlayerOnly"));
@ -64,12 +94,14 @@ public class WordCoordsModule extends TweaksModule implements CommandExecutor, L
z = player.getLocation().getBlockZ(); z = player.getLocation().getBlockZ();
encode = true; encode = true;
} else if (args.length > 1) { } else {
if (Character.isDigit(args[0].codePointAt(0))) {
if (args.length > 1) {
try { try {
double dx = Double.parseDouble(args[0]); double dx = Double.parseDouble(args[0]);
double dz = Double.parseDouble(args[args.length > 2 ? 2 : 1]); double dz = Double.parseDouble(args[args.length > 2 ? 2 : 1]);
if (dx > Integer.MAX_VALUE || dx < Integer.MIN_VALUE || dz > Integer.MAX_VALUE || dz < Integer.MIN_VALUE) { if (dx > MAX_RADIUS || dx < -MAX_RADIUS || dz > MAX_RADIUS || dz < -MAX_RADIUS) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsOutOfRange", ChatColor.RED)); sender.spigot().sendMessage(Language.getComponent("wordCoordsOutOfRange", ChatColor.RED));
return true; return true;
} }
@ -79,82 +111,65 @@ public class WordCoordsModule extends TweaksModule implements CommandExecutor, L
encode = true; encode = true;
} catch (NumberFormatException ignored) { } } catch (NumberFormatException ignored) { }
} else {
sender.spigot().sendMessage(Language.getComponent("wordCoordsProvideZ", ChatColor.RED));
return true;
}
}
} }
if (encode) { if (encode) {
words = converter.encode(x, z); encodeAndSend(x, z, sender);
} else {
decodeAndSend(args, sender);
}
return true;
}
private void encodeAndSend(int x, int z, CommandSender sender) {
String[] words = converter.encodeWords(x, z);
String encoded = "///" + String.join(".", words); String encoded = "///" + String.join(".", words);
BaseComponent[] components = new ComponentBuilder() BaseComponent[] components = new ComponentBuilder()
.append(String.format("%d, %d encodes to ", x, z)) .append("%d, %d -> ".formatted(x, z))
.color(ChatColor.GRAY) .color(ChatColor.GRAY)
.append(encoded) .append(encoded)
.color(ChatColor.AQUA) // TODO improve color .color(ChatColor.AQUA) // TODO improve color
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, encoded)) .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, encoded))
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy"))) .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
.create(); .create();
sender.spigot().sendMessage(components); sender.spigot().sendMessage(components);
} else {
String strArgs = String.join(" ", args);
words = smartDetectWords(strArgs);
if (words.length == 0) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsNoWords", ChatColor.GRAY));
return true;
} }
private void decodeAndSend(String[] words, CommandSender sender) {
int x, z;
try { try {
int[] xz = converter.decode(words); int[] xz = converter.decodeCoords(words);
x = xz[0]; x = xz[0];
z = xz[1]; z = xz[1];
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsInvalidWord", ChatColor.RED, e.getMessage())); sender.spigot().sendMessage(Language.getComponent("wordCoordsInvalidWord", ChatColor.RED, e.getMessage()));
return true; return;
} }
String encoded = "///" + String.join(".", words); String encoded = "///" + String.join(".", words);
BaseComponent[] components = new ComponentBuilder() BaseComponent[] components = new ComponentBuilder()
.append(encoded + " decodes to ") .append(encoded + " -> ")
.color(ChatColor.GRAY) .color(ChatColor.GRAY)
.append("%d, %d".formatted(x, z)) .append("%d, %d".formatted(x, z))
.color(ChatColor.AQUA) // TODO improve color .color(ChatColor.AQUA) // TODO improve color
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "%d, %d".formatted(x, z))) .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "%d, %d".formatted(x, z)))
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy"))) .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
.append(" ±8") .append(" ±8")
.color(ChatColor.GRAY) .color(ChatColor.GRAY)
.create(); .create();
sender.spigot().sendMessage(components); sender.spigot().sendMessage(components);
} }
return true;
}
private String[] smartDetectWords(String str) {
List<String> words = new ArrayList<>();
StringBuilder currentWord = new StringBuilder();
for (int i=0; i<str.length(); i++) {
char c = str.charAt(i);
if (Character.isLetter(c)) {
currentWord.append(c);
} else {
if (!currentWord.isEmpty()) {
words.add(currentWord.toString());
currentWord.setLength(0);
}
}
}
if (!currentWord.isEmpty()) {
words.add(currentWord.toString());
}
return words.toArray(String[]::new);
}
@EventHandler @EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) { public void onCommand(PlayerCommandPreprocessEvent event) {
if (event.getMessage().startsWith("///")) { if (event.getMessage().startsWith("///")) {

View file

@ -51,7 +51,7 @@ public class WordList {
public static WordList fromFile(Path path) throws IOException { public static WordList fromFile(Path path) throws IOException {
try (var lines = Files.lines(path)) { try (var lines = Files.lines(path)) {
var list = lines.filter(s -> !s.isBlank()) var list = lines.filter(s -> !s.isBlank() && !s.startsWith("#"))
.map(String::toLowerCase) .map(String::toLowerCase)
.distinct() .distinct()
.toList(); .toList();

View file

@ -4,7 +4,7 @@
* in the project root for the full license text. * in the project root for the full license text.
*/ */
package eu.m724.tweaks.module.wordcoords.converter; package eu.m724.tweaks.module.wordcoords.codec;
import eu.m724.tweaks.DebugLogger; import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.WordList; import eu.m724.tweaks.module.wordcoords.WordList;
@ -12,16 +12,16 @@ import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Arrays; import java.util.Arrays;
public class Decoder { public class DecoderWordsToCoords {
private final WordList wordList; private final WordList wordList;
private final int bitsPerWord; private final int bitsPerWord;
public Decoder(WordList wordList) { public DecoderWordsToCoords(WordList wordList) {
this.wordList = wordList; this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord(); this.bitsPerWord = wordList.getBitsPerWord();
} }
public int[] decode(String[] words) throws NoSuchElementException { public int[] decodeCoords(String[] words) throws NoSuchElementException {
int[] wordIndexes = new int[words.length]; int[] wordIndexes = new int[words.length];
for (int i=0; i<words.length; i++) { for (int i=0; i<words.length; i++) {
@ -30,10 +30,10 @@ public class Decoder {
throw new NoSuchElementException(words[i]); throw new NoSuchElementException(words[i]);
} }
return decode(wordIndexes); return decodeCoords(wordIndexes);
} }
public int[] decode(int[] wordIndexes) { public int[] decodeCoords(int[] wordIndexes) {
DebugLogger.finer("Decoding word indexes: %s", Arrays.toString(wordIndexes)); DebugLogger.finer("Decoding word indexes: %s", Arrays.toString(wordIndexes));
int bitsRequired = wordIndexes.length * wordList.getBitsPerWord(); int bitsRequired = wordIndexes.length * wordList.getBitsPerWord();
@ -45,7 +45,7 @@ public class Decoder {
DebugLogger.finer("Combined value: %d", combinedValue); DebugLogger.finer("Combined value: %d", combinedValue);
int[] decodedCoords = decodeCoords(combinedValue, bitsRequiredPerCoordinate); int[] decodedCoords = decodeCombined(combinedValue, bitsRequiredPerCoordinate);
int chunkX = decodedCoords[0]; int chunkX = decodedCoords[0];
int chunkZ = decodedCoords[1]; int chunkZ = decodedCoords[1];
DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ); DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
@ -61,15 +61,15 @@ public class Decoder {
private long wordIndexesToCombinedValue(int[] wordIndexes) { private long wordIndexesToCombinedValue(int[] wordIndexes) {
long combinedValue = 0; long combinedValue = 0;
for (int i=0; i<wordIndexes.length; i++) { for (int wordIndex : wordIndexes) {
combinedValue <<= bitsPerWord; combinedValue <<= bitsPerWord;
combinedValue |= wordIndexes[i]; combinedValue |= wordIndex;
} }
return combinedValue; return combinedValue;
} }
private int[] decodeCoords(long combinedValue, int bitsRequiredPerCoordinate) { private int[] decodeCombined(long combinedValue, int bitsRequiredPerCoordinate) {
int coordinateMask = (1 << bitsRequiredPerCoordinate) - 1; int coordinateMask = (1 << bitsRequiredPerCoordinate) - 1;
int coordinateOffset = 1 << (bitsRequiredPerCoordinate - 1); int coordinateOffset = 1 << (bitsRequiredPerCoordinate - 1);

View file

@ -4,21 +4,23 @@
* in the project root for the full license text. * in the project root for the full license text.
*/ */
package eu.m724.tweaks.module.wordcoords.converter; package eu.m724.tweaks.module.wordcoords.codec;
import eu.m724.tweaks.DebugLogger; import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.WordList; import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.Arrays; import java.util.Arrays;
public class Encoder {
public class EncoderCoordsToWords {
private final WordList wordList; private final WordList wordList;
private final int bitsPerWord; private final int bitsPerWord;
public Encoder(WordList wordList) { public EncoderCoordsToWords(WordList wordList) {
this.wordList = wordList; this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord(); this.bitsPerWord = wordList.getBitsPerWord();
} }
public String[] encode(int xCoord, int zCoord) { public String[] encodeWords(int xCoord, int zCoord) {
int chunkX = Math.floorDiv(xCoord, 16); int chunkX = Math.floorDiv(xCoord, 16);
int chunkZ = Math.floorDiv(zCoord, 16); int chunkZ = Math.floorDiv(zCoord, 16);
DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ); DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
@ -39,8 +41,7 @@ public class Encoder {
wordsRequired++; wordsRequired++;
actualTotalBits = wordsRequired * bitsPerWord; actualTotalBits = wordsRequired * bitsPerWord;
} }
} // else: coords are 0 or -1, minTotalBits=0, wordsRequired=0, actualTotalBits=0. Need special handling? }
// If x/z are 0/-1, findBitsRequired returns 1, minTotalBits=2. The loop handles it.
// Final bits per coordinate based on words // Final bits per coordinate based on words
bitsRequiredPerCoordinate = actualTotalBits / 2; bitsRequiredPerCoordinate = actualTotalBits / 2;
@ -60,9 +61,9 @@ public class Encoder {
return wordList.getWords(wordIndexes); return wordList.getWords(wordIndexes);
} }
// Calculates the minimum number of bits required to represent the coordinate /** Calculates the minimum number of bits required to represent the coordinate
// using the encoding scheme (offset + coord) & mask, such that the coordinate * using the encoding scheme (offset + coord) & mask, such that the coordinate
// fits within the range [-(1 << (bits - 1)), (1 << (bits - 1)) - 1]. * fits within the range [-(1 << (bits - 1)), (1 << (bits - 1)) - 1]. */
private int findBitsRequiredPerCoordinate(int x, int z) { private int findBitsRequiredPerCoordinate(int x, int z) {
int maxVal = Math.max(x, z); int maxVal = Math.max(x, z);
int minVal = Math.min(x, z); int minVal = Math.min(x, z);
@ -72,28 +73,9 @@ public class Encoder {
int requiredPositiveMagnitude = Math.max(maxVal + 1, -minVal); int requiredPositiveMagnitude = Math.max(maxVal + 1, -minVal);
if (requiredPositiveMagnitude <= 0) { if (requiredPositiveMagnitude <= 0) {
// Occurs only if maxVal <= -1 and minVal >= 0, which is impossible,
// OR maxVal <= 0 and -minVal <= 0 => maxVal <= 0 and minVal >= 0.
// This means x and z are both 0.
// The range for 1 bit is [-1, 0]. If coords are 0, 1 bit is not enough for offset+coord.
// Example: bits=1. offset=1<<0=1. mask=(1<<1)-1=1.
// encodeCoord(0, 1) = (1+0)&1 = 1.
// decodeCoord(1, 1): val=1. mask=1. offset=1. (1&1)-1 = 0. Correct.
// What if we need to represent -1? encodeCoord(-1, 1) = (1-1)&1 = 0.
// decodeCoord(0, 1): val=0. (0&1)-1 = -1. Correct.
// So 1 bit works for range [-1, 0]. Let's check the condition:
// x=0, z=0 -> maxVal=0, minVal=0. reqPosMag = max(1, 0) = 1.
// x=-1, z=-1 -> maxVal=-1, minVal=-1. reqPosMag = max(0, 1) = 1.
// x=0, z=-1 -> maxVal=0, minVal=-1. reqPosMag = max(1, 1) = 1.
// So requiredPositiveMagnitude is 1 for the range [-1, 0].
requiredPositiveMagnitude = 1; // Ensure it's at least 1 if coords are 0 or -1. requiredPositiveMagnitude = 1; // Ensure it's at least 1 if coords are 0 or -1.
} }
// Calculate p = bits - 1
// We need the smallest integer p such that (1 << p) >= requiredPositiveMagnitude.
// If requiredPositiveMagnitude = 1, we need 1 << p >= 1, smallest p is 0.
// If requiredPositiveMagnitude > 1, this is equivalent to finding the number of bits
// needed to represent (requiredPositiveMagnitude - 1) in binary.
int p; int p;
if (requiredPositiveMagnitude == 1) { if (requiredPositiveMagnitude == 1) {
p = 0; p = 0;
@ -101,7 +83,6 @@ public class Encoder {
p = 32 - Integer.numberOfLeadingZeros(requiredPositiveMagnitude - 1); p = 32 - Integer.numberOfLeadingZeros(requiredPositiveMagnitude - 1);
} }
// bits = p + 1
return p + 1; return p + 1;
} }
@ -119,7 +100,7 @@ public class Encoder {
// Break into word indexes // Break into word indexes
int[] wordIndexes = new int[wordsRequired]; int[] wordIndexes = new int[wordsRequired];
int currentIndex = wordsRequired; // Start filling from end of array int currentIndex = wordsRequired; // Start filling from the end of the array
for (int remainingBits = bitsRequired; remainingBits > 0; remainingBits -= bitsPerWord) { for (int remainingBits = bitsRequired; remainingBits > 0; remainingBits -= bitsPerWord) {
int wordMask = (1 << bitsPerWord) - 1; int wordMask = (1 << bitsPerWord) - 1;

View file

@ -34,7 +34,7 @@ authKickError = An error occurred. Please try again. If this persists, contact a
redstoneGatewayItem = Redstone gateway redstoneGatewayItem = Redstone gateway
clickToCopy = Click to copy to clipboard clickToCopy = Click to copy
clickToExecuteCommand = Click to execute command clickToExecuteCommand = Click to execute command
durabilityEnabled = Enabled durability alert durabilityEnabled = Enabled durability alert
@ -43,8 +43,8 @@ durabilityDisabled = Disabled durability alert
# When console executes /wordcoords without arguments # When console executes /wordcoords without arguments
wordCoordsPlayerOnly = Only players can execute this command without arguments. wordCoordsPlayerOnly = Only players can execute this command without arguments.
wordCoordsOutOfRange = Those coordinates are invalid. wordCoordsOutOfRange = Those coordinates are invalid.
wordCoordsInvalidWord = Invalid word: "%s" wordCoordsInvalidWord = Invalid word or coordinate: "%s"
wordCoordsNoWords = Please provide the Z coordinate. wordCoordsProvideZ = Please provide the Z coordinate.
# /pomodoro # /pomodoro
pomodoroStopped = Pomodoro stopped. Restart it with /%s start pomodoroStopped = Pomodoro stopped. Restart it with /%s start

File diff suppressed because it is too large Load diff