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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ bin/
Thumbs.db

# LuckPerms (runtime-generated: config, H2 database, relocated libs)
data/
# Anchored to the working directories a service runs in: an unanchored "data/" also
# hides the net.onelitefeather.cygnus.setup.data Java package.
/data/
/setup/data/
/game/data/
2 changes: 2 additions & 0 deletions game/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ application {

dependencies {
implementation(platform(libs.aonyx.bom))
implementation(platform(libs.falco.bom))
implementation(project(":common"))
implementation(libs.slf4j.api)
implementation(libs.minestom)
implementation(libs.aves)
implementation(libs.xerus)
implementation(libs.falco.anvil)

// SLF4J needs a binding at runtime; without one it falls back to NOP and the
// server logs nothing at all.
Expand Down
5 changes: 4 additions & 1 deletion game/src/main/java/net/onelitefeather/cygnus/Cygnus.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ public Cygnus() {
this.gameConfig = new GameConfigReader(path).getConfig();
MinecraftServer.getConnectionManager().setPlayerProvider(CygnusPlayer::new);
this.pageProvider = new PageProvider();
this.mapProvider = new GameMapProvider(path);
GameMapProvider gameMapProvider = new GameMapProvider(path);
this.mapProvider = gameMapProvider;
// Falco keeps its region files open, so the loaders have to be released on shutdown
MinecraftServer.getSchedulerManager().buildShutdownTask(gameMapProvider::close);
this.view = new GameViewImpl();
this.createTeams(this.gameConfig, this.teamService);
this.ambientProvider = new AmbientProvider(this.teamService.getTeams().get(TeamHelper.SURVIVOR_TEAM_ID));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.world.DimensionType;
import net.onelitefeather.cygnus.common.map.GameMap;
import net.onelitefeather.cygnus.common.map.filter.MapFilters;
import net.onelitefeather.cygnus.common.util.GsonHelper;
import net.onelitefeather.cygnus.common.util.Helper;
import net.onelitefeather.cygnus.map.event.GameMapLoadedEvent;
import net.onelitefeather.falco.anvil.FalcoAnvilLoader;
import net.theevilreaper.aves.map.BaseMap;
import net.theevilreaper.aves.map.MapEntry;
import net.theevilreaper.aves.map.provider.AbstractMapProvider;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public final class GameMapProvider extends AbstractMapProvider {

private final List<FalcoAnvilLoader> chunkLoaders = new ArrayList<>();
private InstanceContainer gameInstance;
private GameMap gameMap;

Expand All @@ -41,7 +47,7 @@ public void loadGameMap() {
this.gameMap = this.fileHandler.load(gameEntry.getMapFile(), GameMap.class).get();
this.gameInstance = MinecraftServer.getInstanceManager().createInstanceContainer();
this.gameInstance.setTime(Helper.NEW_MOON_TIME);
this.registerInstance(this.gameInstance, gameEntry);
this.registerFalcoInstance(this.gameInstance, gameEntry);
EventDispatcher.call(new GameMapLoadedEvent(this.gameMap, this.gameInstance));
}

Expand All @@ -67,11 +73,55 @@ private BaseMap loadLobbyMap() {

this.activeMap = this.fileHandler.load(lobbyEntry.getMapFile(), BaseMap.class).get();
InstanceContainer instanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer();
this.registerInstance(instanceContainer, lobbyEntry);
this.registerFalcoInstance(instanceContainer, lobbyEntry);
this.activeInstance = instanceContainer;
return this.activeMap;
}

/**
* Registers the given instance with a {@link FalcoAnvilLoader} attached to it.
*
* <p>This mirrors {@link AbstractMapProvider#registerInstance(InstanceContainer, MapEntry)} but
* swaps the chunk loader: the Aves method hard-wires Minestom's own {@code AnvilLoader}, while
* Falco reads region files in parallel and fails loudly on a broken chunk instead of reporting
* it as absent. The loader is kept so it can be closed in {@link #close()}.</p>
*
* @param instance the instance the map is loaded into
* @param mapEntry the map entry whose directory root is the world root of the loader
*/
private void registerFalcoInstance(InstanceContainer instance, MapEntry mapEntry) {
FalcoAnvilLoader chunkLoader =
new FalcoAnvilLoader(mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key());
this.chunkLoaders.add(chunkLoader);

instance.setChunkLoader(chunkLoader);
instance.enableAutoChunkLoad(true);

var defaultClock = instance.defaultClock();
if (defaultClock != null) {
defaultClock.rate(0f);
}
MinecraftServer.getInstanceManager().registerInstance(instance);
}

/**
* Closes every chunk loader this provider opened.
*
* <p>Unlike Minestom's {@code AnvilLoader}, a {@link FalcoAnvilLoader} holds its region files
* open, so it has to be closed once the server shuts down. Calling this more than once is
* harmless.</p>
*/
public void close() {
for (FalcoAnvilLoader chunkLoader : this.chunkLoaders) {
try {
chunkLoader.close();
} catch (IOException exception) {
MinecraftServer.getExceptionManager().handleException(exception);
}
}
this.chunkLoaders.clear();
}

@Override
public void saveMap(Path path, BaseMap baseMap) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package net.onelitefeather.cygnus.map;

import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.testing.Env;
import net.minestom.testing.extension.MicrotusExtension;
import net.onelitefeather.cygnus.common.map.GameMap;
import net.onelitefeather.cygnus.common.util.GsonHelper;
import net.onelitefeather.falco.anvil.FalcoAnvilLoader;
import net.theevilreaper.aves.map.BaseMap;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;

/**
* Verifies that the maps of the game module are read through Falco instead of the chunk loader
* Minestom ships with, for the lobby as well as for the game map.
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.6.7
*/
@ExtendWith(MicrotusExtension.class)
class GameMapProviderIntegrationTest {

private static final String ARENA_NAME = "arena";

@Test
void testLobbyInstanceUsesFalcoChunkLoader(Env env, @TempDir Path root) throws IOException {
GameMapProvider provider = createProvider(root);

InstanceContainer lobbyInstance = (InstanceContainer) provider.getActiveInstance().get();
assertNotNull(lobbyInstance);
assertInstanceOf(FalcoAnvilLoader.class, lobbyInstance.getChunkLoader());

provider.close();
env.destroyInstance(lobbyInstance, true);
}

@Test
void testGameInstanceUsesOwnFalcoChunkLoader(Env env, @TempDir Path root) throws IOException {
GameMapProvider provider = createProvider(root);

InstanceContainer lobbyInstance = (InstanceContainer) provider.getActiveInstance().get();
provider.loadGameMap();
provider.switchToGameMap();

InstanceContainer gameInstance = (InstanceContainer) provider.getActiveInstance().get();
assertNotSame(lobbyInstance, gameInstance);
assertInstanceOf(FalcoAnvilLoader.class, gameInstance.getChunkLoader());
assertNotSame(lobbyInstance.getChunkLoader(), gameInstance.getChunkLoader());
assertNotNull(provider.getGameMap());

provider.close();
env.destroyInstance(gameInstance, true);
}

@Test
void testCloseIsRepeatable(Env env, @TempDir Path root) throws IOException {
GameMapProvider provider = createProvider(root);
InstanceContainer lobbyInstance = (InstanceContainer) provider.getActiveInstance().get();

provider.close();

assertDoesNotThrow(provider::close);
env.destroyInstance(lobbyInstance, true);
}

/**
* Creates a map directory layout the provider accepts and returns a provider reading it.
*
* @param root the directory the {@code game/maps} tree is written below
* @return the provider for the written maps
* @throws IOException if the layout cannot be written
*/
private GameMapProvider createProvider(Path root) throws IOException {
Path maps = root.resolve("game").resolve("maps");
writeMap(maps.resolve("lobby"), new BaseMap("lobby", Pos.ZERO, List.of()));
writeMap(
maps.resolve(ARENA_NAME),
new GameMap(ARENA_NAME, Pos.ZERO, new Pos(1, 1, 1), Set.of(), Set.of(new Pos(2, 2, 2)), List.of())
);
return new GameMapProvider(root);
}

/**
* Writes a map directory the way {@code MapFilters} expects it: a {@code region} directory next
* to a {@code map.json}. The directory stays empty, so the loader simply finds no region file
* and hands out no chunk — which is all this test needs from it.
*
* @param directoryRoot the world root of the map
* @param map the map data written to {@code map.json}
* @throws IOException if the layout cannot be written
*/
private void writeMap(Path directoryRoot, BaseMap map) throws IOException {
Files.createDirectories(directoryRoot.resolve("region"));
Files.writeString(directoryRoot.resolve("map.json"), GsonHelper.GSON.toJson(map), StandardCharsets.UTF_8);
}
}
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencyResolutionManagement {
version("luckperms", "5.5")
version("luckperms-minestom-loader", "5.6-SNAPSHOT")
version("guava", "33.6.0-jre")
version("falco", "1.0.0")

library("aonyx.bom", "net.onelitefeather", "aonyx-bom").versionRef("aonyx")
library("slf4j.api", "org.slf4j", "slf4j-api").versionRef("slf4j")
Expand All @@ -51,6 +52,8 @@ dependencyResolutionManagement {
library("aves", "net.theevilreaper", "aves").withoutVersion()
library("xerus", "net.theevilreaper", "xerus").withoutVersion()
library("pica", "net.onelitefeather", "pica").versionRef("pica")
library("falco.bom", "net.onelitefeather", "falco-bom").versionRef("falco")
library("falco.anvil", "net.onelitefeather", "falco-anvil").withoutVersion()
library("canis", "com.github.theEvilReaper", "Canis").version("master-SNAPSHOT")

library("cloudnet-bom", "eu.cloudnetservice.cloudnet", "bom").versionRef("cloudnet")
Expand Down
2 changes: 2 additions & 0 deletions setup/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ application {

dependencies {
implementation(platform(libs.aonyx.bom))
implementation(platform(libs.falco.bom))
implementation(project(":common"))
implementation(libs.slf4j.api)
implementation(libs.minestom)
Expand All @@ -19,6 +20,7 @@ dependencies {
implementation(libs.adventure)
implementation(libs.pica)
implementation(libs.guira)
implementation(libs.falco.anvil)

// SLF4J needs a binding at runtime; without one it falls back to NOP and the
// server logs nothing at all.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.anvil.AnvilLoader;
import net.minestom.server.utils.Direction;
import net.minestom.server.world.DimensionType;
import net.minestom.server.inventory.InventoryType;
import net.minestom.server.utils.Direction;
import net.onelitefeather.cygnus.common.map.GameMap;
import net.onelitefeather.cygnus.common.map.GameMapBuilder;
import net.onelitefeather.cygnus.common.util.GsonHelper;
Expand Down Expand Up @@ -327,10 +325,7 @@ public void loadData() {
.map(GameMapBuilder::new)
.orElseGet(GameMapBuilder::new);

this.instance = MinecraftServer.getInstanceManager().createInstanceContainer();

AnvilLoader anvilLoader = new AnvilLoader(this.mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key());
this.instance.setChunkLoader(anvilLoader);
this.createInstance();

this.updateTitle();
MinecraftServer.getInstanceManager().registerInstance(this.instance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.world.DimensionType;
import net.onelitefeather.cygnus.setup.map.MapDataCategory;
import net.onelitefeather.falco.anvil.FalcoAnvilLoader;
import net.onelitefeather.guira.data.SetupData;
import net.theevilreaper.aves.map.BaseMapBuilder;
import net.theevilreaper.aves.map.MapEntry;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.UUID;

/**
Expand All @@ -34,6 +37,7 @@ public abstract class InstanceSetupData implements SetupData {
protected UUID uuid;
protected MapEntry mapEntry;
protected @Nullable InstanceContainer instance;
protected @Nullable FalcoAnvilLoader chunkLoader;
protected BossBar bossBar;
protected @Nullable Component title;

Expand Down Expand Up @@ -119,12 +123,35 @@ public boolean hasMapFile() {
}

/**
* Resets this setup and unregisters its instance if present.
* Creates the instance this setup works on and attaches a {@link FalcoAnvilLoader} to it.
*
* <p>The loader reads the world below {@link MapEntry#getDirectoryRoot()} — the world root, not
* its {@code region} directory. It is kept in {@link #chunkLoader} because it holds its region
* files open and has to be closed again in {@link #reset()}.</p>
*/
protected void createInstance() {
this.instance = MinecraftServer.getInstanceManager().createInstanceContainer();
this.chunkLoader = new FalcoAnvilLoader(this.mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key());
this.instance.setChunkLoader(this.chunkLoader);
}

/**
* Resets this setup, unregisters its instance and closes its chunk loader if present.
*/
@Override
public void reset() {
if (instance == null) return;
MinecraftServer.getInstanceManager().unregisterInstance(instance);

if (this.chunkLoader == null) return;

try {
this.chunkLoader.close();
} catch (IOException exception) {
MinecraftServer.getExceptionManager().handleException(exception);
} finally {
this.chunkLoader = null;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.anvil.AnvilLoader;
import net.minestom.server.world.DimensionType;
import net.onelitefeather.cygnus.common.util.GsonHelper;
import net.onelitefeather.cygnus.setup.inventory.view.InventoryMode;
import net.onelitefeather.cygnus.setup.inventory.view.MapDataOverviewInventory;
Expand Down Expand Up @@ -139,10 +137,7 @@ public void loadData() {
.map(BaseMap::builder)
.orElseGet(BaseMap::builder);

this.instance = MinecraftServer.getInstanceManager().createInstanceContainer();

AnvilLoader anvilLoader = new AnvilLoader(this.mapEntry.getDirectoryRoot(), DimensionType.OVERWORLD.key());
this.instance.setChunkLoader(anvilLoader);
this.createInstance();

this.updateTitle();
MinecraftServer.getInstanceManager().registerInstance(this.instance);
Expand Down
Loading
Loading