Initial commit
build / build (push) Successful in 3m28s

This commit is contained in:
ciomek committed 2026-08-15 15:59:56 +02:00
commit 6c112140a9
61 files changed
+2355

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
package ciomek.loon;
import ciomek.loon.commands.ChangePasswordCommand;
import ciomek.loon.commands.RegisterCommand;
import ciomek.loon.mqtt.MQTTClient;
import net.fabricmc.api.ModInitializer;
import net.minecraft.core.net.command.CommandManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import turniplabs.halplibe.HalpLibe;
import turniplabs.halplibe.event.defs.CommonEvents;
import turniplabs.halplibe.util.dependency.Key;
public class Loon implements ModInitializer {
public static final String MOD_ID = HalpLibe.registerMod("loon", true);
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static final String EnvVarPrefix = "LOON__";
@Override
public void onInitialize() {
CommonEvents.BEFORE_GAME_START.listen(Key.of(MOD_ID), this::beforeGameStart);
CommonEvents.AFTER_GAME_START.listen(Key.of(MOD_ID), this::afterGameStart);
LOGGER.info("Loon initialized.");
CommandManager.registerCommand(new RegisterCommand());
CommandManager.registerCommand(new ChangePasswordCommand());
MQTTClient.getInstance();
}
public void beforeGameStart() {
}
public void afterGameStart() {
}
}
+18
View File
@@ -0,0 +1,18 @@
package ciomek.loon;
import org.spongepowered.asm.mixin.Unique;
public class TPSTracker {
@Unique
private static volatile double tps = 0;
public static double getTPS()
{
return tps;
}
public static void setTPS(double tps)
{
TPSTracker.tps = tps;
}
}
@@ -0,0 +1,72 @@
package ciomek.loon.commands;
import ciomek.loon.mqtt.MQTTClient;
import ciomek.loon.mqtt.payload.ChangePasswordPayload;
import ciomek.loon.mqtt.payload.RegisterResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentTypeString;
import com.mojang.brigadier.builder.ArgumentBuilderLiteral;
import com.mojang.brigadier.builder.ArgumentBuilderRequired;
import net.minecraft.core.entity.player.Player;
import net.minecraft.core.net.command.CommandManager;
import net.minecraft.core.net.command.CommandSource;
import static com.mojang.brigadier.builder.ArgumentBuilderLiteral.literal;
public class ChangePasswordCommand implements CommandManager.CommandRegistry {
@SuppressWarnings("unchecked")
@Override
public void register(CommandDispatcher<CommandSource> commandDispatcher) {
commandDispatcher
.register(
(ArgumentBuilderLiteral<CommandSource>) (Object) literal("change-password")
.then(
ArgumentBuilderRequired.argument("password", ArgumentTypeString.string())
.executes(context -> {
CommandSource source = (CommandSource) context.getSource();
Player player = source.getSender();
if (player == null) {
source.sendMessage("This command can only be run by a player!");
return 0;
}
String password = ArgumentTypeString.getString(context, "password");
ChangePasswordPayload payload = new ChangePasswordPayload(player.uuid.toString(), password);
MQTTClient.getInstance().publish(payload.topics().get(0), payload.toJson());
source.sendMessage("Sent request to the server! Waiting for the response. ");
MQTTClient.getInstance().subscribe("loon/register/+/response", (topic, message) -> {
String[] parts = topic.split("/");
String playerUuid = parts[2];
ObjectMapper objectMapper = new ObjectMapper();
RegisterResponse response = objectMapper.readValue(
message.getPayload(),
RegisterResponse.class
);
if (response == null)
return;
if (response.success())
source.sendMessage("Successfully changed your password!");
else
{
source.sendMessage("Failed to change your password!");
source.sendMessage("Here's what went wrong:");
source.sendMessage(response.error());
}
});
return 1;
})
)
);
}
}
@@ -0,0 +1,76 @@
package ciomek.loon.commands;
import ciomek.loon.mqtt.MQTTClient;
import ciomek.loon.mqtt.payload.RegisterPayload;
import ciomek.loon.mqtt.payload.RegisterResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentTypeString;
import com.mojang.brigadier.builder.ArgumentBuilderLiteral;
import com.mojang.brigadier.builder.ArgumentBuilderRequired;
import net.minecraft.core.entity.player.Player;
import net.minecraft.core.net.command.CommandManager;
import net.minecraft.core.net.command.CommandSource;
import static com.mojang.brigadier.builder.ArgumentBuilderLiteral.literal;
public class RegisterCommand implements CommandManager.CommandRegistry {
@SuppressWarnings("unchecked")
@Override
public void register(CommandDispatcher<CommandSource> commandDispatcher) {
commandDispatcher
.register(
(ArgumentBuilderLiteral<CommandSource>) (Object) literal("register")
.then(
ArgumentBuilderRequired.argument("internalUsername", ArgumentTypeString.string())
.then(
ArgumentBuilderRequired.argument("password", ArgumentTypeString.string())
.executes(context -> {
CommandSource source = (CommandSource) context.getSource();
Player player = source.getSender();
if (player == null) {
source.sendMessage("This command can only be run by a player!");
return 0;
}
String internalUsername = ArgumentTypeString.getString(context, "internalUsername");
String password = ArgumentTypeString.getString(context, "password");
RegisterPayload payload = new RegisterPayload(player.uuid.toString(), password, internalUsername);
MQTTClient.getInstance().publish(payload.topics().get(0), payload.toJson());
source.sendMessage("Sent request to the server! Waiting for the response. ");
MQTTClient.getInstance().subscribe("loon/register/+/response", (topic, message) -> {
String[] parts = topic.split("/");
String playerUuid = parts[2];
ObjectMapper objectMapper = new ObjectMapper();
RegisterResponse response = objectMapper.readValue(
message.getPayload(),
RegisterResponse.class
);
if (response == null)
return;
if (response.success())
source.sendMessage("Successfully registered the user!");
else
{
source.sendMessage("Failed to register the user!");
source.sendMessage("Here's what went wrong:");
source.sendMessage(response.error());
}
});
return 1;
})
)
)
);
}
}
@@ -0,0 +1,63 @@
package ciomek.loon.mixin;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.player.inventory.PlayerInventoryPayload;
import net.minecraft.core.entity.player.Player;
import net.minecraft.core.item.ItemStack;
import net.minecraft.core.player.inventory.container.ContainerInventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Arrays;
@Mixin(value = ContainerInventory.class, remap = false)
public class ContainerInventoryMQTTMixin {
@Shadow
public Player player;
@Shadow
public ItemStack[] mainInventory;
@Shadow
public @Nullable ItemStack @NotNull [] armorInventory;
@Unique
private void sendFullInventory() {
if (player == null) {
return;
}
new PlayerInventoryPayload(
new PlayerIdentity(player.uuid, player.username),
Arrays.stream(mainInventory).map(ItemStackData::from).toList(),
Arrays.stream(armorInventory).map(ItemStackData::from).toList()
).send();
}
@Inject(method = {
"setItem",
"insertItem",
"sort",
"dropAllItems",
"transferAllContents",
"load"
}, at = @At("RETURN"))
private void onInventoryChanged(CallbackInfo ci) {
sendFullInventory();
}
@Inject(method = {
"removeItem",
"consumeInventoryItem"
}, at = @At("RETURN"))
private void onInventoryChangedReturn(CallbackInfoReturnable<?> cir) {
sendFullInventory();
}
}
@@ -0,0 +1,43 @@
package ciomek.loon.mixin;
import ciomek.loon.mqtt.MQTTClient;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.player.inventory.PlayerPickUpItemPayload;
import net.minecraft.core.entity.EntityItem;
import net.minecraft.core.entity.player.Player;
import net.minecraft.core.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = EntityItem.class, remap = false)
public class EntityItemMQTTMixin {
@Shadow
public ItemStack item;
@Unique
private int originalStackSize;
@Inject(method = "playerTouch", at = @At("HEAD"))
public void onPlayerTouchHead(Player player, CallbackInfo ci) {
originalStackSize = item != null ? item.stackSize : 0;
}
@Inject(method = "playerTouch", at = @At("TAIL"))
public void onPlayerTouch(Player player, CallbackInfo ci) {
if (item == null || item.stackSize >= originalStackSize) {
return;
}
PlayerPickUpItemPayload payload = new PlayerPickUpItemPayload(
new PlayerIdentity(player.uuid, player.username),
ItemStackData.from(item, originalStackSize - item.stackSize)
);
payload.send();
}
}
@@ -0,0 +1,62 @@
package ciomek.loon.mixin;
import ciomek.loon.mqtt.MQTTClient;
import ciomek.loon.mqtt.payload.CommandPayload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.player.list.PlayerKickedPayload;
import ciomek.loon.mqtt.payload.player.PlayerTeleportedPayload;
import net.minecraft.server.entity.player.PlayerServer;
import net.minecraft.server.net.handler.PacketHandlerServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = PacketHandlerServer.class, remap = false)
public class PacketHandlerServerMQTTMixin {
@Shadow
private PlayerServer playerEntity;
@Inject(method = "handleSlashCommand", at = @At("TAIL"))
private void onSlashCommand(String command, CallbackInfo ci) {
CommandPayload payload = new CommandPayload(command, playerEntity.username);
payload.send();
}
@Inject(method = "kickPlayer", at = @At("HEAD"))
private void onKickPlayer(String reason, CallbackInfo ci) {
PlayerKickedPayload payload = new PlayerKickedPayload(
new PlayerIdentity(playerEntity.uuid, playerEntity.username),
reason
);
payload.send();
}
@Inject(method = "teleport(DDD)V", at = @At("HEAD"))
private void onTeleport(double x, double y, double z, CallbackInfo ci) {
publishTeleport(x, y, z, playerEntity.dimension);
}
@Inject(method = "teleportAndRotate", at = @At("HEAD"))
private void onTeleportAndRotate(double x, double y, double z, float yaw, float pitch, CallbackInfo ci) {
publishTeleport(x, y, z, playerEntity.dimension);
}
private void publishTeleport(double toX, double toY, double toZ, int dimension) {
PlayerTeleportedPayload payload = new PlayerTeleportedPayload(
new PlayerIdentity(playerEntity.uuid, playerEntity.username),
playerEntity.x,
playerEntity.y,
playerEntity.z,
toX,
toY,
toZ,
dimension
);
payload.send();
}
}
@@ -0,0 +1,29 @@
package ciomek.loon.mixin;
import ciomek.loon.mqtt.MQTTClient;
import ciomek.loon.mqtt.payload.player.list.PlayerJoinedPayload;
import ciomek.loon.mqtt.payload.player.list.PlayerLeftPayload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import net.minecraft.server.entity.player.PlayerServer;
import net.minecraft.server.net.PlayerList;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = PlayerList.class, remap = false)
public class PlayerListMQTTMixin {
@Inject(method = "playerLoggedIn", at = @At(value = "TAIL"))
public void onPayerLoggedIn(PlayerServer player, CallbackInfo ci) {
PlayerJoinedPayload payload = new PlayerJoinedPayload(new PlayerIdentity(player.uuid, player.username));
payload.send();
}
@Inject(method = "playerLoggedOut", at = @At(value = "TAIL"))
public void onPayerLoggedOut(PlayerServer entityplayermp, CallbackInfo ci) {
PlayerLeftPayload payload = new PlayerLeftPayload(new PlayerIdentity(entityplayermp.uuid, entityplayermp.username));
payload.send();
}
}
@@ -0,0 +1,91 @@
package ciomek.loon.mixin;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.player.PlayerDiedPayload;
import ciomek.loon.mqtt.payload.player.PlayerHurtPayload;
import ciomek.loon.mqtt.payload.player.inventory.PlayerDropItemPayload;
import ciomek.loon.mqtt.payload.player.inventory.PlayerInventoryPayload;
import net.minecraft.core.entity.Entity;
import net.minecraft.core.entity.player.Player;
import net.minecraft.core.item.ItemStack;
import net.minecraft.core.util.helper.DamageType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Arrays;
@Mixin(value = Player.class, remap = false)
public class PlayerMQTTMixin {
@Unique
DamageType lastDamageType = null;
@Inject(method = "hurt", at = @At(value = "HEAD"))
public void onHurtStart(Entity attacker, int damage, DamageType type, CallbackInfoReturnable<Boolean> cir) {
lastDamageType = type;
}
@Inject(method = "hurt", at = @At(value = "TAIL"))
public void onHurt(Entity attacker, int damage, DamageType type, CallbackInfoReturnable<Boolean> cir) {
Player player = (Player) (Object) this;
PlayerHurtPayload payload = new PlayerHurtPayload(new PlayerIdentity(player.uuid, player.username), damageTypeName(type));
payload.send();
}
@Inject(method = "onDeath", at = @At(value = "TAIL"))
public void onDeath(Entity entityKilledBy, CallbackInfo ci) {
Player player = (Player) (Object) this;
String killerName;
if (entityKilledBy instanceof Player killer) {
killerName = killer.username;
} else if (entityKilledBy != null) {
killerName = Entity.getNameFromEntity(entityKilledBy, true);
} else {
killerName = null;
}
String damageType = lastDamageType == null ? null : damageTypeName(lastDamageType);
PlayerDiedPayload payload = new PlayerDiedPayload(new PlayerIdentity(player.uuid, player.username), killerName, damageType);
payload.send();
}
@Inject(method = "dropItem", at = @At("TAIL"))
public void onDropItem(ItemStack stack, boolean randomDirection, CallbackInfo ci) {
if (stack == null) {
return;
}
Player player = (Player) (Object) this;
PlayerDropItemPayload payload = new PlayerDropItemPayload(
new PlayerIdentity(player.uuid, player.username),
ItemStackData.from(stack)
);
payload.send();
}
@Inject(method = "setItemInArmorSlot", at = @At("TAIL"))
private void onArmorChanged(CallbackInfo ci) {
Player player = (Player) (Object) this;
new PlayerInventoryPayload(
new PlayerIdentity(player.uuid, player.username),
Arrays.stream(player.inventory.mainInventory).map(ItemStackData::from).toList(),
Arrays.stream(player.inventory.armorInventory).map(ItemStackData::from).toList()
).send();
}
@Unique
private static String damageTypeName(DamageType type) {
String key = type.getLanguageKey();
return key.substring(key.lastIndexOf('.') + 1);
}
}
@@ -0,0 +1,97 @@
package ciomek.loon.mixin;
import ciomek.loon.TPSTracker;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.data.PositionData;
import ciomek.loon.mqtt.payload.player.PlayerPositionPayload;
import ciomek.loon.mqtt.request.IRequest;
import ciomek.loon.mqtt.request.RequestManager;
import net.minecraft.core.world.Dimension;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.entity.player.PlayerServer;
import net.minecraft.server.world.WorldServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Mixin(value = WorldServer.class, remap = false)
public class WorldServerMixin {
@Unique
private static final int POSITION_CHECK_INTERVAL_TICKS = 20;
@Shadow
public MinecraftServer mcServer;
@Unique
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
@Unique
private static long lastTickNanos;
@Unique
private static long gapSumNanos;
@Unique
private static int gapTicks;
@Unique
private static int positionCheckTicks;
@Unique
private static final Map<UUID, PositionData> lastSentPositions = new HashMap<>();
@Inject(method = "tick", at = @At("TAIL"))
private void onTick(CallbackInfo ci) {
WorldServer self = (WorldServer) (Object) this;
if (self.dimension == Dimension.OVERWORLD) {
long now = System.nanoTime();
if (lastTickNanos != 0) {
gapSumNanos += now - lastTickNanos;
gapTicks++;
if (gapSumNanos >= 1_000_000_000L) {
TPSTracker.setTPS(Math.min(20.0, gapTicks / (gapSumNanos / 1e9)));
gapSumNanos = 0;
gapTicks = 0;
}
}
lastTickNanos = now;
if (++positionCheckTicks >= POSITION_CHECK_INTERVAL_TICKS) {
positionCheckTicks = 0;
broadcastMovedPlayerPositions();
}
}
IRequest request;
while ((request = RequestManager.pollRequest()) != null) {
IRequest current = request;
EXECUTOR.submit(() -> current.handle(mcServer));
}
}
@Unique
private void broadcastMovedPlayerPositions() {
for (PlayerServer player : mcServer.playerList.playerEntities) {
PositionData position = new PositionData(player.x, player.y, player.z);
if (!position.equals(lastSentPositions.put(player.uuid, position))) {
PlayerPositionPayload payload = new PlayerPositionPayload(
new PlayerIdentity(player.uuid, player.username),
position,
player.dimension
);
payload.send();
}
}
}
}
@@ -0,0 +1,161 @@
package ciomek.loon.mqtt;
import ciomek.loon.Loon;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.request.*;
import org.eclipse.paho.client.mqttv3.*;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MQTTClient {
private static MQTTClient instance;
private MqttClient client;
private MQTTSettings settings;
public static synchronized MQTTClient getInstance() {
if(instance != null)
return instance;
MQTTClient mqttClient = MQTTClient.init();
instance = mqttClient;
return instance;
}
public void subscribe(String topicFilter, IMqttMessageListener messageListener) {
try {
client.subscribe(topicFilter, messageListener);
} catch (MqttException e) {
throw new RuntimeException(e);
}
}
public void publish(String topic, String message, int qos) {
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
mqttMessage.setQos(qos);
try {
client.publish(topic, mqttMessage);
} catch (MqttException e) {
throw new RuntimeException(e);
}
}
public void publish(String topic, String message) {
publish(topic, message, 1);
}
public void publish(String topic, Payload payload) {
publish(topic, payload.toJson());
}
static MQTTClient init() {
MQTTClient client = new MQTTClient();
client.establish();
return client;
}
public void establish() {
settings = MQTTSettings.getFromEnvVars();
String broker = settings.broker();
String clientId = settings.clientId();
String username = settings.username();
String password = settings.password();
try {
client = new MqttClient(broker, clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setMaxInflight(10000);
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
}
@Override
public void messageArrived(String topic, MqttMessage message) {
if (topic.matches("loon/player/[^/]+/inventory/full/request")) {
Pattern pattern = Pattern.compile("loon/player/([^/]+)/inventory/full/request");
Matcher matcher = pattern.matcher(topic);
if (matcher.matches()) {
UUID playerId = UUID.fromString(matcher.group(1));
InventoryRequest inventoryRequest = new InventoryRequest(playerId);
RequestManager.addRequest(inventoryRequest);
}
}
if (topic.matches("loon/player/[^/]+/online/request")) {
Pattern pattern = Pattern.compile("loon/player/([^/]+)/online/request");
Matcher matcher = pattern.matcher(topic);
if (matcher.matches()) {
UUID playerId = UUID.fromString(matcher.group(1));
PlayerOnlineRequest playerOnlineRequest = new PlayerOnlineRequest(playerId);
RequestManager.addRequest(playerOnlineRequest);
}
}
if (topic.matches("loon/player/[^/]+/position/request")) {
Pattern pattern = Pattern.compile("loon/player/([^/]+)/position/request");
Matcher matcher = pattern.matcher(topic);
if (matcher.matches()) {
UUID playerId = UUID.fromString(matcher.group(1));
PlayerPositionRequest request = new PlayerPositionRequest(playerId);
RequestManager.addRequest(request);
}
}
Pattern pattern = Pattern.compile("loon/world/chunks/(-?\\d+):(-?\\d+)/(-?\\d+):(-?\\d+)/request");
Matcher matcher = pattern.matcher(topic);
if (matcher.matches())
{
int xStart = Integer.parseInt(matcher.group(1));
int xEnd = Integer.parseInt(matcher.group(2));
int zStart = Integer.parseInt(matcher.group(3));
int zEnd = Integer.parseInt(matcher.group(4));
ChunkMapRequest request = new ChunkMapRequest(xStart, xEnd, zStart, zEnd);
RequestManager.addRequest(request);
}
if (topic.equals("loon/server/info/request")) {
RequestManager.addRequest(new ServerInfoRequest());
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
}
});
client.connect(options);
client.subscribe("loon/player/+/inventory/full/request");
client.subscribe("loon/player/+/online/request");
client.subscribe("loon/player/+/position/request");
client.subscribe("loon/world/chunks/+/+/request");
client.subscribe("loon/server/info/request");
} catch (MqttException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,30 @@
package ciomek.loon.mqtt;
import ciomek.loon.Loon;
public record MQTTSettings (
String broker,
String username,
String password,
String clientId
)
{
public static MQTTSettings getFromEnvVars()
{
return new MQTTSettings(getEnvVar("broker"), getEnvVar("username"), getEnvVar("password"), getEnvVar("clientid"));
}
static String getEnvVar(String name)
{
String fullName = Loon.EnvVarPrefix + name.toUpperCase();
String value = System.getenv(fullName);
if (value == null || value.isBlank())
throw new IllegalArgumentException(
"Missing environment variable: " + fullName + ". "
);
return value;
}
}
@@ -0,0 +1,13 @@
package ciomek.loon.mqtt.payload;
import java.util.List;
public record ChangePasswordPayload (
String uuid,
String password
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/register/change_password");
}
}
@@ -0,0 +1,13 @@
package ciomek.loon.mqtt.payload;
import java.util.List;
public record CommandPayload(
String command,
String source
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/commands", "loon/all");
}
}
@@ -0,0 +1,34 @@
package ciomek.loon.mqtt.payload;
import ciomek.loon.mqtt.MQTTClient;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public interface Payload {
ObjectMapper MAPPER = new ObjectMapper();
List<String> topics();
default String toJson() {
try {
MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
default void send()
{
topics().forEach(topic -> {
MQTTClient.getInstance().publish(
topic,
this
);
});
}
}
@@ -0,0 +1,14 @@
package ciomek.loon.mqtt.payload;
import java.util.List;
public record RegisterPayload (
String uuid,
String password,
String internalUsername
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/register/request");
}
}
@@ -0,0 +1,6 @@
package ciomek.loon.mqtt.payload;
public record RegisterResponse(
boolean success,
String error
) {}
@@ -0,0 +1,22 @@
package ciomek.loon.mqtt.payload;
import java.util.List;
public record ServerInfoPayload(
double tps,
String version,
String motd,
boolean onlineMode,
int sleepPercentage,
int currentPlayerCount,
int maxPlayerCount,
int difficulty,
boolean pvpOn,
int viewDistance
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/server/info");
}
}
@@ -0,0 +1,8 @@
package ciomek.loon.mqtt.payload.data;
public record BlockData(
String blockName,
int blockId,
int color
) {
}
@@ -0,0 +1,12 @@
package ciomek.loon.mqtt.payload.data;
import java.util.List;
public record ChunkData (
List<List<BlockData>> map,
String biome,
int x,
int z,
boolean loaded
) {
}
@@ -0,0 +1,25 @@
package ciomek.loon.mqtt.payload.data;
import net.minecraft.core.item.ItemStack;
public record ItemStackData(
String itemName,
int itemID,
int itemCount
) {
public static ItemStackData from(ItemStack stack) {
if (stack == null) {
return null;
}
return from(stack, stack.stackSize);
}
public static ItemStackData from(ItemStack stack, int itemCount) {
if (stack == null) {
return null;
}
return new ItemStackData(stack.getDisplayName(), stack.itemID, itemCount);
}
}
@@ -0,0 +1,9 @@
package ciomek.loon.mqtt.payload.data;
import java.util.UUID;
public record PlayerIdentity
(
UUID uuid,
String username
) { }
@@ -0,0 +1,7 @@
package ciomek.loon.mqtt.payload.data;
public record PositionData (
double x,
double y,
double z
) {}
@@ -0,0 +1,17 @@
package ciomek.loon.mqtt.payload.player;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerDiedPayload(
PlayerIdentity player,
String killer,
String damageType
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/died");
}
}
@@ -0,0 +1,16 @@
package ciomek.loon.mqtt.payload.player;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerHurtPayload (
PlayerIdentity player,
String damageType
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/hurt");
}
}
@@ -0,0 +1,16 @@
package ciomek.loon.mqtt.payload.player;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerOnlinePayload(
PlayerIdentity player,
boolean online
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/online", "loon/player/" + player.uuid() + "/online/response");
}
}
@@ -0,0 +1,18 @@
package ciomek.loon.mqtt.payload.player;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.data.PositionData;
import java.util.List;
public record PlayerPositionPayload(
PlayerIdentity player,
PositionData position,
Integer dimension
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/position", "loon/player/" + player.uuid() + "/position/response");
}
}
@@ -0,0 +1,22 @@
package ciomek.loon.mqtt.payload.player;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerTeleportedPayload(
PlayerIdentity player,
double fromX,
double fromY,
double fromZ,
double toX,
double toY,
double toZ,
int dimension
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/teleported");
}
}
@@ -0,0 +1,17 @@
package ciomek.loon.mqtt.payload.player.inventory;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerDropItemPayload(
PlayerIdentity player,
ItemStackData itemStack
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/inventory/dropped");
}
}
@@ -0,0 +1,18 @@
package ciomek.loon.mqtt.payload.player.inventory;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerInventoryPayload(
PlayerIdentity player,
List<ItemStackData> grid,
List<ItemStackData> armor
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/inventory/full/response");
}
}
@@ -0,0 +1,17 @@
package ciomek.loon.mqtt.payload.player.inventory;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerPickUpItemPayload (
PlayerIdentity player,
ItemStackData itemStack
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/player/" + player.uuid() + "/inventory/picked_up");
}
}
@@ -0,0 +1,15 @@
package ciomek.loon.mqtt.payload.player.list;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerJoinedPayload(
PlayerIdentity player
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/playerlist/joined", "loon/player/" + player.uuid() + "/joined");
}
}
@@ -0,0 +1,16 @@
package ciomek.loon.mqtt.payload.player.list;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerKickedPayload(
PlayerIdentity player,
String reason
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/playerlist/kicked", "loon/player/" + player.uuid() + "/kicked");
}
}
@@ -0,0 +1,15 @@
package ciomek.loon.mqtt.payload.player.list;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record PlayerLeftPayload(
PlayerIdentity player
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/playerlist/left", "loon/player/" + player.uuid() + "/left");
}
}
@@ -0,0 +1,17 @@
package ciomek.loon.mqtt.payload.world.chunks;
import ciomek.loon.mqtt.payload.Payload;
import ciomek.loon.mqtt.payload.data.ChunkData;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import java.util.List;
public record ChunkMapPayload(
ChunkData chunk
) implements Payload {
@Override
public List<String> topics() {
return List.of("loon/world/chunk/" + chunk.x() + "/" + chunk.z(), "loon/world/chunk/" + chunk.x() + "/" + chunk.z() + "/response");
}
}
@@ -0,0 +1,89 @@
package ciomek.loon.mqtt.request;
import ciomek.loon.mqtt.payload.data.BlockData;
import ciomek.loon.mqtt.payload.data.ChunkData;
import ciomek.loon.mqtt.payload.world.chunks.ChunkMapPayload;
import net.minecraft.core.block.Block;
import net.minecraft.core.block.material.MaterialColor;
import net.minecraft.core.world.World;
import net.minecraft.core.world.biome.Biome;
import net.minecraft.core.world.chunk.Chunk;
import net.minecraft.core.world.chunk.ChunkLoader;
import net.minecraft.core.world.chunk.provider.ChunkProvider;
import net.minecraft.core.world.pos.ChunkPos;
import net.minecraft.core.world.pos.ChunkTilePos;
import net.minecraft.server.MinecraftServer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public record ChunkMapRequest(int xStart, int xEnd, int zStart, int zEnd) implements IRequest {
@Override
public void handle(MinecraftServer server) {
World world = server.getDimensionWorld(0);
ChunkProvider provider = world.getChunkProvider();
ChunkLoader loader = world.getLevelStorage().getChunkLoader(world.dimension);
for (int x = xStart; x <= xEnd; x++) {
for (int z = zStart; z <= zEnd; z++) {
Chunk currentChunk = resolveChunk(world, provider, loader, x, z);
if (currentChunk == null) {
ChunkData chunkData = new ChunkData(null, "", x, z, false);
new ChunkMapPayload(chunkData).send();
continue;
}
String biomeName = getBiomeName(currentChunk);
List<List<BlockData>> chunkMap = new ArrayList<>();
for (int chunkX = 0; chunkX < 16; chunkX++) {
chunkMap.add(new ArrayList<>());
for (int chunkZ = 0; chunkZ < 16; chunkZ++) {
int highestBlockY = Math.max(0, currentChunk.getHeightValue(chunkX, chunkZ) - 1);
Block block = currentChunk.getBlock(new ChunkTilePos(chunkX, highestBlockY, chunkZ));
int meta = currentChunk.getBlockMetadata(chunkX, highestBlockY, chunkZ);
int rgb = MaterialColor.getColorFromIndex(
MaterialColor.getColorIndexFromBlock(block, meta)
);
BlockData blockData = new BlockData(block.getKey(), block.id(), rgb);
chunkMap.get(chunkX).add(blockData);
}
}
ChunkData chunkData = new ChunkData(chunkMap, biomeName, x, z, true);
new ChunkMapPayload(chunkData).send();
}
}
}
String getBiomeName(Chunk chunk)
{
int surfaceY = Math.max(0, chunk.getHeightValue(8, 8) - 1);
Biome biome = chunk.getBlockBiome(new ChunkTilePos(8, surfaceY, 8));
return biome != null ? biome.getRegistryKey() : "?";
}
Chunk resolveChunk(World world, ChunkProvider provider, ChunkLoader loader, int x, int z)
{
ChunkPos chunkPos = new ChunkPos(x, z);
if (provider.isChunkLoaded(chunkPos))
return world.getChunk(chunkPos);
if (loader == null || !loader.chunkExists(world, x, z))
return null;
try {
return loader.loadChunk(world, x, z);
} catch (IOException e) {
return null;
}
}
}
@@ -0,0 +1,9 @@
package ciomek.loon.mqtt.request;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.entity.player.PlayerServer;
public interface IRequest
{
void handle(MinecraftServer server);
}
@@ -0,0 +1,37 @@
package ciomek.loon.mqtt.request;
import ciomek.loon.mqtt.MQTTClient;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.player.inventory.PlayerInventoryPayload;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.entity.player.PlayerServer;
import java.util.Arrays;
import java.util.UUID;
public record InventoryRequest(UUID playerUuid) implements IRequest {
@Override
public void handle(MinecraftServer server) {
PlayerServer player = server.playerList.playerEntities.stream()
.filter(playerServer -> playerServer.uuid.equals(playerUuid))
.findFirst()
.orElse(null);
if (player == null) {
return;
}
PlayerInventoryPayload payload = new PlayerInventoryPayload(
new PlayerIdentity(player.uuid, player.username),
Arrays.stream(player.inventory.mainInventory)
.map(ItemStackData::from)
.toList(),
Arrays.stream(player.inventory.armorInventory)
.map(ItemStackData::from)
.toList()
);
payload.send();
}
}
@@ -0,0 +1,38 @@
package ciomek.loon.mqtt.request;
import ciomek.loon.mqtt.payload.data.ItemStackData;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.player.PlayerOnlinePayload;
import ciomek.loon.mqtt.payload.player.inventory.PlayerInventoryPayload;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.entity.player.PlayerServer;
import java.util.Arrays;
import java.util.UUID;
public record PlayerOnlineRequest(UUID playerUuid) implements IRequest {
@Override
public void handle(MinecraftServer server) {
boolean online = server.playerList.playerEntities.stream()
.anyMatch(p -> p.uuid.equals(playerUuid));
PlayerServer player = server.playerList.playerEntities.stream()
.filter(p -> p.uuid.equals(playerUuid))
.findFirst()
.orElse(null);
PlayerIdentity identity;
if(player == null) {
identity = new PlayerIdentity(playerUuid, "");
}
else {
identity = new PlayerIdentity(player.uuid, player.username);
}
PlayerOnlinePayload payload = new PlayerOnlinePayload(
identity,
online);
payload.send();
}
}
@@ -0,0 +1,39 @@
package ciomek.loon.mqtt.request;
import ciomek.loon.mqtt.payload.data.PlayerIdentity;
import ciomek.loon.mqtt.payload.data.PositionData;
import ciomek.loon.mqtt.payload.player.PlayerOnlinePayload;
import ciomek.loon.mqtt.payload.player.PlayerPositionPayload;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.entity.player.PlayerServer;
import java.util.UUID;
public record PlayerPositionRequest(UUID playerUuid) implements IRequest {
@Override
public void handle(MinecraftServer server) {
PlayerServer player = server.playerList.playerEntities.stream()
.filter(p -> p.uuid.equals(playerUuid))
.findFirst()
.orElse(null);
if (player == null) {
PlayerPositionPayload payload = new PlayerPositionPayload(
new PlayerIdentity(playerUuid, null),
null,
null
);
payload.send();
}
else {
PositionData playerPosition = new PositionData(player.x, player.y, player.z);
PlayerPositionPayload payload = new PlayerPositionPayload(
new PlayerIdentity(player.uuid, player.username),
playerPosition,
player.dimension
);
payload.send();
}
}
}
@@ -0,0 +1,17 @@
package ciomek.loon.mqtt.request;
import java.util.concurrent.ConcurrentLinkedQueue;
public class RequestManager {
static ConcurrentLinkedQueue<IRequest> queue = new ConcurrentLinkedQueue<>();
public static void addRequest(IRequest request)
{
queue.add(request);
}
public static IRequest pollRequest()
{
return queue.poll();
}
}
@@ -0,0 +1,25 @@
package ciomek.loon.mqtt.request;
import ciomek.loon.TPSTracker;
import ciomek.loon.mixin.WorldServerMixin;
import ciomek.loon.mqtt.payload.ServerInfoPayload;
import net.minecraft.server.MinecraftServer;
public record ServerInfoRequest() implements IRequest {
@Override
public void handle(MinecraftServer server) {
ServerInfoPayload payload = new ServerInfoPayload(
TPSTracker.getTPS(),
server.getMinecraftVersion(),
server.motd,
server.onlineMode,
server.sleepPercentage,
server.playerList.playerEntities.size(),
server.maxPlayers,
server.difficulty,
server.pvpOn,
server.viewDistance);
payload.send();
}
}
Whitespace-only changes.
+33
View File
@@ -0,0 +1,33 @@
{
"schemaVersion": 1,
"id": "loon",
"version": "${version}",
"name": "Example Mod",
"description": "This mod aims to help new BTA modders.",
"authors": [
"Turnip Labs"
],
"contact": {
"homepage": "",
"sources": ""
},
"icon": "icon.png",
"license": "CC0-1.0",
"environment": "*",
"entrypoints": {
"main": [
"ciomek.loon.Loon"
]
},
"mixins": [
"loon.mixins.json"
],
"depends": {
"fabricloader": ">=${fabricloader}",
"minecraft": "*",
"java": ">=${java}"
},
"suggests": {
"modmenu": ">=${modmenu}"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+18
View File
@@ -0,0 +1,18 @@
{
"required": true,
"package": "ciomek.loon.mixin",
"compatibilityLevel": "JAVA_${java}",
"injectors": {
"defaultRequire": 1
},
"mixins": [
"ContainerInventoryMQTTMixin",
"EntityItemMQTTMixin",
"PacketHandlerServerMQTTMixin",
"PlayerListMQTTMixin",
"PlayerMQTTMixin",
"WorldServerMixin"
],
"client": []
}