diff --git a/.gitignore b/.gitignore index b85d271..047fe92 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ hs_err_pid* replay_pid* # End of https://www.toptal.com/developers/gitignore/api/java -local.gradle \ No newline at end of file +local.gradle +config.yml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 14746e7..2a65317 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> + <component name="GradleMigrationSettings" migrationVersion="1" /> <component name="GradleSettings"> <option name="linkedExternalProjectsSettings"> <GradleProjectSettings> diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..95c6c66 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="UnqualifiedFieldAccess" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="UnqualifiedMethodAccess" enabled="true" level="WARNING" enabled_by_default="true" /> + </profile> +</component> \ No newline at end of file diff --git a/build.gradle b/build.gradle index d57aaaf..c161809 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ dependencies { implementation 'net.minestom:minestom-snapshots:fd51c8d17a' implementation 'io.github.TogAr2:MinestomPvP:PR62-SNAPSHOT' implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.spongepowered:configurate-yaml:4.1.2' + implementation 'com.google.guava:guava:32.0.1-android' } java { @@ -31,24 +33,25 @@ java { } } -tasks { - jar { - manifest { - attributes 'Main-Class': 'eu.mhsl.minenet.minigames.Main' - attributes 'Multi-Release': true - } - duplicatesStrategy = 'exclude' - from configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } - build { - dependsOn(shadowJar) - } - shadowJar { - mergeServiceFiles() - archiveClassifier.set("") +shadowJar { + archiveClassifier.set("") // Ohne "-all" im Namen + mergeServiceFiles() + manifest { + attributes( + 'Main-Class': 'eu.mhsl.craftattack.teamLobby.Main', + 'Multi-Release': 'true' + ) } } +jar { + enabled = false +} + +build { + dependsOn(shadowJar) +} + if (file("local.gradle").exists()) { apply from: "local.gradle" } diff --git a/src/main/java/de/mhsl/craftattack/Main.java b/src/main/java/de/mhsl/craftattack/Main.java deleted file mode 100644 index 41b7b4c..0000000 --- a/src/main/java/de/mhsl/craftattack/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.mhsl.craftattack; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello, World!"); - } -} \ No newline at end of file diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/Authentication.java b/src/main/java/eu/mhsl/craftattack/teamLobby/Authentication.java new file mode 100644 index 0000000..efed536 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/Authentication.java @@ -0,0 +1,35 @@ +package eu.mhsl.craftattack.teamLobby; + +import net.minestom.server.extras.MojangAuth; +import net.minestom.server.extras.bungee.BungeeCordProxy; +import net.minestom.server.extras.velocity.VelocityProxy; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class Authentication { + public enum Method { + NONE, + VANILLA, + BUNGEECORD, + VELOCITY + } + + public record Values(String method, @Nullable String secret) { + } + + public static void init(Values values) { + Method method = Method.valueOf(values.method); + + switch(method) { + case VANILLA -> MojangAuth.init(); + case BUNGEECORD -> BungeeCordProxy.enable(); + case VELOCITY -> { + Objects.requireNonNull(values.secret, "Velocity proxy needs an secret"); + VelocityProxy.enable(values.secret); + } + } + + System.out.printf("Server authentication requirement is set to '%s'!%n", method); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/Lobby.java b/src/main/java/eu/mhsl/craftattack/teamLobby/Lobby.java new file mode 100644 index 0000000..cd6b312 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/Lobby.java @@ -0,0 +1,150 @@ +package eu.mhsl.craftattack.teamLobby; + +import eu.mhsl.craftattack.teamLobby.data.Team; +import eu.mhsl.craftattack.teamLobby.util.CommonHandler; +import eu.mhsl.craftattack.teamLobby.util.PluginMessageUtil; +import eu.mhsl.craftattack.teamLobby.util.PosSerializer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.util.Ticks; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.Player; +import net.minestom.server.event.instance.AddEntityToInstanceEvent; +import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.Particle; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.registry.DynamicRegistry; +import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.world.DimensionType; +import org.spongepowered.configurate.ConfigurationNode; + +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class Lobby extends InstanceContainer { + private static final DynamicRegistry.Key<DimensionType> dimension = MinecraftServer.getDimensionTypeRegistry() + .register( + NamespaceID.from("mhsl:team_lobby"), + DimensionType.builder() + .ambientLight(2.0f) + .build() + ); + + private final ConfigurationNode lobbyConfig = Main.getConfig().node("lobby"); + private final Pos buttonLocation = PosSerializer.deserialize(this.lobbyConfig.node("buttonLocation").getString()); + private final String targetServer = this.lobbyConfig.node("connectsTo").getString(); + public final Pos spawnPoint = PosSerializer.deserialize(this.lobbyConfig.node("spawnpoint").getString()); + + private final Team team; + private boolean isComplete; + private boolean isJoining; + + public final ParticlePacket availableParticles = new ParticlePacket( + Particle.COMPOSTER, + this.buttonLocation.add(0.5), + new Vec(0.5, 0.5, 0.5), + 0.001f, + 3 + ); + + public final ParticlePacket progressParticles = new ParticlePacket( + Particle.TOTEM_OF_UNDYING, + this.buttonLocation.add(0.5), + new Vec(0.5, 0.5, 0.5), + 0.1f, + 5 + ); + + public Lobby(Team team) { + super(UUID.randomUUID(), dimension); + MinecraftServer.getInstanceManager().registerInstance(this); + this.team = team; + + //noinspection UnstableApiUsage + this.eventNode() + .addListener(PlayerBlockBreakEvent.class, CommonHandler::cancel) + .addListener(AddEntityToInstanceEvent.class, this::onPlayerChange) + .addListener(RemoveEntityFromInstanceEvent.class, this::onPlayerChange) + .addListener(PlayerBlockInteractEvent.class, this::onBlockInteract); + + MinecraftServer.getSchedulerManager().scheduleTask( + this::particleTick, + TaskSchedule.seconds(3), + TaskSchedule.millis(100) + ); + + this.setGenerator(unit -> unit.modifier().fillHeight(0, 10, Block.BAMBOO_BLOCK)); + + this.setBlock(this.spawnPoint.sub(0, 1 ,0), Block.DIAMOND_BLOCK); + } + + private <TEvent> void onPlayerChange(TEvent event) { + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> { + this.isJoining = false; + this.isComplete = this.getPlayers().stream() + .map(Entity::getUuid) + .collect(Collectors.toSet()) + .containsAll(this.team.players()); + + this.update(); + }); + } + + private synchronized void onBlockInteract(PlayerBlockInteractEvent event) { + if(!event.getBlockPosition().sameBlock(this.buttonLocation)) return; + if(this.isJoining) return; + if(!this.isComplete) { + this.everyMember(p -> p.sendActionBar(Component.text("Dein Team ist nicht vollständig!", NamedTextColor.RED))); + return; + } + this.isJoining = true; + this.connect(); + this.update(); + } + + private void update() { + Block button = (this.isComplete ? Block.WARPED_BUTTON : Block.CRIMSON_BUTTON) + .withProperty("face", "floor") + .withProperty("powered", this.isJoining ? "true" : "false"); + + this.setBlock(this.buttonLocation, button); + } + + private void connect() { + if(!this.isJoining) return; + this.everyMember(p -> { + p.addEffect(new Potion(PotionEffect.DARKNESS, 0, 5 * Ticks.TICKS_PER_SECOND)); + p.sendActionBar(Component.text("Verbinde...", NamedTextColor.GREEN)); + PluginMessageUtil.connect(p, this.targetServer); + }); + MinecraftServer.getSchedulerManager().scheduleTask( + () -> { + this.isJoining = false; + this.update(); + }, + TaskSchedule.seconds(10), + TaskSchedule.stop() + ); + } + + private void particleTick() { + if(!this.isComplete) return; + + this.everyMember(p -> p.sendPacket(this.isJoining ? this.progressParticles : this.availableParticles)); + } + + private void everyMember(Consumer<Player> consumer) { + this.getPlayers().forEach(consumer); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/LobbyManager.java b/src/main/java/eu/mhsl/craftattack/teamLobby/LobbyManager.java new file mode 100644 index 0000000..2e4ff62 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/LobbyManager.java @@ -0,0 +1,34 @@ +package eu.mhsl.craftattack.teamLobby; + +import eu.mhsl.craftattack.teamLobby.data.Team; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class LobbyManager { + private final Set<Team> teams = new HashSet<>() { + { + this.add(new Team(UUID.randomUUID(), "Testerr", "#123123", List.of( + UUID.fromString("c291290d-cffc-4649-aeec-d6f4417896ea"), + UUID.fromString("959ed433-14ea-38fe-918b-75b7d09466af") + ))); + } + }; + private final Map<Team, Lobby> instances = new HashMap<>(); + + public synchronized @NotNull Lobby getPlayerInstance(Player player) { + UUID playerId = player.getUuid(); + Team targetTeam = this.teams.stream() + .filter(team -> team.players().contains(playerId)) + .findAny() + .orElseThrow(() -> new NoSuchElementException("Player is not in any Team!")); + + if(!this.instances.containsKey(targetTeam)) { + Lobby instance = new Lobby(targetTeam); + this.instances.put(targetTeam, instance); + } + + return this.instances.get(targetTeam); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/Main.java b/src/main/java/eu/mhsl/craftattack/teamLobby/Main.java new file mode 100644 index 0000000..572acf5 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/Main.java @@ -0,0 +1,86 @@ +package eu.mhsl.craftattack.teamLobby; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; + +public class Main { + private static ConfigurationNode config; + private static LobbyManager lobbyManager; + public static void main(String[] args) throws ConfigurateException { + String filename = "config.yml"; + File configFile = new File(filename); + + if (!configFile.exists()) { + System.out.println("Konfigurationsdatei nicht gefunden. Defaultkonfiguration wird verwendet."); + + try (InputStream in = Main.class.getResourceAsStream("/config.yml")) { + if (in == null) { + throw new FileNotFoundException("Beispielkonfiguration 'config.yml' nicht im Ressourcenpfad gefunden."); + } + + Files.copy(in, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException("Fehler beim Kopieren der Beispielkonfiguration: ", e); + } + } + + config = YamlConfigurationLoader.builder() + .path(configFile.toPath()) + .build() + .load(); + + lobbyManager = new LobbyManager(); + + ConfigurationNode serverCfg = config.node("server"); + + MinecraftServer server = MinecraftServer.init(); + MinecraftServer.setBrandName("mhsl.eu - TeamLobby"); + MinecraftServer.setCompressionThreshold(serverCfg.node("compressionThreshold").getInt(128)); + System.setProperty("minestom.chunk-view-distance", String.valueOf(serverCfg.node("viewDistance").getInt(12))); + + ConfigurationNode authCfg = config.node("authentication"); + Authentication.Values auth = new Authentication.Values( + authCfg.node("method").getString(Authentication.Method.NONE.name()), + authCfg.node("secret").getString() + ); + Authentication.init(auth); + + MinecraftServer.getGlobalEventHandler().addListener( + AsyncPlayerConfigurationEvent.class, + event -> { + try { + Player p = event.getPlayer(); + System.out.printf("Player %s joined: %s%n", p.getUsername(), p.getUuid()); + Lobby lobby = lobbyManager.getPlayerInstance(p); + event.setSpawningInstance(lobby); + p.setRespawnPoint(lobby.spawnPoint); + } catch(Exception e) { + event.getPlayer().kick(String.format("Login: %s", e.getMessage())); + System.err.println(Arrays.toString(e.getStackTrace())); + } + } + ); + + int port = config.node("server", "port").getInt(25565); + server.start(new InetSocketAddress("0.0.0.0", port)); + + System.out.println("Server is running!"); + } + + public static ConfigurationNode getConfig() { + return config; + } +} \ No newline at end of file diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/data/Team.java b/src/main/java/eu/mhsl/craftattack/teamLobby/data/Team.java new file mode 100644 index 0000000..414ed6f --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/data/Team.java @@ -0,0 +1,12 @@ +package eu.mhsl.craftattack.teamLobby.data; + +import java.util.List; +import java.util.UUID; + +public record Team( + UUID teamId, + String teamName, + String hexColor, + List<UUID> players +) { +} diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/util/CommonHandler.java b/src/main/java/eu/mhsl/craftattack/teamLobby/util/CommonHandler.java new file mode 100644 index 0000000..ed45726 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/util/CommonHandler.java @@ -0,0 +1,9 @@ +package eu.mhsl.craftattack.teamLobby.util; + +import net.minestom.server.event.trait.CancellableEvent; + +public class CommonHandler { + public static void cancel(CancellableEvent event) { + event.setCancelled(true); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/util/PluginMessageUtil.java b/src/main/java/eu/mhsl/craftattack/teamLobby/util/PluginMessageUtil.java new file mode 100644 index 0000000..9844c4b --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/util/PluginMessageUtil.java @@ -0,0 +1,16 @@ +package eu.mhsl.craftattack.teamLobby.util; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.common.PluginMessagePacket; + +public class PluginMessageUtil { + private static final String bungeeTargetSelector = "bungeecord:main"; + public static void connect(Player p, String bungeeServerTargetName) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("Connect"); + out.writeUTF(bungeeServerTargetName); + p.sendPacket(new PluginMessagePacket(bungeeTargetSelector, out.toByteArray())); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/teamLobby/util/PosSerializer.java b/src/main/java/eu/mhsl/craftattack/teamLobby/util/PosSerializer.java new file mode 100644 index 0000000..0d99306 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/teamLobby/util/PosSerializer.java @@ -0,0 +1,46 @@ +package eu.mhsl.craftattack.teamLobby.util; + +import com.google.gson.*; +import net.minestom.server.coordinate.Pos; + +import java.lang.reflect.Type; + +public class PosSerializer { + private static final Gson gson = new GsonBuilder() + .registerTypeAdapter(Pos.class, new PosAdapter()) + .create(); + + public static String serialize(Pos pos) { + return gson.toJson(pos); + } + + public static Pos deserialize(String json) { + return gson.fromJson(json, Pos.class); + } + + private static class PosAdapter implements JsonSerializer<Pos>, JsonDeserializer<Pos> { + + @Override + public JsonElement serialize(Pos src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("x", src.x()); + obj.addProperty("y", src.y()); + obj.addProperty("z", src.z()); + obj.addProperty("yaw", src.yaw()); + obj.addProperty("pitch", src.pitch()); + return obj; + } + + @Override + public Pos deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + double x = obj.get("x").getAsDouble(); + double y = obj.get("y").getAsDouble(); + double z = obj.get("z").getAsDouble(); + float yaw = obj.has("yaw") ? obj.get("yaw").getAsFloat() : 0f; + float pitch = obj.has("pitch") ? obj.get("pitch").getAsFloat() : 0f; + return new Pos(x, y, z, yaw, pitch); + } + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..61440b9 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,13 @@ +server: + port: 25565 + compressionThreshold: 128 + viewDistance: 8 + +lobby: + spawnpoint: '{"x":0.5, "y":10, "z":0.5, "yaw":0, "pitch":90}' + buttonLocation: '{"x":0, "y":10, "z":0}' + connectsTo: 'serverName' + +authentication: + method: 'NONE' # supported values: 'NONE', 'VANILLA', 'BUNGEECORD', 'VELOCITY' + secret: '' # only for VELOCITY proxies