redstone WIP
All checks were successful
/ build (push) Successful in 1m1s

This commit is contained in:
Minecon724 2024-12-29 18:40:56 +01:00
parent 3318863cf8
commit dde8700248
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
13 changed files with 468 additions and 3 deletions

View file

@ -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 \

32
RETSTONE.md Normal file
View file

@ -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

32
retstone.py Normal file
View file

@ -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)

View file

@ -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;

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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<Byte> consumer) {
System.out.println("retieve enqueud " + repeaterId);
runnable.enqueueRetrieve(repeaterId, consumer);
}
}

View file

@ -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<Integer, Location> repeatersById = new HashMap<>();
private final Map<Location, Integer> 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);
}
}

View file

@ -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<Integer, Byte> updateQueue = new HashMap<>();
private Map<Integer, Consumer<Byte>> 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<Byte> 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)));
}
}

View file

@ -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

View file

@ -34,6 +34,7 @@ commands:
emergencyalert:
description: Send emergency alert
permission: tweaks724.emergencyalert
retstone:
permissions:
tweaks724:

View file

@ -24,3 +24,5 @@ chatAlreadyHere = You're already in this room
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!
retstoneBlockItem = Online redstone block