diff --git a/.run/Sculk.run.xml b/.run/Sculk.run.xml index 62af14e..3184fb5 100644 --- a/.run/Sculk.run.xml +++ b/.run/Sculk.run.xml @@ -7,7 +7,7 @@ diff --git a/pom.xml b/pom.xml index d25e1b9..06ca6fe 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ UTF-8 3.0.0.Beta5-SNAPSHOT 1.0.0.CR3-SNAPSHOT - 2.23.1 + 2.17.1 3.23.0 4.1.101.Final @@ -97,12 +97,6 @@ central Maven Central https://repo1.maven.org/maven2/ - - true - - - true - projectlombok.org @@ -167,11 +161,6 @@ ${log4j2.version} compile - - org.slf4j - slf4j-simple - 1.7.21 - org.apache.logging.log4j log4j-core @@ -293,27 +282,58 @@ org.apache.maven.plugins - maven-assembly-plugin - 3.4.2 - - - - org.sculk.Sculk - - - - jar-with-dependencies - - + maven-shade-plugin + 3.5.0 + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.8.1 + + - make-assembly package - single + shade + + + + org.bstats + + org.sculk + + + false + + + + + org.sculk.Sculk + + true + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + 21 + false + none + ${java.home}/bin/javadoc + diff --git a/src/main/java/org/sculk/Server.java b/src/main/java/org/sculk/Server.java index b94f5a0..4244959 100644 --- a/src/main/java/org/sculk/Server.java +++ b/src/main/java/org/sculk/Server.java @@ -9,8 +9,11 @@ import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.apache.logging.log4j.Logger; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; +import org.sculk.api.player.GameMode; +import org.sculk.api.server.Operators; import org.sculk.command.CommandSender; import org.sculk.command.SimpleCommandMap; import org.sculk.config.Config; @@ -20,6 +23,7 @@ import org.sculk.event.EventManager; import org.sculk.event.command.CommandEvent; import org.sculk.event.player.PlayerCreationEvent; +import org.sculk.event.player.PlayerLoginEvent; import org.sculk.lang.Language; import org.sculk.lang.LanguageKeys; import org.sculk.lang.LocalManager; @@ -30,12 +34,13 @@ import org.sculk.network.session.SculkServerSession; import org.sculk.player.Player; import org.sculk.player.client.ClientChainData; -import org.sculk.player.skin.Skin; +import org.sculk.player.text.RawTextBuilder; +import org.sculk.player.text.TranslaterBuilder; import org.sculk.plugin.PluginManager; +import org.sculk.resourcepack.ResourcePackManager; import org.sculk.scheduler.Scheduler; import org.sculk.server.SculkOperators; import org.sculk.server.SculkWhitelist; -import org.sculk.utils.SkinUtils; import org.sculk.utils.TextFormat; import java.lang.reflect.Constructor; @@ -67,6 +72,8 @@ public class Server { private final TerminalConsole console; private final EventManager eventManager; private final PluginManager pluginManager; + @Getter + private final ResourcePackManager resourcePackManager; private final Injector injector; private final Scheduler scheduler; @@ -91,11 +98,15 @@ public class Server { private final Map playerList = new HashMap<>(); private final Map players = new HashMap<>(); + @Getter private String motd; private String submotd; + @Getter private int maxPlayers; - private String defaultGamemode; + @Getter private UUID serverId; + @Getter + private final boolean query; private long nextTick; private int tickCounter; @@ -114,7 +125,8 @@ public Server(LocalManager localManager, Logger logger, String dataPath) { //Language manager this.localManager = localManager; this.language = localManager.getLanguage(this.properties.get(ServerPropertiesKeys.LANGUAGE, DEFAULT_LANGUAGE)); - this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_SELECTED_LANGUAGE, List.of(this.language.getName()))); + System.out.println(this.properties.get(ServerPropertiesKeys.LANGUAGE, DEFAULT_LANGUAGE)); + this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_SELECTED_LANGUAGE, List.of(TextFormat.DARK_AQUA + this.language.getName() + TextFormat.RESET))); //Load server properties this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_LOADING)); @@ -122,6 +134,7 @@ public Server(LocalManager localManager, Logger logger, String dataPath) { this.motd = this.properties.get(ServerPropertiesKeys.MOTD, "A Sculk Server Software"); this.submotd = this.properties.get(ServerPropertiesKeys.SUB_MOTD, "Powered by Sculk"); this.maxPlayers = this.properties.get(ServerPropertiesKeys.MAX_PLAYERS, 20); + this.query = this.properties.get(ServerPropertiesKeys.QUERY, false); this.operators = new SculkOperators(); this.whitelist = new SculkWhitelist(); @@ -132,6 +145,7 @@ public Server(LocalManager localManager, Logger logger, String dataPath) { this.eventManager = injector.getInstance(EventManager.class); this.scheduler = injector.getInstance(Scheduler.class); this.pluginManager = new PluginManager(this); + this.resourcePackManager = new ResourcePackManager(); this.simpleCommandMap = new SimpleCommandMap(this); this.console = new TerminalConsole(this); @@ -149,7 +163,7 @@ public void start() { this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_ONLINE_MODE_DISABLED, TextFormat.RED)); } - log.info(language.translate(LanguageKeys.SCULK_SERVER_STARTING, List.of(TextFormat.DARK_AQUA + CODE_NAME + TextFormat.WHITE, TextFormat.AQUA + CODE_VERSION + TextFormat.WHITE))); + log.info(language.translate(LanguageKeys.SCULK_SERVER_STARTING, List.of(TextFormat.DARK_AQUA + CODE_NAME + TextFormat.RESET, TextFormat.AQUA + CODE_VERSION + TextFormat.WHITE))); InetSocketAddress bindAddress = new InetSocketAddress(this.getProperties().get(ServerPropertiesKeys.SERVER_IP, "0.0.0.0"), this.getProperties().get(ServerPropertiesKeys.SERVER_PORT, 19132)); this.serverId = UUID.randomUUID(); @@ -166,6 +180,8 @@ public void start() { this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_DISTRIBUTED_UNDER, List.of(TextFormat.AQUA + "GNU GENERAL PUBLIC LICENSE"))); + this.resourcePackManager.loadResourcePacks(); + this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_LOADING_COMMANDS)); this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_LOADING_PLUGINS)); @@ -185,13 +201,13 @@ public void start() { public void shutdown() { Map onlinePlayers = this.getOnlinePlayers(); + if (this.shutdown) { + return; + } for (Map.Entry entry : onlinePlayers.entrySet()) { Player player = entry.getValue(); player.getNetworkSession().disconnect(SERVER_CLOSED_MESSAGE); } - if (this.shutdown) { - return; - } this.logger.info(this.language.translate(LanguageKeys.SCULK_SERVER_STOPPING)); this.shutdown = true; @@ -200,8 +216,6 @@ public void shutdown() { pluginManager.disableAllPlugins(); this.logger.info(this.language.translate(LanguageKeys.SCULK_PLUGINS_DISABLED)); - Sculk.shutdown(); - this.logger.info(this.language.translate(LanguageKeys.SCULK_NETWORK_INTERFACES_STOPPING)); for (SourceInterface sourceInterface : this.network.getInterfaces()) { sourceInterface.shutdown(); @@ -222,8 +236,8 @@ public CompletableFuture createPlayer(SculkServerSession session, Client Player player; try { - Constructor constructor = clazz.getConstructor(SculkServerSession.class, ClientChainData.class); - player = constructor.newInstance(session, info); + Constructor constructor = clazz.getConstructor(Server.class, SculkServerSession.class, ClientChainData.class, NbtMap.class); + player = constructor.newInstance(this, session, info, NbtMap.EMPTY); this.addPlayer(session.getSocketAddress(), player); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { this.logger.warn(FAILED_TO_CREATE_PLAYER, e); @@ -262,14 +276,29 @@ public Injector getInjector() { return injector; } + /** + * Checks whether the server is currently running. + * + * @return {@code true} if the server is running, {@code false} if it has been shut down. + */ public boolean isRunning() { return !this.shutdown; } + /** + * Returns the singleton instance of the Server. + * + * @return the single instance of the Server. + */ public static Server getInstance() { return instance; } + /** + * Retrieves the logger instance associated with this class. + * + * @return the logger instance + */ public Logger getLogger() { return logger; } @@ -282,6 +311,12 @@ public Path getPluginDataPath() { return pluginDataPath; } + /** + * Retrieves a map of online players currently active in the system. + * + * @return An unmodifiable map containing the online players, where the keys are their UUIDs + * and the values are the corresponding Player objects. + */ public Map getOnlinePlayers() { return Collections.unmodifiableMap(playerList); } @@ -300,71 +335,45 @@ public void broadcastMessage(String message) { } } - public int getMaxPlayers() { - return maxPlayers; - } - - public String getDefaultGamemode() { - return defaultGamemode; - } - - public String getMotd() { - return motd; + public Integer getDefaultGamemode() { + return this.getProperties().get(ServerPropertiesKeys.GAMEMODE, GameMode.SURVIVAL.getId()); } public String getSubMotd() { return submotd; } - public UUID getServerId() { - return serverId; + public Operators getOperators() { + return this.operators; } public void addPlayer(SocketAddress socketAddress, Player player) { this.players.put(socketAddress, player); } - public void addOnlinePlayer(Player player) { + public boolean addOnlinePlayer(Player player) { + PlayerLoginEvent event = new PlayerLoginEvent(player, "Plugin reason"); + event.call(); + if (event.isCancelled() || !player.getNetworkSession().isConnected()) + return false; + for (Player onlinePlayer : this.playerList.values()) { + onlinePlayer.getNetworkSession().onPlayerAdded(player); + player.spawnTo(onlinePlayer); + onlinePlayer.spawnTo(player); + } this.playerList.put(player.getUniqueId(), player); + return true; } - public void sendFullPlayerList(Player player) { - PlayerListPacket packet = new PlayerListPacket(); - packet.setAction(PlayerListPacket.Action.ADD); - packet.getEntries().addAll(this.playerList.values().stream().map(p -> { - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(p.getUniqueId()); - entry.setEntityId(p.getEntityId()); - entry.setName(p.getName()); - entry.setSkin(SkinUtils.toSerialized(p.getSkin())); - entry.setPlatformChatId(""); - return entry; - }).toList()); - player.sendDataPacket(packet); - } - - public void removeFromTabList(Player player) { - PlayerListPacket packet = new PlayerListPacket(); - packet.setAction(PlayerListPacket.Action.REMOVE); - packet.getEntries().add(new PlayerListPacket.Entry(player.getUniqueId())); - broadcastPacket(packet); + public void removeOnlinePlayer(Player player) { + if (this.playerList.containsKey(player.getUniqueId())) { + this.playerList.remove(player.getUniqueId()); + for (Player onlinePlayer : this.playerList.values()) { + onlinePlayer.getNetworkSession().onPlayerRemoved(player); + } + } } - public void addToTabList(UUID uuid, long entityId, String name, ClientChainData chainData, String xuid, Skin skin) { - PlayerListPacket playerListPacket = new PlayerListPacket(); - playerListPacket.setAction(PlayerListPacket.Action.ADD); - - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid); - entry.setEntityId(entityId); - entry.setName(name); - entry.setXuid(xuid); - entry.setPlatformChatId(chainData.getDeviceId()); - entry.setBuildPlatform(chainData.getDeviceOS()); - entry.setSkin(SkinUtils.toSerialized(skin)); - entry.setTrustedSkin(skin.isTrusted()); - - playerListPacket.getEntries().add(entry); - this.broadcastPacket(playerListPacket); - } public Scheduler getScheduler() { return scheduler; diff --git a/src/main/java/org/sculk/api/player/GameMode.java b/src/main/java/org/sculk/api/player/GameMode.java new file mode 100644 index 0000000..eb36c5e --- /dev/null +++ b/src/main/java/org/sculk/api/player/GameMode.java @@ -0,0 +1,53 @@ +package org.sculk.api.player; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +public enum GameMode { + SURVIVAL("Survival", 0), + CREATIVE("Creative", 1), + ADVENTURE("Adventure", 2), + SPECTATOR("Spectator", 3); + + private final String identifier; + private final int id; + + GameMode(String identifier, int id) { + this.identifier = identifier; + this.id = id; + } + + public String getIdentifier() { + return identifier; + } + + public int getId() { + return id; + } + + public static GameMode fromId(int id) { + switch (id) { + case 0: + return SURVIVAL; + case 1: + return CREATIVE; + case 2: + return ADVENTURE; + default: + return SPECTATOR; + } + } +} diff --git a/src/main/java/org/sculk/server/Operators.java b/src/main/java/org/sculk/api/server/Operators.java similarity index 96% rename from src/main/java/org/sculk/server/Operators.java rename to src/main/java/org/sculk/api/server/Operators.java index 9cf0d29..faf5cf1 100644 --- a/src/main/java/org/sculk/server/Operators.java +++ b/src/main/java/org/sculk/api/server/Operators.java @@ -1,4 +1,4 @@ -package org.sculk.server; +package org.sculk.api.server; public interface Operators { diff --git a/src/main/java/org/sculk/server/Whitelist.java b/src/main/java/org/sculk/api/server/Whitelist.java similarity index 95% rename from src/main/java/org/sculk/server/Whitelist.java rename to src/main/java/org/sculk/api/server/Whitelist.java index e76bf21..0159a83 100644 --- a/src/main/java/org/sculk/server/Whitelist.java +++ b/src/main/java/org/sculk/api/server/Whitelist.java @@ -1,4 +1,4 @@ -package org.sculk.server; +package org.sculk.api.server; public interface Whitelist { diff --git a/src/main/java/org/sculk/command/defaults/HelpCommand.java b/src/main/java/org/sculk/command/defaults/HelpCommand.java index 55d3e22..adb1407 100644 --- a/src/main/java/org/sculk/command/defaults/HelpCommand.java +++ b/src/main/java/org/sculk/command/defaults/HelpCommand.java @@ -60,47 +60,48 @@ public void onRun(CommandSender sender, String commandLabel, Map actualPage = 1; } } else if (args.containsKey("command")) { - Command command = (Command) args.get("command"); + String commandName = args.get("command").toString(); + Command command = Server.getInstance().getCommandMap().getCommand(commandName); if (command != null) { - sender.sendMessage(new RawTextBuilder().add(new TranslaterBuilder() + sender.sendMessage(new RawTextBuilder().add(new TranslaterBuilder() .setTranslate("§6/%%s: §f%%s\nUsage: §e%%s") .setWith(new RawTextBuilder() .add(new TextBuilder().setText(command.getLabel())) - .add(new TextBuilder().setText(command.getDescription())) + .add(new TranslaterBuilder().setTranslate(command.getDescription())) .add(new TextBuilder().setText(command.getUsageMessage())) ) )); } else { - sender.sendMessage(new RawTextBuilder().add(new TranslaterBuilder().setTranslate("§4/%%s§c does not seem to exist, check the list of commands with §4/help§c.") - .setWith(new RawTextBuilder().add(new TextBuilder().setText(args.get("command").toString()))))); + sender.sendMessage(new RawTextBuilder().add(new TranslaterBuilder().setTranslate("§4/%%s§c does not seem to exist, check the list of commands with §4/help§c.") + .setWith(new RawTextBuilder().add(new TextBuilder().setText(commandName))))); } return; } - + RawTextBuilder commands = RawTextBuilder.create(); int startIndex = (actualPage - 1) * commandsPerPage; int endIndex = Math.min(startIndex + commandsPerPage, totalCommands); - + TranslaterBuilder commandSentence = new TranslaterBuilder<>(); + commandSentence.setTranslate("§6/%%s:§f %%s\n"); for (int i = startIndex; i < endIndex; i++) { String commandName = commandSending.toArray(new String[0])[i]; Command command = Server.getInstance().getCommandMap().getCommand(commandName); + if (i == endIndex - 1) + commandSentence.setTranslate("§6/%%s:§f %%s"); if (command != null) { - builder.append("§6/").append(commandName).append(":§f ").append(command.getDescription()).append("\n"); + TranslaterBuilder clone = commandSentence.clone(); + commands.add(clone.setWith(RawTextBuilder.create(new TextBuilder().setText(commandName), new TranslaterBuilder<>().setTranslate(command.getDescription())))); } } - - TranslaterBuilder translaterBuilder = new TranslaterBuilder(); - translaterBuilder.setTranslate("§6-------------- §fHelp - %%s command(s) §7[%%s/%%s] §6--------------\n%%s"); - translaterBuilder.setWith(new RawTextBuilder() - .add(new TextBuilder() - .setText(Integer.toString(totalCommands))) - .add(new TextBuilder() - .setText(Integer.toString(actualPage))) - .add(new TextBuilder() - .setText(Integer.toString(totalPage))) - .add(new TextBuilder() - .setText(builder.substring(0, builder.length() - 1))) + TranslaterBuilder translaterBuilder = new TranslaterBuilder<>(); + translaterBuilder.setTranslate("commands.help.header"); + translaterBuilder.setWith( + RawTextBuilder.create( + new TextBuilder().setText(Integer.toString(totalCommands)), + new TextBuilder().setText(Integer.toString(actualPage)), + new TextBuilder().setText(Integer.toString(totalPage)), + commands) ); sender.sendMessage(new RawTextBuilder().add(translaterBuilder)); } -} +} \ No newline at end of file diff --git a/src/main/java/org/sculk/command/defaults/VersionCommand.java b/src/main/java/org/sculk/command/defaults/VersionCommand.java index 480512f..4f0c61f 100644 --- a/src/main/java/org/sculk/command/defaults/VersionCommand.java +++ b/src/main/java/org/sculk/command/defaults/VersionCommand.java @@ -42,7 +42,7 @@ protected void prepare() { public void onRun(CommandSender sender, String commandLabel, Map args) { sender.sendMessage(new RawTextBuilder().add( new TranslaterBuilder() - .setTranslate("§fThis server is running §a%%s\n§fServer version: §a%%s\n§fCompatible Minecraft version: §a%%s §f(protocol version: §a%%s§f)\nOperating system: §a%%s") + .setTranslate("§fThis server is running §a%s\n§fServer version: §a%s\n§fCompatible Minecraft version: §a%s §f(protocol version: §a%s§f)\nOperating system: §a%s") .setWith(new RawTextBuilder() .add(new TextBuilder().setText(Sculk.CODE_NAME)) // software name .add(new TextBuilder().setText(Sculk.CODE_VERSION)) // software version diff --git a/src/main/java/org/sculk/config/ServerProperties.java b/src/main/java/org/sculk/config/ServerProperties.java index ad2cd9d..a59b1ea 100644 --- a/src/main/java/org/sculk/config/ServerProperties.java +++ b/src/main/java/org/sculk/config/ServerProperties.java @@ -1,5 +1,7 @@ package org.sculk.config; +import org.sculk.api.player.GameMode; + import java.io.File; import java.nio.file.Path; @@ -24,7 +26,7 @@ private ConfigSection getDefaultValues() { defaults.put(ServerPropertiesKeys.SERVER_PORT.toString(), 19132); defaults.put(ServerPropertiesKeys.WHITELIST.toString(), "off"); defaults.put(ServerPropertiesKeys.MAX_PLAYERS.toString(), 20); - defaults.put(ServerPropertiesKeys.GAMEMODE.toString(), 0); + defaults.put(ServerPropertiesKeys.GAMEMODE.toString(), GameMode.SURVIVAL.getId()); defaults.put(ServerPropertiesKeys.PVP.toString(), "on"); defaults.put(ServerPropertiesKeys.DIFFICULTY.toString(), 1); defaults.put(ServerPropertiesKeys.LEVEL_NAME.toString(), "world"); @@ -34,6 +36,8 @@ private ConfigSection getDefaultValues() { defaults.put(ServerPropertiesKeys.SPAWN_MONSTERS.toString(), "on"); defaults.put(ServerPropertiesKeys.AUTO_SAVE.toString(), "on"); defaults.put(ServerPropertiesKeys.XBOX_AUTH.toString(), "on"); + defaults.put(ServerPropertiesKeys.QUERY.toString(), "false"); + defaults.put(ServerPropertiesKeys.FORCE_RESOURCE_PACKS.toString(), "off"); return defaults; } diff --git a/src/main/java/org/sculk/config/ServerPropertiesKeys.java b/src/main/java/org/sculk/config/ServerPropertiesKeys.java index 234a391..7906c9e 100644 --- a/src/main/java/org/sculk/config/ServerPropertiesKeys.java +++ b/src/main/java/org/sculk/config/ServerPropertiesKeys.java @@ -17,7 +17,9 @@ public enum ServerPropertiesKeys { SPAWN_ANIMALS("spawn-animals"), SPAWN_MONSTERS("spawn-monsters"), AUTO_SAVE("auto-save"), - XBOX_AUTH("xbox-auth"); + XBOX_AUTH("xbox-auth"), + QUERY("query"), + FORCE_RESOURCE_PACKS("force-resource-packs"); private final String key; diff --git a/src/main/java/org/sculk/console/ConsoleThread.java b/src/main/java/org/sculk/console/ConsoleThread.java index 87b54d1..6a37771 100644 --- a/src/main/java/org/sculk/console/ConsoleThread.java +++ b/src/main/java/org/sculk/console/ConsoleThread.java @@ -1,7 +1,5 @@ package org.sculk.console; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; - /* * ____ _ _ * / ___| ___ _ _| | | __ diff --git a/src/main/java/org/sculk/console/TerminalConsole.java b/src/main/java/org/sculk/console/TerminalConsole.java index a6503b3..dce1adb 100644 --- a/src/main/java/org/sculk/console/TerminalConsole.java +++ b/src/main/java/org/sculk/console/TerminalConsole.java @@ -56,18 +56,17 @@ public Locale getLocale() { @Override public Language getLanguage() { - return null; + return this.server.getLanguage(); } @Override public void sendMessage(String message) { this.server.getLogger().info(message); - } @Override public void sendMessage(RawTextBuilder textBuilder) { - this.server.getLogger().info(textBuilder.toString()); + this.server.getLogger().info(this.getLanguage().translate(textBuilder)); } @Override diff --git a/src/main/java/org/sculk/entity/Entity.java b/src/main/java/org/sculk/entity/Entity.java index b3497eb..b7a1685 100644 --- a/src/main/java/org/sculk/entity/Entity.java +++ b/src/main/java/org/sculk/entity/Entity.java @@ -1,15 +1,20 @@ package org.sculk.entity; +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import lombok.Getter; -import org.checkerframework.checker.units.qual.t; +import lombok.NonNull; +import lombok.Setter; import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.sculk.Server; import org.sculk.network.broadcaster.EntityEventBroadcaster; import org.sculk.network.session.SculkServerSession; @@ -18,7 +23,6 @@ import javax.annotation.Nullable; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; /* * ____ _ _ @@ -55,7 +59,7 @@ public static long getNextEntityId() { @Getter private float health = 20.0F; - @Getter + @Getter @Setter private int maxHealth = 20; protected float ySize = 0.0F; @@ -63,9 +67,10 @@ public static long getNextEntityId() { public boolean keepMovement = false; public float fallDistance = 0.0F; - public int ticksLived = 0; - public int lastUpdate; + public long ticksLived = 0; + public long lastUpdate = 0; protected int fireTicks = 0; + protected boolean closed = false; protected AttributeMap attributeMap; @@ -76,6 +81,7 @@ public static long getNextEntityId() { @Getter private final EntityDataMap networkProperties; protected boolean networkPropertiesDirty = false; + @Getter protected String nameTag = ""; protected boolean nameTagVisible = true; protected boolean alwaysShowName = false; @@ -91,21 +97,111 @@ public static long getNextEntityId() { protected @Nullable Integer ownerId; protected @Nullable Integer targetId; + protected boolean justCreated = true; + protected boolean constructorCalled = false; public Entity() { + if(this.constructorCalled){ + throw new RuntimeException("Attempted to call constructor for an Entity multiple times"); + } + this.constructorCalled = true; + this.entityId = getNextEntityId(); + this.networkProperties = new EntityDataMap(); + networkPropertiesDirty = true; + this.size = this.getInitialSizeInfo(); + this.ownerId = null; + this.hasSpawned = new ObjectArrayList(); + this.initEntity(NbtMap.EMPTY); + } + public Entity(NbtMap nbt) { + if(this.constructorCalled){ + throw new RuntimeException("Attempted to call constructor for an Entity multiple times"); + } + this.constructorCalled = true; this.entityId = getNextEntityId(); this.networkProperties = new EntityDataMap(); networkPropertiesDirty = true; this.size = this.getInitialSizeInfo(); this.ownerId = null; this.hasSpawned = new ObjectArrayList(); + this.initEntity(nbt); } abstract protected EntitySizeInfo getInitialSizeInfo(); - public void initEntity() { + protected void initEntity(NbtMap nbt) { + } + + public void kill() { + + } + + protected void onDeath() { + + } + + protected boolean onDeathUpdate(long tickDiff) + { + return true; + } + + public boolean isAlive() { + return this.health > 0.0F; + } + + public void setHealth(float health) + { + boolean alive; + if (health == this.health) { + return; + } + alive = this.isAlive(); + if (health <= 0.0F) { + if (alive && !this.justCreated) + { + this.kill(); + } + else if (alive) + this.health = 0.0F; + }else if (health <= this.getMaxHealth() || health > this.health) { + this.health = health; + } else { + this.health = this.getMaxHealth(); + } + } + + + protected void onFirstUpdate(long $currentTick) { + //TODO: EntitySpawnEvent } - public void onUpdate() {} + public boolean onUpdate(long currentTick) { + long tickDiff; + boolean hasUpdate; + Timing timings; + if (this.closed) + return false; + tickDiff = currentTick - this.lastUpdate; + if (tickDiff <= 0.0F) { + //TODO: check !justCreated + return true; + } + this.lastUpdate = currentTick; + if (this.justCreated) + this.onFirstUpdate(currentTick); + if (!this.isAlive()) + { + if (this.onDeathUpdate(tickDiff)) + { + //TODO: create flagForDespawn + } + return true; + } + timings = Timings.entityBaseTickTimer; + timings.startTiming(); + hasUpdate = this.entityBaseTick(tickDiff); + Timings.entityBaseTickTimer.stopTiming(); + return (hasUpdate); + } public List getViewers() { @@ -142,6 +238,24 @@ public void setSize(EntitySizeInfo size) { this.networkPropertiesDirty = true; } + protected boolean entityBaseTick(long tickDiff) { + boolean hasUpdate = false; + if (this.justCreated) + { + this.justCreated = false; + if (!this.isAlive()) + this.kill(); + } + if (networkPropertiesDirty) + { + this.sendData(this.getNetworkProperties()); + networkPropertiesDirty = false; + } + + this.ticksLived += tickDiff; + return hasUpdate; + } + public boolean isOnFire(){ return (this.fireTicks > 0); @@ -181,10 +295,51 @@ protected void syncNetworkData(EntityDataMap properties) { properties.setFlag(EntityFlag.WALL_CLIMBING, this.canClimbWalls); } + public void spawnTo(Player player) { + if (!this.hasSpawned.contains(player)) { + this.hasSpawned.add(player); + this.sendSpawnPacket(player); + } + } + + public void respawnToAll() { + List players = this.getViewers(); + this.hasSpawned.clear(); + for (Player player : players) { + this.spawnTo(player); + } + } + + public void despawnFrom(Player player, boolean send) { + if (this.hasSpawned.contains(player)) { + if(send){ + player.getNetworkSession().getEntityEventBroadcaster().onEntityRemoved(List.of(player.getNetworkSession()), this); + } + this.hasSpawned.remove(player); + } + } + + public void despawnFrom(Player player) { + this.despawnFrom(player, true); + } + + public void despawnFromAll() { + NetworkBroadcastUtils.broadcastEntityEvent( + this.getViewers(), + (entityEventBroadcaster, sculkServerSessions) -> entityEventBroadcaster.onEntityRemoved(sculkServerSessions, this) + ); + this.hasSpawned.clear(); + } + /** * Called by spawnTo() to send whatever packets needed to spawn the entity to the client. */ protected void sendSpawnPacket(Player player) { + player.getNetworkSession().sendPacket(this.createAddEntityPacket()); + } + + protected @NonNull BedrockPacket createAddEntityPacket() + { AddEntityPacket packet = new AddEntityPacket(); packet.setUniqueEntityId(this.getEntityId()); packet.setRuntimeEntityId(this.getEntityId()); @@ -197,7 +352,7 @@ protected void sendSpawnPacket(Player player) { packet.setAttributes(this.attributeMap.getAll().values().stream().map(attr -> new AttributeData(attr.getId(), attr.getMinValue(), attr.getMaxValue(), attr.getCurrentValue(), attr.getDefaultValue())).toList()); packet.setMetadata(this.getAllNetworkData()); packet.setEntityLinks(List.of()); - player.getNetworkSession().sendPacket(packet); + return packet; } public void sendData(List targets, EntityDataMap data) { diff --git a/src/main/java/org/sculk/player/text/SelectorBuilder.java b/src/main/java/org/sculk/entity/EntityRegistry.java similarity index 68% rename from src/main/java/org/sculk/player/text/SelectorBuilder.java rename to src/main/java/org/sculk/entity/EntityRegistry.java index 649d975..85ae8fc 100644 --- a/src/main/java/org/sculk/player/text/SelectorBuilder.java +++ b/src/main/java/org/sculk/entity/EntityRegistry.java @@ -1,4 +1,5 @@ -package org.sculk.player.text; +package org.sculk.entity; + /* * ____ _ _ @@ -15,16 +16,8 @@ * @author: SculkTeams * @link: http://www.sculkmp.org/ */ -public class SelectorBuilder implements IJsonText { - - @Override - public String getName() { - return ""; - } +//TODO: Implement Entity Registry for entities to be registered - @Override - public Object build() { - return null; - } -} \ No newline at end of file +public class EntityRegistry { +} diff --git a/src/main/java/org/sculk/entity/HumanEntity.java b/src/main/java/org/sculk/entity/HumanEntity.java index a8c2837..62687b3 100644 --- a/src/main/java/org/sculk/entity/HumanEntity.java +++ b/src/main/java/org/sculk/entity/HumanEntity.java @@ -2,14 +2,21 @@ import lombok.Getter; +import lombok.NonNull; import lombok.Setter; -import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; +import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; +import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; +import org.cloudburstmc.protocol.bedrock.packet.*; import org.sculk.Server; import org.sculk.data.bedrock.entity.EntityIds; import org.sculk.entity.manager.ExperienceManager; import org.sculk.entity.manager.HungerManager; import org.sculk.event.player.PlayerChangeSkinEvent; +import org.sculk.network.session.SculkServerSession; import org.sculk.network.utils.NetworkBroadcastUtils; import org.sculk.player.Player; import org.sculk.player.skin.Skin; @@ -45,18 +52,22 @@ public class HumanEntity extends Living { @Setter @Getter protected Skin skin; - public HumanEntity(){ - super(); + public HumanEntity(NbtMap nbt){ + super(nbt); this.uuid = UUID.randomUUID(); } @Override - public void initEntity() { - super.initEntity(); + protected void initEntity(NbtMap nbt) { + super.initEntity(nbt); this.hungerManager = new HungerManager(this); this.experienceManager = new ExperienceManager(this); } + public String getName(){ + return this.getNameTag(); + } + public String getNetworkTypeId() { return EntityIds.PLAYER; @@ -96,14 +107,57 @@ public void sendSkin(List target) { NetworkBroadcastUtils.broadcastPackets(target, List.of(skinPacket)); } + @Override + public void spawnTo(Player player) { + if (this != player) { + super.spawnTo(player); + } + } + @Override protected void sendSpawnPacket(Player player) { - //TODO + SculkServerSession networkSession = player.getNetworkSession(); + if(!(this instanceof Player)) { + PlayerListPacket packet = new PlayerListPacket(); + packet.setAction(PlayerListPacket.Action.ADD); + packet.getEntries().add(networkSession.createPlayerEntry(this, true)); + networkSession.sendPacket(packet); + } + AddPlayerPacket addPlayerPacket = createAddEntityPacket(); + networkSession.sendPacket(addPlayerPacket); + + if(!(this instanceof Player)) { + PlayerListPacket packet = new PlayerListPacket(); + packet.setAction(PlayerListPacket.Action.REMOVE); + packet.getEntries().add(networkSession.createPlayerEntry(this, false)); + networkSession.sendPacket(packet); + } } @Override - public void onUpdate() { - super.onUpdate(); + protected @NonNull BedrockPacket createAddEntityPacket() { + AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); + addPlayerPacket.setUuid(this.getUniqueId()); + addPlayerPacket.setUsername(this.getName()); + addPlayerPacket.setRuntimeEntityId(this.getEntityId()); + addPlayerPacket.setUniqueEntityId(this.getEntityId()); + addPlayerPacket.setPlatformChatId(""); + addPlayerPacket.setPosition(Vector3f.ZERO); + addPlayerPacket.setRotation(Vector3f.ZERO); + addPlayerPacket.setMotion(Vector3f.ZERO); + addPlayerPacket.setHand(ItemData.AIR); + addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); + addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); + addPlayerPacket.setGameType(GameType.SURVIVAL); + addPlayerPacket.setMetadata(this.getAllNetworkData()); + addPlayerPacket.setDeviceId(""); + addPlayerPacket.setBuildPlatform(0); + return addPlayerPacket; } + + @Override + public boolean onUpdate(long currentTick) { + return super.onUpdate(currentTick); + } } diff --git a/src/main/java/org/sculk/entity/Living.java b/src/main/java/org/sculk/entity/Living.java index 4328de5..ec5d9a2 100644 --- a/src/main/java/org/sculk/entity/Living.java +++ b/src/main/java/org/sculk/entity/Living.java @@ -1,6 +1,7 @@ package org.sculk.entity; +import org.cloudburstmc.nbt.NbtMap; import org.sculk.entity.manager.HungerManager; /* @@ -20,13 +21,17 @@ */ public abstract class Living extends Entity { + public Living(NbtMap nbt) { + super(nbt); + } + @Override - public void initEntity() { - super.initEntity(); + protected void initEntity(NbtMap nbt) { + super.initEntity(nbt); } @Override - public void onUpdate() { - super.onUpdate(); + public boolean onUpdate(long currentTick) { + return super.onUpdate(currentTick); } } diff --git a/src/main/java/org/sculk/event/player/PlayerJoinEvent.java b/src/main/java/org/sculk/event/player/PlayerJoinEvent.java index 87f404e..9f58ad0 100644 --- a/src/main/java/org/sculk/event/player/PlayerJoinEvent.java +++ b/src/main/java/org/sculk/event/player/PlayerJoinEvent.java @@ -1,6 +1,7 @@ package org.sculk.event.player; +import lombok.NonNull; import org.sculk.player.Player; /* @@ -20,18 +21,19 @@ */ public class PlayerJoinEvent extends PlayerEvent { + @NonNull protected String joinMessage; - public PlayerJoinEvent(Player player, String joinMessage) { + public PlayerJoinEvent(Player player, @NonNull String joinMessage) { super(player); this.joinMessage = joinMessage; } - public String getJoinMessage() { + public @NonNull String getJoinMessage() { return joinMessage; } - public void setJoinMessage(String joinMessage) { + public void setJoinMessage(@NonNull String joinMessage) { this.joinMessage = joinMessage; } } diff --git a/src/main/java/org/sculk/event/player/PlayerLoginEvent.java b/src/main/java/org/sculk/event/player/PlayerLoginEvent.java new file mode 100644 index 0000000..fd55124 --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerLoginEvent.java @@ -0,0 +1,32 @@ +package org.sculk.event.player; + + +import lombok.Getter; +import lombok.Setter; +import org.sculk.event.Cancellable; +import org.sculk.player.Player; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerLoginEvent extends PlayerEvent implements Cancellable { + + @Getter @Setter + private Object kickMessage; + + public PlayerLoginEvent(Player player, Object message) { + super(player); + } +} diff --git a/src/main/java/org/sculk/event/server/QueryHandlerEvent.java b/src/main/java/org/sculk/event/server/QueryHandlerEvent.java new file mode 100644 index 0000000..ca4c6e7 --- /dev/null +++ b/src/main/java/org/sculk/event/server/QueryHandlerEvent.java @@ -0,0 +1,55 @@ +package org.sculk.event.server; + + +import lombok.Getter; +import lombok.Setter; +import org.sculk.player.Player; + +import java.net.InetSocketAddress; +import java.util.Collection; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class QueryHandlerEvent extends ServerEvent { + @Getter @Setter + private String motd; + @Getter @Setter + private String smp; + @Getter @Setter + private String mcpe; + @Getter @Setter + private String s; + @Getter @Setter + private Collection values; + @Getter @Setter + private int maxPlayers; + @Getter @Setter + private String waterdogPE; + @Getter @Setter + private InetSocketAddress address; + + public QueryHandlerEvent(String motd, String smp, String mcpe, String s, Collection values, int maxPlayers, String waterdogPE, InetSocketAddress address) { + super(); + this.motd = motd; + this.smp = smp; + this.mcpe = mcpe; + this.s = s; + this.values = values; + this.maxPlayers = maxPlayers; + this.waterdogPE = waterdogPE; + this.address = address; + } +} diff --git a/src/main/java/org/sculk/event/server/ServerEvent.java b/src/main/java/org/sculk/event/server/ServerEvent.java new file mode 100644 index 0000000..ec7e953 --- /dev/null +++ b/src/main/java/org/sculk/event/server/ServerEvent.java @@ -0,0 +1,24 @@ +package org.sculk.event.server; + + +import org.sculk.Server; +import org.sculk.event.Event; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ServerEvent extends Event { + +} diff --git a/src/main/java/org/sculk/lang/Language.java b/src/main/java/org/sculk/lang/Language.java index e0cdad4..bbc74fc 100644 --- a/src/main/java/org/sculk/lang/Language.java +++ b/src/main/java/org/sculk/lang/Language.java @@ -2,12 +2,16 @@ import lombok.Getter; import lombok.NonNull; -import lombok.Setter; +import org.sculk.player.text.IJsonText; +import org.sculk.player.text.RawTextBuilder; +import org.sculk.player.text.TextBuilder; +import org.sculk.player.text.TranslaterBuilder; import javax.annotation.Nullable; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /* * ____ _ _ @@ -32,23 +36,38 @@ public class Language { @Getter private @NonNull Map translate; + private static final Pattern PATTERN_STRING_EXTERNAL = Pattern.compile("%%[sd]"); + private static final Pattern PATTERN_INDEX_EXTERNAL = Pattern.compile("%%(\\d+)(\\$[sd])|%%"); + private static final Pattern PATTERN_INTERNAL = Pattern.compile("(? translate) { this.name = name; this.locale = locale; - this.translate = translate; + this.translate = new HashMap<>(); + translate.forEach((key, value) -> { + this.translate.put(key, reformatConfigToMinecraft(value)); + }); + translate.clear(); + } + + public static String reformatConfigToMinecraft(String value){ + StringBuilder result = new StringBuilder(); + Matcher matcher = PATTERN_INTERNAL.matcher(value); + while (matcher.find()) { + matcher.appendReplacement(result, "%" + Matcher.quoteReplacement(matcher.group())); + } + matcher.appendTail(result); + return result.toString().replace("\\n" , "\n"); } public final @Nullable String internalGet(String key){ - String value = translate.getOrDefault(key, null); - if (value != null) - return value.replace("\\n", "\n"); - return null; + return translate.getOrDefault(key, null); } public String translate(String str, @Nullable List params, String onlyPrefix) { String replacement; String baseText; + char format; int size; baseText = (onlyPrefix == null || str.startsWith(onlyPrefix)) ? internalGet(str) : null; @@ -70,10 +89,23 @@ public String translate(String str, @Nullable List params, String onlyPr case InetSocketAddress value -> value.toString(); case null, default -> throw new IllegalArgumentException("Illegal parameter: " + param); }; - baseText = baseText.replace("{" + "%" + i + "}", replacement); + format = switch (param) + { + case Translatable value -> 's'; + case String value -> 's'; + case Double value -> 'd'; + case Float value -> 'd'; + case Integer value -> 'd'; + case Long value -> 'd'; + case Boolean value -> 's'; + case Date value -> 's'; + case InetSocketAddress value -> 's'; + case null, default -> throw new IllegalArgumentException("Illegal parameter: " + param); + }; + baseText = baseText.replace("%%" + format, replacement); } } - return baseText; + return baseText.replace("%%", "%"); } public String translate(LanguageKeys str, @Nullable List params, String onlyPrefix) { @@ -121,11 +153,139 @@ public final String translate(Translatable c){ }else { throw new IllegalArgumentException("Illegal parameter: " + entry.getValue()); } - baseText = baseText.replace("{" + "%" + key + "}", replacement); + if (isNumber(key)) + baseText = baseText.replace("%%" + key + "$s", replacement); + else + baseText = baseText.replace("%%" + key, replacement); } return baseText; } + public static boolean isNumber(String str) { + if (str == null || str.isEmpty()) { + return false; + } + try { + Double.parseDouble(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public final String translate(TranslaterBuilder c){ + Object with; + + List data = new ArrayList<>(); + String baseText; + String text; + Matcher matcher; + int i; + int index; + String group; + char format; + + baseText = this.internalGet(c.getTranslate()); + if (baseText == null) + baseText = Language.reformatConfigToMinecraft(c.getTranslate()); + with = c.getWith(); + if (with instanceof RawTextBuilder _withRaw){ + for (IJsonText raw: _withRaw.getBuild()) + { + if (raw instanceof TranslaterBuilder _raw) + data.add(translate(_raw)); + else if(raw instanceof RawTextBuilder _raw) + data.add(translate(_raw)); + else if(raw instanceof TextBuilder _raw) + data.add(_raw.getText()); + } + } else if (with instanceof List _withList) { + for (Object item : _withList) { + if (item instanceof String str) { + data.add(str); + } + } + } + + StringBuilder result = new StringBuilder(); + i = 0; + matcher = PATTERN_STRING_EXTERNAL.matcher(baseText); + int lastEnd = 0; + while (matcher.find()) { + group = matcher.group(); + if (i >= data.size()) { + matcher.appendReplacement(result, ""); + continue; + } + format = group.charAt(group.lastIndexOf("%") + 1); + Object value = data.get(i); + if (format == 's' && value instanceof String && !isNumber((String) value)) { + matcher.appendReplacement(result, value.toString()); + } else if (format == 'd' && (value instanceof Number || (value instanceof String && isNumber((String) value)))) { + matcher.appendReplacement(result, value.toString()); + }else { + matcher.appendReplacement(result, Integer.toString(i + 1)); + } + ++i; + } + matcher.appendTail(result); + baseText = result.toString(); + result.setLength(0); + matcher = PATTERN_INDEX_EXTERNAL.matcher(baseText); + while (matcher.find()) { + group = matcher.group(); + if (group.equals("%%")) { + matcher.appendReplacement(result, "%"); + continue; + } + try { + index = Integer.parseInt(group.substring(group.lastIndexOf("%") + 1, group.indexOf("$"))) + i - 1; + } catch (NumberFormatException e) { + continue; + } + if (index < 0 || index >= data.size()) { + matcher.appendReplacement(result, ""); + continue; + } + format = group.charAt(group.lastIndexOf("$") + 1); + if (format == '\0') { + matcher.appendReplacement(result, Integer.toString(index - i + 1)); + continue; + } + Object value = data.get(index); + if (format == 's' && value instanceof String && !isNumber((String) value)) { + matcher.appendReplacement(result, value.toString()); + } else if (format == 'd' && (value instanceof Number || (value instanceof String && isNumber((String) value)))) { + matcher.appendReplacement(result, value.toString()); + }else { + matcher.appendReplacement(result, Integer.toString(index - i + 1)); + } + } + matcher.appendTail(result); + return result.toString(); + } + + public final String translate(RawTextBuilder c){ + StringBuilder stringBuilder; + String text; + List list; + + list = c.getBuild(); + stringBuilder = new StringBuilder(); + for (IJsonText data: list) + { + if (data instanceof TranslaterBuilder _data) + stringBuilder.append(translate(_data)); + else if(data instanceof RawTextBuilder _data) + stringBuilder.append(translate(_data)); + else if(data instanceof TextBuilder _data) + { + stringBuilder.append(_data.getText()); + } + } + return stringBuilder.toString(); + } + /** diff --git a/src/main/java/org/sculk/lang/LanguageKeys.java b/src/main/java/org/sculk/lang/LanguageKeys.java index 9f75d1b..efd660e 100644 --- a/src/main/java/org/sculk/lang/LanguageKeys.java +++ b/src/main/java/org/sculk/lang/LanguageKeys.java @@ -3,6 +3,8 @@ public enum LanguageKeys { SCULK_SERVER_STARTING("sculk.server.starting"), SCULK_SERVER_LOADING("sculk.server.loading"), + SCULK_SERVER_RESOURCE_PACK_LOADING("sculk.server.resources.pack.loading"), + SCULK_SERVER_RESOURCE_PACK_LOADED("sculk.server.resources.pack.loaded"), SCULK_SERVER_LOADING_COMMANDS("sculk.server.loading.commands"), SCULK_SERVER_SELECTED_LANGUAGE("sculk.server.selected.language"), SCULK_SERVER_STARTING_VERSION("sculk.server.starting.version"), diff --git a/src/main/java/org/sculk/lang/LocalManager.java b/src/main/java/org/sculk/lang/LocalManager.java index de4e2f3..aac99fa 100644 --- a/src/main/java/org/sculk/lang/LocalManager.java +++ b/src/main/java/org/sculk/lang/LocalManager.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; /* * ____ _ _ @@ -46,7 +47,11 @@ public LocalManager(ClassLoader loader, String resourcePath) { } config.load(data); String[] languagePart = key.split("_"); - languageMap.put(key, new Language(value, Locale.of(languagePart[0], languagePart.length > 1 ? languagePart[1] : ""), (Map)config.getAll())); + languageMap.put(key, new Language(value, Locale.of(languagePart[0], languagePart.length > 1 ? languagePart[1] : ""), config.getAll() + .entrySet() + .stream() + .filter(e -> (e.getValue() instanceof String)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> (String) e.getValue())))); } } diff --git a/src/main/java/org/sculk/network/BedrockInterface.java b/src/main/java/org/sculk/network/BedrockInterface.java index 94ed124..0132932 100644 --- a/src/main/java/org/sculk/network/BedrockInterface.java +++ b/src/main/java/org/sculk/network/BedrockInterface.java @@ -14,7 +14,9 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList; import lombok.Getter; import org.cloudburstmc.netty.channel.raknet.RakChannelFactory; +import org.cloudburstmc.netty.channel.raknet.RakServerChannel; import org.cloudburstmc.netty.channel.raknet.config.RakChannelOption; +import org.cloudburstmc.netty.handler.codec.raknet.server.RakServerRateLimiter; import org.cloudburstmc.protocol.bedrock.BedrockPeer; import org.cloudburstmc.protocol.bedrock.BedrockPong; import org.cloudburstmc.protocol.bedrock.BedrockServerSession; @@ -23,8 +25,10 @@ import org.sculk.config.ServerPropertiesKeys; import org.sculk.network.broadcaster.StandardEntityEventBroadcaster; import org.sculk.network.broadcaster.StandardPacketBroadcaster; +import org.sculk.network.channel.OfflineServerChannelInitializer; import org.sculk.network.handler.SessionStartPacketHandler; import org.sculk.network.protocol.ProtocolInfo; +import org.sculk.network.query.QueryHandler; import org.sculk.network.server.SculkBedrockServerInitializer; import org.sculk.network.session.SculkServerSession; @@ -33,6 +37,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -54,14 +59,16 @@ public class BedrockInterface implements AdvancedSourceInterface { private final Server server; - private final List serverChannels = new ObjectArrayList<>(); + private final List serverChannels = new ObjectArrayList<>(); @Getter private final BedrockPong bedrockPong = new BedrockPong(); - @Getter private final EventLoopGroup bossEventLoopGroup; @Getter private final EventLoopGroup workerEventLoopGroup; + @Getter + private final QueryHandler queryHandler; + public BedrockInterface(Server server) throws Exception { String address = server.getProperties().get(ServerPropertiesKeys.SERVER_IP, "0.0.0.0"); Integer port = server.getProperties().get(ServerPropertiesKeys.SERVER_PORT, 19132); @@ -69,6 +76,7 @@ public BedrockInterface(Server server) throws Exception { ThreadFactory workerFactory; ThreadFactory bossFactory; this.server = server; + this.queryHandler = new QueryHandler(server); EventLoops.ChannelType channelType = EventLoops.getChannelType(); server.getLogger().info("Using {} channel implementation as default!", channelType.name()); for (EventLoops.ChannelType type : EventLoops.ChannelType.values()) { @@ -85,6 +93,7 @@ public BedrockInterface(Server server) throws Exception { .setPriority(8) .setDaemon(true) .build(); + this.workerEventLoopGroup = channelType.newEventLoopGroup(0, workerFactory); this.bossEventLoopGroup = channelType.newEventLoopGroup(0, bossFactory); boolean allowEpoll = Epoll.isAvailable(); @@ -94,11 +103,11 @@ public BedrockInterface(Server server) throws Exception { bootstrap = new ServerBootstrap() .channelFactory(RakChannelFactory.server(EventLoops.getChannelType().getDatagramChannel())) .group(this.bossEventLoopGroup, this.workerEventLoopGroup) - // .option(CustomChannelOption.IP_DONT_FRAG, 2 /* IP_PMTUDISC_DO */) - /*.option(RakChannelOption.RAK_GUID, server.getServerId().getMostSignificantBits()) + .option(RakChannelOption.RAK_GUID, server.getServerId().getMostSignificantBits()) .option(RakChannelOption.RAK_HANDLE_PING, true) .childOption(RakChannelOption.RAK_SESSION_TIMEOUT, 10000L) - .childOption(RakChannelOption.RAK_ORDERING_CHANNELS, 1)*/ + .childOption(RakChannelOption.RAK_ORDERING_CHANNELS, 1) + .handler(new OfflineServerChannelInitializer(this, server)) .childHandler(new SculkBedrockServerInitializer(this, this.server)); if (allowEpoll) { bootstrap.option(UnixChannelOption.SO_REUSEPORT, true); @@ -107,7 +116,7 @@ public BedrockInterface(Server server) throws Exception { .bind(address, port) .syncUninterruptibly(); if (future.isSuccess()) { - this.serverChannels.add(future.awaitUninterruptibly().channel()); + this.serverChannels.add((RakServerChannel) future.awaitUninterruptibly().channel()); } else { throw new IllegalStateException("Can not start server on " + address, future.cause()); } @@ -116,16 +125,32 @@ public BedrockInterface(Server server) throws Exception { @Override public void blockAddress(InetAddress address) { - + for (RakServerChannel channel : this.serverChannels) { + channel.pipeline().get(RakServerRateLimiter.class).blockAddress(address, 3, TimeUnit.MILLISECONDS); + } } @Override public void blockAddress(InetAddress address, long timeout, TimeUnit unit) { + for(RakServerChannel channel : this.serverChannels) { + channel.pipeline().get(RakServerRateLimiter.class).blockAddress(address, timeout, unit); + } + } + public boolean isAddressBlocked(InetAddress address) { + for(RakServerChannel channel : this.serverChannels) { + if(channel.pipeline().get(RakServerRateLimiter.class).isAddressBlocked(address)) { + return true; + } + } + return false; } @Override public void unblockAddress(InetAddress address) { + for(RakServerChannel channel : this.serverChannels) { + channel.pipeline().get(RakServerRateLimiter.class).unblockAddress(address); + } } @@ -136,7 +161,9 @@ public void setNetwork(Network network) { @Override public void sendRawPacket(InetSocketAddress socketAddress, ByteBuf payload) { - + for(RakServerChannel channel : this.serverChannels) { + channel.write(payload); + } } @Override diff --git a/src/main/java/org/sculk/network/channel/OfflineServerChannelInitializer.java b/src/main/java/org/sculk/network/channel/OfflineServerChannelInitializer.java index 010e627..5d4fc94 100644 --- a/src/main/java/org/sculk/network/channel/OfflineServerChannelInitializer.java +++ b/src/main/java/org/sculk/network/channel/OfflineServerChannelInitializer.java @@ -2,17 +2,21 @@ import org.cloudburstmc.netty.handler.codec.raknet.common.UnconnectedPongEncoder; import org.cloudburstmc.netty.handler.codec.raknet.server.RakServerOfflineHandler; +import org.sculk.Server; import org.sculk.network.BedrockInterface; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import org.sculk.network.channel.server.RakNetPingHandler; import org.sculk.network.channel.server.ServerDatagramHandler; +import org.sculk.network.query.QueryHandler; public class OfflineServerChannelInitializer extends ChannelInitializer { private final BedrockInterface bedrockInterface; + private final Server server; - public OfflineServerChannelInitializer(BedrockInterface proxy) { + public OfflineServerChannelInitializer(BedrockInterface proxy, Server server) { this.bedrockInterface = proxy; + this.server = server; } @Override @@ -20,8 +24,8 @@ protected void initChannel(Channel channel) throws Exception { channel.pipeline() .addFirst(ServerDatagramHandler.NAME, new ServerDatagramHandler(this.bedrockInterface)) .addAfter(RakServerOfflineHandler.NAME, RakNetPingHandler.NAME, new RakNetPingHandler(this.bedrockInterface)); - if (this.bedrockInterface.getBedrockPong() != null) { - //channel.pipeline().addAfter(UnconnectedPongEncoder.NAME, QueryHandler.NAME, this.bedrockInterface.getQueryHandler()); + if (this.server.isQuery()) { + channel.pipeline().addAfter(UnconnectedPongEncoder.NAME, QueryHandler.NAME, this.bedrockInterface.getQueryHandler()); } } } diff --git a/src/main/java/org/sculk/network/channel/server/PacketQueueHandler.java b/src/main/java/org/sculk/network/channel/server/PacketQueueHandler.java deleted file mode 100644 index aef2427..0000000 --- a/src/main/java/org/sculk/network/channel/server/PacketQueueHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.sculk.network.channel.server; - -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.util.internal.PlatformDependent; -import lombok.extern.log4j.Log4j2; -import org.cloudburstmc.protocol.bedrock.netty.BedrockBatchWrapper; -import org.sculk.network.handler.SculkPacketHandler; - -import java.util.Queue; - -@Log4j2 -public class PacketQueueHandler extends ChannelDuplexHandler { - public static final String NAME = "packet-queue-handler"; - private static final int MAX_BATCHES = 256; - private static final int MAX_PACKETS = 8000; - - private final SculkPacketHandler session; - - private int packetCounter = 0; - private final Queue queue = PlatformDependent.newMpscQueue(MAX_BATCHES); - - private volatile boolean finished; - - public PacketQueueHandler(SculkPacketHandler session) { - this.session = session; - } - - private void finish(ChannelHandlerContext ctx, boolean send) { - if (this.finished) { - return; - } - this.finished = true; - - if (ctx.pipeline().get(NAME) == this) { - ctx.pipeline().remove(this); - } - - BedrockBatchWrapper batch; - while ((batch = this.queue.poll()) != null) { - if (send) { - ctx.write(batch); - } else { - batch.release(); - } - } - - if (send) { - ctx.flush(); - } - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - this.finish(ctx, false); - } - - @Override - public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { - this.finish(ctx, ctx.channel().isActive()); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - /*if (this.finished || !(msg instanceof BedrockBatchWrapper batch) || batch.hasFlag(BatchFlags.SKIP_QUEUE)) { - ctx.write(msg, promise); - return; - } - - if (this.queue.offer(batch) && this.packetCounter < MAX_PACKETS) { - this.packetCounter += batch.getPackets().size(); - } else { - log.warn("[{}] has reached maximum transfer queue capacity: batches={} packets={}", this.session.getSocketAddress(), this.queue.size(), this.packetCounter); - this.finish(ctx, false); - this.session.disconnect("Transfer queue got too large"); - - NetworkMetrics metrics = ctx.channel().attr(NetworkMetrics.ATTRIBUTE).get(); - if (metrics != null) { - metrics.packetQueueTooLarge(); - } - }*/ - } -} diff --git a/src/main/java/org/sculk/network/channel/server/ServerDatagramHandler.java b/src/main/java/org/sculk/network/channel/server/ServerDatagramHandler.java index 9060bea..de54a29 100644 --- a/src/main/java/org/sculk/network/channel/server/ServerDatagramHandler.java +++ b/src/main/java/org/sculk/network/channel/server/ServerDatagramHandler.java @@ -32,15 +32,10 @@ public ServerDatagramHandler(BedrockInterface securityManager) { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - /* if (msg instanceof DatagramPacket packet && this.manager.isAddressBlocked(packet.sender().getAddress())) { - NetworkMetrics metrics = Server.getInstance().getNetworkMetrics(); - if (metrics != null) { - metrics.droppedBytes(packet.content().readableBytes()); - } packet.release(); - return; // drop any incoming messages + return; } - ctx.fireChannelRead(msg);*/ + ctx.fireChannelRead(msg); } } diff --git a/src/main/java/org/sculk/network/handler/LoginPacketHandler.java b/src/main/java/org/sculk/network/handler/LoginPacketHandler.java index 60a2c45..0c04cef 100644 --- a/src/main/java/org/sculk/network/handler/LoginPacketHandler.java +++ b/src/main/java/org/sculk/network/handler/LoginPacketHandler.java @@ -48,7 +48,7 @@ public LoginPacketHandler(SculkServerSession session, Consumer @Override public PacketSignal handle(LoginPacket packet) { ClientChainData clientChainData = ClientChainData.read(packet); - if(!clientChainData.isXboxAuthed()) { + if(this.session.getServer().isXboxAuth() && !clientChainData.isXboxAuthed()) { session.disconnect("disconnectionScreen.notAuthenticated"); return PacketSignal.HANDLED; } @@ -72,7 +72,7 @@ public PacketSignal handle(LoginPacket packet) { return PacketSignal.HANDLED; } - //session.setPacketHandler(new ResourcePackHandler(session, server, loginData)); + //session.setPacketHandler(new ResourcePackHandler(session, loginData)); this.playerInfo.accept(clientChainData); loginData.setPreLoginEventTask(new AsyncTask() { diff --git a/src/main/java/org/sculk/network/handler/ResourcePackChunkRequestHandler.java b/src/main/java/org/sculk/network/handler/ResourcePackChunkRequestHandler.java new file mode 100644 index 0000000..13556fb --- /dev/null +++ b/src/main/java/org/sculk/network/handler/ResourcePackChunkRequestHandler.java @@ -0,0 +1,52 @@ +package org.sculk.network.handler; + + +import io.netty.buffer.Unpooled; +import org.cloudburstmc.protocol.bedrock.packet.RefreshEntitlementsPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkDataPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkRequestPacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.Server; +import org.sculk.network.session.SculkServerSession; +import org.sculk.resourcepack.ResourcePack; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ResourcePackChunkRequestHandler extends SculkPacketHandler{ + + public ResourcePackChunkRequestHandler(SculkServerSession session) { + super(session); + } + + @Override + public PacketSignal handle(ResourcePackChunkRequestPacket packet) { + + ResourcePack resourcePack = Server.getInstance().getResourcePackManager().getResourcePack(packet.getPackId()); + + if (resourcePack != null) { + int size = 1048576; + ResourcePackChunkDataPacket resourcePackChunkDataPacket = new ResourcePackChunkDataPacket(); + resourcePackChunkDataPacket.setPackId(resourcePack.getUuid()); + resourcePackChunkDataPacket.setChunkIndex(packet.getChunkIndex()); + resourcePackChunkDataPacket.setProgress((long) (size * packet.getChunkIndex())); + resourcePackChunkDataPacket.setData(Unpooled.wrappedBuffer(resourcePack.getChunk((int) resourcePackChunkDataPacket.getProgress(), size))); + + session.sendPacket(resourcePackChunkDataPacket); + } + + return super.handle(packet); + } +} diff --git a/src/main/java/org/sculk/network/handler/ResourcePackHandler.java b/src/main/java/org/sculk/network/handler/ResourcePackHandler.java index 993a289..9b0a318 100644 --- a/src/main/java/org/sculk/network/handler/ResourcePackHandler.java +++ b/src/main/java/org/sculk/network/handler/ResourcePackHandler.java @@ -1,10 +1,13 @@ package org.sculk.network.handler; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; +import io.netty.buffer.Unpooled; +import org.cloudburstmc.protocol.bedrock.data.ResourcePackType; +import org.cloudburstmc.protocol.bedrock.packet.*; import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.Server; +import org.sculk.config.ServerPropertiesKeys; import org.sculk.network.session.SculkServerSession; +import org.sculk.resourcepack.ResourcePack; import java.util.ArrayList; import java.util.UUID; @@ -36,8 +39,25 @@ public ResourcePackHandler(SculkServerSession session, Consumer complete @Override public void setUp() { ResourcePacksInfoPacket resourcePacksInfoPacket = new ResourcePacksInfoPacket(); + Server.getInstance().getResourcePackManager().getResourcePacks().forEach(resourcePack -> { + resourcePacksInfoPacket.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry( + resourcePack.getUuid(), + resourcePack.getVersion(), + resourcePack.getSize(), + "", + "", + resourcePack.getUuid().toString(), + false, + false, + false, + "" + )); + }); resourcePacksInfoPacket.setWorldTemplateId(UUID.randomUUID()); resourcePacksInfoPacket.setWorldTemplateVersion(""); + resourcePacksInfoPacket.setForcedToAccept(Server.getInstance().getProperties().get(ServerPropertiesKeys.FORCE_RESOURCE_PACKS, false)); + resourcePacksInfoPacket.setHasAddonPacks(false); + session.sendPacket(resourcePacksInfoPacket); } @@ -49,9 +69,24 @@ public PacketSignal handle(ResourcePackClientResponsePacket packet) { break; } case SEND_PACKS -> { + + for (ResourcePack resourcePack : Server.getInstance().getResourcePackManager().getResourcePacks()) { + long maxChunkSize = 1048576; + + ResourcePackDataInfoPacket resourcePackDataInfoPacket = new ResourcePackDataInfoPacket(); + resourcePackDataInfoPacket.setPackId(resourcePack.getUuid()); + resourcePackDataInfoPacket.setMaxChunkSize(maxChunkSize); + resourcePackDataInfoPacket.setChunkCount(resourcePack.getSize() / maxChunkSize); + resourcePackDataInfoPacket.setCompressedPackSize(resourcePack.getSize()); + resourcePackDataInfoPacket.setHash(resourcePack.getHash()); + resourcePackDataInfoPacket.setType(ResourcePackType.RESOURCES); + + session.sendPacket(resourcePackDataInfoPacket); + } break; } case HAVE_ALL_PACKS -> { + /** ArrayList entries = new ArrayList<>(); ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); @@ -61,7 +96,27 @@ public PacketSignal handle(ResourcePackClientResponsePacket packet) { stackPacket.setGameVersion("*"); stackPacket.setHasEditorPacks(false); //todo: create ResourcePackManager return packs - session.sendPacket(stackPacket); + session.sendPacket(stackPacket);**/ + + ResourcePackStackPacket resourcePackStackPacket = new ResourcePackStackPacket(); + resourcePackStackPacket.setExperimentsPreviouslyToggled(false); + resourcePackStackPacket.setHasEditorPacks(false); + resourcePackStackPacket.setForcedToAccept(Server.getInstance().getProperties().get(ServerPropertiesKeys.FORCE_RESOURCE_PACKS, false)); + + ArrayList entries = new ArrayList<>(); + + for (ResourcePack resourcePack : Server.getInstance().getResourcePackManager().getResourcePacks()) { + entries.add(new ResourcePackStackPacket.Entry( + resourcePack.getUuid().toString(), + resourcePack.getVersion(), + "" + )); + } + + resourcePackStackPacket.getResourcePacks().addAll(entries); + resourcePackStackPacket.setGameVersion("*"); + + session.sendPacket(resourcePackStackPacket); break; } case COMPLETED -> { diff --git a/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java b/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java index ce72f51..61cc5cc 100644 --- a/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java +++ b/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java @@ -5,12 +5,8 @@ import org.cloudburstmc.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket; import org.cloudburstmc.protocol.common.PacketSignal; import org.sculk.Server; -import org.sculk.event.player.PlayerJoinEvent; -import org.sculk.lang.LanguageKeys; import org.sculk.network.session.SculkServerSession; -import org.sculk.utils.TextFormat; -import java.util.List; import java.util.function.Consumer; /* @@ -45,18 +41,7 @@ public PacketSignal handlePacket(BedrockPacket packet) { @Override public PacketSignal handle(SetLocalPlayerAsInitializedPacket packet) { this.responseCallback.accept(null); - Server.getInstance().getLogger().info("§b" + session.getPlayer().getName() + "[/" + session.getPlayerInfo().getServerAddress() + "] logged in with uuid " + session.getPlayer().getUniqueId()); - - PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(session.getPlayer(), session.getPlayer().getLanguage().translate(LanguageKeys.MINECRAFT_PLAYER_JOIN, List.of(session.getPlayer().getName()))); - playerJoinEvent.call(); - String joinMessage = playerJoinEvent.getJoinMessage(); - if (joinMessage.isEmpty()) { - Server.getInstance().broadcastMessage(joinMessage); - } else { - String defaultJoinMessage = session.getPlayer().getLanguage().translate(LanguageKeys.MINECRAFT_PLAYER_JOIN, List.of(session.getPlayer().getName())); - System.out.println(defaultJoinMessage); - Server.getInstance().broadcastMessage(TextFormat.YELLOW + defaultJoinMessage + TextFormat.RESET); - } + Server.getInstance().getLogger().info("§b" + session.getPlayer().getName() + "[/" + session.getSocketAddress() + "]§r logged in with uuid §b" + session.getPlayer().getUniqueId()); return PacketSignal.HANDLED; } } diff --git a/src/main/java/org/sculk/network/query/QueryHandler.java b/src/main/java/org/sculk/network/query/QueryHandler.java new file mode 100644 index 0000000..97168a1 --- /dev/null +++ b/src/main/java/org/sculk/network/query/QueryHandler.java @@ -0,0 +1,197 @@ +/* + * Copyright 2022 WaterdogTEAM + * Licensed under the GNU General Public License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sculk.network.query; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import net.jodah.expiringmap.ExpiringMap; +import org.sculk.Server; +import org.sculk.event.server.QueryHandlerEvent; +import org.sculk.player.Player; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +@ChannelHandler.Sharable +public class QueryHandler extends SimpleChannelInboundHandler { + public static final String NAME = "query-handler"; + + public static final ByteBuf QUERY_SIGNATURE = Unpooled.wrappedBuffer(new byte[]{(byte) 0xFE, (byte) 0xFD}); + public static final byte[] LONG_RESPONSE_PADDING_TOP = new byte[]{115, 112, 108, 105, 116, 110, 117, 109, 0, -128, 0}; + public static final byte[] LONG_RESPONSE_PADDING_BOTTOM = new byte[]{1, 112, 108, 97, 121, 101, 114, 95, 0, 0}; + + public static final short PACKET_HANDSHAKE = 0x09; + public static final short PACKET_STATISTICS = 0x00; + private static final String GAME_ID = "MINECRAFTPE"; + + private final Server server; + + private final ExpiringMap querySessions = ExpiringMap.builder() + .expirationListener(this::onQueryExpired) + .expiration(60, TimeUnit.SECONDS) + .build(); + + public QueryHandler(Server proxy) { + this.server = proxy; + } + + public void onQueryExpired(InetAddress address, QuerySession session) { + this.server.getLogger().warn("Pending query from " + address + " has expired: token=" + session.token); + } + + private void writeInt(ByteBuf buf, int i) { + this.writeString(buf, Integer.toString(i)); + } + + private void writeString(ByteBuf buf, String string) { + for (char c : string.toCharArray()) { + buf.writeByte(c); + } + buf.writeByte(0); + } + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + if (!super.acceptInboundMessage(msg)) { + return false; + } + + DatagramPacket packet = (DatagramPacket) msg; + if (!packet.content().isReadable(7)) { + return false; + } + + int startIndex = packet.content().readerIndex(); + try { + ByteBuf magic = packet.content().readSlice(2); + return ByteBufUtil.equals(magic, QUERY_SIGNATURE); + } finally { + packet.content().readerIndex(startIndex); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) { + try { + ByteBuf buf = packet.content(); + this.onQuery(packet.sender(), buf.skipBytes(2), ctx, (InetSocketAddress) ctx.channel().localAddress()); + } catch (Exception e) { + this.server.getLogger().error("Can not handle query packet!", e); + } + } + + public void onQuery(InetSocketAddress address, ByteBuf packet, ChannelHandlerContext ctx, InetSocketAddress bindAddress) { + if (address.getAddress() == null) { + // We got unresolved address + return; + } + short packetId = packet.readUnsignedByte(); + int sessionId = packet.readInt(); + + if (packetId == PACKET_HANDSHAKE) { + ByteBuf reply = ctx.alloc().ioBuffer(10); + reply.writeByte(PACKET_HANDSHAKE); + reply.writeInt(sessionId); + + int token = ThreadLocalRandom.current().nextInt(); + this.querySessions.put(address.getAddress(), new QuerySession(token, System.currentTimeMillis())); + this.writeInt(reply, token); + ctx.writeAndFlush(new DatagramPacket(reply, address)); + return; + } + + if (packetId == PACKET_STATISTICS && packet.isReadable(4)) { + QuerySession session = this.querySessions.remove(address.getAddress()); + int token = packet.readInt(); + if (session == null || session.token != token) { + return; + } + + ByteBuf reply = ctx.alloc().ioBuffer(64); + reply.writeByte(PACKET_STATISTICS); + reply.writeInt(sessionId); + + this.writeData(address, packet.readableBytes() == 8, reply, bindAddress); + ctx.writeAndFlush(new DatagramPacket(reply, address)); + } + } + + private void writeData(InetSocketAddress address, boolean simple, ByteBuf buf, InetSocketAddress bindAddress) { + QueryHandlerEvent event = new QueryHandlerEvent( + this.server.getMotd(), + "SMP", + "MCPE", + "", + this.server.getOnlinePlayers().values(), + this.server.getMaxPlayers(), + "WaterdogPE", + address + ); + event.call(); + + if (simple) { + this.writeString(buf, event.getMotd()); + this.writeString(buf, event.getSmp()); + this.writeString(buf, event.getS()); + this.writeString(buf, Integer.toString(event.getValues().size())); + this.writeString(buf, Integer.toString(event.getMaxPlayers())); + buf.writeShortLE(bindAddress.getPort()); + this.writeString(buf, bindAddress.getHostName()); + return; + } + + Map map = new Object2ObjectArrayMap<>(); + map.put("hostname", event.getMotd()); + map.put("gametype", event.getSmp()); + map.put("map", event.getS()); + map.put("numplayers", Integer.toString(event.getValues().size())); + map.put("maxplayers", Integer.toString(event.getMaxPlayers())); + map.put("hostport", Integer.toString(bindAddress.getPort())); + map.put("hostip", bindAddress.getHostName()); + map.put("game_id", GAME_ID); + map.put("version", event.getMcpe()); + map.put("plugins", ""); // Do not list plugins + map.put("whitelist", "off"); + + buf.writeBytes(LONG_RESPONSE_PADDING_TOP); + map.forEach((key, value) -> { + this.writeString(buf, key); + this.writeString(buf, value); + }); + buf.writeByte(0); + buf.writeBytes(LONG_RESPONSE_PADDING_BOTTOM); + + if (!event.getValues().isEmpty()) { + for (Player player : event.getValues()) { + this.writeString(buf, player.getName()); + } + } + buf.writeByte(0); + } + + private record QuerySession(int token, long time) { + + } +} diff --git a/src/main/java/org/sculk/network/server/SculkBedrockServerInitializer.java b/src/main/java/org/sculk/network/server/SculkBedrockServerInitializer.java index f7936be..81af920 100644 --- a/src/main/java/org/sculk/network/server/SculkBedrockServerInitializer.java +++ b/src/main/java/org/sculk/network/server/SculkBedrockServerInitializer.java @@ -31,6 +31,7 @@ public SculkBedrockServerInitializer(BedrockInterface bedrockInterface, Server s protected void initSession(BedrockServerSession bedrockServerSession) { bedrockServerSession.setCodec(ProtocolInfo.CODEC); bedrockServerSession.setLogging(false); + System.out.println("Session Connected!"); } @Override diff --git a/src/main/java/org/sculk/network/session/SculkPeer.java b/src/main/java/org/sculk/network/session/SculkPeer.java index 7fcfb5d..c3518bf 100644 --- a/src/main/java/org/sculk/network/session/SculkPeer.java +++ b/src/main/java/org/sculk/network/session/SculkPeer.java @@ -1,8 +1,10 @@ package org.sculk.network.session; +import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import org.cloudburstmc.protocol.bedrock.BedrockPeer; import org.cloudburstmc.protocol.bedrock.BedrockSessionFactory; +import org.cloudburstmc.protocol.bedrock.netty.BedrockPacketWrapper; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import java.util.List; diff --git a/src/main/java/org/sculk/network/session/SculkServerSession.java b/src/main/java/org/sculk/network/session/SculkServerSession.java index 10ffba5..0882cda 100644 --- a/src/main/java/org/sculk/network/session/SculkServerSession.java +++ b/src/main/java/org/sculk/network/session/SculkServerSession.java @@ -5,16 +5,16 @@ import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; -import org.cloudburstmc.protocol.bedrock.BedrockPeer; import org.cloudburstmc.protocol.bedrock.BedrockServerSession; import org.cloudburstmc.protocol.bedrock.packet.*; +import org.sculk.Server; +import org.sculk.entity.HumanEntity; import org.sculk.lang.Translatable; +import org.sculk.network.BedrockInterface; import org.sculk.network.broadcaster.EntityEventBroadcaster; import org.sculk.network.broadcaster.PacketBroadcaster; -import org.sculk.player.Player; -import org.sculk.Server; -import org.sculk.network.BedrockInterface; import org.sculk.network.handler.*; +import org.sculk.player.Player; import org.sculk.player.client.ClientChainData; import org.sculk.player.text.RawTextBuilder; import org.sculk.utils.SkinUtils; @@ -57,7 +57,6 @@ public SculkServerSession(BedrockInterface bedrockInterface, PacketBroadcaster b this.broadcaster = broadcaster; this.entityEventBroadcaster = entityEventBroadcaster; this.bedrockInterface = bedrockInterface; - this.setPacketHandler(new SessionStartPacketHandler(this, this::onSessionStartSuccess)); } @@ -119,6 +118,11 @@ private void createPlayer(Object e) { } private void onPlayerCreated(Player player) { + if (!this.isConnected()) + return; + if (!(this.getServer().addOnlinePlayer(player))){ + return; + } this.player = player; this.setPacketHandler(new PreSpawnPacketHandler(this, player)); this.notifyTerrainReady(null); @@ -134,54 +138,55 @@ private void notifyTerrainReady(Object e) { private void onClientSpawnResponse(Object e) { this.setPacketHandler(new InGamePacketHandler(this.getPlayer(),this)); + assert this.player != null; + this.player.doFirstSpawn(); } - - - public void syncPlayerList(Map players) { - PlayerListPacket packet = new PlayerListPacket(); - packet.setAction(PlayerListPacket.Action.ADD); - packet.getEntries().addAll(players.values().stream().map(p -> { - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(p.getUniqueId()); - String xuid = p.getXuid(); - entry.setEntityId(p.getEntityId()); - entry.setName(p.getName()); + public PlayerListPacket.Entry createPlayerEntry(HumanEntity player, boolean info) { + String xuid = null; + PlayerListPacket.Entry entry = new PlayerListPacket.Entry(player.getUniqueId()); + if (info) { + if (player instanceof Player) { + xuid = ((Player) player).getXuid(); + } + entry.setEntityId(player.getEntityId()); + entry.setName(player.getName()); entry.setXuid(xuid != null ? xuid : ""); - entry.setSkin(SkinUtils.toSerialized(p.getSkin())); + entry.setSkin(SkinUtils.toSerialized(player.getSkin())); entry.setPlatformChatId(""); entry.setTrustedSkin(true); entry.setBuildPlatform(-1); entry.setHost(false); entry.setSubClient(false); - return entry; - }).toList()); - this.sendPacket(packet); + } + return entry; } + private PlayerListPacket.Entry createPlayerEntry(HumanEntity player) { + return createPlayerEntry(player, true); + } - public void onPlayerAdded(Player p){ + public void syncPlayerList(Map players) { PlayerListPacket packet = new PlayerListPacket(); packet.setAction(PlayerListPacket.Action.ADD); - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(p.getUniqueId()); - String xuid = p.getXuid(); - entry.setEntityId(p.getEntityId()); - entry.setName(p.getName()); - entry.setXuid(xuid != null ? xuid : ""); - entry.setSkin(SkinUtils.toSerialized(p.getSkin())); - entry.setPlatformChatId(""); - entry.setTrustedSkin(true); - entry.setBuildPlatform(-1); - entry.setHost(false); - entry.setSubClient(false); - packet.getEntries().add(entry); + packet.getEntries().addAll(players.values().stream().map(this::createPlayerEntry).toList()); + this.sendPacket(packet); + } + + public void onPlayerAdded(Player p){ + if(!p.equals(this.player)) { + PlayerListPacket packet = new PlayerListPacket(); + packet.setAction(PlayerListPacket.Action.ADD); + packet.getEntries().add(this.createPlayerEntry(p)); + this.sendPacket(packet); + } } public void onPlayerRemoved(Player p) { if(!p.equals(this.player)){ PlayerListPacket packet = new PlayerListPacket(); - packet.setAction(PlayerListPacket.Action.REMOVE); - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(p.getUniqueId()); - packet.getEntries().add(entry); + packet.getEntries().add(this.createPlayerEntry(p, false)); + this.sendPacket(packet); } } @@ -201,11 +206,12 @@ public void onChatMessage(String message) { } public void onChatMessage(RawTextBuilder textBuilder) { + assert this.player != null; TextPacket packet = new TextPacket(); packet.setXuid(""); packet.setSourceName(""); packet.setType(TextPacket.Type.JSON); - packet.setMessage(new Gson().toJson(textBuilder.build())); + packet.setMessage(new Gson().toJson(textBuilder.build(this.player.getLanguage()))); this.sendPacket(packet); } diff --git a/src/main/java/org/sculk/player/Player.java b/src/main/java/org/sculk/player/Player.java index 203ebb7..457081e 100644 --- a/src/main/java/org/sculk/player/Player.java +++ b/src/main/java/org/sculk/player/Player.java @@ -3,16 +3,17 @@ import co.aikar.timings.Timings; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.DisconnectFailReason; -import org.cloudburstmc.protocol.bedrock.data.command.*; +import org.cloudburstmc.protocol.bedrock.data.command.CommandData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin; import org.cloudburstmc.protocol.bedrock.packet.*; -import org.cloudburstmc.protocol.common.PacketSignal; import org.sculk.Server; import org.sculk.command.Command; import org.sculk.command.CommandSender; @@ -23,19 +24,19 @@ import org.sculk.entity.data.SyncedEntityData; import org.sculk.event.player.PlayerChangeSkinEvent; import org.sculk.event.player.PlayerChatEvent; +import org.sculk.event.player.PlayerJoinEvent; import org.sculk.form.Form; import org.sculk.lang.Language; +import org.sculk.lang.LanguageKeys; import org.sculk.lang.Translatable; -import org.sculk.network.handler.PlayerSkinHandler; import org.sculk.network.session.SculkServerSession; import org.sculk.player.chat.StandardChatFormatter; import org.sculk.player.client.ClientChainData; import org.sculk.player.client.LoginChainData; import org.sculk.player.skin.Skin; import org.sculk.player.text.RawTextBuilder; -import org.sculk.utils.SkinUtils; +import org.sculk.utils.TextFormat; -import java.lang.ref.WeakReference; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -57,10 +58,8 @@ */ public class Player extends HumanEntity implements PlayerInterface, CommandSender { - public static final Player[] EMPTY_ARRAY = new Player[0]; @Getter private final SculkServerSession networkSession; - private final SyncedEntityData data = new SyncedEntityData(this); private final LoginChainData loginChainData; private final AtomicInteger formId; @@ -73,14 +72,16 @@ public class Player extends HumanEntity implements PlayerInterface, CommandSende @Getter private String xuid; protected AtomicBoolean connected = new AtomicBoolean(true); + private WeakHashMap hiddenPlayers = new WeakHashMap<>(); protected int messageCounter = 2; protected int MAX_CHAT_CHAR_LENGTH = 512; protected int MAX_CHAT_BYTES_LENGTH = MAX_CHAT_CHAR_LENGTH * 2; - public Player(SculkServerSession networkSession, ClientChainData data) { + public Player(Server server, SculkServerSession networkSession, ClientChainData data, NbtMap nbt) { + super(nbt); this.networkSession = networkSession; - this.server = networkSession.getServer(); + this.server = server; this.loginChainData = data; this.formId = new AtomicInteger(0); @@ -91,15 +92,23 @@ public Player(SculkServerSession networkSession, ClientChainData data) { this.xuid = data.getXUID(); this.setSkin(data.getSkin()); this.uuid = data.getClientUUID(); - this.getServer().addOnlinePlayer(this); - this.spawn(this); - - this.initEntity(); } @Override - public void initEntity() { - super.initEntity(); + protected void initEntity(NbtMap nbt) { + super.initEntity(nbt); + this.spawn(this); + } + + public void doFirstSpawn() { + + PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(this, this.getLanguage().translate(LanguageKeys.MINECRAFT_PLAYER_JOIN, List.of(this.getName()))); + playerJoinEvent.call(); + String joinMessage = playerJoinEvent.getJoinMessage(); + if (!joinMessage.isEmpty()) { + String defaultJoinMessage = this.getLanguage().translate(LanguageKeys.MINECRAFT_PLAYER_JOIN, List.of(this.getName())); + Server.getInstance().broadcastMessage(TextFormat.YELLOW + defaultJoinMessage + TextFormat.RESET); + } } /** @@ -132,11 +141,6 @@ public void sendCommandsData() { sendDataPacket(availableCommandsPacket); } - public void updateFlags() { - this.data.setFlags(EntityFlag.BREATHING, true); - this.data.updateFlag(); - } - public void kick(String message) { DisconnectPacket packet = new DisconnectPacket(); packet.setKickMessage(message); @@ -177,6 +181,10 @@ public boolean isOnline() { return this.connected.get(); } + public boolean isConnected() { + return this.networkSession.isConnected() && this.isConnected(); + } + @Override public boolean isBanned() { return false; @@ -197,6 +205,10 @@ public void setWhitelisted(boolean value) { return; } + public boolean isOp() { + return this.getServer().getOperators().isOperator(this.getName()); + } + public void sendDataPacket(BedrockPacket packet) { sendPacketInternal(packet); } @@ -271,10 +283,37 @@ public Form getForm(int id) { return this.forms.remove(id); } + @Override - public void onUpdate() { + public boolean onUpdate(long currentTick) { this.messageCounter = 2; - super.onUpdate(); + return super.onUpdate(currentTick); + } + + @Override + public void spawnTo(@NonNull Player player) { + if (this.isAlive() && player.isAlive()) { + super.spawnTo(player); + } + } + + public boolean canSee(@NonNull Player player) { + return !this.hiddenPlayers.containsKey(player); + } + + public void hidePlayer(@NonNull Player player) { + if (this == player) + return; + this.hiddenPlayers.put(player, true); + player.despawnFrom(this); + } + + public void showPlayer(@NonNull Player player) { + if (this == player) + return; + this.hiddenPlayers.put(player, false); + if (player.isConnected()) + player.spawnTo(this); } public void onChat(String message) { @@ -289,10 +328,10 @@ public void onChat(String message) { } else { PlayerChatEvent playerChatEvent = new PlayerChatEvent(this, message, new StandardChatFormatter()); playerChatEvent.call(); - if (!playerChatEvent.isCancelled()) { + if (!playerChatEvent.isCancelled() && !message.isEmpty()) { // TODO please change for use this.messageCount String messageFormat = playerChatEvent.getChatFormatter().format(this.getName(), message); - this.getNetworkSession().onChatMessage(messageFormat); + Server.getInstance().broadcastMessage(messageFormat); this.getServer().getLogger().info(messageFormat); } } diff --git a/src/main/java/org/sculk/player/text/IJsonText.java b/src/main/java/org/sculk/player/text/IJsonText.java index aa3b255..20dea62 100644 --- a/src/main/java/org/sculk/player/text/IJsonText.java +++ b/src/main/java/org/sculk/player/text/IJsonText.java @@ -1,5 +1,7 @@ package org.sculk.player.text; +import org.sculk.lang.Language; + import java.util.function.Function; /* @@ -22,4 +24,6 @@ public interface IJsonText extends Cloneable{ String getName(); Object build(); + + Object build(Language lang); } \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/RawTextBuilder.java b/src/main/java/org/sculk/player/text/RawTextBuilder.java index 28dc80f..734e38a 100644 --- a/src/main/java/org/sculk/player/text/RawTextBuilder.java +++ b/src/main/java/org/sculk/player/text/RawTextBuilder.java @@ -1,5 +1,8 @@ package org.sculk.player.text; +import lombok.Getter; +import org.sculk.lang.Language; + import java.util.*; /* @@ -19,12 +22,33 @@ */ public class RawTextBuilder implements IJsonText{ - public List build; + @Getter + private final List build; public RawTextBuilder() { build = new ArrayList<>(); } + public static RawTextBuilder create() { + return new RawTextBuilder(); + } + + public static RawTextBuilder create(List text) { + RawTextBuilder builder = new RawTextBuilder(); + for (IJsonText iJsonText : text) { + builder.add(iJsonText); + } + return builder; + } + + public static RawTextBuilder create(IJsonText... text) { + RawTextBuilder builder = new RawTextBuilder(); + for (IJsonText iJsonText : text) { + builder.add(iJsonText); + } + return builder; + } + @Override public String getName() { return "rawtext"; @@ -43,11 +67,9 @@ public Object build() { } @Override - public String toString() { - StringBuilder text = new StringBuilder(); - for (IJsonText jsonText: build){ - text.append(jsonText.toString()); - } - return text.toString(); + public Object build(Language lang) { + HashMap> map = new HashMap<>(); + map.put(this.getName(), this.build.stream().map(iJsonText -> iJsonText.build(lang)).toList()); + return map; } } \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/ScoreBuilder.java b/src/main/java/org/sculk/player/text/ScoreBuilder.java index c9fd705..3334724 100644 --- a/src/main/java/org/sculk/player/text/ScoreBuilder.java +++ b/src/main/java/org/sculk/player/text/ScoreBuilder.java @@ -1,5 +1,8 @@ package org.sculk.player.text; +import lombok.Builder; +import org.sculk.lang.Language; + import java.util.HashMap; /* @@ -37,6 +40,15 @@ public Object build() { score.put("score", data); return score; } + @Override + public Object build(Language lang) { + HashMap> score = new HashMap<>(); + HashMap data = new HashMap<>(); + data.put("name", this.name); + data.put("objective", this.objective); + score.put("score", data); + return score; + } @Override public String toString() { diff --git a/src/main/java/org/sculk/player/text/TextBuilder.java b/src/main/java/org/sculk/player/text/TextBuilder.java index 921dbab..9e96e94 100644 --- a/src/main/java/org/sculk/player/text/TextBuilder.java +++ b/src/main/java/org/sculk/player/text/TextBuilder.java @@ -1,6 +1,8 @@ package org.sculk.player.text; +import lombok.Getter; import lombok.NonNull; +import org.sculk.lang.Language; import java.util.HashMap; @@ -19,6 +21,7 @@ * @author: SculkTeams * @link: http://www.sculkmp.org/ */ +@Getter public class TextBuilder implements IJsonText { private @NonNull String text; @@ -43,7 +46,10 @@ public Object build() { } @Override - public String toString() { - return this.text; + public Object build(Language lang) { + String baseText; + HashMap map = new HashMap<>(); + map.put(this.getName(), text); + return map; } } \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/TranslaterBuilder.java b/src/main/java/org/sculk/player/text/TranslaterBuilder.java index e36b166..2f428d2 100644 --- a/src/main/java/org/sculk/player/text/TranslaterBuilder.java +++ b/src/main/java/org/sculk/player/text/TranslaterBuilder.java @@ -1,5 +1,9 @@ package org.sculk.player.text; +import lombok.Getter; +import org.sculk.lang.Language; + +import javax.annotation.Nullable; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -19,52 +23,26 @@ * @author: SculkTeams * @link: http://www.sculkmp.org/ */ -public class TranslaterBuilder implements IJsonText { +public class TranslaterBuilder implements IJsonText, Cloneable { - public String translate; - private Object with; + @Getter + private String translate; + @Getter + private V with; private static final Pattern PATTERN_STRING = Pattern.compile("%%(s)"); private static final Pattern PATTERN_INDEX = Pattern.compile("%%(\\d+)"); public TranslaterBuilder() { - with = null; - + this.translate = null; } - public TranslaterBuilder setTranslate(String translate) { + public TranslaterBuilder setTranslate(String translate) { this.translate = translate; return this; } - public TranslaterBuilder addWith(String data) { - if (this.with == null) - this.with = Arrays.asList(data); - else if (this.with instanceof ArrayList) { - ArrayList _with = (ArrayList) this.with; - _with.add(data); - } - return this; - } - - public TranslaterBuilder addWith(List data) { - if (this.with == null) - this.with = data; - else if (this.with instanceof ArrayList) { - ArrayList _with = new ArrayList(); - ((ArrayList)this.with).addAll(data); - } - return this; - } - - public TranslaterBuilder setWith(RawTextBuilder text) { - if (this.with == null) - this.with = text; - return this; - } - - public TranslaterBuilder setWith(List data) { - if (this.with == null) - this.with = new ArrayList<>(data); + public TranslaterBuilder setWith(V data) { + this.with = data; return this; } @@ -77,7 +55,7 @@ public String getName() { @Override public Object build() { HashMap map = new HashMap<>(); - map.put(this.getName(), this.translate); + map.put(this.getName(), Language.reformatConfigToMinecraft(this.translate)); if (this.with != null) { Object with = this.with; @@ -89,71 +67,33 @@ public Object build() { return map; } - @Override - public String toString() { - String data = translate; - if (this.with != null) { - List clone = getCloneOfWith(); - - if (clone != null) { - data = replaceStringPatterns(data, clone.iterator()); - data = replaceIndexedPatterns(data, clone); - } - } - return data; - } - - private List getCloneOfWith() { - if (this.with instanceof ArrayList) { - return new ArrayList<>((ArrayList) this.with); - } else if (this.with instanceof RawTextBuilder) { - return new ArrayList<>(((RawTextBuilder) this.with).build); - } - return null; - } - private String replaceIndexedPatterns(String data, List clone) { - Matcher matcher = PATTERN_INDEX.matcher(data); - StringBuilder result = new StringBuilder(); - int lastEnd = 0; + @Override + public Object build(Language language) { + String baseText; + HashMap map = new HashMap<>(); - while (matcher.find()) { - result.append(data, lastEnd, matcher.start()); - String group = matcher.group(); - int index; - try { - index = Integer.parseInt(group.substring(2, 3)); - } catch (NumberFormatException e) { - continue; + baseText = language.internalGet(this.translate); + if (baseText == null) + baseText = Language.reformatConfigToMinecraft(this.translate); + map.put(this.getName(), baseText); + if (this.with != null) { + Object with = this.with; + if (with instanceof RawTextBuilder rawTextBuilder) { + with = rawTextBuilder.build(language); } - String replacer = safeGetAtIndex(clone, index - 1); - result.append(replacer); - lastEnd = matcher.end(); - } - result.append(data.substring(lastEnd)); - return result.toString(); - } - - private String replaceStringPatterns(String data, Iterator clone) { - Matcher matcher = PATTERN_STRING.matcher(data); - StringBuilder result = new StringBuilder(); - int lastEnd = 0; - - while (matcher.find()) { - result.append(data, lastEnd, matcher.start()); - String replacer = clone.hasNext() ? clone.next().toString() : ""; - result.append(replacer); - lastEnd = matcher.end(); + map.put("with", with); } - result.append(data.substring(lastEnd)); - return result.toString(); + return map; } - private String safeGetAtIndex(List list, int index) { - if (!list.isEmpty()) { - Object removed = list.get(index); - return removed != null ? removed.toString() : ""; + @Override + public TranslaterBuilder clone() { + try { + // TODO: copy mutable state here, so the clone can't change the internals of the original + return (TranslaterBuilder) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); } - return ""; } } \ No newline at end of file diff --git a/src/main/java/org/sculk/resourcepack/ResourcePack.java b/src/main/java/org/sculk/resourcepack/ResourcePack.java new file mode 100644 index 0000000..abb7fc1 --- /dev/null +++ b/src/main/java/org/sculk/resourcepack/ResourcePack.java @@ -0,0 +1,112 @@ +package org.sculk.resourcepack; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.UUID; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +public class ResourcePack { + private final File file; + private final String name; + private final String uuid; + private final String version; + private final long size; + private final byte[] hash; + private byte[] chunk; + + public ResourcePack(File file, String name, String uuid, String version, long size, byte[] hash, byte[] chunk) { + this.file = file; + this.name = name; + this.uuid = uuid; + this.version = version; + this.size = size; + this.hash = hash; + this.chunk = chunk; + } + + /** + * Returns a chunk of bytes from the resource pack file starting at the specified offset + * and with the specified length. If the remaining data in the file starting from the offset + * is less than the specified length, the size of the chunk will be adjusted accordingly. + * + * @param offset the starting position, in bytes, from where to read the chunk + * @param length the number of bytes to be read starting from the offset + * @return a byte array containing the requested chunk of data + */ + public byte[] getChunk(int offset, int length) { + if (size - offset > length) { + chunk = new byte[length]; + } else { + chunk = new byte[(int) (size - offset)]; + } + try (FileInputStream fileInputStream = new FileInputStream(file)) { + fileInputStream.skip(offset); + fileInputStream.read(chunk); + } catch (IOException e) { + e.printStackTrace(); + } + return chunk; + } + + /** + * Retrieves the name of the resource pack. + * + * @return the name of the resource pack as a String. + */ + public String getName() { + return this.name; + } + + /** + * Retrieves the UUID of the resource pack. + * + * @return the UUID associated with the resource pack. + */ + public UUID getUuid() { + return UUID.fromString(this.uuid); + } + + /** + * Retrieves the version information. + * + * @return the current version as a string. + */ + public String getVersion() { + return this.version; + } + + /** + * Retrieves the size of the resource pack in bytes. + * + * @return the size of the resource pack as a long value. + */ + public long getSize() { + return this.size; + } + + /** + * Returns the hash value of the resource pack. The hash is represented as a byte array, + * commonly used for verifying the integrity of the resource pack contents. + * + * @return a byte array containing the SHA-256 hash of the resource pack. + */ + public byte[] getHash() { + return this.hash; + } +} diff --git a/src/main/java/org/sculk/resourcepack/ResourcePackManager.java b/src/main/java/org/sculk/resourcepack/ResourcePackManager.java new file mode 100644 index 0000000..d33f7f4 --- /dev/null +++ b/src/main/java/org/sculk/resourcepack/ResourcePackManager.java @@ -0,0 +1,107 @@ +package org.sculk.resourcepack; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.common.io.Files; +import org.sculk.Server; +import org.sculk.lang.LanguageKeys; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ResourcePackManager { + + private final Map resourcePacks = new HashMap<>(); + + public void loadResourcePacks() { + File resourcePacksPath = new File(System.getProperty("user.dir") + "/resource_packs"); + if (!resourcePacksPath.exists()) { + resourcePacksPath.mkdirs(); + } + if (!resourcePacksPath.isDirectory()) { + return; + } + Server.getInstance().getLogger().info(Server.getInstance().getLanguage().translate(LanguageKeys.SCULK_SERVER_RESOURCE_PACK_LOADING)); + for (File file : Objects.requireNonNull(resourcePacksPath.listFiles())) { + if (!file.isDirectory()) { + String fileEnding = Files.getFileExtension(file.getName()); + if (fileEnding.equalsIgnoreCase("zip") || fileEnding.equalsIgnoreCase("mcpack")) { + try (ZipFile zipFile = new ZipFile(file)) { + String manifestFileName = "manifest.json"; + ZipEntry manifestEntry = zipFile.getEntry(manifestFileName); + + if (manifestEntry == null) { + manifestEntry = zipFile.stream().filter(zipEntry -> !zipEntry.isDirectory() && zipEntry.getName().toLowerCase().endsWith(manifestFileName)) + .filter(zipEntry -> { + File zipEntryFile = new File(zipEntry.getName()); + if (!zipEntryFile.getName().equalsIgnoreCase(manifestFileName)) { + return false; + } + return zipEntryFile.getParent() == null || zipEntryFile.getParentFile().getParent() == null; + }).findFirst() + .orElseThrow(() -> new IllegalArgumentException("The " + manifestFileName + " file could not be found")); + } + + JsonObject manifest = JsonParser.parseReader(new InputStreamReader(zipFile.getInputStream(manifestEntry), StandardCharsets.UTF_8)).getAsJsonObject(); + if (!isManifestValid(manifest)) { + throw new IllegalArgumentException("The " + manifestFileName + " file is invalid"); + } + + JsonObject manifestHeader = manifest.getAsJsonObject("header"); + String resourcePackName = manifestHeader.get("name").getAsString(); + String resourcePackUuid = manifestHeader.get("uuid").getAsString(); + String resourcePackVersion = manifestHeader.getAsJsonArray("version").toString().replace("[", "").replace("]", "").replace(",", "."); + int resourcePackSize = (int) file.length(); + byte[] resourcePackSha256 = MessageDigest.getInstance("SHA-256").digest(java.nio.file.Files.readAllBytes(file.toPath())); + + Server.getInstance().getLogger().info(Server.getInstance().getLanguage().translate(LanguageKeys.SCULK_SERVER_RESOURCE_PACK_LOADED, List.of(resourcePackName))); + ResourcePack resourcePack = new ResourcePack(file, resourcePackName, resourcePackUuid, resourcePackVersion, (long) resourcePackSize, resourcePackSha256, new byte[0]); + resourcePacks.put(UUID.fromString(resourcePackUuid), resourcePack); + } catch (IOException | NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + } + } + } + + private boolean isManifestValid(JsonObject manifest) { + if (manifest.has("format_version") && manifest.has("header") && manifest.has("modules")) { + JsonObject manifestHeader = manifest.getAsJsonObject("header"); + if (manifestHeader.has("description") && manifestHeader.has("name") && manifestHeader.has("uuid") && manifestHeader.has("version")) { + return manifestHeader.getAsJsonArray("version").size() == 3; + } + } + return false; + } + + public ResourcePack getResourcePack(UUID uuid) { + return this.resourcePacks.get(uuid); + } + + public Collection getResourcePacks() { + return this.resourcePacks.values(); + } +} diff --git a/src/main/java/org/sculk/server/SculkOperators.java b/src/main/java/org/sculk/server/SculkOperators.java index a0590ec..50d0fee 100644 --- a/src/main/java/org/sculk/server/SculkOperators.java +++ b/src/main/java/org/sculk/server/SculkOperators.java @@ -1,13 +1,14 @@ package org.sculk.server; import org.sculk.Server; +import org.sculk.api.server.Operators; import org.sculk.config.Config; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class SculkOperators implements Operators{ +public class SculkOperators implements Operators { private Config config = new Config(Server.getInstance().getDataPath().resolve("op.txt").toString(), Config.ENUM); diff --git a/src/main/java/org/sculk/server/SculkWhitelist.java b/src/main/java/org/sculk/server/SculkWhitelist.java index b705d10..2afdcbe 100644 --- a/src/main/java/org/sculk/server/SculkWhitelist.java +++ b/src/main/java/org/sculk/server/SculkWhitelist.java @@ -1,6 +1,7 @@ package org.sculk.server; import org.sculk.Server; +import org.sculk.api.server.Whitelist; import org.sculk.config.Config; import java.util.ArrayList; diff --git a/src/main/resources/language/en_GB.ini b/src/main/resources/language/en_GB.ini index 277e7fc..0fead3c 100644 --- a/src/main/resources/language/en_GB.ini +++ b/src/main/resources/language/en_GB.ini @@ -1,20 +1,22 @@ -sculk.server.selected.language=Language {%0} selected as default -sculk.server.starting=Starting software {%0} version {%1} -sculk.server.starting.version=Starting Minecraft: Bedrock Edition server version {%0} +sculk.server.selected.language=Language %s selected as default +sculk.server.starting=Starting software %s version %s +sculk.server.starting.version=Starting Minecraft: Bedrock Edition server version %s sculk.server.loading=Loading configuration files... +sculk.server.resources.pack.loading=Loading resource pack(s)... +sculk.server.resources.pack.loaded=Successfully loaded resource pack: %s sculk.server.loading.commands=Loading commands... sculk.server.loading.plugins=Loading all plugins... -sculk.server.success.plugin=Loading plugin {%0}... +sculk.server.success.plugin=Loading plugin %s... sculk.server.plugins.loaded=All plugins loaded successfully -sculk.server.network.interface.running=Minecraft network interface running on {%0} -sculk.server.failed.bind=**** FAILED TO BIND TO {%0} +sculk.server.network.interface.running=Minecraft network interface running on %s +sculk.server.failed.bind=**** FAILED TO BIND TO %s sculk.server.server.already.running=Maybe a server is already running on this port? sculk.server.online.mode.enabled=Online mode is enabled. The server will verify that players are authenticated on XboxLive. sculk.server.online.mode.disabled=Online mode is not enabled. The server will no longer verify if players are authenticated on XboxLive. -sculk.server.distributed.under=Sculk is distributed under the {%0} +sculk.server.distributed.under=Sculk is distributed under the %s sculk.server.enable.all.plugins=Enabling all plugins... sculk.server.all.plugins.enabled=All plugins enabled successfully -sculk.server.done=Done ({%0}s)! For help, type "help" or "?" +sculk.server.done=Done (%ds)! For help, type "help" or "?" sculk.server.stopping=Stopping server sculk.plugins.disabling=Disabling all plugins... sculk.plugins.disabled=All plugins disabled @@ -24,4 +26,4 @@ sculk.threads.stopping=Stopping other threads sculk.command.list=Lists connected players -multiplayer.player.joined={%0} has joined the game \ No newline at end of file +multiplayer.player.joined=%s has joined the game \ No newline at end of file diff --git a/src/main/resources/language/fr_FR.ini b/src/main/resources/language/fr_FR.ini index 77cb785..30b41e0 100644 --- a/src/main/resources/language/fr_FR.ini +++ b/src/main/resources/language/fr_FR.ini @@ -1,20 +1,22 @@ -sculk.server.selected.language=La langue {%0} sélectionné par defaut -sculk.server.starting=Démarrage du logiciel {%0} en version {%1} -sculk.server.starting.version=Démarrage du serveur Minecraft: Bedrock Edition version {%0} +sculk.server.selected.language=La langue %s sélectionné par defaut +sculk.server.starting=Démarrage du logiciel %s en version %s +sculk.server.starting.version=Démarrage du serveur Minecraft: Bedrock Edition version %s sculk.server.loading=Chargement des fichiers de configuration... +sculk.server.resources.pack.loading=Chargement du/(des) pack(s) de ressources... +sculk.server.resources.pack.loaded=Packs de ressources chargé avec succès: %s sculk.server.loading.commands=Chargement des commandes... sculk.server.loading.plugins=Chargement de tous les plugins... -sculk.server.success.plugin=Chargement du plugin {%0}... +sculk.server.success.plugin=Chargement du plugin %s... sculk.server.plugins.loaded=Tous les plugins ont été chargés avec succès -sculk.server.network.interface.running=Interface réseau Minecraft en cours d'exécution sur {%0} -sculk.server.failed.bind=**** ÉCHEC DE LIAISON À {%0} +sculk.server.network.interface.running=Interface réseau Minecraft en cours d'exécution sur %s +sculk.server.failed.bind=**** ÉCHEC DE LIAISON À %s sculk.server.server.already.running=Peut-être qu'un serveur est déjà en cours d'exécution sur ce port? sculk.server.online.mode.enabled=Le mode en ligne est activé. Le serveur vérifiera que les joueurs sont authentifiés sur XboxLive. sculk.server.online.mode.disabled=Le mode en ligne n'est pas activé. Le serveur ne vérifie plus si les joueurs sont authentifiés sur XboxLive. -sculk.server.distributed.under=Sculk est distribué sous la {%0} +sculk.server.distributed.under=Sculk est distribué sous la %s sculk.server.enable.all.plugins=Activation de tous les plugins... sculk.server.all.plugins.enabled=Tous les plugins ont été activés avec succès -sculk.server.done=Terminé ({%0}s)! Pour obtenir de l'aide, tapez "help" ou "?" +sculk.server.done=Terminé (%ds)! Pour obtenir de l'aide, tapez "help" ou "?" sculk.server.stopping=Arrêt du serveur sculk.plugins.disabling=Désactivation de tous les plugins... sculk.plugins.disabled=Tous les plugins désactivés @@ -1052,9 +1054,9 @@ commands.hasitem.fail.invalidSlot="slot" a une entrée invalide, doit être un e commands.hasitem.fail.noItem="item" manquant, requis pour le filtre "hasitem". commands.hasitem.fail.slotNoLocation=La spécification de "slot" est invalide si "location" n’est pas donné. commands.help.description=Affiche la liste des commandes disponibles. -commands.help.header=§6-------------- §fHelp - %%1 command(s) §7[%%2/%%3] §6--------------\n%%4 +commands.help.header=§6-------------- §fHelp - %%1$d command(s) §7[%%2$d/%%3$d] §6--------------\n%%4$s command.version.description=Gets the version of this server including any plugins in use -command.version=§fThis server is running §a%%s\n§fServer version: §a%%s\n§fCompatible Minecraft version: §a%%s §f(protocol version: §a%%s§f)\nOperating system: §a%%s +command.version=§fThis server is running §a%s\n§fServer version: §a%s\n§fCompatible Minecraft version: §a%s §f(protocol version: §a%s§f)\nOperating system: §a%s commands.hud.description=Modifie la visibilité des éléments de l'ATH. commands.hud.success=La commande de l'ATH a été exécutée avec succès commands.immutableworld.description=Définit le statut immuable du monde. @@ -6061,7 +6063,7 @@ multiplayer.packErrors=Au moins un de vos packs de ressources ou de comportement multiplayer.packErrors.realms=Au moins un de vos packs de ressources ou de comportement n'a pas pu être chargé. Essayez de télécharger ce monde depuis vos paramètres Realm pour plus d'informations sur l'erreur. multiplayer.player.inventory.recovered=L'inventaire a été récupéré et placé dans des coffres à proximité. multiplayer.player.inventory.failed=L'inventaire a été récupéré. Trouvez un lieu sûr et nous placerons un coffre à proximité la prochaine fois que vous rejoindrez ce monde. -multiplayer.player.joined={%0} a rejoint la partie +multiplayer.player.joined=%s a rejoint la partie multiplayer.player.joined.renamed=%s (anciennement %s) a rejoint la partie multiplayer.player.joined.realms=%s a rejoint le Realm multiplayer.player.joined.realms.renamed=%s (anciennement %s) a rejoint le Realm @@ -10113,7 +10115,7 @@ editorMode.text=bientôt disponible ## These are being used by TextObject tests. Changing them will break the test. translation.test.args=%s %s translation.test.complex=Préfixe, %s%2$s à nouveau %s et %1$s finalement %s et aussi %1$s à nouveau! -translation.test.escape=%%s %%%s %%%%s %%%%%s +translation.test.escape=%s %%s %%%s %%%%s translation.test.invalid=salut % translation.test.invalid2=salut %s translation.test.none=Bonjour tout le monde! diff --git a/src/main/resources/log4j2.component.properties b/src/main/resources/log4j2.component.properties index e2d065e..2724000 100644 --- a/src/main/resources/log4j2.component.properties +++ b/src/main/resources/log4j2.component.properties @@ -1,4 +1,4 @@ -log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector log4j2.enableThreadlocals=true log4j2.enableDirectEncoders=true -log4j2.garbagefreeThreadContextMap=true \ No newline at end of file +log4j2.garbagefreeThreadContextMap=true +terminal.ansi=true \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 4db1518..486ccf2 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,5 +1,5 @@ - + - - - - - - - - - - - \ No newline at end of file