diff --git a/extension/deployment/pom.xml b/extension/deployment/pom.xml index 8b3c8df..e2cf01c 100644 --- a/extension/deployment/pom.xml +++ b/extension/deployment/pom.xml @@ -1,44 +1,48 @@ - 4.0.0 - - org.acme - minecrafter-parent - 1.0.0-SNAPSHOT - - minecrafter-deployment - Minecrafter - Deployment - - - io.quarkus - quarkus-rest-client-reactive-jackson-deployment - - - org.acme - minecrafter - ${project.version} - - - io.quarkus - quarkus-junit5-internal - test - - - - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${quarkus.version} - - - - - - + 4.0.0 + + org.acme + minecrafter-parent + 1.0.0-SNAPSHOT + + minecrafter-deployment + Minecrafter - Deployment + + + io.quarkus + quarkus-rest-client-reactive-jackson-deployment + + + io.quarkus + quarkus-devservices-common + + + org.acme + minecrafter + ${project.version} + + + io.quarkus + quarkus-junit5-internal + test + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + diff --git a/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecraftContainer.java b/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecraftContainer.java new file mode 100644 index 0000000..8355106 --- /dev/null +++ b/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecraftContainer.java @@ -0,0 +1,43 @@ +package org.acme.minecrafter.deployment; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.List; + +public class MinecraftContainer extends GenericContainer { + private static final int MINECRAFT_PORT = 25565; + static final int OBSERVABILITY_PORT = 8081; + + public MinecraftContainer(DockerImageName image) { + super(image); + + List portBindings = new ArrayList<>(); + portBindings.add("25565:25565"); // Make life easy for the minecraft client + setPortBindings(portBindings); + //withReuse(true); + + // withExposedPorts(MINECRAFT_PORT); + // This is a bit of a cheat, since at this point the client isn't ready, but otherwise it's too slow + waitingFor(Wait.forLogMessage(".*" + "Preparing" + ".*", 1)); + } + + + @Override + protected void configure() { + withNetwork(Network.SHARED); + addExposedPorts(OBSERVABILITY_PORT); + addExposedPorts(MINECRAFT_PORT); + } + + public Integer getApiPort() { + return this.getMappedPort(OBSERVABILITY_PORT); + } + + public Integer getGamePort() { + return this.getMappedPort(MINECRAFT_PORT); + } +} \ No newline at end of file diff --git a/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecrafterProcessor.java b/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecrafterProcessor.java index ff68f8f..44c8a96 100644 --- a/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecrafterProcessor.java +++ b/extension/deployment/src/main/java/org/acme/minecrafter/deployment/MinecrafterProcessor.java @@ -4,11 +4,15 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; import org.acme.minecrafter.runtime.HelloRecorder; import org.acme.minecrafter.runtime.MinecraftLog; @@ -17,8 +21,10 @@ import org.acme.minecrafter.runtime.MinecraftService; import org.acme.minecrafter.runtime.RestExceptionMapper; import org.jboss.jandex.DotName; +import org.testcontainers.utility.DockerImageName; import javax.ws.rs.Priorities; +import java.util.Map; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; @@ -63,8 +69,12 @@ public boolean appliesTo(org.jboss.jandex.AnnotationTarget.Kind kind) { } public void transform(TransformationContext context) { - if (context.getTarget().asMethod().hasAnnotation(JAX_RS_GET)) { - context.transform().add(MinecraftLog.class).done(); + if (context.getTarget() + .asMethod() + .hasAnnotation(JAX_RS_GET)) { + context.transform() + .add(MinecraftLog.class) + .done(); } } }); @@ -76,4 +86,27 @@ ExceptionMapperBuildItem exceptionMappers() { Exception.class.getName(), Priorities.USER + 100, true); } + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) + public DevServicesResultBuildItem createContainer(LaunchModeBuildItem launchMode) { + // Normally, this would be a remote image, but we need to build one with the right mods, so use a local one + DockerImageName dockerImageName = DockerImageName.parse("minecraft-server"); + + // Don't be tempted to put this in a try-with-resources block, even if the IDE advises it + // Otherwise the dev service gets shut down after startup :) + MinecraftContainer container = new MinecraftContainer(dockerImageName).withExposedPorts(8081, 25565); + container.start(); + + // Set a config property so that anything using the container can find it, even on the random port + + Map props = Map.of("quarkus.minecrafter.base-url", + "http://" + container.getHost() + ":" + container.getApiPort()); + + System.out.println("API port: " + "http://" + container.getHost() + ":" + container.getApiPort()); + System.out.println("Game port: " + "http://" + container.getHost() + ":" + container.getGamePort()); + + return new DevServicesResultBuildItem.RunningDevService(FEATURE, container.getContainerId(), + container::close, props) + .toBuildItem(); + } } + diff --git a/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftLogHandler.java b/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftLogHandler.java index 0fec2cf..6939a7b 100644 --- a/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftLogHandler.java +++ b/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftLogHandler.java @@ -15,7 +15,8 @@ public void publish(LogRecord record) { String formattedMessage = String.format(record.getMessage(), record.getParameters()); System.out.println("⛏️ [Minecrafter] " + formattedMessage); - minecraft.log(formattedMessage); + // TODO this hangs if the minecraft server is a dev service; make it fire-and-forget + // minecraft.log(formattedMessage); } diff --git a/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftService.java b/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftService.java index 4754327..aef54ce 100644 --- a/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftService.java +++ b/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecraftService.java @@ -1,21 +1,10 @@ package org.acme.minecrafter.runtime; import javax.inject.Singleton; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.rest.client.RestClientBuilder; - import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.util.Set; @Singleton public class MinecraftService { @@ -39,8 +28,10 @@ public void boom() { public void log(String message) { try { - client.target(minecrafterConfig.baseURL).path("log") - .request(MediaType.TEXT_PLAIN).post(Entity.text(message)); + client.target(minecrafterConfig.baseURL) + .path("observability/log") + .request(MediaType.TEXT_PLAIN) + .post(Entity.text(message)); // Don't log anything back about the response or it ends up with too much circular logging } catch (Throwable e) { System.out.println("\uD83D\uDDE1️ [Minecrafter] Connection error: " + e); @@ -49,9 +40,10 @@ public void log(String message) { private void invokeMinecraft(String path) { try { - String response = client.target(minecrafterConfig.baseURL).path(path) - .request(MediaType.TEXT_PLAIN) - .get(String.class); + String response = client.target(minecrafterConfig.baseURL) + .path("observability/" + path) + .request(MediaType.TEXT_PLAIN) + .get(String.class); System.out.println("\uD83D\uDDE1️ [Minecrafter] Mod response: " + response); } catch (Throwable e) { diff --git a/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecrafterConfig.java b/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecrafterConfig.java index a92ec26..334adde 100644 --- a/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecrafterConfig.java +++ b/extension/runtime/src/main/java/org/acme/minecrafter/runtime/MinecrafterConfig.java @@ -10,6 +10,6 @@ public class MinecrafterConfig { /** * The minecraft server's observability base URL */ - @ConfigItem(defaultValue = "http://localhost:8081/observability/") + @ConfigItem(defaultValue = "http://localhost:8081/") public String baseURL; } diff --git a/modded-minecraft/Dockerfile b/modded-minecraft/Dockerfile index dfb313b..54bde32 100644 --- a/modded-minecraft/Dockerfile +++ b/modded-minecraft/Dockerfile @@ -10,6 +10,8 @@ FROM webhippie/minecraft-forge:43.0 # This is needed for the client launched with `./gradlew runClient` to be able to connect ENV MINECRAFT_ONLINE_MODE=false +# For performance reasons, keep the world small +ENV MINECRAFT_MAX_WORLD_SIZE=299 RUN find / -name mods EXPOSE 8081