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

View file

@ -51,7 +51,7 @@ public class WordList {
public static WordList fromFile(Path path) throws IOException {
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)
.distinct()
.toList();

View file

@ -4,7 +4,7 @@
* 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.module.wordcoords.WordList;
@ -12,16 +12,16 @@ import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.NoSuchElementException;
import java.util.Arrays;
public class Decoder {
public class DecoderWordsToCoords {
private final WordList wordList;
private final int bitsPerWord;
public Decoder(WordList wordList) {
public DecoderWordsToCoords(WordList wordList) {
this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord();
}
public int[] decode(String[] words) throws NoSuchElementException {
public int[] decodeCoords(String[] words) throws NoSuchElementException {
int[] wordIndexes = new int[words.length];
for (int i=0; i<words.length; i++) {
@ -30,10 +30,10 @@ public class Decoder {
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));
int bitsRequired = wordIndexes.length * wordList.getBitsPerWord();
@ -45,7 +45,7 @@ public class Decoder {
DebugLogger.finer("Combined value: %d", combinedValue);
int[] decodedCoords = decodeCoords(combinedValue, bitsRequiredPerCoordinate);
int[] decodedCoords = decodeCombined(combinedValue, bitsRequiredPerCoordinate);
int chunkX = decodedCoords[0];
int chunkZ = decodedCoords[1];
DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
@ -61,15 +61,15 @@ public class Decoder {
private long wordIndexesToCombinedValue(int[] wordIndexes) {
long combinedValue = 0;
for (int i=0; i<wordIndexes.length; i++) {
for (int wordIndex : wordIndexes) {
combinedValue <<= bitsPerWord;
combinedValue |= wordIndexes[i];
combinedValue |= wordIndex;
}
return combinedValue;
}
private int[] decodeCoords(long combinedValue, int bitsRequiredPerCoordinate) {
private int[] decodeCombined(long combinedValue, int bitsRequiredPerCoordinate) {
int coordinateMask = (1 << bitsRequiredPerCoordinate) - 1;
int coordinateOffset = 1 << (bitsRequiredPerCoordinate - 1);

View file

@ -4,21 +4,23 @@
* 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.module.wordcoords.WordList;
import java.util.Arrays;
public class Encoder {
public class EncoderCoordsToWords {
private final WordList wordList;
private final int bitsPerWord;
public Encoder(WordList wordList) {
public EncoderCoordsToWords(WordList wordList) {
this.wordList = wordList;
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 chunkZ = Math.floorDiv(zCoord, 16);
DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
@ -39,8 +41,7 @@ public class Encoder {
wordsRequired++;
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
bitsRequiredPerCoordinate = actualTotalBits / 2;
@ -60,9 +61,9 @@ public class Encoder {
return wordList.getWords(wordIndexes);
}
// Calculates the minimum number of bits required to represent the coordinate
// using the encoding scheme (offset + coord) & mask, such that the coordinate
// fits within the range [-(1 << (bits - 1)), (1 << (bits - 1)) - 1].
/** Calculates the minimum number of bits required to represent the coordinate
* using the encoding scheme (offset + coord) & mask, such that the coordinate
* fits within the range [-(1 << (bits - 1)), (1 << (bits - 1)) - 1]. */
private int findBitsRequiredPerCoordinate(int x, int z) {
int maxVal = Math.max(x, z);
int minVal = Math.min(x, z);
@ -72,28 +73,9 @@ public class Encoder {
int requiredPositiveMagnitude = Math.max(maxVal + 1, -minVal);
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;
if (requiredPositiveMagnitude == 1) {
p = 0;
@ -101,7 +83,6 @@ public class Encoder {
p = 32 - Integer.numberOfLeadingZeros(requiredPositiveMagnitude - 1);
}
// bits = p + 1
return p + 1;
}
@ -119,7 +100,7 @@ public class Encoder {
// Break into word indexes
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) {
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
clickToCopy = Click to copy to clipboard
clickToCopy = Click to copy
clickToExecuteCommand = Click to execute command
durabilityEnabled = Enabled durability alert
@ -43,8 +43,8 @@ durabilityDisabled = Disabled durability alert
# When console executes /wordcoords without arguments
wordCoordsPlayerOnly = Only players can execute this command without arguments.
wordCoordsOutOfRange = Those coordinates are invalid.
wordCoordsInvalidWord = Invalid word: "%s"
wordCoordsNoWords = Please provide the Z coordinate.
wordCoordsInvalidWord = Invalid word or coordinate: "%s"
wordCoordsProvideZ = Please provide the Z coordinate.
# /pomodoro
pomodoroStopped = Pomodoro stopped. Restart it with /%s start

File diff suppressed because it is too large Load diff