diff --git a/README.md b/README.md index 77f6fe9..4aefca5 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,9 @@ Issue messages that the player needs to read to keep playing, and that make an a `/emergencyalerts` (`tweaks724.emergencyalerts`) +### Remote redstone +Control redstone remotely + ### Utility commands - `/ping` - displays player ping \ diff --git a/RETSTONE.md b/RETSTONE.md new file mode 100644 index 0000000..e406b94 --- /dev/null +++ b/RETSTONE.md @@ -0,0 +1,32 @@ +## Remote redstone + +See [retstone.py](retstone.py) for usage example + +### Glossary +repeaters - the blocks which allow to interact with redstone over internet \ +packet - a bunch of bytes sent over the internet, that do something with a single repeater + +### How it works +A packet is int / 4 bytes / 32 bits + +Packet format: +``` +[ 01 ] [ 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ] [ 29 30 31 32 ] +action bits 2-28 of repeater id payload +``` + +- action: `1` to write, `0` to read +- payload: power level if writing + +If writing, no response \ +If reading, response is the power level, or -1 if no repeater with that id (subject to change) + +Reading powers the block down of course \ +If the block was powered, you should power it down (or read), wait some, and then read again + +### Retstone + +**Network** translates to **reto** in Esperanto \ +So retsomething means networked something (posto - mail, retposto - email, ejo - place (site), retejo - website, etc.) \ +And sometimes we use network instead of internet, same is in that language \ +Hence retstone \ No newline at end of file diff --git a/retstone.py b/retstone.py new file mode 100644 index 0000000..f1bb8dd --- /dev/null +++ b/retstone.py @@ -0,0 +1,32 @@ +# Copyright (C) 2024 Minecon724 +# Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file +# in the project root for the full license text. + +import socket, struct + +ENDPOINT = ("127.0.0.1", 57931) + +def get_power(repeater_id: int) -> int | None: + message = repeater_id & 0x7FFFFFF0 + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.sendto(message.to_bytes(4), ENDPOINT) + + return struct.unpack(">b", sock.recvfrom(1)[0])[0] + +def set_power(repeater_id: int, power: int): + message = repeater_id & 0x7FFFFFF0 + message |= 0x80000000 + message |= power & 0xF + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.sendto(message.to_bytes(4), ENDPOINT) + +repeater_id = 235459920 + +print("Reading from repeater") +power = get_power(repeater_id) +print("Read power:", power) + +print("Powering repeater with power 10") +set_power(repeater_id, 10) \ No newline at end of file diff --git a/src/main/java/eu/m724/tweaks/TweaksConfig.java b/src/main/java/eu/m724/tweaks/TweaksConfig.java index 53e8972..53a8ba5 100644 --- a/src/main/java/eu/m724/tweaks/TweaksConfig.java +++ b/src/main/java/eu/m724/tweaks/TweaksConfig.java @@ -49,7 +49,10 @@ public record TweaksConfig( boolean authEnabled, boolean authForce, - String authDomain + String authDomain, + + boolean redstoneEnabled, + String redstoneListen ) { public static final int CONFIG_VERSION = 1; private static TweaksConfig config; @@ -113,6 +116,9 @@ public record TweaksConfig( boolean authForce = config.getBoolean("auth.force"); String authHostname = config.getString("auth.domain"); + boolean redstoneEnabled = config.getBoolean("retstone.enabled"); + String redstoneListen = config.getString("retstone.listen"); + TweaksConfig.config = new TweaksConfig( metrics, worldborderExpand, worldborderHide, @@ -125,7 +131,8 @@ public record TweaksConfig( updaterEnabled, hardcoreEnabled, hardcoreChance, sleepEnabled, sleepInstant, - authEnabled, authForce, authHostname + authEnabled, authForce, authHostname, + redstoneEnabled, redstoneListen ); return TweaksConfig.config; diff --git a/src/main/java/eu/m724/tweaks/TweaksPlugin.java b/src/main/java/eu/m724/tweaks/TweaksPlugin.java index eff2bf2..dad4bf4 100644 --- a/src/main/java/eu/m724/tweaks/TweaksPlugin.java +++ b/src/main/java/eu/m724/tweaks/TweaksPlugin.java @@ -20,6 +20,7 @@ import eu.m724.tweaks.ping.PingChecker; import eu.m724.tweaks.ping.PingCommands; import eu.m724.tweaks.pomodoro.PomodoroCommands; import eu.m724.tweaks.pomodoro.PomodoroManager; +import eu.m724.tweaks.redstone.RedstoneManager; import eu.m724.tweaks.sleep.SleepManager; import eu.m724.tweaks.updater.UpdaterCommands; import eu.m724.tweaks.updater.UpdaterManager; @@ -112,6 +113,10 @@ public class TweaksPlugin extends MStatsPlugin { this.getServer().getPluginManager().registerEvents(new FullListener(), this); + if (config.redstoneEnabled()) { + new RedstoneManager(this).init(getCommand("retstone")); + } + if (config.metrics()) mStats(1); diff --git a/src/main/java/eu/m724/tweaks/redstone/RedstoneCommands.java b/src/main/java/eu/m724/tweaks/redstone/RedstoneCommands.java new file mode 100644 index 0000000..45522f0 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/redstone/RedstoneCommands.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 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.redstone; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class RedstoneCommands implements CommandExecutor { + private final RedstoneRepeaters redstoneRepeaters; + + public RedstoneCommands(RedstoneRepeaters redstoneRepeaters) { + this.redstoneRepeaters = redstoneRepeaters; + } + + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length > 0) { + if (args[0].equals("give")) { + Player player = null; + + if (args.length > 1) { + player = Bukkit.getPlayerExact(args[1]); + if (player == null) { + sender.sendMessage("No player named " + args[1]); + return true; + } + } else { + if (sender instanceof Player) { + player = (Player) sender; + } else { + sender.sendMessage("Specify a player to give to, or be a player"); + } + } + + var itemStack = redstoneRepeaters.give(); + player.getInventory().addItem(itemStack); + } + } else { + sender.sendMessage("Argument needed"); + } + return true; + } +} diff --git a/src/main/java/eu/m724/tweaks/redstone/RedstoneListener.java b/src/main/java/eu/m724/tweaks/redstone/RedstoneListener.java new file mode 100644 index 0000000..6a3ef7d --- /dev/null +++ b/src/main/java/eu/m724/tweaks/redstone/RedstoneListener.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 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.redstone; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockRedstoneEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +public class RedstoneListener implements Listener { + private final RedstoneRepeaters redstoneRepeaters; + + public RedstoneListener(RedstoneRepeaters redstoneRepeaters) { + this.redstoneRepeaters = redstoneRepeaters; + } + + @EventHandler + public void onPlace(BlockPlaceEvent event) { + if (!redstoneRepeaters.isRepeater(event.getItemInHand())) return; + + var block = event.getBlockPlaced(); + var id = redstoneRepeaters.onPlace(block); + + System.out.println("repeate place " + id); + } + + @EventHandler + public void onBreak(BlockBreakEvent event) { + var id = redstoneRepeaters.getId(event.getBlock()); + if (id == Integer.MIN_VALUE) return; + + redstoneRepeaters.onBreak(id); + + System.out.println("repeate brek " + id); + } + + @EventHandler + public void a(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; + if (event.getClickedBlock() == null) return; + + var id = redstoneRepeaters.getId(event.getClickedBlock()); + if (id == Integer.MIN_VALUE) return; + + event.getPlayer().sendMessage("Repeater ID: " + id); + } +} diff --git a/src/main/java/eu/m724/tweaks/redstone/RedstoneManager.java b/src/main/java/eu/m724/tweaks/redstone/RedstoneManager.java new file mode 100644 index 0000000..b641c3c --- /dev/null +++ b/src/main/java/eu/m724/tweaks/redstone/RedstoneManager.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 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.redstone; + +import eu.m724.tweaks.TweaksConfig; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.Plugin; + +import java.io.IOException; +import java.net.*; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +public class RedstoneManager { + private final Plugin plugin; + private final RedstoneRepeaters redstoneRepeaters; + + private DatagramSocket socket; + private RedstoneStateUpdateRunnable runnable; + + public RedstoneManager(Plugin plugin) { + this.plugin = plugin; + this.redstoneRepeaters = new RedstoneRepeaters(plugin); + } + + public void init(PluginCommand command) { + plugin.getServer().getPluginManager().registerEvents(new RedstoneListener(redstoneRepeaters), plugin); + command.setExecutor(new RedstoneCommands(redstoneRepeaters)); + + this.runnable = new RedstoneStateUpdateRunnable(redstoneRepeaters); + this.runnable.runTaskTimer(plugin, 0, 20); // TODO configurable + + var listenAddress = TweaksConfig.getConfig().redstoneListen().split(":"); + InetSocketAddress bindAddress; + if (listenAddress.length == 1) { + bindAddress = new InetSocketAddress(Integer.parseInt(listenAddress[0])); + } else { + bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1])); + } + + try { + initSocket(bindAddress); + } catch (SocketException e) { + throw new RuntimeException("Starting socket", e); + } + } + + private void initSocket(SocketAddress bindAddress) throws SocketException { + socket = new DatagramSocket(bindAddress); + + Executors.newSingleThreadExecutor().execute(() -> { + byte[] buf = new byte[4]; + + while (!socket.isClosed()) { + DatagramPacket packet + = new DatagramPacket(buf, buf.length); + try { + socket.receive(packet); + } catch (IOException e) { + System.err.println("Error reading packet: " + e.getMessage()); + continue; + } + + boolean write = (buf[0] >> 7 & 1) == 1; + byte state = (byte) (buf[3] & 0xF); + int repeaterId = ((buf[0] & 0x7F) << 24) | ((buf[1] & 0xFF) << 16) | ((buf[2] & 0xFF) << 8) | (buf[3] & 0xF0); + + if (write) { + enqueueUpdate(repeaterId, state); + } else { + var newPacket = new DatagramPacket(new byte[1], 1, packet.getSocketAddress()); + + enqueueRetrieve(repeaterId, value -> { + System.out.println("retieved state " + value); + newPacket.setData(new byte[] { value }); + + try { + socket.send(newPacket); + } catch (IOException e) { + throw new RuntimeException("Sending response to get repeater value", e); + } + }); + } + } + }); + } + + private void enqueueUpdate(int repeaterId, byte state) { + System.out.println("Update enqueud " + repeaterId + " " + state); + runnable.enqueueUpdate(repeaterId, state); + } + + private void enqueueRetrieve(int repeaterId, Consumer consumer) { + System.out.println("retieve enqueud " + repeaterId); + runnable.enqueueRetrieve(repeaterId, consumer); + } +} diff --git a/src/main/java/eu/m724/tweaks/redstone/RedstoneRepeaters.java b/src/main/java/eu/m724/tweaks/redstone/RedstoneRepeaters.java new file mode 100644 index 0000000..5b7bc49 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/redstone/RedstoneRepeaters.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 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.redstone; + +import eu.m724.tweaks.Language; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.block.Block; +import org.bukkit.inventory.ItemStack; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +public class RedstoneRepeaters { + private final Plugin plugin; + private final NamespacedKey repeaterKey; + + private final Map repeatersById = new HashMap<>(); + private final Map repeatersByLocation = new HashMap<>(); + + public RedstoneRepeaters(Plugin plugin) { + this.plugin = plugin; + this.repeaterKey = new NamespacedKey(plugin, "repeater"); + } + + // + + public boolean isRepeater(ItemStack itemStack) { + var meta = itemStack.getItemMeta(); + if (meta == null) return false; + var value = meta.getPersistentDataContainer().get(repeaterKey, PersistentDataType.BOOLEAN); + return value != null; + } + + public int getId(Block block) { + return repeatersByLocation.getOrDefault(block.getLocation(), Integer.MIN_VALUE); + } + + // + + public ItemStack give() { + var itemStack = new ItemStack(Material.RED_STAINED_GLASS); + var meta = itemStack.getItemMeta(); + + meta.setItemName(Language.getString("retstoneBlockItem")); + meta.getPersistentDataContainer().set(repeaterKey, PersistentDataType.BOOLEAN, true); + meta.setEnchantmentGlintOverride(true); + + itemStack.setItemMeta(meta); + return itemStack; + } + + // TODO save in those + + int onPlace(Block block) { + var repeaterId = ThreadLocalRandom.current().nextInt() & 0x7FFFFFF0; + + repeatersById.put(repeaterId, block.getLocation()); + repeatersByLocation.put(block.getLocation(), repeaterId); + block.setMetadata("rid", new FixedMetadataValue(plugin, repeaterId)); + + return repeaterId; + } + + void onBreak(int repeaterId) { + delete(repeaterId); + } + + // + + Block getBlock(int repeaterId) { + var location = repeatersById.get(repeaterId); + if (location == null) return null; + + var storedId = location.getBlock().getMetadata("rid").getFirst().asInt(); + if (storedId != repeaterId) { + System.out.println("retrieved but not exitt"); + delete(repeaterId); + return null; + } + + System.out.println("retrieved exist " + repeaterId); + return location.getBlock(); + } + + void delete(int repeaterId) { + var l = repeatersById.remove(repeaterId); + if (l == null) return; + repeatersByLocation.remove(l); + } + + // + + byte getPower(int repeaterId) { + var block = getBlock(repeaterId); + if (block == null) return -1; + + block.setType(Material.RED_STAINED_GLASS); + + block.getWorld().spawnParticle(Particle.LAVA, block.getLocation().add(0.5, 0.5, 0.5), 3); + + return (byte) block.getBlockPower(); + } + + void setPower(int repeaterId, byte power) { + var block = getBlock(repeaterId); + if (block == null) return; + + if (power == 0) + block.setType(Material.RED_STAINED_GLASS); + else + block.setType(Material.REDSTONE_BLOCK); + + block.getWorld().spawnParticle(Particle.LAVA, block.getLocation().add(0.5, 0.5, 0.5), 3); + } + +} diff --git a/src/main/java/eu/m724/tweaks/redstone/RedstoneStateUpdateRunnable.java b/src/main/java/eu/m724/tweaks/redstone/RedstoneStateUpdateRunnable.java new file mode 100644 index 0000000..03d3d38 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/redstone/RedstoneStateUpdateRunnable.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 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.redstone; + +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class RedstoneStateUpdateRunnable extends BukkitRunnable { + private Map updateQueue = new HashMap<>(); + private Map> retrieveQueue = new HashMap<>(); + + private final RedstoneRepeaters redstoneRepeaters; + + RedstoneStateUpdateRunnable(RedstoneRepeaters redstoneRepeaters) { + this.redstoneRepeaters = redstoneRepeaters; + } + + void enqueueUpdate(int repeaterId, byte power) { + updateQueue.put(repeaterId, power); + } + + void enqueueRetrieve(int repeaterId, Consumer consumer) { + retrieveQueue.put(repeaterId, consumer); + } + + @Override + public void run() { + var updateQueue = this.updateQueue; + this.updateQueue = new HashMap<>(); + + var retrieveQueue = this.retrieveQueue; + this.retrieveQueue = new HashMap<>(); + + updateQueue.forEach((key, value) -> redstoneRepeaters.setPower(key, value)); + retrieveQueue.forEach((key, value) -> value.accept(redstoneRepeaters.getPower(key))); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a5d2d38..6f58952 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -103,6 +103,12 @@ auth: # The domain of the server. Doesn't do anything other than showing in /tauth new domain: "replace.me" +# Adds blocks which allow to interact with redstone over internet +retstone: + enabled: true + # This takes host:port, listens on UDP + listen: 127.0.0.1:57931 + # Finally, thank you for downloading Tweaks724, I hope you enjoy! # Don't modify unless told to diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c8b67c3..f018798 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -34,6 +34,7 @@ commands: emergencyalert: description: Send emergency alert permission: tweaks724.emergencyalert + retstone: permissions: tweaks724: diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index a7dde07..fd93e94 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -23,4 +23,6 @@ chatAlreadyHere = You're already in this room # Used when a player joins using the wrong key or no key authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to. # If force is enabled and player is not registered. Changing this reveals you're using this plugin -authKickUnregistered = You are not whitelisted on this server! \ No newline at end of file +authKickUnregistered = You are not whitelisted on this server! + +retstoneBlockItem = Online redstone block \ No newline at end of file