Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ on: [pull_request]

jobs:
build:
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-build-pr.yml@claude/implement-release-please-9inVJ
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-build-pr.yml@v2.1.0
secrets: inherit
3 changes: 1 addition & 2 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ plugins {
`java-library`
}
dependencies {
implementation(platform(libs.mycelium.bom))
implementation(platform(libs.aonyx.bom))
api(libs.minestom)

testImplementation(platform(libs.mycelium.bom))
testImplementation(platform(libs.aonyx.bom))
testImplementation(libs.junit.api)
testImplementation(libs.junit.platform.launcher)
testRuntimeOnly(libs.junit.engine)
Expand Down
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"allowedSitBlocks": [
{
"domain": "minecraft",
"path": "spruce_stairs"
"namespace": "minecraft",
"value": "spruce_stairs"
}
],
"simulationDistance": 2,
Expand Down
84 changes: 78 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import java.nio.file.Files

plugins {
java
application
Expand All @@ -6,10 +8,11 @@ plugins {
}

dependencies {
compileOnly(libs.luckperms.api)
compileOnly(libs.luckperms.api) {
exclude(group = "net.kyori.adventure")
}
implementation(project(":api"))
implementation(project(":common"))
implementation(platform(libs.mycelium.bom))
implementation(platform(libs.aonyx.bom))
implementation(libs.togglz)
implementation(libs.aves)
Expand All @@ -18,10 +21,13 @@ dependencies {
implementation(libs.minestom)
implementation(libs.butterfly.minestom)

implementation(libs.luckperms.common)
implementation(libs.luckperms.common.loader.utils)
implementation(libs.luckperms.minestom.app)
implementation(libs.luckperms.minestom)
runtimeOnly(libs.luckperms.minestom.loader) {
exclude(group = "net.kyori.adventure")
}
compileOnly(libs.luckperms.minestom.loader) {
exclude(group = "net.kyori.adventure")
}


implementation(platform(libs.cloudnet.bom))
implementation(libs.cloudnet.jvm.wrapper)
Expand All @@ -40,6 +46,15 @@ dependencies {
testImplementation(libs.junit.platform.launcher)
testRuntimeOnly(libs.junit.engine)
}

// The LuckPerms minestom-loader is a JarInJar bootstrap that bundles an
// unrelocated, outdated Gson. Tests don't load LuckPerms, but as a runtimeOnly
// dependency the loader leaks into the test runtime classpath where its bundled
// Gson shadows the real one and breaks Minestom's registry init
// (GsonBuilder.disableJdkUnsafe NoSuchMethodError). Keep it off the test path.
configurations.testRuntimeClasspath {
exclude(group = "net.luckperms", module = "minestom-loader")
}
application {
mainClass.set("net.onelitefeather.titan.app.TitanApplication")
}
Expand All @@ -61,15 +76,72 @@ tasks {
archiveClassifier.set("")
archiveFileName.set("app-titan.jar")
mergeServiceFiles()
// Shaded deps ship signed and multi-release jars that break a
// relocation-free application fat jar; drop signatures and module-info.
exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA")
exclude("module-info.class", "META-INF/versions/**/module-info.class")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
test {
useJUnitPlatform()
jvmArgs("-Dminestom.inside-test=true")
}
}

// ---- Ahead-of-Time cache (JDK 25 / JEP 514) for faster lobby startup ----
// A training run boots the shaded lobby once and records a portable AOT cache.
// Trained with the relative classpath "app-titan.jar" so the cache stays valid
// for any deployment launched as:
// java -XX:AOTCache=app-titan.aot -jar app-titan.jar
// (Cache is tied to the JDK 25 build and this jar; regenerated on every build.)
val aotTrainSeconds = providers.gradleProperty("titan.aot.trainSeconds").orElse("20")
val aotRunDir = layout.buildDirectory.dir("aot")
val aotCacheFile = layout.buildDirectory.file("aot/app-titan.aot")

val generateAotCache = tasks.register<Exec>("generateAotCache") {
group = "build"
description = "Generates a JDK 25 AOT cache (app-titan.aot) for faster lobby startup."

val shadowJarTask = tasks.named("shadowJar")
dependsOn(shadowJarTask)
val jarProvider = shadowJarTask.flatMap { (it as Jar).archiveFile }
val worldsDir = rootProject.layout.projectDirectory.dir("worlds")
inputs.file(jarProvider)
inputs.dir(worldsDir)
outputs.file(aotCacheFile)

val launcher = javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(25)) }
val runDir = aotRunDir.get().asFile
val rootDir = rootProject.projectDir
val trainSeconds = aotTrainSeconds
workingDir = runDir

doFirst {
runDir.deleteRecursively()
runDir.mkdirs()
// Relative classpath: the cache records "app-titan.jar", matching the
// deployment launch command above.
jarProvider.get().asFile.copyTo(runDir.resolve("app-titan.jar"), overwrite = true)
// The lobby loads worlds/ (+ app.json) relative to the CWD while booting.
Files.createSymbolicLink(runDir.resolve("worlds").toPath(), rootDir.resolve("worlds").toPath())
rootDir.resolve("app.json").takeIf { it.exists() }?.copyTo(runDir.resolve("app.json"), overwrite = true)
executable = launcher.get().executablePath.asFile.absolutePath
args(
"-Dtitan.aot.trainSeconds=${trainSeconds.get()}",
"-XX:AOTCacheOutput=app-titan.aot",
"-jar", "app-titan.jar"
)
}
}
publishing {
publications.create<MavenPublication>("maven") {
artifact(project.tasks.getByName("shadowJar"))
// AOT cache shipped alongside the jar for faster startup; see generateAotCache.
artifact(aotCacheFile) {
classifier = "aot"
extension = "aot"
builtBy(generateAotCache)
}
version = rootProject.version as String
artifactId = "titan-app"
groupId = rootProject.group as String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,42 @@
import eu.cloudnetservice.driver.inject.InjectionLayer;
import eu.cloudnetservice.modules.bridge.impl.platform.minestom.MinestomBridgeExtension;
import net.minestom.server.MinecraftServer;
import net.onelitefeather.titan.app.luckperms.MinestomLoader;

import java.nio.file.Files;
import java.nio.file.Path;


public class TitanApplication {

public static void main(String[] args) {
MinecraftServer minecraftServer = MinecraftServer.init();
MinestomLoader.get().load().registerShutdownHook().start();
me.lucko.luckperms.minestom.loader.MinestomLoader.get().load().registerShutdownHook().start();
Titan titan = new Titan();
titan.initialize();
InjectionLayer.ext().instance(MinestomBridgeExtension.class).onLoad();

// CloudNet only provides its runtime when the service is launched through
// its wrapper, which creates a ".wrapper" directory in the working
// directory. When running standalone (local, tests, AOT training) there
// is no CloudNet to bind to, so skip the bridge to avoid failing startup.
if (Files.isDirectory(Path.of(".wrapper"))) {
InjectionLayer.ext().instance(MinestomBridgeExtension.class).onLoad();
}

minecraftServer.start("localhost", 25565);

// AOT training aid: when -Dtitan.aot.trainSeconds=<n> is set, shut down
// cleanly after the server has started so the JVM exit writes the AOT
// configuration/cache. No effect in normal operation.
Long aotTrainSeconds = Long.getLong("titan.aot.trainSeconds");
if (aotTrainSeconds != null) {
Thread.ofVirtual().name("titan-aot-trainer").start(() -> {
try {
Thread.sleep(aotTrainSeconds * 1000L);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
System.exit(0);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ public PlayerConfigurationListener(MapProvider mapProvider) {
@Override
public void accept(AsyncPlayerConfigurationEvent event) {
Optional.ofNullable(this.mapProvider).map(MapProvider::getInstance).ifPresent(event::setSpawningInstance);
Optional.of(this.mapProvider).map(MapProvider::getActiveLobby).map(LobbyMap::getSpawn).ifPresent(event.getPlayer()::setRespawnPoint);
Optional.of(this.mapProvider).map(MapProvider::getActiveLobby).map(LobbyMap::spawn).ifPresent(event.getPlayer()::setRespawnPoint);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ public void accept(PlayerMoveEvent playerMoveEvent) {
if (player.getInstance() == null)
return;
if (player.getPosition().y() < appConfig.minHeightBeforeTeleport()) {
Optional.ofNullable(this.lobbyMap).map(LobbyMap::getSpawn).ifPresent(player::teleport);
Optional.ofNullable(this.lobbyMap).map(LobbyMap::spawn).ifPresent(player::teleport);
return;
}
if (player.getPosition().y() > appConfig.maxHeightBeforeTeleport()) {
Optional.ofNullable(this.lobbyMap).map(LobbyMap::getSpawn).ifPresent(player::teleport);
Optional.ofNullable(this.lobbyMap).map(LobbyMap::spawn).ifPresent(player::teleport);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public PlayerSpawnListener(AppConfig appConfig, LobbyMap lobbyMap, NavigationHel
@Override
public void accept(PlayerSpawnEvent event) {
event.getPlayer().sendPacket(this.simulatedDistancePacket);
Optional.ofNullable(this.lobbyMap).map(LobbyMap::getSpawn).ifPresent(event.getPlayer()::teleport);
Optional.ofNullable(this.lobbyMap).map(LobbyMap::spawn).ifPresent(event.getPlayer()::teleport);
this.navigationHelper.setItems(event.getPlayer());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ void testPlayerTeleportBelowMinHeight(Env env) {
var appConfig = InternalAppConfig.defaultConfig();

// Create a real LobbyMap with a spawn point
var lobbyMap = LobbyMap.lobbyMapBuilder().build();
Pos spawnPos = new Pos(10, 100, 10);
lobbyMap.setSpawn(spawnPos);
var lobbyMap = LobbyMap.lobbyMapBuilder().spawn(spawnPos).build();

// Create the listener with real objects
PlayerMoveListener listener = new PlayerMoveListener(appConfig, lobbyMap);
Expand Down Expand Up @@ -76,9 +75,8 @@ void testPlayerTeleportAboveMaxHeight(Env env) {
var appConfig = InternalAppConfig.defaultConfig();

// Create a real LobbyMap with a spawn point
var lobbyMap = LobbyMap.lobbyMapBuilder().build();
Pos spawnPos = new Pos(10, 100, 10);
lobbyMap.setSpawn(spawnPos);
var lobbyMap = LobbyMap.lobbyMapBuilder().spawn(spawnPos).build();

// Create the listener with real objects
PlayerMoveListener listener = new PlayerMoveListener(appConfig, lobbyMap);
Expand Down Expand Up @@ -109,9 +107,8 @@ void testPlayerNotTeleportedWithinLimits(Env env) {
var appConfig = InternalAppConfig.defaultConfig();

// Create a real LobbyMap with a spawn point
var lobbyMap = LobbyMap.lobbyMapBuilder().build();
Pos spawnPos = new Pos(10, 100, 10);
lobbyMap.setSpawn(spawnPos);
var lobbyMap = LobbyMap.lobbyMapBuilder().spawn(spawnPos).build();

// Create the listener with real objects
PlayerMoveListener listener = new PlayerMoveListener(appConfig, lobbyMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void testPlayerTeleport(Env env) {

LobbyMap lobbyMap = mock(LobbyMap.class);
var lobbyMapSpawn = new Pos(1, 2, 3);
when(lobbyMap.getSpawn()).thenReturn(lobbyMapSpawn);
when(lobbyMap.spawn()).thenReturn(lobbyMapSpawn);
NavigationHelper navigationHelper = mock(NavigationHelper.class);

MinecraftServer.getGlobalEventHandler().addListener(PlayerSpawnEvent.class, new PlayerSpawnListener(InternalAppConfig.defaultConfig(), lobbyMap, navigationHelper));
Expand Down
1 change: 0 additions & 1 deletion common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ plugins {

dependencies {
implementation(project(":api"))
implementation(enforcedPlatform(libs.mycelium.bom))
implementation(platform(libs.aonyx.bom))
implementation(libs.minestom)
implementation(libs.togglz)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package net.onelitefeather.titan.common.config;

import com.google.gson.Gson;
import net.kyori.adventure.key.Key;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.theevilreaper.aves.file.GsonFileHandler;
import net.theevilreaper.aves.file.gson.KeyGsonAdapter;
import net.theevilreaper.aves.file.gson.PositionGsonAdapter;
import org.jetbrains.annotations.NotNull;

Expand All @@ -35,7 +37,7 @@ public final class AppConfigProvider {
private AppConfigProvider(Path path) {
this.path = path;
var typeAdapter = new PositionGsonAdapter();
this.gson = new Gson().newBuilder().registerTypeAdapter(Pos.class, typeAdapter).registerTypeAdapter(Vec.class, typeAdapter).create();
this.gson = new Gson().newBuilder().registerTypeAdapter(Pos.class, typeAdapter).registerTypeAdapter(Vec.class, typeAdapter).registerTypeHierarchyAdapter(Key.class, KeyGsonAdapter.create()).create();
this.fileHandler = new GsonFileHandler(this.gson);
this.loadConfig();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,27 @@
import net.minestom.server.coordinate.Pos;
import net.theevilreaper.aves.map.BaseMap;

import java.util.List;

public final class LobbyMap extends BaseMap {

public LobbyMap(String name, Pos spawn, List<String> builders) {
super(name, spawn, builders);
}

public static Builder lobbyMapBuilder(LobbyMap map) {
var builder = lobbyMapBuilder();
if (map == null) {
return builder;
}
if (map.getSpawn() != null) {
builder.spawn(map.getSpawn());
if (map.spawn() != null) {
builder.spawn(map.spawn());
}
if (map.getName() != null) {
builder.name(map.getName());
if (map.name() != null) {
builder.name(map.name());
}
if (map.getBuilders() != null) {
builder.author(map.getBuilders());
if (map.builders() != null) {
builder.author(map.builders().toArray(new String[0]));
}
return builder;
}
Expand Down
Loading
Loading